consumer模块完成度95%,能编译在安卓端运行,在解决数据获取和页面布局问题

This commit is contained in:
cyh666666
2026-02-26 17:27:15 +08:00
parent e606c597ca
commit b34f960624
1412 changed files with 3304 additions and 804 deletions

View File

@@ -28,6 +28,17 @@
- 使用可选链 `?.` 和空值合并 `??` 处理可空类型
- `uni.showModal` 的 `success` 回调不能是 `async` 函数
- 函数必须在使用前定义
- Promise.all 需要显式指定类型参数,如 Promise.all<any>([...])
- CSS 伪类选择器 :nth-child() 不支持,需要移除
- 如果函数 A 调用了函数 B则函数 B 必须在函数 A 之前定义(不仅是调用顺序,而是定义顺序)
- 不支持 Record<K, V> 对象字面量语法,需要改用 if-else 或 Map
- Promise.all 在某些情况下类型推断失败,可以改用顺序执行 await
- onLoad 的 options 参数是 any 类型,需要转换为 UTSJSONObject 后使用 getString() 方法访问属性
- decodeURIComponent 返回 String? 类型,需要检查 null 后再赋值
- 条件判断必须使用布尔类型,字符串要用 `!= ''` 代替 truthy 判断
- 数组元素类型要明确,不要用 any[],否则模板中无法访问元素属性
- 可空数组类型调用方法需要使用 `!` 非空断言,如 `arr!.join(',')`
- v-model 绑定的变量需要明确类型,如 `quantity: 1 as number`
- 不支持 `Record<K, V>` 对象字面量语法
- 模板中可空类型必须使用 `?.` 安全访问
- `uni.showModal` 的 `success` 回调不能是 `async` 函数

View File

