Files
medical-mall/pages/mall/merchant/exclusive-discounts.uvue
2026-03-20 15:43:33 +08:00

545 lines
25 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="exclusive-discounts-page">
<view class="header">
<text class="title">由于您的需求,为 {{ userName }} 配置专属商品折扣</text>
<text class="subtitle">专属打折商品的折扣不受全场默认 VIP 折扣影响。</text>
</view>
<view class="action-bar">
<button class="add-btn" @click="openProductSelect"> 添加更多覆盖商品</button>
</view>
<scroll-view scroll-y class="list-container">
<view v-if="discounts.length === 0" class="empty-tip">该客户暂无专属折扣商品</view>
<view v-for="item in discounts" :key="item.id" class="discount-item">
<view class="product-info">
<image :src="item.main_image_url || '/static/images/default-product.png'" class="product-image" mode="aspectFill" />
<view class="product-details">
<text class="product-name">{{ item.product_name }}</text>
<view class="price-row">
<text class="original-price">原价 ¥{{ item.base_price }}</text>
<text class="current-discount">当前设置: {{ parseFloat(item.discount_rate) * 10 }} 折</text>
</view>
</view>
</view>
<view class="actions">
<button class="edit-btn" @click="editDiscount(item)">改价</button>
<button class="del-btn" @click="removeDiscount(item.id)">移除</button>
</view>
</view>
</scroll-view>
<!-- 修改折扣弹窗 -->
<view class="modal" v-if="showEditModal">
<view class="modal-content">
<view class="modal-header">设置该商品的折扣倍数</view>
<view class="modal-body">
<input type="digit" class="input" v-model="editForm.rate" placeholder="示例:填写 0.85 代表 85折" />
</view>
<view class="modal-footer">
<button class="btn cancel" @click="showEditModal = false">取消</button>
<button class="btn confirm" @click="saveDiscount">确定</button>
</view>
</view>
</view>
<!-- 选择商品弹窗 -->
<view class="modal" v-if="showProductSelect">
<view class="modal-content product-modal-content">
<view class="modal-header">选择要设置折扣的商品</view>
<scroll-view scroll-y class="product-scroll">
<view class="product-p-item" v-for="p in allProducts" :key="p.id" @click="selectProductForDiscount(p)">
<image :src="p.main_image_url || '/static/images/default-product.png'" class="p-img" mode="aspectFill" />
<view class="p-info">
<text class="p-name">{{p.name}}</text>
<text class="p-price">¥{{p.base_price}}</text>
</view>
</view>
<view v-if="allProducts.length === 0" class="empty-tip">暂无可选商品</view>
</scroll-view>
<view class="modal-footer">
<button class="btn cancel" @click="showProductSelect = false">关闭</button>
</view>
</view>
</view>
</view>
</template>
<script lang="ts">
import supa from '@/components/supadb/aksupainstance.uts'
type DiscountDoc = {
id: string
user_id: string
product_id: string
discount_rate: string
product_name: string
main_image_url: string
base_price: string
}
export default {
data() {
return {
userId: '',
userName: '',
discounts: [] as DiscountDoc[],
allProducts: [] as any[], // 所有商品列表
showProductSelect: false,
showEditModal: false,
editForm: {
id: '',
product_id: '',
rate: ''
}
}
},
onLoad(options: any) {
if (options['user_id']) {
this.userId = String(options['user_id'])
} else if (options.user_id) {
this.userId = String(options.user_id)
}
if (options['user_name']) {
this.userName = decodeURIComponent(String(options['user_name']))
} else if (options.user_name) {
this.userName = decodeURIComponent(String(options.user_name))
}
if (this.userId !== '') {
this.loadDiscounts()
}
},
methods: {
async openProductSelect() {
this.showProductSelect = true
if (this.allProducts.length === 0) {
uni.showLoading({ title: '获取商品中' })
try {
let merchantId = ''
const session = supa.getSession()
if (session != null && session.user != null) {
merchantId = session.user.getString('id') || ''
}
if (merchantId === '') {
const storageId = uni.getStorageSync('user_id')
if (storageId != null) {
merchantId = String(storageId)
}
}
const res = await supa.from('ml_products')
.select('id, name, main_image_url, base_price')
.eq('status', 1)
.eq('merchant_id', merchantId)
.execute()
uni.hideLoading()
if (res.data) {
this.allProducts = res.data as any[]
}
} catch (e) {
uni.hideLoading()
console.error(e)
}
}
},
selectProductForDiscount(p: any) {
this.showProductSelect = false
// Reset form and ID when selecting a new product
this.editForm.id = ''
this.editForm.product_id = String(p['id'])
this.editForm.rate = '1.0'
this.showEditModal = true
},
async loadDiscounts() {
uni.showLoading({ title: '加载中' })
try {
// 1. 获取折扣记录(不依赖数据库外键,避免报错)
const response = await supa
.from('ml_user_product_discounts')
.select('*')
.eq('user_id', this.userId)
.execute()
if (response.error != null) {
uni.hideLoading()
const errMsg = response.error['message'] != null ? String(response.error['message']) : '加载失败'
uni.showToast({ title: '加载失败: ' + errMsg, icon: 'none', duration: 3000 })
console.error('加载折扣异常', response.error)
return
}
const rawData = response.data as any[]
if (rawData == null || rawData.length === 0) {
uni.hideLoading()
this.discounts = []
return
}
// 2. 收集所有相关的 product_id
const productIds: string[] = []
for (let i = 0; i < rawData.length; i++) {
const pid = rawData[i]['product_id']
if (pid != null) {
productIds.push(String(pid))
}
}
// 3. 查出对应商品的详情信息
let productsMap = {} as UTSJSONObject
if (productIds.length > 0) {
const prodRes = await supa
.from('ml_products')
.select('id, name, main_image_url, base_price')
.in('id', productIds)
.execute()
if (prodRes.data != null) {
const pData = prodRes.data as any[]
for (let j = 0; j < pData.length; j++) {
const p = pData[j] as any
if (p['id'] != null) {
productsMap[String(p['id'])] = p
}
}
}
}
// 4. 组装数据
this.discounts = []
for (let i = 0; i < rawData.length; i++) {
const item = rawData[i] as any
const pid = item['product_id'] != null ? String(item['product_id']) : ''
const prod = productsMap[pid] as any
this.discounts.push({
id: item['id'] != null ? String(item['id']) : '',
user_id: item['user_id'] != null ? String(item['user_id']) : '',
product_id: pid,
discount_rate: item['discount_rate'] != null ? String(item['discount_rate']) : '1.0',
product_name: prod != null && prod['name'] != null ? String(prod['name']) : '未知商品',
main_image_url: prod != null && prod['main_image_url'] != null ? String(prod['main_image_url']) : '',
base_price: prod != null && prod['base_price'] != null ? String(prod['base_price']) : '0'
} as DiscountDoc)
}
uni.hideLoading()
} catch (e) {
uni.hideLoading()
console.error(e)
}
},
editDiscount(item: DiscountDoc) {
this.editForm.id = item.id
this.editForm.product_id = item.product_id
this.editForm.rate = item.discount_rate
this.showEditModal = true
},
async saveDiscount() {
if (!this.editForm.rate) {
uni.showToast({ title: '请输入折扣', icon: 'none' })
return
}
const rate = parseFloat(this.editForm.rate)
if (isNaN(rate) || rate <= 0 || rate > 1) {
uni.showToast({ title: '折扣应当在0~1之间', icon: 'none' })
return
}
uni.showLoading({ title: '保存中' })
try {
const payload = {
user_id: this.userId,
product_id: this.editForm.product_id,
discount_rate: rate,
updated_at: new Date().toISOString()
} as UTSJSONObject
let res: any = null
if (this.editForm.id !== '') {
res = await supa.from('ml_user_product_discounts').update(payload).eq('id', this.editForm.id).execute()
} else {
// Check if actually modifying an existing one they just didn't click "edit" on
const existing = this.discounts.find(d => d.product_id === this.editForm.product_id)
if (existing != null && existing.id !== '') {
res = await supa.from('ml_user_product_discounts').update(payload).eq('id', existing.id).execute()
} else {
res = await supa.from('ml_user_product_discounts').insert([payload]).execute()
}
}
uni.hideLoading()
if (res.error != null) {
const errMsg = res.error['message'] != null ? String(res.error['message']) : '未知错误'
uni.showToast({ title: '保存失败: ' + errMsg, icon: 'none', duration: 3000 })
console.error('保存折扣失败:', res.error)
return
}
this.showEditModal = false
uni.showToast({ title: '保存成功', icon: 'success', duration: 1500 })
setTimeout(() => {
this.loadDiscounts()
}, 1500)
} catch (e) {
uni.hideLoading()
uni.showToast({ title: '网络异常或请求中断', icon: 'none' })
console.error(e)
}
},
async removeDiscount(id: string) {
const that = this
uni.showModal({
title: '提醒',
content: '确定移除此商品的打折?移除后将恢复通常价格',
success: async (res) => {
if (res.confirm) {
uni.showLoading({ title: '移除中' })
try {
const s_res = await supa.from('ml_user_product_discounts').eq('id', id).delete().execute()
uni.hideLoading()
if (s_res.error == null) {
uni.showToast({ title: '移除成功', icon: 'success' })
setTimeout(() => {
that.loadDiscounts()
}, 1500)
} else {
const errMsg = s_res.error['message'] != null ? String(s_res.error['message']) : '未知错误'
uni.showToast({ title: '移除失败: ' + errMsg, icon: 'none' })
}
} catch (e: any) {
uni.hideLoading()
const errMsg = e instanceof Error ? e.message : String(e)
uni.showToast({ title: '移除异常: ' + errMsg, icon: 'none', duration: 4000 })
console.error('移除折扣报错:', e)
}
}
}
})
}
}
}
</script>
<style>
.exclusive-discounts-page {
padding: 20rpx;
background: #f5f5f5;
min-height: 100vh;
}
.header {
padding: 30rpx;
background: #fff;
border-radius: 12rpx;
margin-bottom: 20rpx;
}
.title {
font-size: 32rpx;
font-weight: bold;
color: #333;
display: block;
}
.subtitle {
font-size: 24rpx;
color: #999;
margin-top: 10rpx;
display: block;
}
.action-bar {
margin-bottom: 20rpx;
}
.add-btn {
background: #2196F3;
color: #fff;
border-radius: 8rpx;
font-size: 28rpx;
}
.list-container {
height: calc(100vh - 260rpx);
}
.empty-tip {
text-align: center;
color: #999;
padding: 60rpx;
font-size: 28rpx;
}
.discount-item {
background: #fff;
border-radius: 12rpx;
padding: 20rpx;
margin-bottom: 20rpx;
display: flex;
justify-content: space-between;
align-items: center;
}
.product-info {
display: flex;
flex: 1;
}
.product-image {
width: 100rpx;
height: 100rpx;
border-radius: 8rpx;
margin-right: 20rpx;
}
.product-details {
flex: 1;
}
.product-name {
font-size: 28rpx;
color: #333;
display: block;
}
.price-row {
margin-top: 10rpx;
display: flex;
align-items: center;
gap: 16rpx;
}
.original-price {
font-size: 24rpx;
color: #999;
text-decoration: line-through;
}
.current-discount {
font-size: 24rpx;
color: #F44336;
font-weight: bold;
}
.actions {
display: flex;
flex-direction: column;
gap: 10rpx;
}
.edit-btn {
font-size: 24rpx;
padding: 4rpx 16rpx;
background: #4CAF50;
color: #fff;
line-height: normal;
}
.del-btn {
font-size: 24rpx;
padding: 4rpx 16rpx;
background: #f44336;
color: #fff;
line-height: normal;
}
/* Modal */
.modal {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 100;
}
.modal-content {
background: #fff;
width: 600rpx;
border-radius: 16rpx;
overflow: hidden;
}
.modal-header {
padding: 30rpx;
font-size: 32rpx;
font-weight: bold;
text-align: center;
border-bottom: 1rpx solid #eee;
}
.modal-body {
padding: 40rpx;
}
.input {
background: #f5f5f5;
padding: 20rpx;
border-radius: 8rpx;
font-size: 28rpx;
width: 100%;
}
.modal-footer {
display: flex;
border-top: 1rpx solid #eee;
}
.modal-footer .btn {
flex: 1;
background: none;
border: none;
font-size: 32rpx;
padding: 30rpx 0;
}
.btn.cancel {
color: #999;
border-right: 1rpx solid #eee;
}
.btn.confirm {
color: #2196F3;
font-weight: bold;
}
/* Product Selection Modal Styles */
.product-modal-content {
display: flex;
flex-direction: column;
max-height: 80vh;
}
.product-scroll {
flex: 1;
overflow-y: auto;
padding: 20rpx;
min-height: 400rpx;
}
.product-p-item {
display: flex;
align-items: center;
padding: 20rpx;
border-bottom: 1rpx solid #eee;
}
.product-p-item:active {
background: #f9f9f9;
}
.p-img {
width: 100rpx;
height: 100rpx;
border-radius: 8rpx;
margin-right: 20rpx;
}
.p-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
.p-name {
font-size: 28rpx;
color: #333;
margin-bottom: 10rpx;
}
.p-price {
font-size: 26rpx;
color: #F44336;
font-weight: bold;
}
.empty-tip {
text-align: center;
padding: 40rpx;
color: #999;
font-size: 28rpx;
}
</style>