Files
medical-mall/pages/mall/admin/marketing/newcomer/index.uvue
2026-02-15 16:37:37 +08:00

590 lines
12 KiB
Plaintext
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>
<view class="admin-main">
<view class="card-container">
<view class="card-header">
<text class="card-header-title">新人礼设置</text>
</view>
<view class="form-content">
<!-- 赠送余额 -->
<view class="form-item">
<view class="label-col">
<text class="form-label">赠送余额(元):</text>
</view>
<view class="input-col">
<input type="number" class="form-input" v-model="formData.balance" />
<text class="form-tip">新用户奖励金额必须大于等于00为不赠送</text>
</view>
</view>
<!-- 赠送积分 -->
<view class="form-item">
<view class="label-col">
<text class="form-label">赠送积分:</text>
</view>
<view class="input-col">
<input type="number" class="form-input" v-model="formData.integral" />
<text class="form-tip">新用户奖励积分必须大于等于00为不赠送</text>
</view>
</view>
<!-- 赠送优惠券 -->
<view class="form-item row-center">
<view class="label-col">
<text class="form-label">赠送优惠券:</text>
</view>
<view class="input-col row-layout">
<view class="coupon-display-area" v-if="formData.coupons.length > 0">
<view v-for="(coupon, index) in formData.coupons" :key="index" class="coupon-tag-group">
<view class="coupon-tag-main">
<text class="coupon-tag-name">{{ coupon.name }}</text>
<text class="coupon-tag-del" @click="removeCoupon(index)">×</text>
</view>
<view class="explicit-edit-btn" @click="editCoupon(index)">
<text class="edit-btn-text">修改设置</text>
</view>
</view>
</view>
<button class="btn-select-action" @click="openCouponModal">选择优惠券</button>
</view>
</view>
<!-- 确认按钮 -->
<view class="form-submit-bar">
<button class="btn-primary-confirm" @click="handleSubmit" :disabled="isLoading">确认</button>
</view>
</view>
</view>
<!-- 优惠券选择与详情配置弹窗 -->
<view class="modal-mask" v-if="showCouponModal" @click="closeModal">
<view class="modal-box" @click.stop>
<view class="modal-head">
<text class="modal-head-title">{{ isEditing ? '配置赠送详情' : '选择优惠券' }}</text>
<text class="modal-head-close" @click="closeModal">×</text>
</view>
<view class="modal-body">
<!-- 加载中 -->
<view v-if="isLoading" class="modal-loading">
<text>加载中...</text>
</view>
<!-- 编辑/设置模式 -->
<view v-else-if="isEditing" class="setting-form">
<view class="setting-row">
<text class="setting-label">显示名称:</text>
<input class="setting-input" v-model="editingCoupon.name" placeholder="请输入页面显示的名称" />
</view>
<view class="setting-row">
<text class="setting-label">发放描述:</text>
<input class="setting-input" v-model="editingCoupon.desc" placeholder="请输入发放时的描述" />
</view>
</view>
<!-- 选择模式 -->
<view v-else class="selection-list">
<view v-if="couponOptions.length === 0" class="empty-tip">
<text>暂无可用优惠券</text>
</view>
<view v-for="(item, index) in couponOptions" :key="index"
class="selection-card" :class="{'selected-card': isSelected(item)}"
@click="toggleCoupon(item)">
<view class="card-left">
<text class="card-name">{{ item.name }}</text>
<text class="card-desc">{{ item.desc }}</text>
</view>
<view class="card-right">
<view class="check-circle" :class="{'checked-circle': isSelected(item)}">
<text class="check-mark" v-if="isSelected(item)">✓</text>
</view>
</view>
</view>
</view>
</view>
<view class="modal-foot">
<button class="foot-btn-cancel" @click="closeModal">取消</button>
<button class="foot-btn-ok" @click="confirmModal">确定</button>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, reactive, onMounted } from 'vue'
import { fetchNewcomerConfig, saveNewcomerConfig, fetchAdminCoupons, NewcomerConfig, CouponTemplate } from '@/services/admin/marketingService.uts'
interface NewcomerCoupon {
id : string;
name : string;
desc : string;
}
const formData = reactive({
balance: '0',
integral: '0',
coupons: [] as NewcomerCoupon[]
})
const isLoading = ref(false)
const showCouponModal = ref(false)
const isEditing = ref(false)
const editingIndex = ref(-1)
const editingCoupon = reactive<NewcomerCoupon>({ id: '', name: '', desc: '' })
const couponOptions = ref<NewcomerCoupon[]>([])
onMounted(() => {
loadConfig()
})
async function loadConfig() {
isLoading.value = true
try {
const res = await fetchNewcomerConfig()
if (res != null) {
formData.balance = String(res.balance_reward)
formData.integral = String(res.integral_reward)
formData.coupons = res.coupons_json.map((c : any) : NewcomerCoupon => ({
id: String(c.id),
name: String(c.name),
desc: String(c.desc)
} as NewcomerCoupon))
}
} catch (e) {
console.error('加载配置失败:', e)
} finally {
isLoading.value = false
}
}
async function openCouponModal() {
showCouponModal.value = true
isLoading.value = true
try {
const res = await fetchAdminCoupons({ page: 1, pageSize: 100, status: 1 })
couponOptions.value = res.items.map((item : CouponTemplate) : NewcomerCoupon => ({
id: item.id!,
name: item.name,
desc: item.description ?? ''
} as NewcomerCoupon))
} finally {
isLoading.value = false
}
}
function isSelected(item : NewcomerCoupon) : boolean {
return formData.coupons.some(c => c.id === item.id)
}
function toggleCoupon(item : NewcomerCoupon) {
const index = formData.coupons.findIndex(c => c.id === item.id)
if (index > -1) {
formData.coupons.splice(index, 1)
} else {
formData.coupons.push({ ...item })
}
}
function removeCoupon(index : number) {
formData.coupons.splice(index, 1)
}
function editCoupon(index : number) {
editingIndex.value = index
const coupon = formData.coupons[index]
editingCoupon.id = coupon.id
editingCoupon.name = coupon.name
editingCoupon.desc = coupon.desc
isEditing.value = true
showCouponModal.value = true
}
function closeModal() {
showCouponModal.value = false
isEditing.value = false
}
function confirmModal() {
if (isEditing.value && editingIndex.value > -1) {
formData.coupons[editingIndex.value].name = editingCoupon.name
formData.coupons[editingIndex.value].desc = editingCoupon.desc
}
closeModal()
}
async function handleSubmit() {
uni.showLoading({ title: '保存中...' })
try {
const payload : NewcomerConfig = {
balance_reward: parseFloat(formData.balance),
integral_reward: parseInt(formData.integral),
coupons_json: formData.coupons as any[]
}
const success = await saveNewcomerConfig(payload)
if (success) {
uni.showToast({ title: '设置已生效', icon: 'success' })
} else {
uni.showToast({ title: '保存失败', icon: 'none' })
}
} catch (e) {
uni.showToast({ title: '系统异常', icon: 'none' })
} finally {
uni.hideLoading()
}
}
</script>
<style scoped lang="scss">
.admin-main {
padding: 24px;
background-color: #f0f2f5;
min-height: 100vh;
}
.card-container {
background-color: #fff;
border-radius: 2px;
}
.card-header {
padding: 16px 24px;
border-bottom: 1px solid #f0f0f0;
}
.card-header-title {
font-size: 16px;
font-weight: 500;
color: rgba(0, 0, 0, 0.85);
}
.form-content {
padding: 40px 60px;
}
.form-item {
display: flex;
flex-direction: row;
margin-bottom: 24px;
}
.row-center {
align-items: flex-start;
}
.label-col {
width: 140px;
padding-right: 12px;
text-align: right;
}
.form-label {
font-size: 14px;
color: #333;
line-height: 40px;
}
.input-col {
flex: 1;
}
.row-layout {
display: flex;
flex-direction: row;
align-items: center;
flex-wrap: wrap;
}
.form-input {
width: 320px;
height: 40px;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 0 12px;
font-size: 14px;
transition: all 0.3s;
}
.form-input:focus {
border-color: #1890ff;
}
.form-tip {
display: block;
margin-top: 10px;
font-size: 14px;
color: #bfbfbf;
}
.coupon-display-area {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.coupon-tag-group {
display: flex;
flex-direction: row;
align-items: center;
margin-right: 16px;
margin-bottom: 8px;
}
.coupon-tag-main {
display: flex;
flex-direction: row;
align-items: center;
background-color: #f5f5f5;
border: 1px solid #d9d9d9;
padding: 0 10px;
height: 32px;
border-radius: 4px;
}
.coupon-tag-name {
font-size: 14px;
color: #555;
}
.coupon-tag-del {
margin-left: 8px;
font-size: 16px;
color: #999;
cursor: pointer;
}
.explicit-edit-btn {
margin-left: 8px;
cursor: pointer;
}
.edit-btn-text {
font-size: 13px;
color: #1890ff;
text-decoration: underline;
}
.btn-select-action {
height: 36px;
line-height: 36px;
padding: 0 16px;
font-size: 14px;
background-color: #fff;
border: 1px solid #d9d9d9;
border-radius: 4px;
color: #666;
margin-left: 0;
cursor: pointer;
}
.form-submit-bar {
margin-top: 32px;
padding-left: 140px;
}
.btn-primary-confirm {
width: 88px;
height: 38px;
line-height: 38px;
background-color: #1890ff;
color: #fff;
font-size: 14px;
border-radius: 4px;
margin-left: 0;
border: none;
cursor: pointer;
}
/* Modal */
.modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-box {
width: 560px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
overflow: hidden;
}
.modal-head {
padding: 20px 24px;
border-bottom: 1px solid #f0f0f0;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.modal-head-title {
font-size: 18px;
font-weight: 500;
color: #222;
}
.modal-head-close {
font-size: 24px;
color: #aaa;
cursor: pointer;
}
.modal-body {
padding: 24px;
max-height: 450px;
overflow-y: auto;
}
.setting-form {
padding: 10px 0;
}
.setting-row {
margin-bottom: 24px;
}
.setting-label {
display: block;
margin-bottom: 10px;
font-size: 14px;
color: #333;
font-weight: 500;
}
.setting-input {
width: 100%;
height: 42px;
border: 1px solid #d9d9d9;
border-radius: 4px;
padding: 0 12px;
font-size: 14px;
}
.setting-tip {
margin-top: 16px;
}
.tip-text {
font-size: 12px;
color: #ff4d4f;
}
.selection-list {
display: flex;
flex-direction: column;
}
.selection-card {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border: 1px solid #f0f0f0;
margin-bottom: 14px;
border-radius: 6px;
transition: all 0.2s;
cursor: pointer;
}
.selection-card:hover {
border-color: #1890ff;
}
.selected-card {
border-color: #1890ff;
background-color: #f0faff;
}
.card-name {
font-size: 15px;
color: #333;
font-weight: 500;
}
.card-desc {
font-size: 13px;
color: #888;
margin-top: 6px;
}
.check-circle {
width: 20px;
height: 20px;
border: 1px solid #d9d9d9;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
}
.checked-circle {
background-color: #1890ff;
border-color: #1890ff;
}
.check-mark {
color: #fff;
font-size: 12px;
}
.modal-foot {
padding: 16px 24px;
border-top: 1px solid #f0f0f0;
display: flex;
flex-direction: row;
justify-content: flex-end;
}
.foot-btn-cancel, .foot-btn-ok {
height: 34px;
line-height: 34px;
padding: 0 20px;
margin-left: 12px;
font-size: 14px;
border-radius: 4px;
cursor: pointer;
}
.foot-btn-cancel {
background-color: #fff;
border: 1px solid #d9d9d9;
color: #666;
}
.foot-btn-ok {
background-color: #1890ff;
color: #fff;
border: none;
}
.modal-loading {
padding: 40px 0;
display: flex;
align-items: center;
justify-content: center;
color: #1890ff;
font-size: 14px;
}
.empty-tip {
padding: 40px 0;
text-align: center;
color: #999;
font-size: 14px;
}
</style>