545 lines
25 KiB
Plaintext
545 lines
25 KiB
Plaintext
<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>
|