Files
ApiServer-Web-admin_dashboa…/src/views/product/DiscountManage.vue
T
shiran bdf6dd9382
Build and Deploy Vue3 / build (push) Successful in 1m31s
Build and Deploy Vue3 / deploy (push) Successful in 39s
feat: 优惠管理合并重构与商品续费价格参数
- 合并优惠码/代金券为商品管理下优惠管理页面,卡片化展示与过期遮罩

- 用户组新增优惠绑定,商品关联改用懒加载树选择器

- 商品/套餐表单新增 renew_price、renew_recommend_rebate、renew_fixed_price

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-18 17:06:23 +08:00

1051 lines
30 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>
<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>