hookehuyr

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

- 新增 `ConfirmDialog` 组件,用于确认删除操作
- 在 `CheckoutPage` 中集成 `ConfirmDialog`,支持删除单个商品和清空购物车
- 优化 `FrostedGlass` 组件,支持自定义背景透明度和模糊级别
- 改进结账页面的表单验证和错误处理逻辑
1 +<template>
2 + <Teleport to="body">
3 + <div
4 + v-if="show"
5 + class="fixed inset-0 z-50 flex items-center justify-center px-4"
6 + >
7 + <div
8 + class="fixed inset-0 bg-black/50 backdrop-blur-sm"
9 + @click="handleCancel"
10 + ></div>
11 + <FrostedGlass class="w-full max-w-sm p-6 relative z-10" :bg-opacity="90" blur-level="md">
12 + <h3 class="text-lg font-medium mb-2">{{ title }}</h3>
13 + <p class="text-gray-600 text-sm mb-6">{{ message }}</p>
14 + <div class="flex justify-end space-x-3">
15 + <button
16 + @click="handleCancel"
17 + class="px-4 py-2 text-sm text-gray-600 hover:text-gray-800"
18 + >
19 + 取消
20 + </button>
21 + <button
22 + @click="handleConfirm"
23 + class="px-4 py-2 text-sm text-white bg-red-500 hover:bg-red-600 rounded-lg"
24 + >
25 + 确认
26 + </button>
27 + </div>
28 + </FrostedGlass>
29 + </div>
30 + </Teleport>
31 +</template>
32 +
33 +<script setup>
34 +import { defineProps, defineEmits } from 'vue'
35 +import FrostedGlass from './FrostedGlass.vue'
36 +
37 +const props = defineProps({
38 + show: {
39 + type: Boolean,
40 + default: false
41 + },
42 + title: {
43 + type: String,
44 + default: '确认'
45 + },
46 + message: {
47 + type: String,
48 + required: true
49 + }
50 +})
51 +
52 +const emit = defineEmits(['confirm', 'cancel', 'update:show'])
53 +
54 +const handleConfirm = () => {
55 + emit('confirm')
56 + emit('update:show', false)
57 +}
58 +
59 +const handleCancel = () => {
60 + emit('cancel')
61 + emit('update:show', false)
62 +}
63 +</script>
1 <template> 1 <template>
2 <div 2 <div
3 :class="[ 3 :class="[
4 - 'bg-white/20 backdrop-blur-md rounded-xl border border-white/30 shadow-lg', 4 + `bg-white/${bgOpacity} backdrop-blur-${blurLevel} rounded-xl border border-white/30 shadow-lg`,
5 className 5 className
6 ]" 6 ]"
7 > 7 >
...@@ -16,6 +16,16 @@ defineProps({ ...@@ -16,6 +16,16 @@ defineProps({
16 className: { 16 className: {
17 type: String, 17 type: String,
18 default: '' 18 default: ''
19 + },
20 + bgOpacity: {
21 + type: Number,
22 + default: 80,
23 + validator: (value) => value >= 0 && value <= 100
24 + },
25 + blurLevel: {
26 + type: String,
27 + default: 'sm',
28 + validator: (value) => ['none', 'sm', 'md', 'lg', 'xl', '2xl', '3xl'].includes(value)
19 } 29 }
20 }) 30 })
21 </script> 31 </script>
......
...@@ -51,12 +51,24 @@ ...@@ -51,12 +51,24 @@
51 @error="handleImageError" 51 @error="handleImageError"
52 /> 52 />
53 </div> 53 </div>
54 - <div> 54 + <div class="flex-1">
55 <p class="font-medium text-sm line-clamp-1">{{ item.title }}</p> 55 <p class="font-medium text-sm line-clamp-1">{{ item.title }}</p>
56 <p class="text-xs text-gray-500"> 56 <p class="text-xs text-gray-500">
57 {{ item.type === 'course' ? '课程' : '活动' }} · {{ item.quantity }} 份 57 {{ item.type === 'course' ? '课程' : '活动' }} · {{ item.quantity }} 份
58 </p> 58 </p>
59 </div> 59 </div>
60 + <button
61 + @click="() => {
62 + itemToDelete = item
63 + showConfirmDialog = true
64 + }"
65 + class="text-red-500 hover:text-red-600 p-1 -mr-1"
66 + title="删除"
67 + >
68 + <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
69 + <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" />
70 + </svg>
71 + </button>
60 </div> 72 </div>
61 <div class="ml-2 text-right"> 73 <div class="ml-2 text-right">
62 <p class="font-medium text-sm">{{ formatPrice(item.price * item.quantity) }}</p> 74 <p class="font-medium text-sm">{{ formatPrice(item.price * item.quantity) }}</p>
...@@ -204,7 +216,10 @@ ...@@ -204,7 +216,10 @@
204 </div> 216 </div>
205 <button 217 <button
206 type="button" 218 type="button"
207 - @click="clearCart" 219 + @click="() => {
220 + showConfirmDialog = true
221 + itemToDelete = null
222 + }"
208 class="text-sm text-red-500" 223 class="text-sm text-red-500"
209 > 224 >
210 清空购物车 225 清空购物车
...@@ -227,6 +242,26 @@ ...@@ -227,6 +242,26 @@
227 </form> 242 </form>
228 </div> 243 </div>
229 </div> 244 </div>
245 +
246 + <!-- Confirm Dialog -->
247 + <ConfirmDialog
248 + v-model:show="showConfirmDialog"
249 + title="确认删除"
250 + :message="itemToDelete ? `确定要删除 ${itemToDelete.title || '此商品'} 吗?` : '确定要清空购物车吗?'"
251 + @confirm="() => {
252 + if (itemToDelete) {
253 + if (cartItems.length === 1) {
254 + clearCart()
255 + } else {
256 + removeFromCart(itemToDelete.id, itemToDelete.type)
257 + }
258 + itemToDelete = null
259 + } else {
260 + clearCart()
261 + }
262 + }"
263 + @cancel="itemToDelete = null"
264 + />
230 </AppLayout> 265 </AppLayout>
231 </template> 266 </template>
232 267
...@@ -235,10 +270,11 @@ import { ref } from 'vue' ...@@ -235,10 +270,11 @@ import { ref } from 'vue'
235 import { useRouter } from 'vue-router' 270 import { useRouter } from 'vue-router'
236 import AppLayout from '@/components/layout/AppLayout.vue' 271 import AppLayout from '@/components/layout/AppLayout.vue'
237 import FrostedGlass from '@/components/ui/FrostedGlass.vue' 272 import FrostedGlass from '@/components/ui/FrostedGlass.vue'
273 +import ConfirmDialog from '@/components/ui/ConfirmDialog.vue'
238 import { useCart } from '@/contexts/cart' 274 import { useCart } from '@/contexts/cart'
239 275
240 const router = useRouter() 276 const router = useRouter()
241 -const { items: cartItems, getTotalPrice, handleCheckout, clearCart } = useCart() 277 +const { items: cartItems, getTotalPrice, handleCheckout, clearCart, removeFromCart } = useCart()
242 278
243 // Form state 279 // Form state
244 const formData = ref({ 280 const formData = ref({
...@@ -255,6 +291,10 @@ const isProcessing = ref(false) ...@@ -255,6 +291,10 @@ const isProcessing = ref(false)
255 const orderComplete = ref(false) 291 const orderComplete = ref(false)
256 const orderId = ref('') 292 const orderId = ref('')
257 293
294 +// Confirm dialog state
295 +const showConfirmDialog = ref(false)
296 +const itemToDelete = ref(null)
297 +
258 // Format price with Chinese Yuan symbol 298 // Format price with Chinese Yuan symbol
259 const formatPrice = (price) => { 299 const formatPrice = (price) => {
260 return `¥${price.toFixed(2)}` 300 return `¥${price.toFixed(2)}`
...@@ -267,24 +307,39 @@ const handleImageError = (e) => { ...@@ -267,24 +307,39 @@ const handleImageError = (e) => {
267 307
268 // Handle form submission 308 // Handle form submission
269 const handleSubmit = async (e) => { 309 const handleSubmit = async (e) => {
270 - if (!formData.value.name || !formData.value.phone || !formData.value.address) { 310 + try {
271 - alert('请填写必要信息') 311 + // 表单验证
272 - return 312 + if (!formData.value.name?.trim()) {
273 - } 313 + throw new Error('请输入姓名')
314 + }
315 + if (!formData.value.phone?.trim()) {
316 + throw new Error('请输入手机号码')
317 + }
318 + if (!formData.value.address?.trim()) {
319 + throw new Error('请输入联系地址')
320 + }
321 + if (!formData.value.paymentMethod) {
322 + throw new Error('请选择支付方式')
323 + }
274 324
275 - isProcessing.value = true 325 + isProcessing.value = true
276 326
277 - try {
278 // Process checkout 327 // Process checkout
279 const result = await handleCheckout(formData.value) 328 const result = await handleCheckout(formData.value)
280 329
330 + if (!result) {
331 + throw new Error('支付处理失败,请重试')
332 + }
333 +
281 if (result.success) { 334 if (result.success) {
282 - orderId.value = result.orderId 335 + orderId.value = result.orderId || ''
283 orderComplete.value = true 336 orderComplete.value = true
337 + } else {
338 + throw new Error(result.message || '支付失败,请重试')
284 } 339 }
285 } catch (error) { 340 } catch (error) {
286 console.error('Checkout failed:', error) 341 console.error('Checkout failed:', error)
287 - alert('支付失败,请重试') 342 + alert(error.message || '支付失败,请重试')
288 } finally { 343 } finally {
289 isProcessing.value = false 344 isProcessing.value = false
290 } 345 }
......