1770 lines
42 KiB
Plaintext
1770 lines
42 KiB
Plaintext
<!-- 我的收藏 -->
|
||
<template>
|
||
<view class="favorites-page">
|
||
<view class="favorites-nav">
|
||
<view class="nav-left" @click="handleBack">
|
||
<text class="back-icon">‹</text>
|
||
</view>
|
||
|
||
<view v-if="!isSearchMode" class="nav-center">
|
||
<text class="nav-title">我的收藏</text>
|
||
</view>
|
||
<view v-else class="nav-search-panel">
|
||
<view class="search-input-wrap">
|
||
<view class="search-icon">
|
||
<view class="search-icon-circle"></view>
|
||
<view class="search-icon-handle"></view>
|
||
</view>
|
||
<input class="search-input" :value="searchKeyword" placeholder="搜索收藏的商品、服务或店铺" @input="onSearchInput" />
|
||
<text v-if="searchKeyword != ''" class="search-clear" @click="clearSearchKeyword">×</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="nav-right">
|
||
<text v-if="isSearchMode" class="nav-action-text" @click="exitSearchMode">取消</text>
|
||
<text v-else-if="isEditMode" class="nav-action-text" @click="toggleManageMode">完成</text>
|
||
<view v-else class="nav-actions-normal">
|
||
<view class="nav-search-trigger" @click="enterSearchMode">
|
||
<view class="search-icon nav-search-icon-small">
|
||
<view class="search-icon-circle"></view>
|
||
<view class="search-icon-handle"></view>
|
||
</view>
|
||
</view>
|
||
<text :class="['nav-action-text', { 'nav-action-disabled': totalItemsCount == 0 }]" @click="toggleManageMode">管理</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<scroll-view :class="favoritesContentClass" direction="vertical" scroll-y="true">
|
||
<view v-if="isLoading" class="state-box">
|
||
<text class="state-title">加载中...</text>
|
||
</view>
|
||
|
||
<view v-else-if="totalItemsCount == 0" class="state-box empty-box">
|
||
<text class="state-icon">♡</text>
|
||
<text class="state-title">暂无收藏内容</text>
|
||
<text class="state-subtitle">快去收藏感兴趣的商品或服务吧</text>
|
||
<button class="go-shopping-btn" @click="goShopping">去逛逛</button>
|
||
</view>
|
||
|
||
<view v-else-if="visibleItemsCount == 0" class="state-box empty-box">
|
||
<text class="state-icon">⌕</text>
|
||
<text class="state-title">未找到相关收藏</text>
|
||
<text class="state-subtitle">换个关键词再试试</text>
|
||
</view>
|
||
|
||
<view v-else class="group-list">
|
||
<view v-for="group in filteredGroups" :key="group.group_id" class="group-section">
|
||
<view class="group-header">
|
||
<view v-if="group.group_logo_url != ''" class="group-logo-wrap">
|
||
<image class="group-logo" :src="group.group_logo_url" mode="aspectFill" />
|
||
</view>
|
||
<view v-else class="group-logo-fallback">
|
||
<text class="group-logo-text">{{ getGroupInitial(group.group_name) }}</text>
|
||
</view>
|
||
<view class="group-info">
|
||
<text class="group-name">{{ group.group_name }}</text>
|
||
<view v-if="group.group_tags.length > 0" class="group-tag-row">
|
||
<text v-for="(groupTag, groupTagIndex) in group.group_tags" :key="group.group_id + '-tag-' + groupTagIndex" class="group-tag">{{ groupTag }}</text>
|
||
</view>
|
||
</view>
|
||
<text class="group-arrow">›</text>
|
||
</view>
|
||
|
||
<view v-for="(item, itemIndex) in group.items" :key="item.favorite_id" :class="['favorite-row', { 'favorite-row-last': itemIndex == group.items.length - 1 }]" @click="handleItemClick(item)">
|
||
<view class="selector-column">
|
||
<view v-if="isEditMode" :class="['select-icon', { selected: item.manage_selected == true }]" @click.stop="toggleManageSelect(item)">
|
||
<text v-if="item.manage_selected == true" class="icon-text">✓</text>
|
||
</view>
|
||
<view v-else-if="item.target_type == 'product'" :class="['select-icon', { selected: item.purchase_selected == true, disabled: item.can_purchase == false }]" @click.stop="togglePurchaseSelect(item)">
|
||
<text v-if="item.purchase_selected == true" class="icon-text">✓</text>
|
||
</view>
|
||
<view v-else class="selector-placeholder"></view>
|
||
</view>
|
||
|
||
<image class="favorite-image" :src="item.image_url" mode="aspectFill" @error="handleImageError(item)" @click.stop="openFavorite(item, false)" />
|
||
|
||
<view class="favorite-main" @click.stop="openFavorite(item, false)">
|
||
<text class="favorite-title" :lines="2">{{ item.title }}</text>
|
||
|
||
<view v-if="item.display_tags.length > 0" class="tag-row">
|
||
<text v-for="(tag, tagIndex) in item.display_tags" :key="item.favorite_id + '-tag-' + tagIndex" class="tag-chip">{{ tag }}</text>
|
||
</view>
|
||
|
||
<text v-if="item.description_text != ''" class="favorite-desc" :lines="2">{{ item.description_text }}</text>
|
||
<text v-if="item.availability_status != ''" class="favorite-status">{{ item.availability_status }}</text>
|
||
|
||
<view v-if="!isEditMode && item.target_type == 'product'" class="spec-row">
|
||
<text class="spec-label">{{ getSelectedSkuSummary(item) }}</text>
|
||
<text v-if="item.has_skus" class="spec-action" @click.stop="openSkuPopup(item)">{{ item.selected_sku_id == '' ? '选规格' : '重选' }}</text>
|
||
</view>
|
||
|
||
<view class="favorite-footer">
|
||
<view class="price-wrap">
|
||
<text class="current-price">¥{{ formatPrice(getDisplayPrice(item)) }}</text>
|
||
<text v-if="item.has_original_price" class="original-price">¥{{ formatPrice(item.original_price) }}</text>
|
||
</view>
|
||
|
||
<view v-if="!isEditMode && item.target_type == 'product'" class="purchase-actions">
|
||
<text class="quantity-tip">{{ getQuantityHint(item) }}</text>
|
||
<view class="quantity-stepper">
|
||
<view :class="['step-btn', { disabled: canDecreaseQuantity(item) == false }]" @click.stop="decreaseItemQuantity(item)">
|
||
<text class="step-text">-</text>
|
||
</view>
|
||
<text class="quantity-value">{{ item.quantity }}</text>
|
||
<view :class="['step-btn', { disabled: canIncreaseQuantity(item) == false }]" @click.stop="increaseItemQuantity(item)">
|
||
<text class="step-text">+</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<button v-else-if="!isEditMode && item.target_type == 'service' && item.can_appoint == true" class="service-action-btn" @click.stop="openFavorite(item, true)">去预约</button>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="!isEditMode" class="more-column" @click.stop="openMoreActions(item)">
|
||
<view class="more-dot"></view>
|
||
<view class="more-dot"></view>
|
||
<view class="more-dot"></view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<view v-if="totalItemsCount > 0 && !isEditMode" class="purchase-bar">
|
||
<view class="bottom-left" @click="togglePurchaseSelectAll">
|
||
<view :class="['select-icon', { selected: isPurchaseAllSelected }]">
|
||
<text v-if="isPurchaseAllSelected" class="icon-text">✓</text>
|
||
</view>
|
||
<text class="bottom-text">全选</text>
|
||
</view>
|
||
<view class="purchase-total-box">
|
||
<text class="purchase-count-text">已选 {{ selectedPurchaseCount }} 件</text>
|
||
<text class="purchase-total-text">合计:<text class="purchase-total-amount">¥{{ formatAmount(selectedPurchaseTotalAmount) }}</text></text>
|
||
</view>
|
||
<button :class="['checkout-btn', { 'checkout-btn-disabled': selectedPurchaseItems.length == 0 }]" @click="goToCheckout">去结算</button>
|
||
</view>
|
||
|
||
<view v-if="isEditMode && totalItemsCount > 0" class="manage-bar">
|
||
<view class="bottom-left" @click="toggleManageSelectAll">
|
||
<view :class="['select-icon', { selected: isManageAllSelected }]">
|
||
<text v-if="isManageAllSelected" class="icon-text">✓</text>
|
||
</view>
|
||
<text class="bottom-text">全选</text>
|
||
</view>
|
||
<button :class="['delete-btn', { 'delete-btn-disabled': selectedManageCount == 0 }]" @click="deleteSelected">删除{{ selectedManageCount > 0 ? '(' + selectedManageCount + ')' : '' }}</button>
|
||
</view>
|
||
|
||
<page-container
|
||
v-if="skuPopupVisible"
|
||
:show="skuPopupVisible"
|
||
position="bottom"
|
||
:round="true"
|
||
:overlay="true"
|
||
:close-on-slide-down="true"
|
||
@clickoverlay="closeSkuPopup"
|
||
@afterleave="onSkuPopupAfterLeave"
|
||
>
|
||
<view class="sku-popup">
|
||
<view class="sku-popup-header">
|
||
<image class="sku-popup-image" :src="getPopupImage()" mode="aspectFill" />
|
||
<view class="sku-popup-summary">
|
||
<text class="sku-popup-price">¥{{ formatPrice(getPopupPrice()) }}</text>
|
||
<view v-if="activePurchaseItem != null && activePurchaseItem.display_tags.length > 0" class="tag-row popup-tag-row">
|
||
<text v-for="(tag, tagIndex) in activePurchaseItem.display_tags" :key="activePurchaseItem.favorite_id + '-popup-tag-' + tagIndex" class="tag-chip">{{ tag }}</text>
|
||
</view>
|
||
<text class="sku-popup-selected">已选择:{{ getPopupSelectedSummary() }}</text>
|
||
</view>
|
||
<text class="sku-popup-close" @click="closeSkuPopup">×</text>
|
||
</view>
|
||
|
||
<view v-if="skuPopupLoading" class="sku-popup-state">
|
||
<text class="sku-popup-state-text">规格加载中...</text>
|
||
</view>
|
||
|
||
<view v-else-if="skuPopupErrorMessage != ''" class="sku-popup-state">
|
||
<text class="sku-popup-state-text">{{ skuPopupErrorMessage }}</text>
|
||
<text class="sku-popup-retry" @click="retryPopupSkuLoad">重试</text>
|
||
</view>
|
||
|
||
<scroll-view v-else class="sku-popup-scroll" scroll-y="true">
|
||
<view class="sku-section">
|
||
<text class="sku-section-title">规格</text>
|
||
<view class="sku-chip-row">
|
||
<view v-for="sku in popupSkuList" :key="sku.sku_id" :class="getPopupSkuClass(sku)" @click="selectPopupSku(sku)">
|
||
<text :class="['sku-chip-text', { 'sku-chip-text-disabled': popupSkuDisabled(sku) }]">{{ sku.spec_text }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="sku-section quantity-section">
|
||
<text class="sku-section-title">数量</text>
|
||
<view class="popup-stepper">
|
||
<view :class="['step-btn', { disabled: popupQuantity <= 1 }]" @click="decreasePopupQuantity">
|
||
<text class="step-text">-</text>
|
||
</view>
|
||
<text class="popup-quantity-value">{{ popupQuantity }}</text>
|
||
<view :class="['step-btn', { disabled: canIncreasePopupQuantity() == false }]" @click="increasePopupQuantity">
|
||
<text class="step-text">+</text>
|
||
</view>
|
||
</view>
|
||
<text class="popup-stock-text">库存 {{ getPopupStock() }} 件</text>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<view class="sku-popup-footer">
|
||
<button :class="['sku-confirm-btn', { 'sku-confirm-btn-disabled': canConfirmPopupSelection() == false }]" @click="confirmPopupSelection">确定</button>
|
||
</view>
|
||
</view>
|
||
</page-container>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import { computed, onMounted, ref } from 'vue'
|
||
import { supabaseService } from '@/utils/supabaseService.uts'
|
||
import type { FavoriteGroup, FavoritePurchaseItem, FavoriteSkuItem, ProductSku } from '@/utils/supabaseService.uts'
|
||
import { goToLogin } from '@/utils/utils.uts'
|
||
|
||
const groups = ref<Array<FavoriteGroup>>([])
|
||
const isEditMode = ref<boolean>(false)
|
||
const isLoading = ref<boolean>(false)
|
||
const isSearchMode = ref<boolean>(false)
|
||
const searchKeyword = ref<string>('')
|
||
|
||
const skuPopupVisible = ref<boolean>(false)
|
||
const skuPopupLoading = ref<boolean>(false)
|
||
const skuPopupErrorMessage = ref<string>('')
|
||
const activePurchaseItem = ref<FavoritePurchaseItem | null>(null)
|
||
const popupSkuList = ref<Array<FavoriteSkuItem>>([])
|
||
const popupSelectedSkuId = ref<string>('')
|
||
const popupQuantity = ref<number>(1)
|
||
|
||
const totalItemsCount = computed((): number => {
|
||
let total = 0
|
||
for (let i = 0; i < groups.value.length; i++) {
|
||
total += groups.value[i].items.length
|
||
}
|
||
return total
|
||
})
|
||
|
||
const filteredGroups = computed((): Array<FavoriteGroup> => {
|
||
const keyword = searchKeyword.value.trim().toLowerCase()
|
||
if (keyword == '') {
|
||
return groups.value
|
||
}
|
||
|
||
const result = [] as Array<FavoriteGroup>
|
||
for (let i = 0; i < groups.value.length; i++) {
|
||
const group = groups.value[i]
|
||
const matchedItems = [] as Array<FavoritePurchaseItem>
|
||
for (let j = 0; j < group.items.length; j++) {
|
||
const item = group.items[j]
|
||
const titleMatch = item.title.toLowerCase().indexOf(keyword) >= 0
|
||
const merchantMatch = item.merchant_name.toLowerCase().indexOf(keyword) >= 0
|
||
const descMatch = item.description_text.toLowerCase().indexOf(keyword) >= 0
|
||
const skuMatch = item.selected_sku_text.toLowerCase().indexOf(keyword) >= 0
|
||
if (titleMatch || merchantMatch || descMatch || skuMatch) {
|
||
matchedItems.push(item)
|
||
}
|
||
}
|
||
if (matchedItems.length > 0) {
|
||
result.push({
|
||
group_id: group.group_id,
|
||
group_name: group.group_name,
|
||
group_logo_url: group.group_logo_url,
|
||
group_tags: group.group_tags,
|
||
items: matchedItems
|
||
} as FavoriteGroup)
|
||
}
|
||
}
|
||
return result
|
||
})
|
||
|
||
const visibleItemsCount = computed((): number => {
|
||
let total = 0
|
||
for (let i = 0; i < filteredGroups.value.length; i++) {
|
||
total += filteredGroups.value[i].items.length
|
||
}
|
||
return total
|
||
})
|
||
|
||
const purchasableProductItems = computed((): Array<FavoritePurchaseItem> => {
|
||
const result = [] as Array<FavoritePurchaseItem>
|
||
for (let i = 0; i < groups.value.length; i++) {
|
||
for (let j = 0; j < groups.value[i].items.length; j++) {
|
||
const item = groups.value[i].items[j]
|
||
if (item.target_type == 'product' && item.can_purchase == true) {
|
||
result.push(item)
|
||
}
|
||
}
|
||
}
|
||
return result
|
||
})
|
||
|
||
const readyPurchaseItems = computed((): Array<FavoritePurchaseItem> => {
|
||
const result = [] as Array<FavoritePurchaseItem>
|
||
for (let i = 0; i < purchasableProductItems.value.length; i++) {
|
||
const item = purchasableProductItems.value[i]
|
||
if (item.has_skus == false || item.selected_sku_id != '') {
|
||
result.push(item)
|
||
}
|
||
}
|
||
return result
|
||
})
|
||
|
||
const selectedPurchaseItems = computed((): Array<FavoritePurchaseItem> => {
|
||
const result = [] as Array<FavoritePurchaseItem>
|
||
for (let i = 0; i < readyPurchaseItems.value.length; i++) {
|
||
const item = readyPurchaseItems.value[i]
|
||
if (item.purchase_selected == true) {
|
||
result.push(item)
|
||
}
|
||
}
|
||
return result
|
||
})
|
||
|
||
const selectedPurchaseCount = computed((): number => {
|
||
let total = 0
|
||
for (let i = 0; i < selectedPurchaseItems.value.length; i++) {
|
||
total += selectedPurchaseItems.value[i].quantity
|
||
}
|
||
return total
|
||
})
|
||
|
||
const selectedPurchaseTotalAmount = computed((): number => {
|
||
let totalCents = 0
|
||
for (let i = 0; i < selectedPurchaseItems.value.length; i++) {
|
||
const item = selectedPurchaseItems.value[i]
|
||
totalCents += getItemUnitPriceCents(item) * item.quantity
|
||
}
|
||
return totalCents / 100
|
||
})
|
||
|
||
const isPurchaseAllSelected = computed((): boolean => {
|
||
return readyPurchaseItems.value.length > 0 && selectedPurchaseItems.value.length == readyPurchaseItems.value.length
|
||
})
|
||
|
||
const selectedManageItems = computed((): Array<FavoritePurchaseItem> => {
|
||
const result = [] as Array<FavoritePurchaseItem>
|
||
for (let i = 0; i < groups.value.length; i++) {
|
||
for (let j = 0; j < groups.value[i].items.length; j++) {
|
||
const item = groups.value[i].items[j]
|
||
if (item.manage_selected == true) {
|
||
result.push(item)
|
||
}
|
||
}
|
||
}
|
||
return result
|
||
})
|
||
|
||
const selectedManageCount = computed((): number => {
|
||
return selectedManageItems.value.length
|
||
})
|
||
|
||
const isManageAllSelected = computed((): boolean => {
|
||
return totalItemsCount.value > 0 && selectedManageCount.value == totalItemsCount.value
|
||
})
|
||
|
||
const favoritesContentClass = computed((): Array<string> => {
|
||
const classes = ['favorites-content'] as Array<string>
|
||
if (isEditMode.value) {
|
||
classes.push('favorites-content-manage')
|
||
} else if (totalItemsCount.value > 0) {
|
||
classes.push('favorites-content-purchase')
|
||
}
|
||
return classes
|
||
})
|
||
|
||
function toCents(value: number): number {
|
||
return Math.round(value * 100)
|
||
}
|
||
|
||
function formatPrice(value: number): string {
|
||
return value.toFixed(2)
|
||
}
|
||
|
||
function formatAmount(value: number): string {
|
||
return value.toFixed(2)
|
||
}
|
||
|
||
function getItemUnitPrice(item: FavoritePurchaseItem): number {
|
||
if (item.target_type != 'product') {
|
||
return 0
|
||
}
|
||
if (item.has_skus && item.selected_sku_id != '') {
|
||
return item.selected_sku_price
|
||
}
|
||
return item.current_price
|
||
}
|
||
|
||
function getItemUnitPriceCents(item: FavoritePurchaseItem): number {
|
||
return toCents(getItemUnitPrice(item))
|
||
}
|
||
|
||
function getDisplayPrice(item: FavoritePurchaseItem): number {
|
||
return getItemUnitPrice(item)
|
||
}
|
||
|
||
function getSelectedSkuSummary(item: FavoritePurchaseItem): string {
|
||
if (item.target_type != 'product') {
|
||
return ''
|
||
}
|
||
if (item.has_skus) {
|
||
return item.selected_sku_id != '' ? ('已选:' + item.selected_sku_text) : '请选择规格'
|
||
}
|
||
return '默认规格'
|
||
}
|
||
|
||
function getQuantityHint(item: FavoritePurchaseItem): string {
|
||
if (item.can_purchase == false) {
|
||
return item.availability_status != '' ? item.availability_status : '暂不可购买'
|
||
}
|
||
if (item.has_skus && item.selected_sku_id == '') {
|
||
return '选规格后可结算'
|
||
}
|
||
return '数量'
|
||
}
|
||
|
||
function clearManageSelections(): void {
|
||
for (let i = 0; i < groups.value.length; i++) {
|
||
for (let j = 0; j < groups.value[i].items.length; j++) {
|
||
groups.value[i].items[j].manage_selected = false
|
||
}
|
||
}
|
||
groups.value = [...groups.value]
|
||
}
|
||
|
||
async function loadFavoriteGroups(): Promise<void> {
|
||
isLoading.value = true
|
||
try {
|
||
const result = await supabaseService.getFavoriteGroups()
|
||
groups.value = result
|
||
clearManageSelections()
|
||
} catch (e) {
|
||
console.error('加载收藏列表失败', e)
|
||
groups.value = [] as Array<FavoriteGroup>
|
||
} finally {
|
||
isLoading.value = false
|
||
}
|
||
}
|
||
|
||
function handleBack(): void {
|
||
uni.navigateBack({
|
||
fail: () => {
|
||
uni.switchTab({ url: '/pages/main/index' })
|
||
}
|
||
})
|
||
}
|
||
|
||
function enterSearchMode(): void {
|
||
isEditMode.value = false
|
||
clearManageSelections()
|
||
isSearchMode.value = true
|
||
}
|
||
|
||
function exitSearchMode(): void {
|
||
isSearchMode.value = false
|
||
searchKeyword.value = ''
|
||
}
|
||
|
||
function clearSearchKeyword(): void {
|
||
searchKeyword.value = ''
|
||
}
|
||
|
||
function onSearchInput(e: any): void {
|
||
const detail = e.detail
|
||
if (detail != null && detail.value != null) {
|
||
searchKeyword.value = detail.value as string
|
||
}
|
||
}
|
||
|
||
function toggleManageMode(): void {
|
||
if (totalItemsCount.value == 0) {
|
||
return
|
||
}
|
||
if (isEditMode.value == false) {
|
||
isSearchMode.value = false
|
||
searchKeyword.value = ''
|
||
}
|
||
isEditMode.value = !isEditMode.value
|
||
clearManageSelections()
|
||
}
|
||
|
||
function handleItemClick(item: FavoritePurchaseItem): void {
|
||
if (isEditMode.value) {
|
||
toggleManageSelect(item)
|
||
return
|
||
}
|
||
openFavorite(item, false)
|
||
}
|
||
|
||
function toggleManageSelect(item: FavoritePurchaseItem): void {
|
||
item.manage_selected = item.manage_selected != true
|
||
groups.value = [...groups.value]
|
||
}
|
||
|
||
function toggleManageSelectAll(): void {
|
||
const nextState = !isManageAllSelected.value
|
||
for (let i = 0; i < groups.value.length; i++) {
|
||
for (let j = 0; j < groups.value[i].items.length; j++) {
|
||
groups.value[i].items[j].manage_selected = nextState
|
||
}
|
||
}
|
||
groups.value = [...groups.value]
|
||
}
|
||
|
||
function togglePurchaseSelect(item: FavoritePurchaseItem): void {
|
||
if (item.can_purchase == false) {
|
||
uni.showToast({ title: item.availability_status != '' ? item.availability_status : '当前商品不可购买', icon: 'none' })
|
||
return
|
||
}
|
||
if (item.has_skus && item.selected_sku_id == '') {
|
||
openSkuPopup(item)
|
||
return
|
||
}
|
||
item.purchase_selected = item.purchase_selected != true
|
||
groups.value = [...groups.value]
|
||
}
|
||
|
||
function togglePurchaseSelectAll(): void {
|
||
const nextState = !isPurchaseAllSelected.value
|
||
for (let i = 0; i < readyPurchaseItems.value.length; i++) {
|
||
readyPurchaseItems.value[i].purchase_selected = nextState
|
||
}
|
||
groups.value = [...groups.value]
|
||
}
|
||
|
||
function canDecreaseQuantity(item: FavoritePurchaseItem): boolean {
|
||
return isEditMode.value == false && item.target_type == 'product' && item.quantity > 1
|
||
}
|
||
|
||
function canIncreaseQuantity(item: FavoritePurchaseItem): boolean {
|
||
if (isEditMode.value || item.target_type != 'product' || item.can_purchase == false) {
|
||
return false
|
||
}
|
||
if (item.has_skus && item.selected_sku_id == '') {
|
||
return true
|
||
}
|
||
const maxStock = item.has_skus ? item.selected_sku_stock : item.selected_sku_stock
|
||
if (maxStock <= 0) {
|
||
return false
|
||
}
|
||
return item.quantity < maxStock
|
||
}
|
||
|
||
function decreaseItemQuantity(item: FavoritePurchaseItem): void {
|
||
if (item.quantity <= 1) {
|
||
return
|
||
}
|
||
item.quantity -= 1
|
||
item.purchase_selected = true
|
||
groups.value = [...groups.value]
|
||
}
|
||
|
||
function increaseItemQuantity(item: FavoritePurchaseItem): void {
|
||
if (item.can_purchase == false) {
|
||
uni.showToast({ title: item.availability_status != '' ? item.availability_status : '当前商品不可购买', icon: 'none' })
|
||
return
|
||
}
|
||
if (item.has_skus && item.selected_sku_id == '') {
|
||
openSkuPopup(item)
|
||
return
|
||
}
|
||
if (canIncreaseQuantity(item) == false) {
|
||
uni.showToast({ title: '库存不足', icon: 'none' })
|
||
return
|
||
}
|
||
item.quantity += 1
|
||
item.purchase_selected = true
|
||
groups.value = [...groups.value]
|
||
}
|
||
|
||
function collectSelectedFavoriteIds(): Array<string> {
|
||
const result = [] as Array<string>
|
||
for (let i = 0; i < selectedManageItems.value.length; i++) {
|
||
const favoriteId = selectedManageItems.value[i].favorite_id
|
||
if (favoriteId != '') {
|
||
result.push(favoriteId)
|
||
}
|
||
}
|
||
return result
|
||
}
|
||
|
||
async function executeDeleteFavorites(favoriteIds: Array<string>): Promise<void> {
|
||
if (favoriteIds.length == 0) {
|
||
uni.showToast({ title: '请选择要删除的内容', icon: 'none' })
|
||
return
|
||
}
|
||
uni.showLoading({ title: '删除中...' })
|
||
const success = await supabaseService.softDeleteFavorites(favoriteIds)
|
||
uni.hideLoading()
|
||
if (success == false) {
|
||
uni.showToast({ title: '删除失败,请稍后重试', icon: 'none' })
|
||
return
|
||
}
|
||
await loadFavoriteGroups()
|
||
if (totalItemsCount.value == 0) {
|
||
isEditMode.value = false
|
||
}
|
||
uni.showToast({ title: '删除成功', icon: 'success' })
|
||
}
|
||
|
||
function deleteSelected(): void {
|
||
const favoriteIds = collectSelectedFavoriteIds()
|
||
if (favoriteIds.length == 0) {
|
||
uni.showToast({ title: '请选择要删除的内容', icon: 'none' })
|
||
return
|
||
}
|
||
uni.showModal({
|
||
title: '删除收藏',
|
||
content: '确定删除选中的收藏内容吗?',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
executeDeleteFavorites(favoriteIds)
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
function deleteSingleFavorite(item: FavoritePurchaseItem): void {
|
||
if (item.favorite_id == '') {
|
||
uni.showToast({ title: '收藏记录异常', icon: 'none' })
|
||
return
|
||
}
|
||
uni.showModal({
|
||
title: '删除收藏',
|
||
content: '确定删除该收藏内容吗?',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
executeDeleteFavorites([item.favorite_id])
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
function openFavorite(item: FavoritePurchaseItem, bookingMode: boolean): void {
|
||
if (item.target_type == 'service') {
|
||
let url = '/pages/mall/consumer/home-service/service-detail?id=' + item.target_id
|
||
if (bookingMode) {
|
||
url += '&mode=booking'
|
||
}
|
||
uni.navigateTo({ url: url })
|
||
return
|
||
}
|
||
uni.navigateTo({ url: '/pages/mall/consumer/product-detail?id=' + item.target_id })
|
||
}
|
||
|
||
function normalizeSkuSpecText(rawValue: string): string {
|
||
if (rawValue == '') {
|
||
return '默认规格'
|
||
}
|
||
return rawValue
|
||
}
|
||
|
||
function convertProductSkuToFavoriteSku(productSku: ProductSku): FavoriteSkuItem {
|
||
let specText = ''
|
||
if (productSku.specifications != '') {
|
||
try {
|
||
const specs = JSON.parse(productSku.specifications) as UTSJSONObject
|
||
for (const key in specs) {
|
||
const value = specs[key]
|
||
if (value != null) {
|
||
specText += (specText == '' ? '' : ' | ') + value.toString()
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error('解析收藏页SKU规格失败', e)
|
||
}
|
||
}
|
||
if (specText == '') {
|
||
specText = productSku.sku_code != '' ? productSku.sku_code : '默认规格'
|
||
}
|
||
return {
|
||
sku_id: productSku.id,
|
||
product_id: productSku.product_id,
|
||
sku_name: specText,
|
||
spec_text: specText,
|
||
sku_image_url: productSku.image_url ?? '',
|
||
sale_price: productSku.price,
|
||
stock_quantity: productSku.stock ?? 0,
|
||
status: productSku.status ?? 1
|
||
} as FavoriteSkuItem
|
||
}
|
||
|
||
function resetSkuPopupState(): void {
|
||
skuPopupLoading.value = false
|
||
skuPopupErrorMessage.value = ''
|
||
activePurchaseItem.value = null
|
||
popupSkuList.value = [] as Array<FavoriteSkuItem>
|
||
popupSelectedSkuId.value = ''
|
||
popupQuantity.value = 1
|
||
}
|
||
|
||
async function loadPopupSkus(item: FavoritePurchaseItem): Promise<void> {
|
||
skuPopupLoading.value = true
|
||
skuPopupErrorMessage.value = ''
|
||
try {
|
||
let skuList = item.sku_list
|
||
if (skuList.length == 0) {
|
||
const productSkus = await supabaseService.getProductSkus(item.target_id)
|
||
const mapped = [] as Array<FavoriteSkuItem>
|
||
for (let i = 0; i < productSkus.length; i++) {
|
||
mapped.push(convertProductSkuToFavoriteSku(productSkus[i]))
|
||
}
|
||
skuList = mapped
|
||
item.sku_list = mapped
|
||
}
|
||
popupSkuList.value = skuList
|
||
if (popupSkuList.value.length == 0) {
|
||
skuPopupErrorMessage.value = '暂无可选规格'
|
||
return
|
||
}
|
||
popupSelectedSkuId.value = item.selected_sku_id
|
||
if (popupSelectedSkuId.value == '') {
|
||
for (let i = 0; i < popupSkuList.value.length; i++) {
|
||
const sku = popupSkuList.value[i]
|
||
if (popupSkuDisabled(sku) == false) {
|
||
popupSelectedSkuId.value = sku.sku_id
|
||
if (item.selected_sku_id != '' || popupSkuList.value.length == 1) {
|
||
break
|
||
}
|
||
popupSelectedSkuId.value = ''
|
||
break
|
||
}
|
||
}
|
||
}
|
||
popupQuantity.value = item.quantity > 0 ? item.quantity : 1
|
||
} catch (e) {
|
||
console.error('加载收藏页SKU失败', e)
|
||
skuPopupErrorMessage.value = '规格加载失败,请重试'
|
||
} finally {
|
||
skuPopupLoading.value = false
|
||
}
|
||
}
|
||
|
||
async function openSkuPopup(item: FavoritePurchaseItem): Promise<void> {
|
||
if (item.target_type != 'product') {
|
||
return
|
||
}
|
||
activePurchaseItem.value = item
|
||
popupSkuList.value = [] as Array<FavoriteSkuItem>
|
||
popupSelectedSkuId.value = ''
|
||
popupQuantity.value = item.quantity > 0 ? item.quantity : 1
|
||
skuPopupVisible.value = true
|
||
await loadPopupSkus(item)
|
||
}
|
||
|
||
function retryPopupSkuLoad(): void {
|
||
if (activePurchaseItem.value == null) {
|
||
return
|
||
}
|
||
loadPopupSkus(activePurchaseItem.value)
|
||
}
|
||
|
||
function closeSkuPopup(): void {
|
||
skuPopupVisible.value = false
|
||
}
|
||
|
||
function onSkuPopupAfterLeave(): void {
|
||
if (skuPopupVisible.value == false) {
|
||
resetSkuPopupState()
|
||
}
|
||
}
|
||
|
||
function popupSkuDisabled(sku: FavoriteSkuItem): boolean {
|
||
return sku.status != 1 || sku.stock_quantity <= 0
|
||
}
|
||
|
||
function getPopupSkuClass(sku: FavoriteSkuItem): Array<string> {
|
||
const classes = ['sku-chip'] as Array<string>
|
||
if (popupSelectedSkuId.value == sku.sku_id) {
|
||
classes.push('sku-chip-active')
|
||
}
|
||
if (popupSkuDisabled(sku)) {
|
||
classes.push('sku-chip-disabled')
|
||
}
|
||
return classes
|
||
}
|
||
|
||
function selectPopupSku(sku: FavoriteSkuItem): void {
|
||
if (popupSkuDisabled(sku)) {
|
||
uni.showToast({ title: '该规格暂不可购买', icon: 'none' })
|
||
return
|
||
}
|
||
popupSelectedSkuId.value = sku.sku_id
|
||
if (popupQuantity.value > sku.stock_quantity) {
|
||
popupQuantity.value = sku.stock_quantity > 0 ? sku.stock_quantity : 1
|
||
}
|
||
if (popupQuantity.value < 1) {
|
||
popupQuantity.value = 1
|
||
}
|
||
}
|
||
|
||
function getPopupSelectedSku(): FavoriteSkuItem | null {
|
||
for (let i = 0; i < popupSkuList.value.length; i++) {
|
||
if (popupSkuList.value[i].sku_id == popupSelectedSkuId.value) {
|
||
return popupSkuList.value[i]
|
||
}
|
||
}
|
||
return null
|
||
}
|
||
|
||
function getPopupPrice(): number {
|
||
const selectedSku = getPopupSelectedSku()
|
||
if (selectedSku != null) {
|
||
return selectedSku.sale_price
|
||
}
|
||
return activePurchaseItem.value != null ? activePurchaseItem.value.current_price : 0
|
||
}
|
||
|
||
function getPopupStock(): number {
|
||
const selectedSku = getPopupSelectedSku()
|
||
if (selectedSku != null) {
|
||
return selectedSku.stock_quantity
|
||
}
|
||
return 0
|
||
}
|
||
|
||
function getPopupImage(): string {
|
||
const selectedSku = getPopupSelectedSku()
|
||
if (selectedSku != null && selectedSku.sku_image_url != '') {
|
||
return selectedSku.sku_image_url
|
||
}
|
||
return activePurchaseItem.value != null ? activePurchaseItem.value.image_url : '/static/images/default.png'
|
||
}
|
||
|
||
function getPopupSelectedSummary(): string {
|
||
const selectedSku = getPopupSelectedSku()
|
||
if (selectedSku != null) {
|
||
return selectedSku.spec_text + ' × ' + popupQuantity.value.toString()
|
||
}
|
||
return '请选择规格'
|
||
}
|
||
|
||
function decreasePopupQuantity(): void {
|
||
if (popupQuantity.value > 1) {
|
||
popupQuantity.value -= 1
|
||
}
|
||
}
|
||
|
||
function canIncreasePopupQuantity(): boolean {
|
||
const selectedSku = getPopupSelectedSku()
|
||
if (selectedSku == null) {
|
||
return false
|
||
}
|
||
return popupQuantity.value < selectedSku.stock_quantity
|
||
}
|
||
|
||
function increasePopupQuantity(): void {
|
||
const selectedSku = getPopupSelectedSku()
|
||
if (selectedSku == null) {
|
||
uni.showToast({ title: '请先选择规格', icon: 'none' })
|
||
return
|
||
}
|
||
if (popupQuantity.value >= selectedSku.stock_quantity) {
|
||
uni.showToast({ title: '库存不足', icon: 'none' })
|
||
return
|
||
}
|
||
popupQuantity.value += 1
|
||
}
|
||
|
||
function canConfirmPopupSelection(): boolean {
|
||
return skuPopupLoading.value == false && skuPopupErrorMessage.value == '' && getPopupSelectedSku() != null
|
||
}
|
||
|
||
function confirmPopupSelection(): void {
|
||
const item = activePurchaseItem.value
|
||
const selectedSku = getPopupSelectedSku()
|
||
if (item == null || selectedSku == null) {
|
||
uni.showToast({ title: '请选择有效规格', icon: 'none' })
|
||
return
|
||
}
|
||
if (popupQuantity.value > selectedSku.stock_quantity) {
|
||
uni.showToast({ title: '库存不足', icon: 'none' })
|
||
return
|
||
}
|
||
item.selected_sku_id = selectedSku.sku_id
|
||
item.selected_sku_text = normalizeSkuSpecText(selectedSku.spec_text)
|
||
item.selected_sku_price = selectedSku.sale_price
|
||
item.selected_sku_stock = selectedSku.stock_quantity
|
||
item.quantity = popupQuantity.value
|
||
item.purchase_selected = true
|
||
groups.value = [...groups.value]
|
||
closeSkuPopup()
|
||
uni.showToast({ title: '已更新规格', icon: 'success' })
|
||
}
|
||
|
||
async function goToCheckout(): Promise<void> {
|
||
const userId = supabaseService.getCurrentUserId()
|
||
if (userId == null || userId == '') {
|
||
goToLogin('/pages/mall/consumer/favorites')
|
||
return
|
||
}
|
||
if (selectedPurchaseItems.value.length == 0) {
|
||
uni.showToast({ title: '请选择可结算商品', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
const checkoutItems = [] as any[]
|
||
for (let i = 0; i < selectedPurchaseItems.value.length; i++) {
|
||
const item = selectedPurchaseItems.value[i]
|
||
let skuSpecifications: any = {}
|
||
if (item.selected_sku_text != '') {
|
||
skuSpecifications = { spec: item.selected_sku_text } as any
|
||
}
|
||
checkoutItems.push({
|
||
id: item.selected_sku_id != '' ? item.selected_sku_id : item.target_id,
|
||
product_id: item.target_id,
|
||
sku_id: item.has_skus ? item.selected_sku_id : '',
|
||
product_name: item.title,
|
||
product_image: item.image_url,
|
||
sku_specifications: skuSpecifications,
|
||
price: formatPrice(getItemUnitPrice(item)),
|
||
quantity: item.quantity,
|
||
shop_id: item.merchant_id,
|
||
shop_name: item.merchant_name,
|
||
merchant_id: item.merchant_id
|
||
})
|
||
}
|
||
|
||
try {
|
||
uni.setStorageSync('checkout_type', 'buy_now')
|
||
uni.setStorageSync('checkout_items', JSON.stringify(checkoutItems))
|
||
} catch (e) {
|
||
console.error('收藏页写入结算数据失败', e)
|
||
uni.showToast({ title: '系统异常,请重试', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
uni.navigateTo({ url: '/pages/mall/consumer/checkout' })
|
||
}
|
||
|
||
function openMoreActions(item: FavoritePurchaseItem): void {
|
||
const actionKeys = [] as Array<string>
|
||
const itemList = [] as Array<string>
|
||
if (item.target_type == 'service') {
|
||
itemList.push('查看服务')
|
||
actionKeys.push('view')
|
||
if (item.can_appoint == true) {
|
||
itemList.push('去预约')
|
||
actionKeys.push('book')
|
||
}
|
||
} else {
|
||
itemList.push('查看商品')
|
||
actionKeys.push('view')
|
||
if (item.can_purchase == true) {
|
||
itemList.push(item.has_skus ? '选择规格' : '加入结算')
|
||
actionKeys.push(item.has_skus ? 'spec' : 'purchase')
|
||
}
|
||
}
|
||
itemList.push('删除收藏')
|
||
actionKeys.push('delete')
|
||
|
||
uni.showActionSheet({
|
||
itemList: itemList,
|
||
success: (res) => {
|
||
const actionKey = actionKeys[res.tapIndex]
|
||
if (actionKey == 'view') {
|
||
openFavorite(item, false)
|
||
return
|
||
}
|
||
if (actionKey == 'book') {
|
||
openFavorite(item, true)
|
||
return
|
||
}
|
||
if (actionKey == 'spec') {
|
||
openSkuPopup(item)
|
||
return
|
||
}
|
||
if (actionKey == 'purchase') {
|
||
item.purchase_selected = true
|
||
groups.value = [...groups.value]
|
||
return
|
||
}
|
||
if (actionKey == 'delete') {
|
||
deleteSingleFavorite(item)
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
function handleImageError(item: FavoritePurchaseItem): void {
|
||
if (item.image_url == '/static/images/default.png') {
|
||
return
|
||
}
|
||
item.image_url = '/static/images/default.png'
|
||
groups.value = [...groups.value]
|
||
}
|
||
|
||
function getGroupInitial(name: string): string {
|
||
if (name == '') {
|
||
return '收'
|
||
}
|
||
return name.substring(0, 1)
|
||
}
|
||
|
||
function goShopping(): void {
|
||
uni.switchTab({ url: '/pages/main/index' })
|
||
}
|
||
|
||
onMounted(() => {
|
||
loadFavoriteGroups()
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.favorites-page {
|
||
position: relative;
|
||
display: flex;
|
||
flex-direction: column;
|
||
flex: 1;
|
||
background-color: #f4f4f4;
|
||
}
|
||
|
||
.favorites-nav {
|
||
height: 48px;
|
||
padding: 0 10px;
|
||
background-color: #ffffff;
|
||
border-bottom: 1px solid #ededed;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
}
|
||
|
||
.nav-left {
|
||
width: 36px;
|
||
height: 36px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
}
|
||
|
||
.back-icon {
|
||
font-size: 26px;
|
||
color: #222222;
|
||
line-height: 1;
|
||
}
|
||
|
||
.nav-center {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.nav-title {
|
||
font-size: 17px;
|
||
font-weight: 600;
|
||
color: #222222;
|
||
}
|
||
|
||
.nav-search-panel {
|
||
flex: 1;
|
||
padding-right: 10px;
|
||
}
|
||
|
||
.search-input-wrap {
|
||
height: 34px;
|
||
border-radius: 17px;
|
||
background-color: #f3f4f6;
|
||
padding: 0 12px;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
}
|
||
|
||
.search-icon {
|
||
width: 16px;
|
||
height: 16px;
|
||
position: relative;
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.search-icon-circle {
|
||
width: 10px;
|
||
height: 10px;
|
||
border: 1.5px solid #666666;
|
||
border-radius: 6px;
|
||
position: absolute;
|
||
top: 1px;
|
||
left: 1px;
|
||
}
|
||
|
||
.search-icon-handle {
|
||
width: 7px;
|
||
height: 1.5px;
|
||
background-color: #666666;
|
||
position: absolute;
|
||
right: 0;
|
||
bottom: 1px;
|
||
transform: rotate(45deg);
|
||
transform-origin: center;
|
||
}
|
||
|
||
.search-input {
|
||
flex: 1;
|
||
height: 34px;
|
||
font-size: 14px;
|
||
color: #222222;
|
||
}
|
||
|
||
.search-clear {
|
||
font-size: 18px;
|
||
color: #999999;
|
||
line-height: 1;
|
||
}
|
||
|
||
.nav-right {
|
||
min-width: 72px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.nav-actions-normal {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.nav-search-trigger {
|
||
width: 28px;
|
||
height: 28px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.nav-search-icon-small {
|
||
margin-right: 0;
|
||
}
|
||
|
||
.nav-action-text {
|
||
font-size: 14px;
|
||
color: #222222;
|
||
}
|
||
|
||
.nav-action-disabled {
|
||
color: #b5b5b5;
|
||
}
|
||
|
||
.favorites-content {
|
||
flex: 1;
|
||
height: 0px;
|
||
padding: 0 0 calc(12px + env(safe-area-inset-bottom));
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.favorites-content-purchase {
|
||
padding-bottom: calc(74px + env(safe-area-inset-bottom));
|
||
}
|
||
|
||
.favorites-content-manage {
|
||
padding-bottom: calc(74px + env(safe-area-inset-bottom));
|
||
}
|
||
|
||
.state-box {
|
||
background-color: #ffffff;
|
||
margin-top: 12px;
|
||
padding: 52px 24px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.empty-box {
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.state-icon {
|
||
font-size: 28px;
|
||
color: #c3c3c3;
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
.state-title {
|
||
font-size: 16px;
|
||
color: #333333;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.state-subtitle {
|
||
font-size: 13px;
|
||
color: #999999;
|
||
margin-bottom: 20px;
|
||
text-align: center;
|
||
}
|
||
|
||
.go-shopping-btn {
|
||
background-color: #e1251b;
|
||
color: #ffffff;
|
||
border-radius: 18px;
|
||
padding: 10px 34px;
|
||
font-size: 14px;
|
||
border: none;
|
||
}
|
||
|
||
.group-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
padding-top: 8px;
|
||
}
|
||
|
||
.group-section {
|
||
background-color: #ffffff;
|
||
padding-left: 10px;
|
||
padding-right: 10px;
|
||
}
|
||
|
||
.group-header {
|
||
height: 42px;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
border-bottom: 1px solid #f2f2f2;
|
||
}
|
||
|
||
.group-logo-wrap,
|
||
.group-logo-fallback {
|
||
width: 26px;
|
||
height: 26px;
|
||
border-radius: 13px;
|
||
overflow: hidden;
|
||
margin-right: 8px;
|
||
background-color: #f3f4f6;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.group-logo {
|
||
width: 26px;
|
||
height: 26px;
|
||
}
|
||
|
||
.group-logo-text {
|
||
font-size: 12px;
|
||
color: #666666;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.group-info {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
min-width: 0;
|
||
}
|
||
|
||
.group-name {
|
||
font-size: 14px;
|
||
color: #222222;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.group-tag-row {
|
||
display: flex;
|
||
flex-direction: row;
|
||
gap: 4px;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.group-tag {
|
||
font-size: 10px;
|
||
color: #888888;
|
||
}
|
||
|
||
.group-arrow {
|
||
font-size: 16px;
|
||
color: #b5b5b5;
|
||
}
|
||
|
||
.favorite-row {
|
||
padding-top: 10px;
|
||
padding-bottom: 10px;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: stretch;
|
||
border-bottom: 1px solid #f4f4f4;
|
||
}
|
||
|
||
.favorite-row-last {
|
||
border-bottom-width: 0;
|
||
}
|
||
|
||
.selector-column {
|
||
width: 28px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
padding-top: 36px;
|
||
padding-right: 2px;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.selector-placeholder {
|
||
width: 20px;
|
||
height: 20px;
|
||
}
|
||
|
||
.select-icon {
|
||
width: 20px;
|
||
height: 20px;
|
||
border: 1px solid #d0d0d0;
|
||
border-radius: 10px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background-color: #ffffff;
|
||
}
|
||
|
||
.select-icon.selected {
|
||
background-color: #e1251b;
|
||
border-color: #e1251b;
|
||
}
|
||
|
||
.select-icon.disabled {
|
||
background-color: #f3f3f3;
|
||
border-color: #e0e0e0;
|
||
}
|
||
|
||
.icon-text {
|
||
font-size: 12px;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.favorite-image {
|
||
width: 96px;
|
||
height: 96px;
|
||
border-radius: 8px;
|
||
background-color: #f5f5f5;
|
||
margin-right: 10px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.favorite-main {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: space-between;
|
||
min-height: 96px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.favorite-title {
|
||
font-size: 14px;
|
||
line-height: 1.45;
|
||
color: #222222;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.tag-row {
|
||
display: flex;
|
||
flex-direction: row;
|
||
flex-wrap: wrap;
|
||
margin-bottom: 4px;
|
||
gap: 6px;
|
||
}
|
||
|
||
.tag-chip {
|
||
padding: 2px 6px;
|
||
border-radius: 10px;
|
||
background-color: #fff4f2;
|
||
color: #d94841;
|
||
font-size: 11px;
|
||
}
|
||
|
||
.favorite-desc {
|
||
font-size: 12px;
|
||
line-height: 1.45;
|
||
color: #777777;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.favorite-status {
|
||
font-size: 12px;
|
||
color: #999999;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.spec-row {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 6px;
|
||
gap: 8px;
|
||
}
|
||
|
||
.spec-label {
|
||
flex: 1;
|
||
font-size: 12px;
|
||
color: #999999;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.spec-action {
|
||
font-size: 12px;
|
||
color: #d94841;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.favorite-footer {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: flex-end;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
}
|
||
|
||
.price-wrap {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: baseline;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.current-price {
|
||
font-size: 17px;
|
||
font-weight: 700;
|
||
color: #e1251b;
|
||
margin-right: 6px;
|
||
}
|
||
|
||
.original-price {
|
||
font-size: 11px;
|
||
color: #b5b5b5;
|
||
text-decoration: line-through;
|
||
}
|
||
|
||
.purchase-actions {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-end;
|
||
gap: 4px;
|
||
}
|
||
|
||
.quantity-tip {
|
||
font-size: 11px;
|
||
color: #999999;
|
||
}
|
||
|
||
.quantity-stepper,
|
||
.popup-stepper {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.step-btn {
|
||
width: 22px;
|
||
height: 22px;
|
||
border-radius: 11px;
|
||
background-color: #f5f5f5;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.step-btn.disabled {
|
||
background-color: #f0f0f0;
|
||
opacity: 0.5;
|
||
}
|
||
|
||
.step-text {
|
||
font-size: 14px;
|
||
color: #333333;
|
||
line-height: 1;
|
||
}
|
||
|
||
.quantity-value,
|
||
.popup-quantity-value {
|
||
font-size: 13px;
|
||
color: #222222;
|
||
min-width: 18px;
|
||
text-align: center;
|
||
}
|
||
|
||
.service-action-btn {
|
||
min-width: 68px;
|
||
height: 30px;
|
||
padding: 0 14px;
|
||
border-radius: 15px;
|
||
border: none;
|
||
background-color: #fff2ef;
|
||
color: #d94841;
|
||
font-size: 12px;
|
||
line-height: 30px;
|
||
}
|
||
|
||
.more-column {
|
||
width: 24px;
|
||
margin-left: 8px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 3px;
|
||
}
|
||
|
||
.more-dot {
|
||
width: 4px;
|
||
height: 4px;
|
||
border-radius: 2px;
|
||
background-color: #a8a8a8;
|
||
}
|
||
|
||
.purchase-bar,
|
||
.manage-bar {
|
||
position: absolute;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
padding: 10px 14px calc(10px + env(safe-area-inset-bottom));
|
||
background-color: #ffffff;
|
||
border-top: 1px solid #ededed;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 10px;
|
||
}
|
||
|
||
.bottom-left {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.bottom-text {
|
||
font-size: 14px;
|
||
color: #222222;
|
||
margin-left: 8px;
|
||
}
|
||
|
||
.purchase-total-box {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
justify-content: center;
|
||
min-width: 0;
|
||
}
|
||
|
||
.purchase-count-text {
|
||
font-size: 12px;
|
||
color: #666666;
|
||
margin-bottom: 2px;
|
||
}
|
||
|
||
.purchase-total-text {
|
||
font-size: 13px;
|
||
color: #333333;
|
||
}
|
||
|
||
.purchase-total-amount {
|
||
color: #e1251b;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.checkout-btn,
|
||
.delete-btn,
|
||
.sku-confirm-btn {
|
||
min-width: 106px;
|
||
height: 38px;
|
||
padding: 0 18px;
|
||
border-radius: 19px;
|
||
background-color: #e1251b;
|
||
color: #ffffff;
|
||
font-size: 14px;
|
||
line-height: 38px;
|
||
border: none;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.checkout-btn-disabled,
|
||
.delete-btn-disabled,
|
||
.sku-confirm-btn-disabled {
|
||
background-color: #f3b9b5;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.sku-popup {
|
||
background-color: #ffffff;
|
||
border-top-left-radius: 18px;
|
||
border-top-right-radius: 18px;
|
||
max-height: 76vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.sku-popup-header {
|
||
padding: 16px 16px 12px;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: flex-start;
|
||
border-bottom: 1px solid #f1f1f1;
|
||
position: relative;
|
||
}
|
||
|
||
.sku-popup-image {
|
||
width: 88px;
|
||
height: 88px;
|
||
border-radius: 10px;
|
||
background-color: #f5f5f5;
|
||
margin-right: 12px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.sku-popup-summary {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding-top: 4px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.sku-popup-price {
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
color: #e1251b;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.popup-tag-row {
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.sku-popup-selected {
|
||
font-size: 12px;
|
||
line-height: 1.5;
|
||
color: #666666;
|
||
}
|
||
|
||
.sku-popup-close {
|
||
position: absolute;
|
||
top: 14px;
|
||
right: 16px;
|
||
font-size: 20px;
|
||
line-height: 1;
|
||
color: #999999;
|
||
}
|
||
|
||
.sku-popup-state {
|
||
padding: 30px 16px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.sku-popup-state-text {
|
||
font-size: 13px;
|
||
color: #666666;
|
||
}
|
||
|
||
.sku-popup-retry {
|
||
font-size: 13px;
|
||
color: #d94841;
|
||
}
|
||
|
||
.sku-popup-scroll {
|
||
flex: 1;
|
||
height: 0px;
|
||
padding: 0 16px;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.sku-section {
|
||
padding-top: 16px;
|
||
padding-bottom: 8px;
|
||
}
|
||
|
||
.quantity-section {
|
||
padding-bottom: 18px;
|
||
}
|
||
|
||
.sku-section-title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #222222;
|
||
margin-bottom: 12px;
|
||
display: block;
|
||
}
|
||
|
||
.sku-chip-row {
|
||
display: flex;
|
||
flex-direction: row;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
}
|
||
|
||
.sku-chip {
|
||
padding: 8px 12px;
|
||
border-radius: 18px;
|
||
background-color: #f5f5f5;
|
||
border: 1px solid #f5f5f5;
|
||
}
|
||
|
||
.sku-chip-active {
|
||
background-color: #fff4f2;
|
||
border-color: #f1b2ad;
|
||
}
|
||
|
||
.sku-chip-disabled {
|
||
background-color: #f2f2f2;
|
||
border-color: #f2f2f2;
|
||
}
|
||
|
||
.sku-chip-text {
|
||
font-size: 13px;
|
||
color: #333333;
|
||
}
|
||
|
||
.sku-chip-text-disabled {
|
||
color: #b5b5b5;
|
||
}
|
||
|
||
.popup-stock-text {
|
||
font-size: 12px;
|
||
color: #999999;
|
||
margin-top: 8px;
|
||
display: block;
|
||
}
|
||
|
||
.sku-popup-footer {
|
||
padding: 10px 16px calc(10px + env(safe-area-inset-bottom));
|
||
border-top: 1px solid #f1f1f1;
|
||
background-color: #ffffff;
|
||
}
|
||
|
||
.sku-confirm-btn {
|
||
width: 100%;
|
||
border-radius: 22px;
|
||
}
|
||
|
||
@media (min-width: 768px) {
|
||
.favorites-content {
|
||
max-width: 960px;
|
||
margin: 0 auto;
|
||
width: 100%;
|
||
}
|
||
|
||
.purchase-bar,
|
||
.manage-bar {
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
max-width: 960px;
|
||
}
|
||
}
|
||
</style> |