Commit 5e7a12a9 authored by Jose's avatar Jose

优化服务详情页功能:优惠券样式、底部栏布局和局域网访问配置

parent 19ad2be1
......@@ -1155,7 +1155,6 @@
"resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
......@@ -1425,7 +1424,6 @@
"resolved": "https://registry.npmmirror.com/eslint/-/eslint-9.39.2.tgz",
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true,
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
......@@ -1485,7 +1483,6 @@
"resolved": "https://registry.npmmirror.com/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz",
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
"dev": true,
"peer": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
......@@ -2092,7 +2089,6 @@
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"peer": true,
"engines": {
"node": ">=12"
},
......@@ -2174,7 +2170,6 @@
"resolved": "https://registry.npmmirror.com/prettier/-/prettier-3.7.4.tgz",
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
"dev": true,
"peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
......@@ -2411,7 +2406,6 @@
"resolved": "https://registry.npmmirror.com/vite/-/vite-6.4.1.tgz",
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"dev": true,
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
......@@ -2485,7 +2479,6 @@
"version": "3.5.26",
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.26.tgz",
"integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==",
"peer": true,
"dependencies": {
"@vue/compiler-dom": "3.5.26",
"@vue/compiler-sfc": "3.5.26",
......
......@@ -53,16 +53,22 @@
<!-- Date selection -->
<div class="popup-section">
<h3 class="section-title">Date</h3>
<div class="date-option">Any Time</div>
</div>
<!-- Number of travelers - highlighted -->
<div class="popup-section flex-row highlighted-section">
<h3 class="section-title">Number of travelers</h3>
<div class="quantity-control">
<button class="quantity-btn minus"></button>
<span class="quantity-value">1</span>
<button class="quantity-btn plus">+</button>
<div class="date-options-row">
<div
class="date-option"
:class="{ selected: selectedDate === 'anytime' }"
@click="selectedDate = 'anytime'"
>Any Time</div>
<div
class="date-option"
:class="{ selected: selectedDate === 'jan31' }"
@click="selectedDate = 'jan31'"
>1月31日</div>
<div
class="date-option"
:class="{ selected: selectedDate === 'feb20' }"
@click="selectedDate = 'feb20'"
>2月20日</div>
</div>
</div>
......@@ -70,7 +76,6 @@
<div class="popup-section">
<!-- Add-ons wrapper with background -->
<div class="addons-wrapper">
<h3 class="section-title">Add-ons</h3>
<div
class="addon-item"
v-for="(addon, index) in addons"
......@@ -118,7 +123,7 @@
v-for="coupon in coupons"
:key="coupon.id"
:class="{ active: selectedCoupon === coupon.id }"
@click="selectCoupon(coupon.id)"
@click="toggleCoupon(coupon.id)"
>
<div class="coupon-info">
<div class="coupon-name">{{ coupon.name }}</div>
......@@ -127,6 +132,16 @@
<div class="coupon-discount-container">
<div class="coupon-discount">{{ coupon.discount }}</div>
</div>
<div class="coupon-checkbox-container">
<input
type="checkbox"
class="coupon-checkbox"
:id="`coupon-${coupon.id}`"
:checked="selectedCoupon === coupon.id"
@click.stop
/>
<label :for="`coupon-${coupon.id}`" class="coupon-checkbox-label"></label>
</div>
</div>
</div>
</div>
......@@ -136,23 +151,200 @@
<!-- Price and pay button - fixed at bottom -->
<div class="bottom-bar">
<div class="price">$899</div>
<div class="price-info">
<div class="discount-details" v-if="discountAmount > 0">
<div class="original-price">${{ originalPrice }}</div>
<div class="discount-saving">Saving: ${{ discountAmount }}</div>
</div>
<div class="price">${{ totalPrice }}</div>
</div>
<button class="pay-button" @click="openPopup">Order Now</button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { ref, watch } from 'vue'
// 控制弹层显示/隐藏的状态,初始为关闭
const showPopup = ref(false)
// 日期选择状态
const selectedDate = ref('anytime')
// 折扣码相关状态
const discountCode = ref('')
const showCoupons = ref(false)
const selectedCoupon = ref(null)
// Add-ons data - initial data for anytime
const addons = ref([
{
name: 'Guide service',
price: '$50 per day - Professional local guide with deep knowledge of the area, available in multiple languages including English, Mandarin, and Japanese',
quantity: 0
},
{
name: 'Transportation',
price: '$30 per person - Comfortable air-conditioned vehicle transfer between cities, including hotel pickup and drop-off service',
quantity: 0
},
{
name: 'Meals included',
price: '$25 per meal - Authentic local cuisine experience, including breakfast, lunch, and dinner options with vegetarian alternatives available',
quantity: 0
}
])
// Reset discount code result set
const resetDiscountCode = () => {
discountCode.value = ''
showCoupons.value = false
selectedCoupon.value = null
}
// Update addons based on selected date
const updateAddonsByDate = () => {
switch (selectedDate.value) {
case 'anytime':
addons.value = [
{
name: 'Guide service',
price: '$50 per day - Professional local guide with deep knowledge of the area, available in multiple languages including English, Mandarin, and Japanese',
quantity: 1 // First addon default quantity is 1
},
{
name: 'Transportation',
price: '$30 per person - Comfortable air-conditioned vehicle transfer between cities, including hotel pickup and drop-off service',
quantity: 0 // Other addons default quantity is 0
},
{
name: 'Meals included',
price: '$25 per meal - Authentic local cuisine experience, including breakfast, lunch, and dinner options with vegetarian alternatives available',
quantity: 0 // Other addons default quantity is 0
}
]
break
case 'jan31':
addons.value = [
{
name: 'Guide service',
price: '$60 per day - Special Chinese New Year guide service with cultural insights, available in English and Mandarin',
quantity: 1 // First addon default quantity is 1
},
{
name: 'Transportation',
price: '$35 per person - Luxury minivan transfer with complimentary snacks and drinks for the January 31st tour',
quantity: 0 // Other addons default quantity is 0
},
{
name: 'Meals included',
price: '$30 per meal - Traditional Chinese New Year feast menu with special dishes and festive decorations',
quantity: 0 // Other addons default quantity is 0
},
{
name: 'Fireworks viewing',
price: '$20 per person - Exclusive access to fireworks viewing area for the January 31st celebration',
quantity: 0 // Other addons default quantity is 0
}
]
break
case 'feb20':
addons.value = [
{
name: 'Guide service',
price: '$55 per day - Spring Festival guide service with temple visit arrangements, available in English, Mandarin, and Japanese',
quantity: 1 // First addon default quantity is 1
},
{
name: 'Transportation',
price: '$28 per person - Group bus transfer with festival decorations and cultural activities on board',
quantity: 0 // Other addons default quantity is 0
},
{
name: 'Meals included',
price: '$28 per meal - Spring Festival family-style meal with traditional dishes and tea ceremony',
quantity: 0 // Other addons default quantity is 0
},
{
name: 'Temple entrance',
price: '$15 per person - Included entrance fees to famous temples for the February 20th tour',
quantity: 0 // Other addons default quantity is 0
}
]
break
default:
break
}
// Reset discount code result set when date changes
resetDiscountCode()
// Update total price after resetting addons quantities
updateTotalPrice()
}
// Price related state - initial values
const originalPrice = ref(0) // Original price before discount
const discountAmount = ref(0) // Discount amount
const totalPrice = ref(0) // Final price after discount
// Extract numeric price from price string
const extractPrice = (priceString) => {
const match = priceString.match(/\$([\d.]+)/)
return match ? parseFloat(match[1]) : 0
}
// Calculate discount based on selected coupon
const calculateDiscount = (price) => {
if (!selectedCoupon.value) {
return 0
}
const coupon = coupons.value.find(c => c.id === selectedCoupon.value)
if (!coupon) {
return 0
}
const discount = coupon.discount
if (discount.endsWith('%')) {
// Percentage discount
const percent = parseFloat(discount) / 100
return price * Math.abs(percent)
} else if (discount.startsWith('-$')) {
// Fixed amount discount
return parseFloat(discount.slice(2))
}
return 0
}
// Update total price based on addon quantities and discount
const updateTotalPrice = () => {
// Calculate original price from addons
let addonsTotal = 0
addons.value.forEach(addon => {
const price = extractPrice(addon.price)
addonsTotal += price * addon.quantity
})
originalPrice.value = addonsTotal
// Calculate discount amount
const discount = calculateDiscount(addonsTotal)
discountAmount.value = discount
// Calculate final price (ensure it's not negative)
totalPrice.value = Math.max(0, addonsTotal - discount)
}
// Watch selectedDate changes and update addons
watch(selectedDate, () => {
updateAddonsByDate()
})
// Initialize when component mounts - set default addons and calculate price
updateAddonsByDate()
// 优惠券数据
const coupons = ref([
{
......@@ -175,29 +367,32 @@ const coupons = ref([
}
])
// Add-ons data
const addons = ref([
{
name: 'Guide service',
price: '$50 per day - Professional local guide with deep knowledge of the area, available in multiple languages including English, Mandarin, and Japanese',
quantity: 0
},
{
name: 'Transportation',
price: '$30 per person - Comfortable air-conditioned vehicle transfer between cities, including hotel pickup and drop-off service',
quantity: 0
},
{
name: 'Meals included',
price: '$25 per meal - Authentic local cuisine experience, including breakfast, lunch, and dinner options with vegetarian alternatives available',
quantity: 0
}
])
// Update addon quantity
// Update addon quantity - ensure at least one item has quantity >= 1
const updateAddonQuantity = (index, change) => {
const newQuantity = addons.value[index].quantity + change
const currentQuantity = addons.value[index].quantity
const newQuantity = currentQuantity + change
// If we're decreasing quantity
if (change < 0) {
// Check if this is the last item with quantity >= 1
const totalItemsWithQuantity = addons.value.reduce((count, addon) => {
return count + (addon.quantity >= 1 ? 1 : 0)
}, 0)
// If this is the last item with quantity >= 1 and we're trying to decrease it below 1
if (totalItemsWithQuantity === 1 && currentQuantity <= 1) {
// Don't allow decreasing - keep it at 1
return
}
}
// Update quantity if valid
addons.value[index].quantity = Math.max(0, newQuantity)
updateTotalPrice()
// Reset discount code result set when addon quantity changes
resetDiscountCode()
}
// 打开弹层的函数
......@@ -210,20 +405,35 @@ const closePopup = () => {
showPopup.value = false
}
// 切换优惠券选择状态 - 实现单选和取消选择
const toggleCoupon = (couponId) => {
if (selectedCoupon.value === couponId) {
// 如果当前优惠券已选中,取消选择
selectedCoupon.value = null
} else {
// 否则选中当前优惠券
selectedCoupon.value = couponId
}
// 重新计算总价
updateTotalPrice()
}
// 应用折扣码的函数
const applyDiscount = () => {
if (discountCode.value.trim()) {
showCoupons.value = true
// 默认选中第一个优惠券
if (!selectedCoupon.value) {
selectedCoupon.value = coupons.value[0].id
}
selectedCoupon.value = coupons.value[0].id
// 联动价格更新
updateTotalPrice()
}
}
// 选择优惠券的函数
// 选择优惠券的函数(保留兼容性,实际使用toggleCoupon)
const selectCoupon = (couponId) => {
selectedCoupon.value = couponId
// Recalculate total price when coupon is selected
updateTotalPrice()
}
</script>
......@@ -514,6 +724,14 @@ const selectCoupon = (couponId) => {
margin-bottom: 0;
}
/* Date options row container */
.date-options-row {
display: flex;
gap: 8pt;
flex-wrap: wrap;
margin-top: 6pt;
}
/* Date option */
.date-option {
display: inline-flex;
......@@ -529,6 +747,27 @@ const selectCoupon = (couponId) => {
border: 0.75pt solid #d4edda; /* 1px = 0.75pt */
text-align: center;
padding: 12pt;
cursor: pointer;
transition: all 0.2s ease;
}
/* Date option selected state */
.date-option.selected {
background-color: #42b883;
color: white;
border-color: #369c6a;
box-shadow: 0 2pt 8pt rgba(66, 184, 131, 0.3);
}
/* Date option hover state */
.date-option:hover {
background-color: #d4edda;
transform: translateY(-1pt);
}
.date-option.selected:hover {
background-color: #369c6a;
transform: translateY(-1pt);
}
/* Discount code styles */
......@@ -600,6 +839,7 @@ const selectCoupon = (couponId) => {
cursor: pointer;
transition: all 0.2s ease;
width: 100%;
align-items: flex-start;
}
.coupon-item:last-child {
......@@ -612,7 +852,51 @@ const selectCoupon = (couponId) => {
.coupon-item.active {
background-color: #e8f5e8;
border-left: 3pt solid #42b883; /* 4px = 3pt */
}
/* Coupon checkbox styling */
.coupon-checkbox-container {
margin-left: 12pt;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin-top: 3pt;
}
.coupon-checkbox {
display: none;
}
.coupon-checkbox-label {
width: 24px;
height: 24px;
border: 2pt solid #ddd;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
background-color: white;
position: relative;
}
.coupon-checkbox:checked + .coupon-checkbox-label {
background-color: #42b883;
border-color: #42b883;
}
.coupon-checkbox:checked + .coupon-checkbox-label::after {
content: '';
width: 6px;
height: 11px;
border: solid white;
border-width: 0 2pt 2pt 0;
transform: rotate(45deg);
position: absolute;
left: 8px;
top: 5px;
}
/* Coupon info */
......@@ -764,7 +1048,7 @@ const selectCoupon = (couponId) => {
display: flex;
align-items: center;
justify-content: space-between;
padding: 15pt 24pt;
padding: 12pt 24pt;
border-top: 0.75pt solid #eee;
background-color: white;
box-shadow: 0 -2pt 10pt rgba(0, 0, 0, 0.1);
......@@ -773,21 +1057,61 @@ const selectCoupon = (couponId) => {
z-index: 31;
margin: 0 auto;
box-sizing: border-box;
min-height: 80pt;
height: 80pt;
}
.price-info {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
height: 100%;
max-height: 60pt;
}
.discount-details {
display: flex;
align-items: center;
gap: 12pt;
margin-bottom: 2pt;
height: 20pt;
line-height: 20pt;
}
.original-price {
font-size: 12pt; /* 16px = 12pt */
font-weight: 500;
color: #999;
text-decoration: line-through;
height: 100%;
line-height: 20pt;
}
.discount-saving {
font-size: 11pt; /* 14.67px = 11pt */
font-weight: 500;
color: #42b883;
height: 100%;
line-height: 20pt;
}
.price {
font-size: 22.5pt; /* 30px = 22.5pt */
font-size: 21pt; /* 28px = 21pt */
font-weight: 700;
color: #ff6b35;
height: 30pt;
line-height: 30pt;
margin-top: 2pt;
}
.pay-button {
background-color: #42b883;
color: white;
border: none;
padding: 13.5pt 36pt; /* 18px = 13.5pt, 48px = 36pt */
border-radius: 22.5pt; /* 30px = 22.5pt */
font-size: 15.75pt; /* 21px = 15.75pt */
padding: 10.5pt 18pt; /* 14px = 10.5pt, 24px = 18pt */
border-radius: 18pt; /* 24px = 18pt */
font-size: 13.5pt; /* 18px = 13.5pt */
font-weight: 600;
cursor: pointer;
box-shadow: 0 1.5pt 4.5pt rgba(66, 184, 131, 0.3); /* 2px = 1.5pt, 6px = 4.5pt */
......
......@@ -4,4 +4,7 @@ import vue from '@vitejs/plugin-vue'
// https://vite.dev/config/
export default defineConfig({
plugins: [vue()],
server: {
host: true // 启用局域网访问
}
})
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment