hookehuyr

feat(ui): 新增确认对话框组件并优化结账页面

- 新增 `ConfirmDialog` 组件,用于确认删除操作
- 在 `CheckoutPage` 中集成 `ConfirmDialog`,支持删除单个商品和清空购物车
- 优化 `FrostedGlass` 组件,支持自定义背景透明度和模糊级别
- 改进结账页面的表单验证和错误处理逻辑
<template>
<Teleport to="body">
<div
v-if="show"
class="fixed inset-0 z-50 flex items-center justify-center px-4"
>
<div
class="fixed inset-0 bg-black/50 backdrop-blur-sm"
@click="handleCancel"
></div>
<FrostedGlass class="w-full max-w-sm p-6 relative z-10" :bg-opacity="90" blur-level="md">
<h3 class="text-lg font-medium mb-2">{{ title }}</h3>
<p class="text-gray-600 text-sm mb-6">{{ message }}</p>
<div class="flex justify-end space-x-3">
<button
@click="handleCancel"
class="px-4 py-2 text-sm text-gray-600 hover:text-gray-800"
>
取消
</button>
<button
@click="handleConfirm"
class="px-4 py-2 text-sm text-white bg-red-500 hover:bg-red-600 rounded-lg"
>
确认
</button>
</div>
</FrostedGlass>
</div>
</Teleport>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue'
import FrostedGlass from './FrostedGlass.vue'
const props = defineProps({
show: {
type: Boolean,
default: false
},
title: {
type: String,
default: '确认'
},
message: {
type: String,
required: true
}
})
const emit = defineEmits(['confirm', 'cancel', 'update:show'])
const handleConfirm = () => {
emit('confirm')
emit('update:show', false)
}
const handleCancel = () => {
emit('cancel')
emit('update:show', false)
}
</script>
<template>
<div
:class="[
'bg-white/20 backdrop-blur-md rounded-xl border border-white/30 shadow-lg',
`bg-white/${bgOpacity} backdrop-blur-${blurLevel} rounded-xl border border-white/30 shadow-lg`,
className
]"
>
......@@ -16,6 +16,16 @@ defineProps({
className: {
type: String,
default: ''
},
bgOpacity: {
type: Number,
default: 80,
validator: (value) => value >= 0 && value <= 100
},
blurLevel: {
type: String,
default: 'sm',
validator: (value) => ['none', 'sm', 'md', 'lg', 'xl', '2xl', '3xl'].includes(value)
}
})
</script>
......
......@@ -51,12 +51,24 @@
@error="handleImageError"
/>
</div>
<div>
<div class="flex-1">
<p class="font-medium text-sm line-clamp-1">{{ item.title }}</p>
<p class="text-xs text-gray-500">
{{ item.type === 'course' ? '课程' : '活动' }} · {{ item.quantity }} 份
</p>
</div>
<button
@click="() => {
itemToDelete = item
showConfirmDialog = true
}"
class="text-red-500 hover:text-red-600 p-1 -mr-1"
title="删除"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
<div class="ml-2 text-right">
<p class="font-medium text-sm">{{ formatPrice(item.price * item.quantity) }}</p>
......@@ -204,7 +216,10 @@
</div>
<button
type="button"
@click="clearCart"
@click="() => {
showConfirmDialog = true
itemToDelete = null
}"
class="text-sm text-red-500"
>
清空购物车
......@@ -227,6 +242,26 @@
</form>
</div>
</div>
<!-- Confirm Dialog -->
<ConfirmDialog
v-model:show="showConfirmDialog"
title="确认删除"
:message="itemToDelete ? `确定要删除 ${itemToDelete.title || '此商品'} 吗?` : '确定要清空购物车吗?'"
@confirm="() => {
if (itemToDelete) {
if (cartItems.length === 1) {
clearCart()
} else {
removeFromCart(itemToDelete.id, itemToDelete.type)
}
itemToDelete = null
} else {
clearCart()
}
}"
@cancel="itemToDelete = null"
/>
</AppLayout>
</template>
......@@ -235,10 +270,11 @@ import { ref } from 'vue'
import { useRouter } from 'vue-router'
import AppLayout from '@/components/layout/AppLayout.vue'
import FrostedGlass from '@/components/ui/FrostedGlass.vue'
import ConfirmDialog from '@/components/ui/ConfirmDialog.vue'
import { useCart } from '@/contexts/cart'
const router = useRouter()
const { items: cartItems, getTotalPrice, handleCheckout, clearCart } = useCart()
const { items: cartItems, getTotalPrice, handleCheckout, clearCart, removeFromCart } = useCart()
// Form state
const formData = ref({
......@@ -255,6 +291,10 @@ const isProcessing = ref(false)
const orderComplete = ref(false)
const orderId = ref('')
// Confirm dialog state
const showConfirmDialog = ref(false)
const itemToDelete = ref(null)
// Format price with Chinese Yuan symbol
const formatPrice = (price) => {
return `¥${price.toFixed(2)}`
......@@ -267,24 +307,39 @@ const handleImageError = (e) => {
// Handle form submission
const handleSubmit = async (e) => {
if (!formData.value.name || !formData.value.phone || !formData.value.address) {
alert('请填写必要信息')
return
}
try {
// 表单验证
if (!formData.value.name?.trim()) {
throw new Error('请输入姓名')
}
if (!formData.value.phone?.trim()) {
throw new Error('请输入手机号码')
}
if (!formData.value.address?.trim()) {
throw new Error('请输入联系地址')
}
if (!formData.value.paymentMethod) {
throw new Error('请选择支付方式')
}
isProcessing.value = true
isProcessing.value = true
try {
// Process checkout
const result = await handleCheckout(formData.value)
if (!result) {
throw new Error('支付处理失败,请重试')
}
if (result.success) {
orderId.value = result.orderId
orderId.value = result.orderId || ''
orderComplete.value = true
} else {
throw new Error(result.message || '支付失败,请重试')
}
} catch (error) {
console.error('Checkout failed:', error)
alert('支付失败,请重试')
alert(error.message || '支付失败,请重试')
} finally {
isProcessing.value = false
}
......