Files
ApiServer-Web-admin_dashboa…/src/views/marketing/Voucher.vue
T
lin b3ed406f84
Build and Deploy Vue3 / build (push) Successful in 1m31s
Build and Deploy Vue3 / deploy (push) Successful in 1m9s
fix: 提交修改
2026-04-15 16:02:36 +08:00

570 lines
18 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="voucher-container">
<!-- 搜索和操作栏 -->
<el-card class="filter-container" shadow="never">
<div class="action-bar">
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>新增代金券
</el-button>
<el-button type="success" @click="fetchVoucherList">
<el-icon><Refresh /></el-icon>刷新
</el-button>
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
<el-icon><Delete /></el-icon>批量删除
</el-button>
</div>
</el-card>
<!-- 代金券列表 -->
<el-card class="table-container" shadow="never">
<el-table
v-loading="loading"
:data="voucherList"
@selection-change="handleSelectionChange"
style="width: 100%"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="name" label="代金券名称" min-width="200" />
<el-table-column label="面额" width="120">
<template #default="{ row }">
<span class="amount">¥{{ (row.amount / 100).toFixed(2) }}</span>
</template>
</el-table-column>
<el-table-column label="最低消费" width="120">
<template #default="{ row }">
¥{{ (row.minAmount / 100).toFixed(2) }}
</template>
</el-table-column>
<el-table-column label="最大抵扣" width="120">
<template #default="{ row }">
<span v-if="row.maxAmount">¥{{ (row.maxAmount / 100).toFixed(2) }}</span>
<span v-else>无限制</span>
</template>
</el-table-column>
<el-table-column prop="maxTimes" label="最大使用次数" width="130">
<template #default="{ row }">
{{ row.maxTimes || '无限制' }}
</template>
</el-table-column>
<el-table-column prop="userTimes" label="单用户次数" width="120">
<template #default="{ row }">
{{ row.userTimes || '无限制' }}
</template>
</el-table-column>
<el-table-column label="有效期(天)" width="100">
<template #default="{ row }">
{{ row.duration ? (row.duration / 86400).toFixed(0) : '-' }}
</template>
</el-table-column>
<el-table-column label="续费可用" width="100" align="center">
<template #default="{ row }">
<el-icon v-if="row.renew" color="#67c23a" :size="20"><SuccessFilled /></el-icon>
<el-icon v-else color="#f56c6c" :size="20"><CircleCloseFilled /></el-icon>
</template>
</el-table-column>
<el-table-column label="操作" width="280" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
<el-button type="primary" link @click="handleManage(row)">管理</el-button>
<el-button type="success" link @click="handleView(row)">查看</el-button>
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<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="dialogType === 'add' ? '新增代金券' : '编辑代金券'"
width="700px"
>
<el-form
ref="voucherFormRef"
:model="voucherForm"
:rules="voucherRules"
label-width="140px"
>
<el-form-item label="代金券名称" prop="name">
<el-input v-model="voucherForm.name" placeholder="请输入代金券名称" />
</el-form-item>
<el-form-item label="备注" prop="note">
<el-input v-model="voucherForm.note" type="textarea" :rows="2" placeholder="请输入备注" />
</el-form-item>
<el-form-item label="面额" prop="amount">
<div class="unit-input-row">
<el-input-number v-model="voucherForm.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="voucherForm.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="voucherForm.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="voucherForm.max_times" :min="0" placeholder="0表示无限制" style="width: 100%" />
</el-form-item>
<el-form-item label="单用户最大次数" prop="user_times">
<el-input-number v-model="voucherForm.user_times" :min="0" placeholder="0表示无限制" style="width: 100%" />
</el-form-item>
<el-form-item label="有效期" prop="duration_days">
<div class="unit-input-row">
<el-input-number v-model="voucherForm.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="发放时间范围" prop="timeRange">
<el-date-picker
v-model="voucherForm.timeRange"
type="datetimerange"
range-separator=""
start-placeholder="开始时间"
end-placeholder="结束时间"
value-format="YYYY-MM-DD HH:mm:ss"
:teleported="true"
popper-class="voucher-date-picker"
placement="top-start"
:editable="true"
:clearable="true"
style="width: 100%"
@keyup.enter="handleDatePickerEnter"
/>
<div class="form-tip">代金券可以发放给用户的时间范围</div>
</el-form-item>
<el-form-item label="续费可用" prop="renew">
<el-switch v-model="voucherForm.renew" active-text="是" inactive-text="否" />
</el-form-item>
<el-form-item label="同类型可叠加" prop="can_stacking">
<el-switch v-model="voucherForm.can_stacking" active-text="是" inactive-text="否" />
</el-form-item>
<el-form-item label="其他类型可叠加" prop="can_combine">
<el-switch v-model="voucherForm.can_combine" active-text="是" inactive-text="否" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</template>
</el-dialog>
<!-- 详情查看对话框 -->
<DiscountDetailDialog
v-model="detailDialogVisible"
type="coupon"
:detail-data="currentDetail"
/>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Delete, Refresh, SuccessFilled, CircleCloseFilled } 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'
const router = useRouter()
// 查询参数
const queryParams = reactive({
discount_type: 'coupon', // 固定为coupon表示代金券
page: 1,
count: 10
})
// 代金券表单
const voucherForm = reactive({
code_id: undefined,
discount_type: 'coupon', // 固定为coupon
name: '',
note: '',
amount: 0,
min_amount: 0,
max_amount: 0,
max_times: 0,
user_times: 0,
duration_days: 30, // 默认30天
timeRange: [],
renew: false,
can_stacking: false,
can_combine: false
})
const voucherRules = {
name: [
{ required: true, message: '请输入代金券名称', trigger: 'blur' }
],
amount: [
{ required: true, message: '请输入面额', trigger: 'blur' }
],
duration_days: [
{ required: true, message: '请输入有效期天数', trigger: 'blur' }
]
}
// 状态数据
const loading = ref(false)
const voucherList = ref([])
const total = ref(0)
const selectedRows = ref([])
const dialogVisible = ref(false)
const dialogType = ref('add')
const voucherFormRef = ref(null)
const detailDialogVisible = ref(false)
const currentDetail = ref(null)
// 获取代金券列表
const fetchVoucherList = async () => {
loading.value = true
try {
const res = await getDiscountCodeList(queryParams)
console.log('代金券列表数据:', res.data)
if (res.data.code === 200) {
voucherList.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 handleSelectionChange = (selection) => {
selectedRows.value = selection
}
// 分页
const handleSizeChange = (size) => {
queryParams.count = size
fetchVoucherList()
}
const handleCurrentChange = (page) => {
queryParams.page = page
fetchVoucherList()
}
// 新增代金券
const handleAdd = () => {
dialogType.value = 'add'
dialogVisible.value = true
Object.assign(voucherForm, {
code_id: undefined,
discount_type: 'coupon',
name: '',
note: '',
amount: 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
})
voucherFormRef.value?.resetFields()
}
// 编辑代金券
const handleEdit = (row) => {
dialogType.value = 'edit'
dialogVisible.value = true
console.log('编辑代金券原始数据:', row)
// 转换时间为日期字符串(YYYY-MM-DD HH:mm:ss 格式)
let startTime = ''
let endTime = ''
if (row.startTime) {
// 处理字符串格式的时间(如 "2026-01-08T00:00:00+08:00"
const start = new Date(row.startTime)
if (!isNaN(start.getTime())) {
startTime = start.getFullYear() + '-' +
String(start.getMonth() + 1).padStart(2, '0') + '-' +
String(start.getDate()).padStart(2, '0') + ' ' +
String(start.getHours()).padStart(2, '0') + ':' +
String(start.getMinutes()).padStart(2, '0') + ':' +
String(start.getSeconds()).padStart(2, '0')
}
}
if (row.endTime) {
// 处理字符串格式的时间(如 "2026-02-25T00:00:00+08:00"
const end = new Date(row.endTime)
if (!isNaN(end.getTime())) {
endTime = end.getFullYear() + '-' +
String(end.getMonth() + 1).padStart(2, '0') + '-' +
String(end.getDate()).padStart(2, '0') + ' ' +
String(end.getHours()).padStart(2, '0') + ':' +
String(end.getMinutes()).padStart(2, '0') + ':' +
String(end.getSeconds()).padStart(2, '0')
}
}
console.log('转换后的时间:', { startTime, endTime })
Object.assign(voucherForm, {
code_id: row.id,
discount_type: 'coupon',
name: row.name,
note: row.note || '',
amount: row.amount ? row.amount / 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
})
console.log('表单数据:', voucherForm)
}
// 管理代金券
const handleManage = (row) => {
router.push(`/marketing/voucher/${row.id}/manage`)
}
// 查看代金券详情
const handleView = async (row) => {
try {
const res = await getDiscountCodeDetail({ code_id: row.id })
console.log('代金券详情:', res.data)
if (res.data.code === 200) {
currentDetail.value = res.data.data
detailDialogVisible.value = true
}
} catch (error) {
console.error('获取代金券详情失败:', error)
ElMessage.error('获取代金券详情失败')
}
}
// 删除代金券
const handleDelete = (row) => {
ElMessageBox.confirm(`确认删除代金券 ${row.name} 吗?`, '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
const res = await deleteDiscountCode({ code_id: row.id })
console.log('删除响应:', res.data)
if (res.data.code === 200) {
ElMessage.success('删除成功')
fetchVoucherList()
}
} 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}`)
}
fetchVoucherList()
} catch (error) {
console.error('批量删除失败:', error)
ElMessage.error('批量删除操作异常')
} finally {
loading.value = false
}
}).catch(() => {})
}
// 处理日期选择器回车事件
const handleDatePickerEnter = (event) => {
// 回车键确认日期选择
const datePicker = event.target.closest('.el-date-editor')
if (datePicker) {
// 触发失焦事件,确认日期选择
event.target.blur()
}
}
// 提交代金券表单
const submitForm = () => {
voucherFormRef.value?.validate(async (valid) => {
if (valid) {
try {
const submitData = {
discount_type: 'coupon',
name: voucherForm.name,
note: voucherForm.note,
amount: Math.round(voucherForm.amount * 100),
percentage: 0, // 代金券固定为0
min_amount: Math.round(voucherForm.min_amount * 100),
max_amount: Math.round(voucherForm.max_amount * 100),
max_times: voucherForm.max_times || 0,
user_times: voucherForm.user_times || 0,
duration: voucherForm.duration_days * 86400, // 天转秒
renew: voucherForm.renew,
can_stacking: voucherForm.can_stacking,
can_combine: voucherForm.can_combine
}
// 处理时间(转换为秒级时间戳)
if (voucherForm.timeRange && voucherForm.timeRange.length === 2) {
submitData.start_time = timeToTimestamp(voucherForm.timeRange[0])
submitData.end_time = timeToTimestamp(voucherForm.timeRange[1])
} else {
submitData.start_time = ''
submitData.end_time = ''
}
// 如果是编辑,添加code_id
if (dialogType.value === 'edit') {
submitData.code_id = voucherForm.code_id
}
console.log('提交代金券数据:', submitData)
let res
if (dialogType.value === 'add') {
res = await createDiscountCode(submitData)
} else {
res = await updateDiscountCode(submitData)
}
console.log('提交响应:', res.data)
if (res.data.code === 200) {
ElMessage.success(dialogType.value === 'add' ? '新增成功' : '修改成功')
dialogVisible.value = false
fetchVoucherList()
}
} catch (error) {
console.error('操作失败:', error)
ElMessage.error(error.response?.data?.message || '操作失败')
}
}
})
}
// 初始化
onMounted(() => {
fetchVoucherList()
})
</script>
<style scoped>
.voucher-container {
padding: 0;
}
.filter-container {
margin-bottom: 20px;
border-radius: 8px;
}
.action-bar {
display: flex;
gap: 12px;
}
.table-container {
border-radius: 8px;
}
.amount {
color: #f56c6c;
font-weight: bold;
font-size: 14px;
}
.form-tip {
font-size: 12px;
color: #909399;
margin-top: 4px;
}
.pagination {
margin-top: 24px;
justify-content: flex-end;
}
.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>
/* 时间选择器弹出层样式 - 非 scoped */
.voucher-date-picker {
z-index: 9999 !important;
}
.voucher-date-picker .el-picker-panel {
max-width: 90vw;
}
</style>