@@ -83,21 +83,44 @@
<text class="section-desc">快速定位</text>
</view>
<view class="category-grid" v-if="categoryTab === 'category'">
<!-- 一级分类 -->
<view
v-for="category in categories"
v-for="category in parentCategories"
:key="category.id"
class="category-card"
@click="switchCategory(category)"
@click="onParentCategoryClick(category)"
:style="{ '--card-color': category.color }"
>
<view class="card-icon">
<text class="card-icon-text">{{ category.icon }}</text>
</view>
<text class="card-name">{{ category.name }}</text>
<text class="card-desc">{{ category.description }}</text>
</view>
</view>
<view class="category-grid" v-else>
<!-- 二级分类 -->
<view v-if="categoryTab === 'category' && showSubCategories && subCategories.length > 0" class="sub-category-grid">
<view class="sub-category-header">
<text class="sub-category-title">{{ selectedParentCategory?.name }}分类</text>
<text class="sub-category-close" @click="showSubCategories = false">✕</text>
</view>
<view class="sub-category-wrapper">
<view
v-for="subCat in subCategories"
:key="subCat.id"
class="sub-category-card"
@click="onSubCategoryClick(subCat)"
>
<view class="card-icon">
<text class="card-icon-text">{{ subCat.icon }}</text>
</view>
<text class="card-name">{{ subCat.name }}</text>
</view>
</view>
</view>
<!-- 品牌列表 -->
<view class="category-grid" v-if="categoryTab === 'brand'">
<view
v-for="brand in brands"
:key="brand.id"
@@ -304,6 +327,12 @@ const categoryTab = ref<string>('category')
const categories = ref<Category[]>([])
const brands = ref<Brand[]>([])
// 一级分类和二级分类
const parentCategories = ref<Category[]>([])
const subCategories = ref<Category[]>([])
const selectedParentCategory = ref<Category | null>(null)
const showSubCategories = ref(false)
// 排序标签类型
type SortTab = {
id: string
@@ -342,38 +371,60 @@ const healthNews = [
}
]
// 获取分类数据
// 获取一级分类数据
const loadCategories = async (): Promise<void> => {
try {
const categoriesData = await supabaseService.getCategories()
// 映射字段根据ml_categories表结构映射
const mappedCategories: Category[] = []
const rawList = categoriesData as any[]
for (let i = 0; i < rawList.length; i++) {
const raw = rawList[i]
const catObj = (raw instanceof UTSJSONObject) ? (raw as UTSJSONObject) : (JSON.parse(JSON.stringify(raw)) as UTSJSONObject)
const name = catObj.getString('name') ?? ''
// 过滤掉医药健康相关分类
if (name.includes('医药') || name.includes('健康')) {
continue
}
const id = catObj.getString('id') ?? ''
const description = catObj.getString('description') ?? ''
const icon = catObj.getString('icon') ?? catObj.getString('icon_url') ?? '📦'
const color = catObj.getString('color') ?? '#4CAF50'
// 使用 JSON.parse 创建对象,避免接口实例化问题
const categoryItem = JSON.parse(`{"id":"${id}","name":"${name}","icon":"${icon}","description":"${description}","color":"${color}"}`) as Category
mappedCategories.push(categoryItem)
}
// 保持原始顺序或按ID排序移除随机打乱
categories.value = mappedCategories
const categoriesData = await supabaseService.getParentCategories()
parentCategories.value = categoriesData
// 兼容其他使用 categories 的地方
categories.value = categoriesData
} catch (error) {
console.error('加载分类数据失败:', error)
// 如果加载失败,使用默认分类作为后备
parentCategories.value = []
categories.value = []
}
}
// 获取二级分类数据
const loadSubCategories = async (parentId: string): Promise<void> => {
try {
const subData = await supabaseService.getSubCategories(parentId)
subCategories.value = subData
} catch (error) {
console.error('加载子分类数据失败:', error)
subCategories.value = []
}
}
// 点击一级分类
const onParentCategoryClick = async (category: Category): Promise<void> => {
// 如果已经选中,则切换显示/隐藏二级分类
if (selectedParentCategory.value != null && selectedParentCategory.value.id === category.id) {
showSubCategories.value = !showSubCategories.value
return
}
// 选中新的分类
selectedParentCategory.value = category
showSubCategories.value = true
// 加载二级分类
await loadSubCategories(category.id)
}
// 点击二级分类
const onSubCategoryClick = (category: Category): void => {
// 跳转到分类页面
uni.setStorageSync('selectedCategory', category.id)
const timestamp = Date.now()
const randomParam = Math.random().toString(36).substring(2, 8)
const url = `/pages/mall/consumer/category?categoryId=${category.id}&name=${encodeURIComponent(category.name)}&timestamp=${timestamp}&random=${randomParam}`
uni.switchTab({
url: '/pages/mall/consumer/category'
})
}
// 获取品牌数据
const loadBrands = async () => {
try {
@@ -1138,7 +1189,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
}
.category-card {
width: 47%; /* 50 - 3 */
width: 23%; /* 一行4个 */
margin: 0 1.5% 16px 1.5%;
display: flex;
flex-direction: column;
@@ -1187,6 +1238,78 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
text-align: center;
}
/* 二级分类样式 */
.sub-category-grid {
background: #f8f9fa;
border-radius: 12px;
padding: 16px;
margin-top: 16px;
}
.sub-category-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #e0e0e0;
}
.sub-category-title {
font-size: 14px;
font-weight: bold;
color: #333;
}
.sub-category-close {
font-size: 16px;
color: #999;
padding: 4px 8px;
}
.sub-category-wrapper {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: flex-start;
}
.sub-category-card {
width: 23%;
background: white;
border-radius: 8px;
padding: 10px 4px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border: 1px solid #eee;
margin-right: 2%;
margin-bottom: 10px;
}
.sub-category-card .card-icon {
width: 36px;
height: 36px;
border-radius: 18px;
margin-bottom: 6px;
display: flex;
align-items: center;
justify-content: center;
}
.sub-category-card .card-icon-text {
font-size: 18px;
}
.sub-category-card .card-name {
font-size: 11px;
color: #333;
text-align: center;
lines: 1;
text-overflow: ellipsis;
}
/* 健康资讯 */
.health-news {
background: white;

View File

@@ -55,24 +55,8 @@ const totalPoints = ref<number>(0)
const records = ref<PointRecord[]>([])
const loading = ref<boolean>(true)
onMounted(() => {
loadData()
})
const loadData = async () => {
loading.value = true
await Promise.all([
loadPoints(),
loadRecords()
])
loading.value = false
}
const loadPoints = async () => {
// 调用 service 获取积分 (需要supabaseService支持)
// 暂时如果service没更新先用mock
// const res = await supabaseService.getUserPoints()
// if (res != null) totalPoints.value = res
// 函数必须按调用顺序定义:先定义被调用的函数
const loadPoints = async (): Promise<void> => {
try {
const points = await supabaseService.getUserPoints()
totalPoints.value = points
@@ -81,15 +65,27 @@ const loadPoints = async () => {
}
}
const loadRecords = async () => {
const loadRecords = async (): Promise<void> => {
try {
const list = await supabaseService.getPointRecords()
records.value = list
records.value = list as PointRecord[]
} catch (e) {
console.error('获取积分记录失败', e)
}
}
// loadData 在 loadPoints 和 loadRecords 之后定义
const loadData = async (): Promise<void> => {
loading.value = true
await loadPoints()
await loadRecords()
loading.value = false
}
onMounted(() => {
loadData()
})
const handleExchange = () => {
uni.showToast({
title: '积分商城开发中',
@@ -98,14 +94,20 @@ const handleExchange = () => {
}
const getTypeText = (type: string): string => {
const map: Record<string, string> = {
'signin': '每日签到',
'shopping': '购物奖励',
'redeem': '积分兑换',
'admin': '系统调整',
'register': '注册赠送'
// 不支持 Record<string, string>,使用 if-else
if (type == 'signin') {
return '每日签到'
} else if (type == 'shopping') {
return '购物奖励'
} else if (type == 'redeem') {
return '积分兑换'
} else if (type == 'admin') {
return '系统调整'
} else if (type == 'register') {
return '注册赠送'
} else {
return '积分变动'
}
return map[type] ?? '积分变动'
}
const formatTime = (timeStr: string): string => {

View File

@@ -81,9 +81,9 @@
</view>
<input class="quantity-input"
type="number"
v-model="quantity"
:value="quantity"
:min="1"
:max="getMaxQuantity()"
:max="getMaxQuantity()"
@input="validateQuantity" />
<view class="quantity-btn plus" @click="increaseQuantity">
<text class="quantity-btn-text">+</text>
@@ -97,7 +97,7 @@
<view class="section-title">商品详情</view>
<text class="description-text">{{ product.description ?? '暂无详细描述' }}</text>
<!-- 商品详情图片 -->
<view class="detail-images" v-if="product.images && product.images.length > 0">
<view class="detail-images" v-if="product.images != null && product.images.length > 0">
<image v-for="(img, index) in product.images"
:key="index"
:src="img"
@@ -187,9 +187,9 @@
<text class="params-label">批准文号</text>
<text class="params-value">{{ product.approval_number }}</text>
</view>
<view class="params-item" v-if="product.tags && product.tags.length > 0">
<view class="params-item" v-if="product.tags != null && product.tags.length > 0">
<text class="params-label">标签</text>
<text class="params-value">{{ product.tags.join(', ') }}</text>
<text class="params-value">{{ product.tags!.join(', ') }}</text>
</view>
</scroll-view>
</view>
@@ -226,7 +226,7 @@
</template>
<script>
import { ProductType, MerchantType, ProductSkuType } from '@/types/mall-types.uts'
import { ProductType, MerchantType, ProductSkuType, CouponTemplateType } from '@/types/mall-types.uts'
import { supabaseService } from '@/utils/supabaseService.uts'
export default {
@@ -265,40 +265,49 @@ export default {
showSpec: false,
selectedSkuId: '',
selectedSpec: '',
quantity: 1,
quantity: 1 as number,
isFavorite: false,
showParams: false,
// 新增: 优惠券相关
coupons: [] as any[],
coupons: [] as CouponTemplateType[],
showCoupons: false
}
},
onLoad(options: any) {
const productId = (options['productId'] ?? options['id']) as string
const productPrice = options.price ? parseFloat(options.price) : null
const productOriginalPrice = options.originalPrice ? parseFloat(options.originalPrice) : null
const opts = options as UTSJSONObject
const productId = (opts.getString('productId') ?? opts.getString('id')) ?? ''
const priceStr = opts.getString('price')
const productPrice = priceStr != null ? parseFloat(priceStr) : null
const originalPriceStr = opts.getString('originalPrice')
const productOriginalPrice = originalPriceStr != null ? parseFloat(originalPriceStr) : null
// 处理商品名称:如果是编码的则解码,否则直接使用
let productName = options.name as string
if (productName) {
let productName = opts.getString('name') ?? ''
if (productName != '') {
try {
// 尝试解码如果失败不是有效的URI组件则使用原值
productName = decodeURIComponent(productName)
const decoded = decodeURIComponent(productName)
if (decoded != null) {
productName = decoded
}
} catch (e) {
console.warn('ProductName decode failed, using original:', productName)
}
}
let productImage = options.image as string
if (productImage) {
let productImage = opts.getString('image') ?? ''
if (productImage != '') {
try {
productImage = decodeURIComponent(productImage)
const decoded = decodeURIComponent(productImage)
if (decoded != null) {
productImage = decoded
}
} catch (e) {
console.warn('ProductImage decode failed, using original:', productImage)
}
}
if (productId) {
if (productId != '') {
this.loadProductDetail(productId, {
price: productPrice,
originalPrice: productOriginalPrice,
@@ -309,7 +318,7 @@ export default {
this.saveFootprint(productId)
// 设置导航栏标题为商品名称
if (productName) {
if (productName != '') {
uni.setNavigationBarTitle({
title: productName
})
@@ -318,9 +327,9 @@ export default {
},
computed: {
displayPrice(): number {
if (this.selectedSkuId) {
if (this.selectedSkuId != '') {
const sku = this.productSkus.find(s => s.id === this.selectedSkuId)
if (sku) return sku.price
if (sku != null) return sku.price
}
return this.product.price
}
@@ -334,10 +343,10 @@ export default {
}
})
const footprintData = uni.getStorageSync('footprints')
const footprintData = uni.getStorageSync('footprints') as string
let footprints: any[] = []
if (footprintData) {
if (footprintData != null && footprintData !== '') {
try {
footprints = JSON.parse(footprintData as string) as any[]
} catch (e) {
@@ -346,7 +355,12 @@ export default {
}
// 移除已存在的相同商品(为了将其移到最新位置)
footprints = footprints.filter(item => item.id !== productId)
const productIdStr = productId
footprints = footprints.filter(function(item: any): boolean {
const itemObj = item as UTSJSONObject
const itemId = itemObj.getString('id') ?? ''
return itemId != productIdStr
})
// 添加到头部
footprints.unshift({
@@ -373,54 +387,62 @@ export default {
try {
const dbProductResponse = await supabaseService.getProductById(productId)
let dbProduct: any | null = null
if (Array.isArray(dbProductResponse) && dbProductResponse.length > 0) {
dbProduct = dbProductResponse[0]
} else if (dbProductResponse && !Array.isArray(dbProductResponse)) {
if (Array.isArray(dbProductResponse) && (dbProductResponse as any[]).length! > 0) {
dbProduct = (dbProductResponse as any[])[0]
} else if (dbProductResponse != null) {
dbProduct = dbProductResponse
}
if (dbProduct) {
if (dbProduct != null) {
// Map DB product to local product
const dbObj = dbProduct as UTSJSONObject
this.product = {
id: dbProduct.id,
merchant_id: dbProduct.merchant_id ?? dbProduct.shop_id ?? '',
category_id: dbProduct.category_id ?? '',
name: dbProduct.name,
description: dbProduct.description ?? '',
id: dbObj.getString('id') ?? '',
merchant_id: dbObj.getString('merchant_id') ?? dbObj.getString('shop_id') ?? '',
category_id: dbObj.getString('category_id') ?? '',
name: dbObj.getString('name') ?? '',
description: dbObj.getString('description') ?? '',
images: [] as string[],
price: dbProduct.base_price ?? dbProduct.price ?? 0,
original_price: dbProduct.market_price ?? dbProduct.original_price ?? 0,
stock: dbProduct.available_stock ?? dbProduct.total_stock ?? dbProduct.stock ?? 0,
sales: dbProduct.sale_count ?? dbProduct.sales ?? 0,
status: dbProduct.status !== undefined ? dbProduct.status : 1,
created_at: dbProduct.created_at ?? new Date().toISOString(),
price: dbObj.getNumber('base_price') ?? dbObj.getNumber('price') ?? 0,
original_price: dbObj.getNumber('market_price') ?? dbObj.getNumber('original_price') ?? 0,
stock: dbObj.getNumber('available_stock') ?? dbObj.getNumber('total_stock') ?? dbObj.getNumber('stock') ?? 0,
sales: dbObj.getNumber('sale_count') ?? dbObj.getNumber('sales') ?? 0,
status: dbObj.getNumber('status') ?? 1,
created_at: dbObj.getString('created_at') ?? new Date().toISOString(),
// Attributes
specification: dbProduct.specification ?? null,
usage: dbProduct.usage ?? null,
side_effects: dbProduct.side_effects ?? null,
precautions: dbProduct.precautions ?? null,
expiry_date: dbProduct.expiry_date ?? null,
storage_conditions: dbProduct.storage_conditions ?? null,
approval_number: dbProduct.approval_number ?? null,
specification: dbObj.getString('specification') ?? null,
usage: dbObj.getString('usage') ?? null,
side_effects: dbObj.getString('side_effects') ?? null,
precautions: dbObj.getString('precautions') ?? null,
expiry_date: dbObj.getString('expiry_date') ?? null,
storage_conditions: dbObj.getString('storage_conditions') ?? null,
approval_number: dbObj.getString('approval_number') ?? null,
tags: [] as string[]
} as ProductType
// Handle Images
if (dbProduct.image_urls) {
const imageUrls = dbObj.get('image_urls')
if (imageUrls != null) {
try {
const parsed = typeof dbProduct.image_urls === 'string' ? JSON.parse(dbProduct.image_urls) : dbProduct.image_urls
const parsed = typeof imageUrls === 'string' ? JSON.parse(imageUrls as string) : imageUrls
if (Array.isArray(parsed)) {
this.product.images = parsed.map((i: any) => String(i))
this.product.images = parsed.map((i: any): string => '' + i)
}
} catch (e) { console.error('Error parsing image_urls', e) }
}
// Fallback to main_image_url if no images found
if (this.product.images.length === 0 && dbProduct.main_image_url) {
this.product.images.push(dbProduct.main_image_url)
if (this.product.images.length === 0) {
const mainImg = dbObj.getString('main_image_url')
if (mainImg != null && mainImg != '') {
this.product.images.push(mainImg)
}
}
// Fallback to 'image' field (legacy)
if (this.product.images.length === 0 && dbProduct.image) {
this.product.images.push(dbProduct.image)
if (this.product.images.length === 0) {
const legacyImg = dbObj.getString('image')
if (legacyImg != null && legacyImg != '') {
this.product.images.push(legacyImg)
}
}
// Final fallback
if (this.product.images.length === 0) {
@@ -428,24 +450,28 @@ export default {
}
// Handle Tags
if (dbProduct.tags) {
const tagsVal = dbObj.get('tags')
if (tagsVal != null) {
try {
const parsedTags = typeof dbProduct.tags === 'string' ? JSON.parse(dbProduct.tags) : dbProduct.tags
const parsedTags = typeof tagsVal === 'string' ? JSON.parse(tagsVal as string) : tagsVal
if (Array.isArray(parsedTags)) {
this.product.tags = parsedTags.map((t: any) => String(t))
this.product.tags = parsedTags.map((t: any): string => '' + t)
}
} catch (e) {}
}
// Handle JSON attributes if present
if (dbProduct.attributes && typeof dbProduct.attributes === 'string') {
const attrsVal = dbObj.get('attributes')
if (attrsVal != null && typeof attrsVal === 'string') {
try {
const attrs = JSON.parse(dbProduct.attributes)
if (attrs) {
// Merge attributes into product if they match keys
if (attrs.specification) this.product.specification = attrs.specification
if (attrs.usage) this.product.usage = attrs.usage
// ... augment as needed
const attrsObj = JSON.parse(attrsVal as string) as UTSJSONObject
const specVal = attrsObj.getString('specification')
if (specVal != null) {
this.product.specification = specVal
}
const usageVal = attrsObj.getString('usage')
if (usageVal != null) {
this.product.usage = usageVal
}
} catch(e) {}
}
@@ -456,18 +482,22 @@ export default {
console.error('Failed to load product detail:', e)
// Fallback to options if available
this.product.id = productId
this.product.name = options.name ? decodeURIComponent(options.name) : '未知商品'
this.product.price = options.price ? parseFloat(options.price) : 0
this.product.images = options.image ? [decodeURIComponent(options.image)] : ['/static/default-product.png']
const optsFallback = options as UTSJSONObject
const nameVal = optsFallback.getString('name')
this.product.name = (nameVal != null && nameVal != '') ? decodeURIComponent(nameVal) : '未知商品'
const priceVal = optsFallback.getString('price')
this.product.price = (priceVal != null && priceVal != '') ? parseFloat(priceVal) : 0
const imageVal = optsFallback.getString('image')
this.product.images = (imageVal != null && imageVal != '') ? [decodeURIComponent(imageVal)] : ['/static/default-product.png']
}
// Load Merchant and SKUs
if (this.product.merchant_id) {
if (this.product.merchant_id != '') {
await this.loadMerchantInfo(this.product.merchant_id)
// 加载优惠券
this.loadCoupons()
}
if (this.product.id) {
if (this.product.id != '') {
this.loadProductSkus(this.product.id)
}
@@ -647,15 +677,20 @@ export default {
},
getSkuSpecText(sku: ProductSkuType): string {
if (sku.specifications) {
const specs: any = sku.specifications
return Object.keys(specs).map(key => `${key}: ${specs[key]}`).join(', ')
if (sku.specifications != null) {
const specs = sku.specifications as Record<string, any>
const parts: string[] = []
// 使用 for-in 循环代替 Object.keys
for (const key in specs) {
parts.push(`${key}: ${specs[key]}`)
}
return parts.join(', ')
}
return sku.sku_code
return sku.sku_code ?? ''
},
async addToCart() {
if (this.productSkus.length > 0 && !this.selectedSkuId) {
if (this.productSkus.length > 0 && this.selectedSkuId == '') {
uni.showToast({
title: '请选择规格',
icon: 'none'
@@ -669,7 +704,8 @@ export default {
const success = await supabaseService.addToCart(
this.product.id,
this.quantity,
this.selectedSkuId
this.selectedSkuId,
this.product.merchant_id
)
uni.hideLoading()
@@ -687,7 +723,7 @@ export default {
},
buyNow() {
if (this.productSkus.length > 0 && !this.selectedSkuId) {
if (this.productSkus.length > 0 && this.selectedSkuId == '') {
uni.showToast({
title: '请选择规格',
icon: 'none'
@@ -695,16 +731,16 @@ export default {
return
}
const sku = this.selectedSkuId ? this.productSkus.find(s => s.id === this.selectedSkuId) : null
const sku = this.selectedSkuId != '' ? this.productSkus.find(s => s.id === this.selectedSkuId) : null
const selectedItem = {
id: this.selectedSkuId,
product_id: this.product.id,
sku_id: this.selectedSkuId,
product_name: this.product.name,
product_image: (sku && sku.image_url) ? sku.image_url : this.product.images[0],
sku_specifications: sku ? sku.specifications : {},
price: Number(parseFloat((sku ? sku.price : this.product.price).toString()).toFixed(2)),
product_image: (sku != null && sku.image_url != '') ? sku.image_url : this.product.images[0],
sku_specifications: sku != null ? sku.specifications : {},
price: Number(parseFloat((sku != null ? sku.price : this.product.price).toString()).toFixed(2)),
quantity: Number(this.quantity)
}
@@ -761,7 +797,7 @@ export default {
},
goToShop() {
if (this.merchant.user_id) {
if (this.merchant.user_id != '') {
uni.navigateTo({
url: `/pages/mall/consumer/shop-detail?merchantId=${this.merchant.id}`
})
@@ -788,7 +824,7 @@ export default {
},
validateQuantity() {
let num = parseInt(this.quantity)
let num = Number(this.quantity)
if (isNaN(num)) num = 1
const maxQuantity = this.getMaxQuantity()
if (num < 1) num = 1
@@ -799,10 +835,10 @@ export default {
this.quantity = num
},
getMaxQuantity() {
if (this.selectedSkuId) {
getMaxQuantity(): number {
if (this.selectedSkuId != '') {
const sku = this.productSkus.find(s => s.id === this.selectedSkuId)
if (sku) return sku.stock
if (sku != null) return sku.stock
}
return this.product.stock
},