Files
medical-mall/pages/mall/admin/product/product-management/components/member-price.uvue
2026-03-18 17:14:05 +08:00

206 lines
7.3 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="member-price-page">
<view class="page-header">
<view class="back-link" @click="goBack">
<text class="arrow">{"<"}</text>
<text class="back-txt">返回</text>
</view>
<text class="header-title">会员价管理</text>
</view>
<view class="content-card">
<view v-if="isLoading" class="loading-box">
<text>加载中...</text>
</view>
<template v-else>
<view class="product-info">
<image class="p-img" :src="productImage || '/static/logo.png'" mode="aspectFill" />
<text class="p-name">{{ productName }}</text>
</view>
<view class="price-table">
<view class="th-row">
<view class="th flex-2"><text>规格名称</text></view>
<view class="th flex-1"><text>售价</text></view>
<view v-for="level in levels" :key="level.id" class="th flex-1">
<text>{{ level.name }}</text>
</view>
</view>
<view v-if="skus.length === 0" class="empty-table">
<text>该商品暂无规格信息</text>
</view>
<view v-for="(sku, sIdx) in skus" :key="sku.id" class="tr-row">
<view class="td flex-2"><text>{{ formatSpecs(sku.specifications) }}</text></view>
<view class="td flex-1"><text>¥{{ sku.price.toFixed(2) }}</text></view>
<view v-for="level in levels" :key="level.id" class="td flex-1">
<input
class="price-input"
type="digit"
v-model="priceMatrix[sku.id][level.id]"
:placeholder="calcDefaultPrice(sku.price, level.discount_percent)"
/>
</view>
</view>
</view>
<view class="footer-btns">
<button class="btn-save" :disabled="isSaving" @click="handleSave">保存设置</button>
</view>
</template>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted, reactive } from 'vue'
import { openRoute } from '@/layouts/admin/store/adminNavStore.uts'
import {
fetchActiveUserLevels,
fetchProductSkus,
fetchMemberPrices,
saveMemberPrices,
UserLevel,
ProductSku
} from '@/services/admin/productMemberPriceService.uts'
// --- State ---
const productId = ref<string>('')
const productName = ref<string>('加载中...')
const productImage = ref<string>('')
const levels = ref<UserLevel[]>([])
const skus = ref<ProductSku[]>([])
const priceMatrix = reactive<Record<string, Record<string, string>>>({})
const isLoading = ref(true)
const isSaving = ref(false)
onMounted(() => {
// 从路由参数获取 productId
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const options = currentPage.options as Record<string, string | undefined>
productId.value = options['id'] ?? ''
if (!productId.value) {
uni.showToast({ title: '未找到商品ID', icon: 'none' })
return
}
initData()
})
async function initData() {
isLoading.value = true
try {
// 并行获取等级、SKU和已设价格
const [levelRes, skuRes, priceRes] = await Promise.all([
fetchActiveUserLevels(),
fetchProductSkus(productId.value),
fetchMemberPrices(productId.value)
])
levels.value = levelRes
skus.value = skuRes
// 初始化矩阵并填充已设价格
skuRes.forEach(sku => {
priceMatrix[sku.id] = {}
levelRes.forEach(level => {
// 查找是否已有定价
const existing = priceRes.find(p => p.sku_id === sku.id && p.level_id === level.id)
priceMatrix[sku.id][level.id] = existing != null ? String(existing.member_price) : ''
})
})
// 如果有 SKU拿第一个的信息展示在头部
if (skuRes.length > 0) {
// 实际开发中建议单独查一次 Product 信息,这里简化处理
productImage.value = skuRes[0].image_url ?? ''
}
} catch (e) {
uni.showToast({ title: '加载数据失败', icon: 'none' })
} finally {
isLoading.value = false
}
}
function formatSpecs(specs: any): string {
if (specs == null) return '默认规格'
// 假设规格存的是对象 { "颜色": "红色", "尺寸": "XL" }
if (typeof specs === 'object') {
const vals = Object.values(specs as Record<string, any>)
return vals.join(',')
}
return String(specs)
}
function calcDefaultPrice(basePrice: number, discount: number): string {
return (basePrice * discount / 100).toFixed(2)
}
async function handleSave() {
isSaving.value = true
try {
const saveRows: Array<{ sku_id: string; level_id: string; member_price: number }> = []
// 遍历矩阵,只保存有输入数值的项
Object.keys(priceMatrix).forEach(skuId => {
const levelPrices = priceMatrix[skuId]
Object.keys(levelPrices).forEach(levelId => {
const val = levelPrices[levelId]
if (val != null && val.trim() !== '') {
saveRows.push({
sku_id: skuId,
level_id: levelId,
member_price: parseFloat(val)
})
}
})
})
const ok = await saveMemberPrices(productId.value, saveRows)
if (ok) {
uni.showToast({ title: '保存成功', icon: 'success' })
} else {
uni.showToast({ title: '保存失败', icon: 'none' })
}
} catch (e) {
uni.showToast({ title: '保存异常', icon: 'none' })
} finally {
isSaving.value = false
}
}
function goBack() {
openRoute('product_productList')
}
</script>
<style scoped lang="scss">
.member-price-page { padding: 0; background: transparent; min-height: auto; }
.page-header { display: flex; flex-direction: row; align-items: center; gap: 16px; margin-bottom: 20px;
.back-link { display: flex; flex-direction: row; align-items: center; gap: 4px; color: #666; cursor: pointer; }
.header-title { font-size: 16px; font-weight: bold; color: #333; }
}
.content-card { background: #fff; border-radius: 4px; padding: 24px; min-height: 400px; }
.loading-box, .empty-table { padding: 60px 0; text-align: center; color: #999; font-size: 14px; }
.product-info { display: flex; flex-direction: row; align-items: center; gap: 16px; margin-bottom: 30px;
.p-img { width: 64px; height: 64px; border-radius: 4px; background: #f5f5f5; }
.p-name { font-size: 15px; font-weight: bold; color: #333; flex: 1; }
}
.price-table { border: 1px solid #f0f0f0; border-radius: 4px; overflow-x: auto; }
.th-row { display: flex; flex-direction: row; background: #f8f9fa; border-bottom: 1px solid #f0f0f0; min-width: 800px; }
.th { padding: 12px; font-size: 14px; font-weight: bold; color: #333; text-align: center; }
.tr-row { display: flex; flex-direction: row; border-bottom: 1px solid #f0f0f0; min-width: 800px; &:last-child { border-bottom: none; } }
.td { padding: 16px 12px; font-size: 14px; color: #666; text-align: center; display: flex; align-items: center; justify-content: center; }
.flex-1 { flex: 1; min-width: 120px; } .flex-2 { flex: 2; min-width: 200px; }
.price-input { border: 1px solid #dcdfe6; border-radius: 4px; height: 32px; padding: 0 8px; text-align: center; width: 90%; font-size: 13px; background: #fff; }
.footer-btns { margin-top: 40px; display: flex; justify-content: center; .btn-save { background: #1890ff; color: #fff; border: none; padding: 0 32px; height: 40px; border-radius: 4px; font-size: 14px;
&[disabled] { opacity: 0.6; }
} }
</style>