206 lines
7.3 KiB
Plaintext
206 lines
7.3 KiB
Plaintext
<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>
|