合并merchant文件

This commit is contained in:
2026-03-20 15:43:33 +08:00
parent 29f588a2b2
commit 620ae742df
12 changed files with 3477 additions and 0 deletions

View File

@@ -0,0 +1,544 @@
<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>