增加发放优惠券

This commit is contained in:
aaron 2025-04-06 20:21:04 +08:00
parent 79ec214050
commit ffad366f63
5 changed files with 585 additions and 3 deletions

View File

@ -99,6 +99,9 @@
<a-menu-item key="coupon-activity">
<router-link to="/coupon/activity/list">优惠券活动</router-link>
</a-menu-item>
<a-menu-item key="coupon-issue">
<router-link to="/coupon/issue/list">发放优惠券</router-link>
</a-menu-item>
</a-sub-menu>
<a-sub-menu key="merchant">
@ -371,6 +374,11 @@ export default defineComponent({
key: 'coupon-activity',
title: '优惠券活动',
path: '/coupon/activity/list'
},
{
key: 'coupon-issue',
title: '发放优惠券',
path: '/coupon/issue/list'
}
]
},

View File

@ -52,6 +52,12 @@ const routes = [
component: () => import('../views/coupon/ActivityList.vue'),
meta: { title: '优惠券活动' }
},
{
path: '/coupon/issue/list',
name: 'CouponIssueList',
component: () => import('../views/coupon/IssueList.vue'),
meta: { title: '发放优惠券' }
},
{
path: '/community/building',
name: 'BuildingList',

View File

@ -0,0 +1,568 @@
<template>
<page-container>
<div class="coupon-issue-list">
<div class="table-header">
<h1>优惠券发放记录</h1>
<a-button type="primary" @click="showIssueModal">
发放优惠券
</a-button>
</div>
<a-table
:columns="columns"
:data-source="tableData"
:pagination="pagination"
:loading="loading"
@change="handleTableChange"
row-key="id"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'coupon_name'">
<span>{{ record.coupon_name }}</span>
</template>
<template v-if="column.key === 'count'">
<span>{{ record.count }}</span>
</template>
<template v-if="column.key === 'issue_time'">
{{ formatDateTime(record.issue_time) }}
</template>
</template>
</a-table>
<!-- 发放优惠券模态框 -->
<a-modal
v-model:visible="issueModalVisible"
title="发放优惠券"
@ok="handleIssueSubmit"
@cancel="handleIssueCancel"
:confirmLoading="submitting"
width="500px"
>
<a-form
ref="issueFormRef"
:model="issueForm"
:rules="issueRules"
layout="vertical"
>
<a-form-item
label="选择用户"
name="user_id"
:rules="[{ required: true, message: '请输入用户手机号搜索' }]"
>
<div class="user-search-wrapper">
<div class="phone-search">
<a-input
v-model:value="phoneSearch"
placeholder="请输入手机号搜索用户"
style="width: 100%"
:maxLength="11"
@change="validatePhone"
@pressEnter="searchUserByPhone"
/>
<a-button
type="primary"
@click="searchUserByPhone"
:disabled="!isValidPhone"
:loading="userSearching"
>
搜索
</a-button>
</div>
<div v-if="userInfo" class="user-info">
<div class="user-avatar">
<img :src="userInfo.avatar" alt="用户头像" />
</div>
<div class="user-details">
<div class="user-name">{{ userInfo.nickname }}</div>
<div class="user-phone">{{ userInfo.phone }}</div>
</div>
</div>
</div>
<a-input
v-model:value="issueForm.user_id"
type="hidden"
/>
</a-form-item>
<a-form-item
label="选择优惠券"
name="coupon_id"
:rules="[{ required: true, message: '请选择优惠券' }]"
>
<a-select
v-model:value="issueForm.coupon_id"
placeholder="请选择优惠券"
style="width: 100%"
@change="handleCouponChange"
>
<a-select-option
v-for="item in couponList"
:key="item.id"
:value="item.id"
>
{{ item.name }} {{ item.amount > 0 ? `(${item.amount}元)` : '' }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item
label="发放数量"
name="count"
:rules="[{ required: true, message: '请输入发放数量' }]"
>
<a-input-number
v-model:value="issueForm.count"
:min="1"
:max="100"
:precision="0"
style="width: 100%"
placeholder="请输入发放数量"
/>
</a-form-item>
<a-form-item
label="有效期(天)"
name="validity_days"
:rules="[{ required: true, message: '请选择有效期天数' }]"
>
<div class="validity-input">
<a-input-number
v-model:value="issueForm.validity_days"
:min="1"
:max="365"
:precision="0"
style="width: 100%"
placeholder="请输入有效期"
@change="updateExpireTime"
/>
</div>
<div class="expire-time-hint" v-if="issueForm.expire_time">
过期时间{{ formatDateTime(issueForm.expire_time) }}
</div>
</a-form-item>
<a-form-item
label="备注"
name="remark"
>
<a-textarea
v-model:value="issueForm.remark"
placeholder="请输入备注信息"
:rows="3"
:maxLength="200"
/>
</a-form-item>
</a-form>
</a-modal>
</div>
</page-container>
</template>
<script>
import { defineComponent, ref, onMounted, reactive } from 'vue'
import { message } from 'ant-design-vue'
import PageContainer from '@/components/PageContainer.vue'
import dayjs from 'dayjs'
import request from '@/utils/request'
export default defineComponent({
components: {
PageContainer
},
setup() {
const loading = ref(false)
const tableData = ref([])
const couponList = ref([])
const submitting = ref(false)
//
const phoneSearch = ref('')
const isValidPhone = ref(false)
const userSearching = ref(false)
const userInfo = ref(null)
const pagination = ref({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showTotal: (total) => `${total} 条记录`
})
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 80,
align: 'center'
},
{
title: '优惠券名称',
dataIndex: 'coupon_name',
key: 'coupon_name',
width: 200
},
{
title: '用户',
dataIndex: 'user_nickname',
key: 'user_nickname',
width: 150
},
{
title: '发放数量',
dataIndex: 'count',
key: 'count',
width: 100,
align: 'center'
},
{
title: '发放时间',
dataIndex: 'issue_time',
key: 'issue_time',
width: 180
},
{
title: '操作人',
dataIndex: 'operator_nickname',
key: 'operator_nickname',
width: 150
},
{
title: '备注',
dataIndex: 'remark',
key: 'remark',
width: 200,
ellipsis: true
}
]
//
const fetchData = async () => {
try {
loading.value = true
const params = {
skip: (pagination.value.current - 1) * pagination.value.pageSize,
limit: pagination.value.pageSize
}
const res = await request.get('/api/coupon/issue/records', { params })
if (res.code === 200) {
tableData.value = res.data.items
pagination.value.total = res.data.total
} else {
message.error(res.message || '获取发放记录失败')
}
} catch (error) {
message.error('获取发放记录失败')
} finally {
loading.value = false
}
}
//
const fetchCouponList = async () => {
try {
const res = await request.get('/api/coupon/list', {
params: { limit: 1000 }
})
if (res.code === 200) {
couponList.value = res.data.items
} else {
message.error(res.message || '获取优惠券列表失败')
}
} catch (error) {
message.error('获取优惠券列表失败')
}
}
//
const formatDateTime = (value) => {
if (!value) return '-'
return dayjs(value).format('YYYY-MM-DD HH:mm:ss')
}
//
const handleTableChange = (pag) => {
pagination.value.current = pag.current
pagination.value.pageSize = pag.pageSize
fetchData()
}
//
const disabledDate = (current) => {
return current && current < dayjs().startOf('day')
}
//
const issueModalVisible = ref(false)
const issueFormRef = ref(null)
const issueForm = reactive({
user_id: undefined,
coupon_id: undefined,
count: 1,
validity_days: 30,
expire_time: undefined,
remark: ''
})
const issueRules = {
user_id: [{ required: true, message: '请输入用户ID' }],
coupon_id: [{ required: true, message: '请选择优惠券' }],
count: [
{ required: true, message: '请输入发放数量' },
{ type: 'number', min: 1, max: 100, message: '发放数量需在1-100之间' }
],
validity_days: [
{ required: true, message: '请输入有效期天数' },
{ type: 'number', min: 1, max: 365, message: '有效期需在1-365天之间' }
]
}
//
const handleCouponChange = (value) => {
issueForm.coupon_id = value
}
//
const validatePhone = () => {
const phonePattern = /^1[3-9]\d{9}$/
isValidPhone.value = phonePattern.test(phoneSearch.value)
}
//
const searchUserByPhone = async () => {
if (!isValidPhone.value) {
message.error('请输入有效的手机号')
return
}
try {
userSearching.value = true
const res = await request.get(`/api/user/search_by_phone/${phoneSearch.value}`)
if (res.code === 200) {
userInfo.value = res.data
issueForm.user_id = res.data.userid
message.success('用户查询成功')
} else {
message.error(res.message || '未找到该用户')
userInfo.value = null
issueForm.user_id = undefined
}
} catch (error) {
message.error('搜索用户失败')
userInfo.value = null
issueForm.user_id = undefined
} finally {
userSearching.value = false
}
}
//
const resetUserSearch = () => {
phoneSearch.value = ''
userInfo.value = null
issueForm.user_id = undefined
isValidPhone.value = false
}
//
const updateExpireTime = (days) => {
if (!days || days < 1) return
// +23:59:59
const expiryDate = dayjs().add(days, 'day').endOf('day')
issueForm.expire_time = expiryDate.toDate()
}
//
const showIssueModal = () => {
//
Object.assign(issueForm, {
user_id: undefined,
coupon_id: undefined,
count: 1,
validity_days: 30,
expire_time: undefined,
remark: ''
})
//
updateExpireTime(30)
//
resetUserSearch()
//
if (couponList.value.length === 0) {
fetchCouponList()
}
issueModalVisible.value = true
}
//
const handleIssueSubmit = async () => {
try {
await issueFormRef.value.validate()
//
if (!issueForm.expire_time) {
updateExpireTime(issueForm.validity_days)
}
submitting.value = true
const params = {
user_id: issueForm.user_id,
coupon_id: issueForm.coupon_id,
count: issueForm.count,
expire_time: dayjs(issueForm.expire_time).format('YYYY-MM-DDTHH:mm:ss.SSS[Z]'),
remark: issueForm.remark
}
const res = await request.post('/api/coupon/issue', params)
if (res.code === 200) {
message.success('优惠券发放成功')
issueModalVisible.value = false
fetchData() //
} else {
message.error(res.message || '优惠券发放失败')
}
} catch (error) {
if (error.errorFields) {
//
message.error('请完善表单信息')
} else {
message.error('优惠券发放失败')
}
} finally {
submitting.value = false
}
}
//
const handleIssueCancel = () => {
issueModalVisible.value = false
}
onMounted(() => {
fetchData()
})
return {
loading,
tableData,
pagination,
couponList,
columns,
formatDateTime,
handleTableChange,
//
phoneSearch,
isValidPhone,
userSearching,
userInfo,
validatePhone,
searchUserByPhone,
//
issueModalVisible,
issueFormRef,
issueForm,
issueRules,
submitting,
showIssueModal,
handleIssueSubmit,
handleIssueCancel,
handleCouponChange,
updateExpireTime,
disabledDate
}
}
})
</script>
<style scoped>
.table-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.table-header h1 {
margin: 0;
}
:deep(.ant-form-item) {
margin-bottom: 16px;
}
:deep(.ant-form-item:last-child) {
margin-bottom: 0;
}
:deep(.ant-table-wrapper) {
margin-bottom: 20px;
}
.user-search-wrapper {
width: 100%;
}
.phone-search {
display: flex;
margin-bottom: 10px;
}
.phone-search .ant-input {
margin-right: 8px;
}
.user-info {
display: flex;
align-items: center;
background-color: #f9f9f9;
border-radius: 4px;
padding: 10px;
margin-top: 10px;
}
.user-avatar {
width: 50px;
height: 50px;
border-radius: 25px;
overflow: hidden;
margin-right: 12px;
}
.user-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.user-details {
flex: 1;
}
.user-name {
font-weight: bold;
font-size: 16px;
margin-bottom: 4px;
}
.user-phone {
color: #666;
font-size: 14px;
}
</style>

View File

@ -211,8 +211,8 @@ export default defineComponent({
const res = await request.get('/api/coupon/list', { params })
if (res.code === 200) {
tableData.value = res.data
pagination.value.total = res.data.length
tableData.value = res.data.items
pagination.value.total = res.data.total
} else {
message.error(res.message || '获取数据失败')
}

View File

@ -88,7 +88,7 @@
type="primary"
size="small"
@click="showRefundModal(record)"
:disabled="record.status === 'CANCELLED' || record.refund_amount > 0"
:disabled="record.status === 'CANCELLED' || record.status === 'UNPAID' || record.refund_amount > 0"
>
退款
</a-button>