bdf6dd9382
- 合并优惠码/代金券为商品管理下优惠管理页面,卡片化展示与过期遮罩 - 用户组新增优惠绑定,商品关联改用懒加载树选择器 - 商品/套餐表单新增 renew_price、renew_recommend_rebate、renew_fixed_price Co-authored-by: Cursor <cursoragent@cursor.com>
1051 lines
30 KiB
Vue
1051 lines
30 KiB
Vue
<template>
|
||
<div class="discount-manage-container">
|
||
<el-card class="main-container" shadow="never">
|
||
<!-- 类型切换 Tab -->
|
||
<el-tabs v-model="activeType" class="discount-tabs" @tab-change="handleTypeChange">
|
||
<el-tab-pane label="优惠码" name="code" />
|
||
<el-tab-pane label="代金券" name="coupon" />
|
||
</el-tabs>
|
||
|
||
<!-- 操作栏 -->
|
||
<div class="action-bar">
|
||
<el-button type="primary" @click="handleAdd">
|
||
<el-icon><Plus /></el-icon>{{ isCode ? '新增优惠码' : '新增代金券' }}
|
||
</el-button>
|
||
<el-button type="success" @click="fetchList">
|
||
<el-icon><Refresh /></el-icon>刷新
|
||
</el-button>
|
||
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
|
||
<el-icon><Delete /></el-icon>批量删除
|
||
</el-button>
|
||
<el-checkbox
|
||
v-if="dataList.length"
|
||
v-model="allSelected"
|
||
:indeterminate="isIndeterminate"
|
||
class="select-all"
|
||
@change="toggleSelectAll"
|
||
>全选</el-checkbox>
|
||
</div>
|
||
|
||
<!-- 卡片列表 -->
|
||
<div v-loading="loading" class="card-grid">
|
||
<div
|
||
v-for="row in dataList"
|
||
:key="row.id"
|
||
class="discount-card"
|
||
:class="[isCode ? 'card-code' : 'card-coupon', { 'is-selected': isSelected(row), 'is-expired': isExpired(row) }]"
|
||
>
|
||
<!-- 过期遮罩 -->
|
||
<div v-if="isExpired(row)" class="card-expired-mask">
|
||
<span class="expired-badge">已过期</span>
|
||
</div>
|
||
|
||
<!-- 选择框 -->
|
||
<el-checkbox
|
||
class="card-check"
|
||
:model-value="isSelected(row)"
|
||
@change="() => toggleSelect(row)"
|
||
/>
|
||
|
||
<!-- 头部:图标 + 名称 -->
|
||
<div class="card-header">
|
||
<div class="card-icon">
|
||
<!-- 优惠码图标 -->
|
||
<svg v-if="isCode" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M3 7v3a2 2 0 0 1 0 4v3a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1v-3a2 2 0 0 1 0-4V7a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1Z" />
|
||
<path d="M9 8l6 8" stroke-dasharray="0.1 4" />
|
||
</svg>
|
||
<!-- 代金券图标 -->
|
||
<svg v-else viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M4 6h16a1 1 0 0 1 1 1v3a2 2 0 0 0 0 4v3a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-3a2 2 0 0 0 0-4V7a1 1 0 0 1 1-1Z" />
|
||
<path d="M9 10h6M9 14h4" />
|
||
</svg>
|
||
</div>
|
||
<div class="card-title-wrap">
|
||
<div class="card-title" :title="row.name">{{ row.name }}</div>
|
||
<div class="card-sub">
|
||
<span class="card-id">ID: {{ row.id }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 优惠码独立行 + 复制按钮 -->
|
||
<div v-if="isCode && row.code" class="card-code-row">
|
||
<el-icon class="code-row-icon"><Ticket /></el-icon>
|
||
<span class="card-code-text">{{ row.code }}</span>
|
||
<el-button type="primary" link class="code-copy-btn" @click="handleCopyCode(row.code)">
|
||
<el-icon><CopyDocument /></el-icon>复制
|
||
</el-button>
|
||
</div>
|
||
|
||
<!-- 金额突出展示 -->
|
||
<div class="card-amount">
|
||
<template v-if="isCode">
|
||
<span v-if="row.percentage" class="amount-num">{{ (row.percentage / 100).toFixed(0) }}<span class="amount-unit">%</span></span>
|
||
<span v-else class="amount-num"><span class="amount-unit">¥</span>{{ (row.amount / 100).toFixed(2) }}</span>
|
||
<span class="amount-label">{{ row.percentage ? '百分比折扣' : '固定金额' }}</span>
|
||
</template>
|
||
<template v-else>
|
||
<span class="amount-num"><span class="amount-unit">¥</span>{{ (row.amount / 100).toFixed(2) }}</span>
|
||
<span class="amount-label">代金券面额</span>
|
||
</template>
|
||
</div>
|
||
|
||
<!-- 详情统计 -->
|
||
<div class="card-stats">
|
||
<div class="stat-item">
|
||
<el-icon><Wallet /></el-icon>
|
||
<span class="stat-label">最低消费</span>
|
||
<span class="stat-value">¥{{ (row.minAmount / 100).toFixed(2) }}</span>
|
||
</div>
|
||
<div class="stat-item">
|
||
<el-icon><Discount /></el-icon>
|
||
<span class="stat-label">最大抵扣</span>
|
||
<span class="stat-value">{{ row.maxAmount ? `¥${(row.maxAmount / 100).toFixed(2)}` : '无限制' }}</span>
|
||
</div>
|
||
<div class="stat-item">
|
||
<el-icon><Tickets /></el-icon>
|
||
<span class="stat-label">最大次数</span>
|
||
<span class="stat-value">{{ row.maxTimes || '无限制' }}</span>
|
||
</div>
|
||
<div class="stat-item">
|
||
<el-icon><User /></el-icon>
|
||
<span class="stat-label">单用户</span>
|
||
<span class="stat-value">{{ row.userTimes || '无限制' }}</span>
|
||
</div>
|
||
<div v-if="!isCode" class="stat-item">
|
||
<el-icon><Timer /></el-icon>
|
||
<span class="stat-label">有效期</span>
|
||
<span class="stat-value">{{ row.duration ? `${(row.duration / 86400).toFixed(0)}天` : '-' }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 标志 -->
|
||
<div class="card-flags">
|
||
<el-tag v-if="isCode" size="small" :type="row.canStacking ? 'success' : 'info'" effect="plain">
|
||
{{ row.canStacking ? '可叠加' : '不可叠加' }}
|
||
</el-tag>
|
||
<el-tag size="small" :type="row.renew ? 'success' : 'info'" effect="plain">
|
||
{{ row.renew ? '续费可用' : '续费不可用' }}
|
||
</el-tag>
|
||
</div>
|
||
|
||
<!-- 操作 -->
|
||
<div class="card-actions">
|
||
<el-button type="primary" link @click="handleEdit(row)">
|
||
<el-icon><Edit /></el-icon>编辑
|
||
</el-button>
|
||
<el-button type="warning" link @click="openGoodsBinding(row)">
|
||
<el-icon><Goods /></el-icon>商品
|
||
</el-button>
|
||
<el-button type="warning" link @click="openUsersBinding(row)">
|
||
<el-icon><UserFilled /></el-icon>用户
|
||
</el-button>
|
||
<el-button v-if="!isCode" type="info" link @click="handleManage(row)">
|
||
<el-icon><Share /></el-icon>分发
|
||
</el-button>
|
||
<el-button type="success" link @click="handleView(row)">
|
||
<el-icon><View /></el-icon>查看
|
||
</el-button>
|
||
<el-button type="danger" link @click="handleDelete(row)">
|
||
<el-icon><Delete /></el-icon>删除
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<el-empty v-if="!loading && !dataList.length" description="暂无数据" :image-size="100" />
|
||
</div>
|
||
|
||
<!-- 分页 -->
|
||
<el-pagination
|
||
v-model:current-page="queryParams.page"
|
||
v-model:page-size="queryParams.count"
|
||
:page-sizes="[10, 20, 50, 100]"
|
||
layout="total, sizes, prev, pager, next, jumper"
|
||
:total="total"
|
||
@size-change="handleSizeChange"
|
||
@current-change="handleCurrentChange"
|
||
background
|
||
class="pagination"
|
||
/>
|
||
</el-card>
|
||
|
||
<!-- 优惠码/代金券表单对话框 -->
|
||
<el-dialog
|
||
v-model="dialogVisible"
|
||
:title="dialogTitle"
|
||
width="700px"
|
||
append-to-body
|
||
>
|
||
<el-form
|
||
ref="formRef"
|
||
:model="discountForm"
|
||
:rules="formRules"
|
||
label-width="140px"
|
||
>
|
||
<el-form-item v-if="isCode" label="优惠码" prop="code">
|
||
<el-input v-model="discountForm.code" placeholder="请输入优惠码" />
|
||
</el-form-item>
|
||
<el-form-item :label="isCode ? '优惠码名称' : '代金券名称'" prop="name">
|
||
<el-input v-model="discountForm.name" :placeholder="isCode ? '请输入优惠码名称' : '请输入代金券名称'" />
|
||
</el-form-item>
|
||
<el-form-item label="备注" prop="note">
|
||
<el-input v-model="discountForm.note" type="textarea" :rows="2" placeholder="请输入备注" />
|
||
</el-form-item>
|
||
|
||
<!-- 优惠码:优惠类型选择 -->
|
||
<template v-if="isCode">
|
||
<el-form-item label="优惠类型" prop="discount_mode">
|
||
<el-radio-group v-model="discountForm.discount_mode">
|
||
<el-radio label="amount">固定金额</el-radio>
|
||
<el-radio label="percentage">百分比折扣</el-radio>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
<el-form-item v-if="discountForm.discount_mode === 'amount'" label="优惠金额" prop="amount">
|
||
<div class="unit-input-row">
|
||
<el-input-number v-model="discountForm.amount" :min="0" :precision="2" :step="0.01" placeholder="请输入优惠金额" style="flex:1" />
|
||
<span class="unit-text">元</span>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item v-if="discountForm.discount_mode === 'percentage'" label="优惠百分比(%)" prop="percentage">
|
||
<el-input-number v-model="discountForm.percentage" :min="0" :max="100" :precision="0" placeholder="请输入百分比(1-100)" style="width: 100%" />
|
||
</el-form-item>
|
||
</template>
|
||
|
||
<!-- 代金券:面额 -->
|
||
<el-form-item v-else label="面额" prop="amount">
|
||
<div class="unit-input-row">
|
||
<el-input-number v-model="discountForm.amount" :min="0" :precision="2" :step="0.01" placeholder="请输入面额" style="flex:1" />
|
||
<span class="unit-text">元</span>
|
||
</div>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="最低消费" prop="min_amount">
|
||
<div class="unit-input-row">
|
||
<el-input-number v-model="discountForm.min_amount" :min="0" :precision="2" :step="0.01" placeholder="满多少可使用" style="flex:1" />
|
||
<span class="unit-text">元</span>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="最大抵扣" prop="max_amount">
|
||
<div class="unit-input-row">
|
||
<el-input-number v-model="discountForm.max_amount" :min="0" :precision="2" :step="0.01" placeholder="0表示无限制" style="flex:1" />
|
||
<span class="unit-text">元</span>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="最大使用次数" prop="max_times">
|
||
<el-input-number v-model="discountForm.max_times" :min="0" placeholder="0表示无限制" style="width: 100%" />
|
||
</el-form-item>
|
||
<el-form-item label="单用户最大次数" prop="user_times">
|
||
<el-input-number v-model="discountForm.user_times" :min="0" placeholder="0表示无限制" style="width: 100%" />
|
||
</el-form-item>
|
||
|
||
<!-- 代金券:有效期天数 -->
|
||
<el-form-item v-if="!isCode" label="有效期" prop="duration_days">
|
||
<div class="unit-input-row">
|
||
<el-input-number v-model="discountForm.duration_days" :min="1" placeholder="代金券有效天数" style="flex:1" />
|
||
<span class="unit-text">天</span>
|
||
</div>
|
||
<div class="form-tip">代金券领取后的有效持续时间</div>
|
||
</el-form-item>
|
||
|
||
<el-form-item :label="isCode ? '有效期' : '发放时间范围'" prop="timeRange">
|
||
<el-date-picker
|
||
v-model="discountForm.timeRange"
|
||
type="datetimerange"
|
||
range-separator="至"
|
||
start-placeholder="开始时间"
|
||
end-placeholder="结束时间"
|
||
value-format="YYYY-MM-DD HH:mm:ss"
|
||
:teleported="true"
|
||
popper-class="discount-date-picker"
|
||
placement="top-start"
|
||
:editable="true"
|
||
:clearable="true"
|
||
style="width: 100%"
|
||
/>
|
||
<div v-if="!isCode" class="form-tip">代金券可以发放给用户的时间范围</div>
|
||
</el-form-item>
|
||
<el-form-item label="续费可用" prop="renew">
|
||
<el-switch v-model="discountForm.renew" active-text="是" inactive-text="否" />
|
||
</el-form-item>
|
||
<el-form-item label="同类型可叠加" prop="can_stacking">
|
||
<el-switch v-model="discountForm.can_stacking" active-text="是" inactive-text="否" />
|
||
</el-form-item>
|
||
<el-form-item label="其他类型可叠加" prop="can_combine">
|
||
<el-switch v-model="discountForm.can_combine" active-text="是" inactive-text="否" />
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<div class="dialog-footer">
|
||
<el-button @click="dialogVisible = false">取消</el-button>
|
||
<el-button type="primary" @click="submitForm">确定</el-button>
|
||
</div>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 商品绑定对话框 -->
|
||
<el-dialog
|
||
v-model="goodsBindingVisible"
|
||
:title="`商品绑定 - ${currentRow?.name || ''}`"
|
||
width="1000px"
|
||
append-to-body
|
||
destroy-on-close
|
||
>
|
||
<DiscountGoods v-if="goodsBindingVisible" :code-id="currentRow?.id" />
|
||
</el-dialog>
|
||
|
||
<!-- 用户绑定对话框 -->
|
||
<el-dialog
|
||
v-model="usersBindingVisible"
|
||
:title="`用户绑定 - ${currentRow?.name || ''}`"
|
||
width="1000px"
|
||
append-to-body
|
||
destroy-on-close
|
||
>
|
||
<DiscountUsers v-if="usersBindingVisible" :code-id="currentRow?.id" />
|
||
</el-dialog>
|
||
|
||
<!-- 详情查看对话框 -->
|
||
<DiscountDetailDialog
|
||
v-model="detailDialogVisible"
|
||
:type="activeType"
|
||
:detail-data="currentDetail"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, reactive, computed, onMounted } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import {
|
||
Plus, Delete, Refresh, Edit, View, Goods, UserFilled, User,
|
||
Share, Wallet, Discount, Tickets, Timer, Ticket, CopyDocument
|
||
} from '@element-plus/icons-vue'
|
||
import {
|
||
getDiscountCodeList,
|
||
getDiscountCodeDetail,
|
||
createDiscountCode,
|
||
updateDiscountCode,
|
||
deleteDiscountCode
|
||
} from '@/api/admin/discount'
|
||
import { timeToTimestamp } from '@/utils/tool'
|
||
import DiscountDetailDialog from '@/components/marketing/DiscountDetailDialog.vue'
|
||
import DiscountGoods from '@/views/marketing/DiscountGoods.vue'
|
||
import DiscountUsers from '@/views/marketing/DiscountUsers.vue'
|
||
|
||
const router = useRouter()
|
||
|
||
// 当前类型:code 优惠码 / coupon 代金券
|
||
const activeType = ref('code')
|
||
const isCode = computed(() => activeType.value === 'code')
|
||
|
||
// 查询参数
|
||
const queryParams = reactive({
|
||
discount_type: 'code',
|
||
page: 1,
|
||
count: 10
|
||
})
|
||
|
||
// 表单数据
|
||
const discountForm = reactive({
|
||
code_id: undefined,
|
||
code: '',
|
||
name: '',
|
||
note: '',
|
||
discount_mode: 'amount',
|
||
amount: 0,
|
||
percentage: 0,
|
||
min_amount: 0,
|
||
max_amount: 0,
|
||
max_times: 0,
|
||
user_times: 0,
|
||
duration_days: 30,
|
||
timeRange: [],
|
||
renew: false,
|
||
can_stacking: false,
|
||
can_combine: false
|
||
})
|
||
|
||
const formRules = computed(() => {
|
||
const rules = {
|
||
name: [{ required: true, message: '请输入名称', trigger: 'blur' }]
|
||
}
|
||
if (isCode.value) {
|
||
rules.code = [{ required: true, message: '请输入优惠码', trigger: 'blur' }]
|
||
rules.discount_mode = [{ required: true, message: '请选择优惠类型', trigger: 'change' }]
|
||
} else {
|
||
rules.amount = [{ required: true, message: '请输入面额', trigger: 'blur' }]
|
||
rules.duration_days = [{ required: true, message: '请输入有效期天数', trigger: 'blur' }]
|
||
}
|
||
return rules
|
||
})
|
||
|
||
// 状态数据
|
||
const loading = ref(false)
|
||
const dataList = ref([])
|
||
const total = ref(0)
|
||
const selectedRows = ref([])
|
||
const dialogVisible = ref(false)
|
||
const dialogType = ref('add')
|
||
const formRef = ref(null)
|
||
const detailDialogVisible = ref(false)
|
||
const currentDetail = ref(null)
|
||
const currentRow = ref(null)
|
||
const goodsBindingVisible = ref(false)
|
||
const usersBindingVisible = ref(false)
|
||
|
||
const dialogTitle = computed(() => {
|
||
const typeName = isCode.value ? '优惠码' : '代金券'
|
||
return `${dialogType.value === 'add' ? '新增' : '编辑'}${typeName}`
|
||
})
|
||
|
||
// 类型切换
|
||
const handleTypeChange = () => {
|
||
queryParams.discount_type = activeType.value
|
||
queryParams.page = 1
|
||
selectedRows.value = []
|
||
fetchList()
|
||
}
|
||
|
||
// 获取列表
|
||
const fetchList = async () => {
|
||
loading.value = true
|
||
selectedRows.value = []
|
||
try {
|
||
const res = await getDiscountCodeList(queryParams)
|
||
if (res.data.code === 200) {
|
||
dataList.value = res.data.data?.data || []
|
||
total.value = res.data.data?.all_count || 0
|
||
}
|
||
} catch (error) {
|
||
console.error('获取列表失败:', error)
|
||
ElMessage.error('获取列表失败')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
// 判断是否已过期(结束时间早于当前时间)
|
||
const isExpired = (row) => {
|
||
if (!row.endTime) return false
|
||
const end = new Date(row.endTime)
|
||
if (isNaN(end.getTime())) return false
|
||
return end.getTime() < Date.now()
|
||
}
|
||
|
||
// 复制优惠码
|
||
const handleCopyCode = async (code) => {
|
||
try {
|
||
if (navigator.clipboard && window.isSecureContext) {
|
||
await navigator.clipboard.writeText(code)
|
||
} else {
|
||
const textarea = document.createElement('textarea')
|
||
textarea.value = code
|
||
textarea.style.position = 'fixed'
|
||
textarea.style.opacity = '0'
|
||
document.body.appendChild(textarea)
|
||
textarea.select()
|
||
document.execCommand('copy')
|
||
document.body.removeChild(textarea)
|
||
}
|
||
ElMessage.success('优惠码已复制')
|
||
} catch (error) {
|
||
console.error('复制失败:', error)
|
||
ElMessage.error('复制失败,请手动复制')
|
||
}
|
||
}
|
||
|
||
// 卡片选择逻辑
|
||
const isSelected = (row) => selectedRows.value.some(item => item.id === row.id)
|
||
|
||
const toggleSelect = (row) => {
|
||
const idx = selectedRows.value.findIndex(item => item.id === row.id)
|
||
if (idx >= 0) {
|
||
selectedRows.value.splice(idx, 1)
|
||
} else {
|
||
selectedRows.value.push(row)
|
||
}
|
||
}
|
||
|
||
const allSelected = computed({
|
||
get: () => dataList.value.length > 0 && selectedRows.value.length === dataList.value.length,
|
||
set: () => {}
|
||
})
|
||
|
||
const isIndeterminate = computed(
|
||
() => selectedRows.value.length > 0 && selectedRows.value.length < dataList.value.length
|
||
)
|
||
|
||
const toggleSelectAll = (val) => {
|
||
selectedRows.value = val ? [...dataList.value] : []
|
||
}
|
||
|
||
// 分页
|
||
const handleSizeChange = (size) => {
|
||
queryParams.count = size
|
||
fetchList()
|
||
}
|
||
|
||
const handleCurrentChange = (page) => {
|
||
queryParams.page = page
|
||
fetchList()
|
||
}
|
||
|
||
// 重置表单数据
|
||
const resetForm = () => {
|
||
Object.assign(discountForm, {
|
||
code_id: undefined,
|
||
code: '',
|
||
name: '',
|
||
note: '',
|
||
discount_mode: 'amount',
|
||
amount: 0,
|
||
percentage: 0,
|
||
min_amount: 0,
|
||
max_amount: 0,
|
||
max_times: 0,
|
||
user_times: 0,
|
||
duration_days: 30,
|
||
timeRange: [],
|
||
renew: false,
|
||
can_stacking: false,
|
||
can_combine: false
|
||
})
|
||
}
|
||
|
||
// 新增
|
||
const handleAdd = () => {
|
||
dialogType.value = 'add'
|
||
dialogVisible.value = true
|
||
resetForm()
|
||
formRef.value?.resetFields()
|
||
}
|
||
|
||
// 将后端时间转换为表单日期字符串
|
||
const formatTimeStr = (timeVal) => {
|
||
if (!timeVal) return ''
|
||
const d = new Date(timeVal)
|
||
if (isNaN(d.getTime())) return ''
|
||
return d.getFullYear() + '-' +
|
||
String(d.getMonth() + 1).padStart(2, '0') + '-' +
|
||
String(d.getDate()).padStart(2, '0') + ' ' +
|
||
String(d.getHours()).padStart(2, '0') + ':' +
|
||
String(d.getMinutes()).padStart(2, '0') + ':' +
|
||
String(d.getSeconds()).padStart(2, '0')
|
||
}
|
||
|
||
// 编辑
|
||
const handleEdit = (row) => {
|
||
dialogType.value = 'edit'
|
||
dialogVisible.value = true
|
||
|
||
const startTime = formatTimeStr(row.startTime)
|
||
const endTime = formatTimeStr(row.endTime)
|
||
|
||
Object.assign(discountForm, {
|
||
code_id: row.id,
|
||
code: row.code || '',
|
||
name: row.name,
|
||
note: row.note || '',
|
||
discount_mode: row.percentage ? 'percentage' : 'amount',
|
||
amount: row.amount ? row.amount / 100 : 0,
|
||
percentage: row.percentage ? row.percentage / 100 : 0,
|
||
min_amount: row.minAmount ? row.minAmount / 100 : 0,
|
||
max_amount: row.maxAmount ? row.maxAmount / 100 : 0,
|
||
max_times: row.maxTimes || 0,
|
||
user_times: row.userTimes || 0,
|
||
duration_days: row.duration ? Math.round(row.duration / 86400) : 30,
|
||
timeRange: startTime && endTime ? [startTime, endTime] : [],
|
||
renew: row.renew || false,
|
||
can_stacking: row.canStacking || false,
|
||
can_combine: row.canCombine || false
|
||
})
|
||
}
|
||
|
||
// 商品绑定
|
||
const openGoodsBinding = (row) => {
|
||
currentRow.value = row
|
||
goodsBindingVisible.value = true
|
||
}
|
||
|
||
// 用户绑定
|
||
const openUsersBinding = (row) => {
|
||
currentRow.value = row
|
||
usersBindingVisible.value = true
|
||
}
|
||
|
||
// 分发管理(仅代金券)
|
||
const handleManage = (row) => {
|
||
router.push(`/product/discount/voucher/${row.id}/manage`)
|
||
}
|
||
|
||
// 查看详情
|
||
const handleView = async (row) => {
|
||
try {
|
||
const res = await getDiscountCodeDetail({ code_id: row.id })
|
||
if (res.data.code === 200) {
|
||
currentDetail.value = res.data.data
|
||
detailDialogVisible.value = true
|
||
}
|
||
} catch (error) {
|
||
console.error('获取详情失败:', error)
|
||
ElMessage.error('获取详情失败')
|
||
}
|
||
}
|
||
|
||
// 删除
|
||
const handleDelete = (row) => {
|
||
const label = isCode.value ? `优惠码 ${row.code}` : `代金券 ${row.name}`
|
||
ElMessageBox.confirm(`确认删除${label} 吗?`, '警告', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
}).then(async () => {
|
||
try {
|
||
const res = await deleteDiscountCode({ code_id: row.id })
|
||
if (res.data.code === 200) {
|
||
ElMessage.success('删除成功')
|
||
fetchList()
|
||
}
|
||
} catch (error) {
|
||
console.error('删除失败:', error)
|
||
ElMessage.error(error.response?.data?.message || '删除失败')
|
||
}
|
||
}).catch(() => {})
|
||
}
|
||
|
||
// 批量删除
|
||
const handleBatchDelete = () => {
|
||
if (selectedRows.value.length === 0) {
|
||
ElMessage.warning('请至少选择一条记录')
|
||
return
|
||
}
|
||
ElMessageBox.confirm(`确认删除选中的 ${selectedRows.value.length} 条记录吗?`, '警告', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
}).then(async () => {
|
||
loading.value = true
|
||
try {
|
||
const deletePromises = selectedRows.value.map(row =>
|
||
deleteDiscountCode({ code_id: row.id })
|
||
)
|
||
const results = await Promise.allSettled(deletePromises)
|
||
const successCount = results.filter(r => r.status === 'fulfilled' && r.value?.data?.code === 200).length
|
||
const failCount = results.length - successCount
|
||
|
||
if (failCount === 0) {
|
||
ElMessage.success(`批量删除成功,共删除 ${successCount} 条记录`)
|
||
} else if (successCount === 0) {
|
||
ElMessage.error(`批量删除失败,所有 ${failCount} 条记录删除失败`)
|
||
} else {
|
||
ElMessage.warning(`批量删除完成,成功 ${successCount} 条,失败 ${failCount} 条`)
|
||
}
|
||
fetchList()
|
||
} catch (error) {
|
||
console.error('批量删除失败:', error)
|
||
ElMessage.error('批量删除操作异常')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}).catch(() => {})
|
||
}
|
||
|
||
// 提交表单
|
||
const submitForm = () => {
|
||
formRef.value?.validate(async (valid) => {
|
||
if (!valid) return
|
||
try {
|
||
const submitData = {
|
||
discount_type: activeType.value,
|
||
name: discountForm.name,
|
||
note: discountForm.note,
|
||
min_amount: Math.round(discountForm.min_amount * 100),
|
||
max_amount: Math.round(discountForm.max_amount * 100),
|
||
max_times: discountForm.max_times || 0,
|
||
user_times: discountForm.user_times || 0,
|
||
renew: discountForm.renew,
|
||
can_stacking: discountForm.can_stacking,
|
||
can_combine: discountForm.can_combine
|
||
}
|
||
|
||
if (isCode.value) {
|
||
submitData.code = discountForm.code
|
||
if (discountForm.discount_mode === 'percentage') {
|
||
submitData.percentage = Math.round(discountForm.percentage * 100)
|
||
submitData.amount = 0
|
||
} else {
|
||
submitData.amount = Math.round(discountForm.amount * 100)
|
||
submitData.percentage = 0
|
||
}
|
||
} else {
|
||
submitData.amount = Math.round(discountForm.amount * 100)
|
||
submitData.percentage = 0
|
||
submitData.duration = discountForm.duration_days * 86400
|
||
}
|
||
|
||
// 处理时间范围
|
||
if (discountForm.timeRange && discountForm.timeRange.length === 2) {
|
||
submitData.start_time = timeToTimestamp(discountForm.timeRange[0])
|
||
submitData.end_time = timeToTimestamp(discountForm.timeRange[1])
|
||
} else {
|
||
submitData.start_time = ''
|
||
submitData.end_time = ''
|
||
}
|
||
|
||
if (dialogType.value === 'edit') {
|
||
submitData.code_id = discountForm.code_id
|
||
}
|
||
|
||
let res
|
||
if (dialogType.value === 'add') {
|
||
res = await createDiscountCode(submitData)
|
||
} else {
|
||
res = await updateDiscountCode(submitData)
|
||
}
|
||
|
||
if (res.data.code === 200) {
|
||
ElMessage.success(dialogType.value === 'add' ? '新增成功' : '修改成功')
|
||
dialogVisible.value = false
|
||
fetchList()
|
||
}
|
||
} catch (error) {
|
||
console.error('操作失败:', error)
|
||
ElMessage.error(error.response?.data?.message || '操作失败')
|
||
}
|
||
})
|
||
}
|
||
|
||
onMounted(() => {
|
||
fetchList()
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.discount-manage-container {
|
||
padding: 0;
|
||
}
|
||
|
||
.main-container {
|
||
border: 1px solid #e1e8ed;
|
||
background: #ffffff;
|
||
}
|
||
|
||
.discount-tabs {
|
||
padding: 0 20px;
|
||
}
|
||
|
||
.discount-tabs :deep(.el-tabs__header) {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.action-bar {
|
||
display: flex;
|
||
gap: 12px;
|
||
align-items: center;
|
||
padding: 16px 20px;
|
||
border-bottom: 1px solid #e1e8ed;
|
||
background: #fafbfc;
|
||
}
|
||
|
||
.select-all {
|
||
margin-left: auto;
|
||
}
|
||
|
||
/* 卡片网格 */
|
||
.card-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||
gap: 24px;
|
||
padding: 28px;
|
||
min-height: 200px;
|
||
}
|
||
|
||
.discount-card {
|
||
position: relative;
|
||
border: 1px solid #ebeef5;
|
||
border-radius: 14px;
|
||
padding: 28px;
|
||
background: #fff;
|
||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||
}
|
||
|
||
.discount-card:hover {
|
||
transform: translateY(-4px);
|
||
box-shadow: 0 14px 32px -14px rgba(0, 0, 0, 0.22);
|
||
border-color: #c6e2ff;
|
||
}
|
||
|
||
.discount-card.card-coupon:hover {
|
||
border-color: #fde2e2;
|
||
}
|
||
|
||
.discount-card.is-selected {
|
||
border-color: #409eff;
|
||
box-shadow: 0 0 0 1px #409eff inset;
|
||
}
|
||
|
||
/* 过期卡片:灰色遮罩 */
|
||
.discount-card.is-expired {
|
||
background: #fafafa;
|
||
}
|
||
|
||
.discount-card.is-expired:hover {
|
||
transform: none;
|
||
box-shadow: none;
|
||
border-color: #ebeef5;
|
||
}
|
||
|
||
.discount-card.is-expired .card-header,
|
||
.discount-card.is-expired .card-code-row,
|
||
.discount-card.is-expired .card-amount,
|
||
.discount-card.is-expired .card-stats,
|
||
.discount-card.is-expired .card-flags {
|
||
filter: grayscale(100%);
|
||
opacity: 0.6;
|
||
}
|
||
|
||
.card-expired-mask {
|
||
position: absolute;
|
||
inset: 0;
|
||
z-index: 3;
|
||
pointer-events: none;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 14px;
|
||
background: rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.expired-badge {
|
||
padding: 6px 22px;
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
color: #909399;
|
||
border: 3px solid #c0c4cc;
|
||
border-radius: 8px;
|
||
letter-spacing: 4px;
|
||
transform: rotate(-15deg);
|
||
background: rgba(255, 255, 255, 0.85);
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||
}
|
||
|
||
.card-check {
|
||
position: absolute;
|
||
top: 20px;
|
||
right: 20px;
|
||
z-index: 2;
|
||
}
|
||
|
||
.card-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 14px;
|
||
padding-right: 28px;
|
||
}
|
||
|
||
.card-icon {
|
||
width: 48px;
|
||
height: 48px;
|
||
border-radius: 12px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.card-code .card-icon {
|
||
background: #ecf5ff;
|
||
color: #409eff;
|
||
}
|
||
|
||
.card-coupon .card-icon {
|
||
background: #fef0f0;
|
||
color: #f56c6c;
|
||
}
|
||
|
||
.card-title-wrap {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.card-title {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #2c3e50;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.card-sub {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-top: 6px;
|
||
}
|
||
|
||
.card-id {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
}
|
||
|
||
/* 优惠码独立行 */
|
||
.card-code-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-top: 16px;
|
||
padding: 10px 14px;
|
||
background: #ecf5ff;
|
||
border: 1px dashed #a0cfff;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.code-row-icon {
|
||
color: #409eff;
|
||
font-size: 16px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.card-code-text {
|
||
flex: 1;
|
||
min-width: 0;
|
||
font-family: monospace;
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
letter-spacing: 0.5px;
|
||
color: #409eff;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.code-copy-btn {
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.card-amount {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 10px;
|
||
margin: 22px 0;
|
||
}
|
||
|
||
.amount-num {
|
||
font-size: 30px;
|
||
font-weight: 700;
|
||
color: #f56c6c;
|
||
line-height: 1;
|
||
}
|
||
|
||
.amount-unit {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.amount-label {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
}
|
||
|
||
.card-stats {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 14px 20px;
|
||
margin-bottom: 22px;
|
||
}
|
||
|
||
.stat-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 13px;
|
||
color: #606266;
|
||
}
|
||
|
||
.stat-item .el-icon {
|
||
color: #909399;
|
||
font-size: 15px;
|
||
}
|
||
|
||
.stat-label {
|
||
color: #909399;
|
||
}
|
||
|
||
.stat-value {
|
||
color: #2c3e50;
|
||
font-weight: 500;
|
||
margin-left: auto;
|
||
}
|
||
|
||
.card-flags {
|
||
display: flex;
|
||
gap: 8px;
|
||
margin-bottom: 22px;
|
||
}
|
||
|
||
.card-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
padding-top: 18px;
|
||
border-top: 1px solid #f2f3f5;
|
||
}
|
||
|
||
.card-actions .el-button {
|
||
margin-left: 0;
|
||
}
|
||
|
||
.amount {
|
||
color: #f56c6c;
|
||
font-weight: bold;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.discount-value {
|
||
color: #67c23a;
|
||
font-weight: bold;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.form-tip {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.pagination {
|
||
margin-top: 20px;
|
||
padding: 16px 20px;
|
||
border-top: 1px solid #e1e8ed;
|
||
background: #fafbfc;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.dialog-footer {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 12px;
|
||
}
|
||
|
||
:deep(.el-card__body) {
|
||
padding: 0;
|
||
}
|
||
|
||
.unit-input-row { display: flex; align-items: center; gap: 6px; width: 100%; }
|
||
.unit-text { font-size: 13px; color: #606266; flex-shrink: 0; white-space: nowrap; }
|
||
</style>
|
||
|
||
<style>
|
||
.discount-date-picker {
|
||
z-index: 9999 !important;
|
||
}
|
||
|
||
.discount-date-picker .el-picker-panel {
|
||
max-width: 90vw;
|
||
}
|
||
</style>
|