| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510 |
- <template>
- <div class="vip-package-container">
- <h2 class="page-title">VIP套餐管理</h2>
-
- <!-- 工具栏 -->
- <el-card shadow="never" class="toolbar-card">
- <el-row :gutter="20">
- <el-col :span="12">
- <el-button type="primary" icon="Plus" @click="showCreateDialog">添加套餐</el-button>
- <el-button icon="Refresh" @click="loadList">刷新</el-button>
- </el-col>
- <el-col :span="12">
- <el-space wrap style="float: right">
- <el-select v-model="filters.status" placeholder="状态" clearable style="width: 120px" @change="loadList">
- <el-option label="全部" :value="undefined" />
- <el-option label="启用" :value="1" />
- <el-option label="禁用" :value="0" />
- </el-select>
-
- <el-input v-model="filters.keyword" placeholder="搜索套餐名称" clearable style="width: 200px" @keyup.enter="loadList">
- <template #append>
- <el-button icon="Search" @click="loadList" />
- </template>
- </el-input>
- </el-space>
- </el-col>
- </el-row>
- </el-card>
-
- <!-- 卡片列表 -->
- <div v-loading="loading" class="package-cards-container">
- <el-row :gutter="20">
- <el-col :xs="24" :sm="12" :md="8" :lg="6" v-for="pkg in list" :key="pkg.packageId">
- <el-card shadow="hover" class="package-card" :class="{ 'recommend-card': pkg.isRecommend === 1 }">
- <!-- 推荐标签 -->
- <div v-if="pkg.isRecommend === 1" class="recommend-badge">
- <span class="badge-text">🔥 推荐</span>
- </div>
-
- <!-- 套餐名称 -->
- <div class="package-header">
- <h3 class="package-name">{{ pkg.packageName }}</h3>
- <el-tag :type="pkg.status === 1 ? 'success' : 'info'" size="small">
- {{ pkg.status === 1 ? '启用' : '禁用' }}
- </el-tag>
- </div>
-
- <!-- 价格 -->
- <div class="package-price">
- <div class="original-price">原价:¥{{ pkg.originalPrice }}</div>
- <div class="current-price">¥{{ pkg.currentPrice }}</div>
- <div class="price-unit">/ {{ pkg.durationDays }}天</div>
- </div>
-
- <!-- 权益说明 -->
- <div class="package-benefits">
- <div class="benefits-title">套餐权益:</div>
- <div class="benefits-content">{{ pkg.benefits || '暂无说明' }}</div>
- </div>
-
- <!-- 底部操作 -->
- <div class="package-footer">
- <el-space>
- <el-button type="primary" size="small" @click="showEditDialog(pkg)">
- <el-icon><Edit /></el-icon>
- 编辑
- </el-button>
- <el-button type="danger" size="small" @click="handleDelete(pkg)">
- <el-icon><Delete /></el-icon>
- 删除
- </el-button>
- <el-switch
- :model-value="pkg.status"
- :active-value="1"
- :inactive-value="0"
- inline-prompt
- active-text="启"
- inactive-text="禁"
- @change="handleStatusChange(pkg)"
- />
- </el-space>
- </div>
-
- <!-- 角标信息 -->
- <div class="package-badge-info">
- <span class="sort-badge">排序: {{ pkg.sortOrder }}</span>
- </div>
- </el-card>
- </el-col>
- </el-row>
-
- <!-- 空状态 -->
- <el-empty v-if="!loading && list.length === 0" description="暂无VIP套餐" />
-
- <!-- 分页 -->
- <div class="pagination-container" v-if="list.length > 0">
- <el-pagination
- v-model:current-page="currentPage"
- v-model:page-size="pageSize"
- :total="total"
- :page-sizes="[8, 12, 20, 40]"
- layout="total, sizes, prev, pager, next, jumper"
- @size-change="loadList"
- @current-change="loadList"
- />
- </div>
- </div>
-
- <!-- 创建/编辑对话框 -->
- <el-dialog
- v-model="dialogVisible"
- :title="isEdit ? '编辑VIP套餐' : '添加VIP套餐'"
- width="600px"
- @close="resetForm"
- >
- <el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
- <el-form-item label="套餐名称" prop="packageName">
- <el-input v-model="form.packageName" placeholder="例如:月度会员" />
- </el-form-item>
-
- <el-row :gutter="20">
- <el-col :span="12">
- <el-form-item label="时长(天)" prop="durationDays">
- <el-input-number v-model="form.durationDays" :min="1" :max="3650" style="width: 100%" />
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="排序" prop="sortOrder">
- <el-input-number v-model="form.sortOrder" :min="0" style="width: 100%" />
- </el-form-item>
- </el-col>
- </el-row>
-
- <el-row :gutter="20">
- <el-col :span="12">
- <el-form-item label="原价(元)" prop="originalPrice">
- <el-input-number v-model="form.originalPrice" :min="0" :precision="2" :step="0.01" style="width: 100%" />
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="现价(元)" prop="currentPrice">
- <el-input-number v-model="form.currentPrice" :min="0" :precision="2" :step="0.01" style="width: 100%" />
- </el-form-item>
- </el-col>
- </el-row>
-
- <el-form-item label="权益说明" prop="benefits">
- <el-input v-model="form.benefits" type="textarea" :rows="4" placeholder="请输入VIP权益说明,多个权益用换行分隔" />
- </el-form-item>
-
- <el-row :gutter="20">
- <el-col :span="12">
- <el-form-item label="推荐套餐" prop="isRecommend">
- <el-switch v-model="form.isRecommend" :active-value="1" :inactive-value="0" />
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="状态" prop="status">
- <el-switch v-model="form.status" :active-value="1" :inactive-value="0" active-text="启用" inactive-text="禁用" />
- </el-form-item>
- </el-col>
- </el-row>
- </el-form>
-
- <template #footer>
- <el-button @click="dialogVisible = false">取消</el-button>
- <el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
- </template>
- </el-dialog>
- </div>
- </template>
- <script setup>
- import { ref, reactive, onMounted } from 'vue'
- import { ElMessage, ElMessageBox } from 'element-plus'
- import { Plus, Search, Refresh, Edit, Delete } from '@element-plus/icons-vue'
- import request from '@/utils/request'
- const loading = ref(false)
- const dialogVisible = ref(false)
- const isEdit = ref(false)
- const submitLoading = ref(false)
- const formRef = ref(null)
- const currentPage = ref(1)
- const pageSize = ref(10)
- const total = ref(0)
- const list = ref([])
- const filters = reactive({
- status: null,
- keyword: ''
- })
- const form = reactive({
- packageId: null,
- packageName: '',
- durationDays: 30,
- originalPrice: 0,
- currentPrice: 0,
- isRecommend: 0,
- benefits: '',
- sortOrder: 0,
- status: 1
- })
- const rules = {
- packageName: [{ required: true, message: '请输入套餐名称', trigger: 'blur' }],
- durationDays: [{ required: true, message: '请输入时长', trigger: 'blur' }],
- originalPrice: [{ required: true, message: '请输入原价', trigger: 'blur' }],
- currentPrice: [{ required: true, message: '请输入现价', trigger: 'blur' }]
- }
- const loadList = async () => {
- loading.value = true
- try {
- const params = {
- page: currentPage.value,
- size: pageSize.value
- }
-
- if (filters.status !== null && filters.status !== undefined) params.status = filters.status
- if (filters.keyword) params.keyword = filters.keyword
-
- const response = await request.get('/admin/vip/package/list', { params })
-
- if (response.code === 200) {
- // 后端返回格式: Result<Page<VipPackage>>,使用records字段
- list.value = response.data.records || []
- total.value = response.data.total || 0
- }
- } catch (error) {
- console.error('加载失败:', error)
- ElMessage.error('加载失败')
- } finally {
- loading.value = false
- }
- }
- const showCreateDialog = () => {
- isEdit.value = false
- resetForm()
- dialogVisible.value = true
- }
- const showEditDialog = (row) => {
- isEdit.value = true
- Object.assign(form, row)
- dialogVisible.value = true
- }
- const resetForm = () => {
- if (formRef.value) {
- formRef.value.resetFields()
- }
- form.packageId = null
- form.packageName = ''
- form.durationDays = 30
- form.originalPrice = 0
- form.currentPrice = 0
- form.isRecommend = 0
- form.benefits = ''
- form.sortOrder = 0
- form.status = 1
- }
- const handleSubmit = async () => {
- if (!formRef.value) return
- try {
- await formRef.value.validate()
- submitLoading.value = true
-
- const url = isEdit.value ? '/admin/vip/package/update' : '/admin/vip/package/create'
- const method = isEdit.value ? 'put' : 'post'
-
- const response = await request[method](url, form)
- if (response.code === 200) {
- ElMessage.success(isEdit.value ? '更新成功' : '创建成功')
- dialogVisible.value = false
- loadList()
- } else {
- ElMessage.error(response.msg || '操作失败')
- }
- } catch (error) {
- console.error('提交失败:', error)
- if (error !== 'cancel') {
- ElMessage.error('操作失败')
- }
- } finally {
- submitLoading.value = false
- }
- }
- const handleStatusChange = async (row) => {
- try {
- const newStatus = row.status === 1 ? 0 : 1
- const response = await request.put('/admin/vip/package/status', {
- id: row.packageId,
- status: newStatus
- })
-
- if (response.code === 200) {
- ElMessage.success('状态更新成功')
- loadList()
- } else {
- ElMessage.error(response.msg || '状态更新失败')
- }
- } catch (error) {
- console.error('状态更新失败:', error)
- ElMessage.error('状态更新失败')
- }
- }
- const handleDelete = async (row) => {
- try {
- await ElMessageBox.confirm('确定要删除这个VIP套餐吗?', '提示', {
- confirmButtonText: '确定',
- cancelButtonText: '取消',
- type: 'warning'
- })
-
- const response = await request.delete(`/admin/vip/package/delete/${row.packageId}`)
- if (response.code === 200) {
- ElMessage.success('删除成功')
- loadList()
- } else {
- ElMessage.error(response.msg || '删除失败')
- }
- } catch (error) {
- if (error !== 'cancel') {
- console.error('删除失败:', error)
- }
- }
- }
- onMounted(() => loadList())
- </script>
- <style scoped>
- .vip-package-container {
- padding: 0;
- }
- .page-title {
- font-size: 24px;
- font-weight: bold;
- color: #333;
- margin: 0 0 20px 0;
- }
- .toolbar-card {
- margin-bottom: 20px;
- }
- .package-cards-container {
- min-height: 400px;
- padding: 20px 0;
- }
- /* 套餐卡片 */
- .package-card {
- position: relative;
- border-radius: 16px;
- margin-bottom: 20px;
- transition: all 0.3s ease;
- overflow: visible;
- }
- .package-card:hover {
- transform: translateY(-8px);
- box-shadow: 0 12px 24px rgba(233, 30, 99, 0.2) !important;
- }
- .recommend-card {
- border: 2px solid #E91E63;
- background: linear-gradient(135deg, #FFF5F7 0%, #FFFFFF 100%);
- }
- /* 推荐标签 */
- .recommend-badge {
- position: absolute;
- top: -10px;
- right: 20px;
- background: linear-gradient(135deg, #FF6B9D 0%, #E91E63 100%);
- color: white;
- padding: 6px 16px;
- border-radius: 20px;
- font-size: 12px;
- font-weight: bold;
- box-shadow: 0 4px 12px rgba(233, 30, 99, 0.4);
- z-index: 10;
- }
- .badge-text {
- display: flex;
- align-items: center;
- gap: 4px;
- }
- /* 套餐头部 */
- .package-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 20px;
- padding-bottom: 16px;
- border-bottom: 2px solid #f0f0f0;
- }
- .package-name {
- font-size: 20px;
- font-weight: bold;
- color: #333;
- margin: 0;
- }
- /* 价格区域 */
- .package-price {
- text-align: center;
- padding: 24px 0;
- background: linear-gradient(135deg, #FFF9F9 0%, #FFFFFF 100%);
- border-radius: 12px;
- margin-bottom: 20px;
- }
- .original-price {
- font-size: 14px;
- color: #999;
- text-decoration: line-through;
- margin-bottom: 8px;
- }
- .current-price {
- font-size: 40px;
- font-weight: bold;
- color: #E91E63;
- line-height: 1;
- margin-bottom: 4px;
- }
- .price-unit {
- font-size: 14px;
- color: #666;
- }
- /* 权益说明 */
- .package-benefits {
- padding: 16px;
- background: #F5F5F5;
- border-radius: 8px;
- margin-bottom: 16px;
- min-height: 80px;
- }
- .benefits-title {
- font-size: 12px;
- color: #999;
- margin-bottom: 8px;
- }
- .benefits-content {
- font-size: 14px;
- color: #666;
- line-height: 1.6;
- white-space: pre-line;
- }
- /* 底部操作 */
- .package-footer {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding-top: 16px;
- border-top: 1px solid #f0f0f0;
- }
- /* 角标信息 */
- .package-badge-info {
- position: absolute;
- bottom: 16px;
- right: 16px;
- }
- .sort-badge {
- font-size: 12px;
- color: #999;
- background: #F5F5F5;
- padding: 4px 12px;
- border-radius: 12px;
- }
- /* 分页 */
- .pagination-container {
- display: flex;
- justify-content: center;
- margin-top: 40px;
- padding: 20px 0;
- }
- /* 响应式调整 */
- @media (max-width: 768px) {
- .package-card {
- margin-bottom: 16px;
- }
-
- .current-price {
- font-size: 32px;
- }
- }
- </style>
|