refactor(goods): clean up components, update imports and add album page

- replace scoped SCSS styles with inline utility classes for goods components
- move LocationCard to goods subdirectory and update relative imports
- fix DebounceUtils import path in FooterSection
- update goods index page: replace scroll wrapper, switch to vue-router composable, replace uni modal with vant showDialog
- add new album page component
- remove unused PNG assets, old README and deprecated style files
- update global type declarations for vant showDialog
This commit is contained in:
duanshuwen
2026-05-27 22:17:57 +08:00
parent cd39a9a65c
commit 479c5175ec
17 changed files with 133 additions and 594 deletions

1
auto-imports.d.ts vendored
View File

@@ -6,5 +6,6 @@
// biome-ignore lint: disable
export {}
declare global {
const showDialog: typeof import('vant/es').showDialog
const showToast: typeof import('vant/es').showToast
}

View File

@@ -1,80 +0,0 @@
<template>
<div class="store-address">
<div class="text-container" @click.stop="openMap">
<span class="location-label">位于 {{ orderData.oneLevelAddress }}</span>
<span class="address-text">{{ orderData.commodityAddress }}</span>
</div>
<div class="actions">
<div>
<div class="actions-btn" @click.stop="openMap">
<uni-icons type="paperplane-filled" size="16" color="#171717" />
</div>
<span class="actions-text">导航</span>
</div>
<div>
<div class="actions-btn" @click.stop="callPhone">
<uni-icons type="phone-filled" size="16" color="#171717" />
</div>
<span class="actions-text">电话</span>
</div>
</div>
</div>
</template>
<script setup>
import { defineProps } from "vue";
const props = defineProps({
orderData: {
type: Object,
required: true,
default: () => ({}),
},
});
// 打开地图
const openMap = () => {
const address = props.orderData.commodityAddress;
const latitude = Number(props.orderData.commodityLatitude);
const longitude = Number(props.orderData.commodityLongitude);
uni.getLocation({
type: "gcj02",
success: () => {
uni.openLocation({
latitude: latitude,
longitude: longitude,
address: address,
});
},
fail: () => {
uni.openLocation({
latitude: latitude,
longitude: longitude,
address: address,
});
},
});
};
// 拨打电话
const getPhoneNumber = () => {
const o = props.orderData || {};
return o.commodityPhone || o.phone || o.contactPhone || "15608199221";
};
const callPhone = () => {
const phone = getPhoneNumber();
if (!phone) {
uni.showToast({ title: "未提供电话号码", icon: "none" });
return;
}
uni.makePhoneCall({ phoneNumber: String(phone) });
};
</script>
<style scoped lang="scss">
@import "./styles/index.scss";
</style>

View File

@@ -1,56 +0,0 @@
.store-address {
display: flex;
align-items: center;
margin-top: 12px;
padding: 12px;
background-color: #ffffff;
border-top: 1px solid #e0e0e0;
// 左侧文本容器
.text-container {
display: flex;
flex-direction: column;
flex: 1;
}
.location-label {
color: $text-color-800;
font-size: 14px;
font-weight: 500;
}
.address-text {
margin-top: 4px;
color: $text-color-600;
font-size: 12px;
font-weight: 400;
}
// 右侧操作按钮组
.actions {
display: flex;
align-items: center;
gap: 10px;
margin-left: 16px;
}
.actions-btn {
width: 28px;
height: 28px;
border-radius: 10px;
background-color: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
}
.actions-icon {
font-size: 16px;
line-height: 16px;
}
.actions-text {
font-size: 12px;
color: $text-color-600;
}
}

View File

@@ -1,25 +1,25 @@
<template>
<div class="date-selector" @click="showCalendar">
<div class="date-item">
<div class="date-label">入住日期</div>
<div class="date-box">
<div class="date-content">
<span class="date-span">{{ checkInDate }}</span>
<span class="day-span">{{ checkInDay }}</span>
<div class="flex items-end justify-between my-[12px] px-[12px]" @click="showCalendar">
<div class="flex-1 relative">
<div class="absolute top-[-6px] left-[12px] text-[100px] text-ink-600 bg-white px-[4px]">入住日期</div>
<div class="flex items-center justify-start border border-[#f0f0f0] rounded-[8px] px-[12px] py-[10px] bg-white">
<div class="flex items-baseline gap-[4px]">
<span class="text-[16px] font-medium text-[#333]">{{ checkInDate }}</span>
<span class="text-[10px] text-[#666]">{{ checkInDay }}</span>
</div>
</div>
</div>
<div class="nights-info">
<span class="nights-span">{{ nights }}</span>
<div class="flex items-center justify-center min-w-[40px] mx-[8px] mb-[10px]">
<span class="text-[12px] text-[#666]">{{ nights }}</span>
</div>
<div class="date-item">
<div class="date-label">退房日期</div>
<div class="date-box">
<div class="date-content">
<span class="date-span">{{ checkOutDate }}</span>
<span class="day-span">{{ checkOutDay }}</span>
<div class="flex-1 relative">
<div class="absolute top-[-6px] left-[12px] text-[100px] text-ink-600 bg-white px-[4px]">退房日期</div>
<div class="flex items-center justify-start border border-[#f0f0f0] rounded-[8px] px-[12px] py-[10px] bg-white">
<div class="flex items-baseline gap-[4px]">
<span class="text-[16px] font-medium text-[#333]">{{ checkOutDate }}</span>
<span class="text-[10px] text-[#666]">{{ checkOutDay }}</span>
</div>
</div>
</div>

View File

@@ -1,64 +0,0 @@
.date-selector {
display: flex;
align-items: flex-end;
justify-content: space-between;
margin: 12px 0;
padding: 0 12px;
}
.date-item {
flex: 1;
position: relative;
}
.date-label {
position: absolute;
top: -12rpx;
left: 24rpx;
font-size: 20rpx;
color: #333-grey;
background-color: #fff;
padding: 0 8rpx;
z-index: 1;
}
.date-box {
padding: 20rpx 24rpx;
background-color: #fff;
border-radius: 16rpx;
border: 2rpx solid #f0f0f0;
display: flex;
align-items: center;
justify-content: start;
}
.date-content {
display: flex;
align-items: baseline;
gap: 8rpx;
}
.date-text {
font-size: 32rpx;
font-weight: 500;
color: #333;
}
.day-text {
font-size: 20rpx;
color: #666666;
}
.nights-info {
display: flex;
align-items: center;
justify-content: center;
min-width: 80rpx;
margin: 0 16rpx;
margin-bottom: 20rpx;
}
.nights-text {
font-size: 24rpx;
color: #666666;
}

View File

@@ -1,19 +1,19 @@
<template>
<div class="border-box border-top-8">
<div class="border-box pt-12 pl-12 pr-12" v-for="(moduleItem, index) in goodsData.commodityEquipment" :key="index">
<div class="flex flex-items-start flex-col" :class="{
<div class="border-top-8">
<div class="pt-[12px] pl-[12px] pr-[12px]" v-for="(moduleItem, index) in goodsData.commodityEquipment" :key="index">
<div class="flex flex-col items-start" :class="{
'border-bottom': index < goodsData.commodityEquipment.length - 1,
}">
<div class="flex flex-items-center flex-row flex-shrink-0">
<uni-icons fontFamily="znicons" size="20" color="#171717">
{{ zniconsMap[moduleItem.icon] }}
</uni-icons>
<span class="ml-4 font-size-12 color-171717 line-height-20">
<div class="flex items-center flex-row shrink-0">
<van-icon fontFamily="znicons" size="20" color="#171717">
</van-icon>
<span class="ml-[4px] text-[12px] text-[#171717] leading-[20px]">
{{ moduleItem.title }}
</span>
</div>
<div class="border-box flex flex-items-center flex-row mt-4 pb-12">
<span class="font-size-12 color-525866 line-height-20 mr-4" v-for="(span, index) in moduleItem.desc"
<div class="flex items-center flex-row mt-[4px] pb-[12px]">
<span class="text-[12px] text-[#525866] leading-[20px] mr-[4px]" v-for="(span, index) in moduleItem.desc"
:key="index">
{{ span }}
</span>
@@ -35,10 +35,3 @@ const props = defineProps({
},
});
</script>
<style scoped lang="scss">
@font-face {
font-family: znicons;
src: url("@/assets/fonts/znicons.ttf");
}
</style>

View File

@@ -1,255 +0,0 @@
# GoodInfo 商品信息组件
## 概述
`GoodInfo` 是一个高性能的商品信息展示组件专为电商、旅游、服务类应用设计。组件采用现代化的UI设计支持响应式布局和暗色模式提供优秀的用户体验。
## 功能特性
### 🎯 核心功能
- **价格展示**: 突出显示商品价格,支持货币符号和价格标签
- **商品标题**: 清晰展示商品名称和相关标签
- **地址信息**: 显示商品/服务地址,支持图标和交互
- **设施展示**: 网格布局展示商品特色设施或服务项目
### ⚡ 性能优化
- **计算属性缓存**: 使用 `computed` 优化设施列表渲染
- **按需渲染**: 条件渲染减少不必要的DOM节点
- **轻量级设计**: 最小化组件体积和依赖
- **懒加载支持**: 支持图标和内容的懒加载
### 🎨 UI特性
- **现代化设计**: 圆角卡片、阴影效果、渐变背景
- **响应式布局**: 适配不同屏幕尺寸
- **暗色模式**: 自动适配系统主题
- **交互反馈**: 悬停效果和过渡动画
## 基础用法
### 简单使用
```vue
<template>
<GoodInfo :goodsInfo="goodsData" />
</template>
<script setup>
import GoodInfo from "./components/GoodInfo/index.vue";
const goodsData = {
price: 399,
title: "【成人票】云从朵花温泉门票",
timeTag: "随时可退",
address: "距您43.1公里 黔南州布依族苗族自治州龙里县",
};
</script>
```
### 完整配置
```vue
<template>
<GoodInfo :goodsInfo="fullGoodsData" />
</template>
<script setup>
const fullGoodsData = {
price: 399,
title: "【成人票】云从朵花温泉门票",
tag: "热销",
timeTag: "随时可退",
address: "距您43.1公里 黔南州布依族苗族自治州龙里县",
facilities: [
{ icon: "home", name: "48个泡池" },
{ icon: "color", name: "11个特色药池" },
{ icon: "fire", name: "4个汗蒸房" },
{ icon: "person", name: "儿童充气水上乐园" },
{ icon: "game", name: "儿童戏水区" },
{ icon: "home-filled", name: "石板浴" },
],
};
</script>
```
## API 文档
### Props
| 参数 | 类型 | 默认值 | 说明 |
| --------- | ------ | ------ | ------------ |
| goodsInfo | Object | `{}` | 商品信息对象 |
### goodsInfo 对象结构
| 字段 | 类型 | 必填 | 默认值 | 说明 |
| ---------- | ------------- | ---- | --------------------------------------------- | ------------------------------ |
| price | Number/String | 否 | `399` | 商品价格 |
| title | String | 否 | `'【成人票】云从朵花温泉门票'` | 商品标题 |
| tag | String | 否 | - | 价格标签(如:热销、限时优惠) |
| timeTag | String | 否 | `'随时可退'` | 时间相关标签 |
| address | String | 否 | `'距您43.1公里 黔南州布依族苗族自治州龙里县'` | 地址信息 |
| facilities | Array | 否 | 默认设施列表 | 设施/特色列表 |
### facilities 数组结构
```javascript
[
{
icon: "home", // uni-icons 图标名称
name: "48个泡池", // 设施名称
},
];
```
## 样式定制
### CSS 变量
组件支持通过 CSS 变量进行主题定制:
```scss
.good-info {
--primary-color: #ff6b35; // 主色调
--background-color: #fff; // 背景色
--text-color: #333; // 文字颜色
--border-radius: 16rpx; // 圆角大小
--shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08); // 阴影
}
```
### 响应式断点
- **小屏设备**: `max-width: 750rpx`
- **暗色模式**: `prefers-color-scheme: dark`
## 性能优化建议
### 1. 数据结构优化
```javascript
// ✅ 推荐:使用 reactive 包装数据
const goodsData = reactive({
price: 399,
title: "商品标题",
});
// ❌ 避免:频繁的深层对象更新
const goodsData = ref({
nested: {
deep: {
value: "data",
},
},
});
```
### 2. 设施列表优化
```javascript
// ✅ 推荐:预定义设施列表
const FACILITY_PRESETS = {
spa: [
{ icon: "home", name: "48个泡池" },
{ icon: "water", name: "恒温泳池" },
],
hotel: [
{ icon: "bed", name: "豪华客房" },
{ icon: "car", name: "免费停车" },
],
};
// 使用预设
const goodsData = {
facilities: FACILITY_PRESETS.spa,
};
```
### 3. 图标优化
```javascript
// ✅ 推荐:使用常见图标
const commonIcons = ["home", "person", "heart", "star"];
// ❌ 避免:使用过多不同图标增加包体积
```
## 最佳实践
### 1. 数据验证
```javascript
// 添加数据验证
const validateGoodsInfo = (data) => {
return {
price: Number(data.price) || 0,
title: String(data.title || ""),
facilities: Array.isArray(data.facilities) ? data.facilities : [],
};
};
```
### 2. 错误处理
```vue
<template>
<GoodInfo :goodsInfo="goodsData" @error="handleError" />
</template>
<script setup>
const handleError = (error) => {
console.error("GoodInfo Error:", error);
// 错误上报或用户提示
};
</script>
```
### 3. 无障碍访问
```vue
<template>
<div
class="good-info"
role="article"
:aria-label="`商品信息:${goodsInfo.title}`"
>
<!-- 组件内容 -->
</div>
</template>
```
## 注意事项
1. **图标依赖**: 组件依赖 `uni-icons`,请确保项目中已安装
2. **单位适配**: 样式使用 `rpx` 单位适配小程序和H5
3. **性能考虑**: 设施列表较多时建议分页或虚拟滚动
4. **主题适配**: 支持暗色模式,但需要系统支持
## 更新日志
### v1.0.0 (2024-01-XX)
- ✨ 初始版本发布
- 🎨 现代化UI设计
- ⚡ 性能优化
- 📱 响应式布局
- 🌙 暗色模式支持
## 技术栈
- **框架**: Vue 3 Composition API
- **样式**: SCSS
- **图标**: uni-icons
- **构建**: Vite/Webpack
## 浏览器支持
- ✅ Chrome 80+
- ✅ Firefox 75+
- ✅ Safari 13+
- ✅ Edge 80+
- ✅ 微信小程序
- ✅ 支付宝小程序
- ✅ H5

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

View File

@@ -1,24 +1,27 @@
<template>
<div class="good-info border-box pl-12 pr-12">
<div class="bg-white px-[12px]">
<!-- 标题区域 -->
<div class="title-section">
<span class="title">
{{ goodsData.commodityName || "【成人票】云从朵花温泉门票" }}
<div class="flex items-center mb-[12px]">
<span
class="text-[16px] font-semibold text-[#333] leading-[1.4] flex-0-[280px] text-ellipsis overflow-hidden whitespace-nowrap">
{{ goodsData.commodityName }}
</span>
<div class="tag-wrapper" v-if="goodsData.timeTag">
<div class="time-tag">{{ goodsData.timeTag || "随时可退" }}</div>
<div class="flex items-center" v-if="goodsData.timeTag">
<div
class="text-[9px] text-semibold text-[#f55726] border-[1px] border-[#f55726] rounded-[6px] px-[6px] py-[3px]">
{{ goodsData.timeTag }}
</div>
</div>
</div>
<!-- 设施信息区域 -->
<div class="facilities-section" v-if="facilitiesList.length">
<div class="facilities-grid">
<div
class="facility-item"
v-for="(facility, index) in facilitiesList"
:key="index"
>
<span class="facility-span">{{ facility }}</span>
<div class="mt-[12px]" v-if="facilitiesList.length">
<div class="flex flex-wrap gap-[8px]">
<div class="flex items-center gap-[4px] p-[8px] bg-[#fafafa] rounded-[6px]"
v-for="(facility, index) in facilitiesList" :key="index">
<span class="text-[12px] text-[#333] leading-[1] white-space-nowrap overflow-hidden text-ellipsis">
{{ facility }}
</span>
</div>
</div>
</div>
@@ -48,7 +51,3 @@ const facilitiesList = computed(() => {
return [];
});
</script>
<style scoped lang="scss">
@import "./styles/index.scss";
</style>

View File

@@ -1,40 +1,5 @@
.good-info {
background: #fff;
// 标题区域
.title-section {
display: flex;
align-items: center;
margin-bottom: 12px;
.title {
font-size: 16px;
color: #333;
font-weight: 600;
line-height: 1.4;
flex: 0 280px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tag-wrapper {
display: flex;
align-items: center;
.time-tag {
color: #f55726;
padding: 3px 6px;
border-radius: 6px;
font-size: 9px;
border: 1px solid #f55726;
}
}
.calender-icon {
margin-left: auto;
}
}
// 设施信息区域
.facilities-section {

View File

@@ -1,17 +1,17 @@
<template>
<div class="border-box border-top-8 pl-12 pr-12 pt-6 pb-12">
<div class="border-top border-[#E5E5E5] border-[8px] px-[12px] pt-[6px] pb-[12px]">
<ModuleTitle v-if="showTitle" title="套餐包含内容" />
<div class="flex flex-col items-start " v-for="(item, index) in goodsData.commodityPackageConfig" :key="index">
<div class="flex items-center justify-start py-[4px] ml-[4px]">
<span class="whitespace-nowrap flex-auto text-[14px] text-[#171717]">
{{ item.name }}
</span>
<div
class="flex flex-items-start flex-col"
v-for="(item, index) in goodsData.commodityPackageConfig"
:key="index"
>
<div class="title-row py-4 ml-4">
<span class="left font-size-14 color-171717">{{ item.name }}</span>
<div class="sep" aria-hidden="true"></div>
<span class="right font-size-14 color-171717"
>{{ item.count }}{{ item.unit }}</span
>
class="flex-1 h-px mx-2 bg-[repeating-linear-gradient(to_right,#CACFD8_0_10px,transparent_10px_16px)] bg-repeat-x bg-center">
</div>
<span class="whitespace-nowrap flex-auto text-[14px] text-[#171717]">
{{ item.count }}{{ item.unit }}
</span>
</div>
</div>
</div>
@@ -33,7 +33,3 @@ const props = defineProps({
},
});
</script>
<style scoped lang="scss">
@import "./styles/index.scss";
</style>

View File

@@ -1,20 +0,0 @@
.title-row {
display: flex;
align-items: center;
justify-content: flex-start;
}
.title-row .left,
.title-row .right {
white-space: nowrap;
flex: 0 0 auto;
}
.title-row .sep {
flex: 1 1 auto;
height: 1px;
margin: 0 8px;
background-image: repeating-linear-gradient(to right, #CACFD8 0 10px, transparent 10px 16px);
background-repeat: repeat-x;
background-position: center;
}

View File

@@ -0,0 +1,53 @@
<template>
<div class="flex items-center mt-[12px] p-[12px] bg-white border border-t border-[#e0e0e0]">
<div class="flex flex-col flex-1">
<span class="text-[14px] font-medium text-ink-800">位于 {{ orderData.oneLevelAddress }}</span>
<span class="mt-[4px] text-[12px] text-ink-500">{{ orderData.commodityAddress }}</span>
</div>
<div class="flex items-center gap-[10px] ml-[16px]">
<div>
<div class="actions-btn">
<van-icon type="paperplane-filled" size="16" color="#171717" />
</div>
<span class="text-[12px] text-ink-600">导航</span>
</div>
<div>
<div class="actions-btn" @click.stop="callPhone">
<van-icon type="phone-filled" size="16" color="#171717" />
</div>
<span class="text-[12px] text-ink-600">电话</span>
</div>
</div>
</div>
</template>
<script setup>
import { defineProps } from "vue";
const props = defineProps({
orderData: {
type: Object,
required: true,
default: () => ({}),
},
});
// 拨打电话
const getPhoneNumber = () => {
const o = props.orderData || {};
return o.commodityPhone || o.phone || o.contactPhone;
};
const callPhone = () => {
const phone = getPhoneNumber();
if (!phone) {
showToast("未提供电话号码");
return;
}
window.location.href = `tel:${phone}`;
};
</script>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 585 KiB

View File

@@ -1,11 +1,11 @@
<template>
<div class="goods-container bg-gray">
<div class="goods-container">
<TopNavBar :title="navOpacity < 0.5 ? '' : '商品详情'" :background="`rgba(217, 238, 255, ${navOpacity})`"
:titleColor="navOpacity < 0.5 ? '#ffffff' : '#000000'"
:backIconColor="navOpacity < 0.5 ? '#ffffff' : '#000000'" />
<!-- 滚动区域 -->
<scroll-div class="content-wrapper" scroll-y @scroll="handleScroll">
<div class="content-wrapper" @scroll="handleScroll">
<imgSwiper :border-radius="0" :height="300" :images="goodsData.commodityPhotoList" thumbnailBottom="42px" />
<div class="goods-content">
@@ -23,19 +23,19 @@
<GoodPackage v-if="
goodsData.orderType != 0 &&
goodsData.commodityPackageConfig &&
goodsData.commodityPackageConfig.length > 0
goodsData.commodityPackageConfig.length
" :goodsData="goodsData" />
<!-- 商品设施组件 -->
<GoodFacility v-if="
goodsData.commodityEquipment &&
goodsData.commodityEquipment.length > 0
goodsData.commodityEquipment.length
" :goodsData="goodsData" />
<!-- 商品详情组件 -->
<GoodDetail :goodsData="goodsData" />
</div>
</scroll-div>
</div>
<!-- 立即抢购 -->
<div class="footer border-top">
@@ -57,19 +57,23 @@
<script setup>
import { ref } from "vue";
import { useRouter } from 'vue-router'
import { goodsDetail, commodityDailyPriceList } from "@/api/goods";
import { DateUtils } from "@/utils";
import { DateUtils } from "@/utils/dateUtils";
import TopNavBar from "@/components/TopNavBar/index.vue";
import ImageSwiper from "@/components/ImageSwiper/index.vue";
import GoodInfo from "./components/GoodInfo/index.vue";
import Calender from "@/components/Calender/index.vue";
import LocationCard from "@/components/LocationCard/index.vue";
import LocationCard from "./components/LocationCard/index.vue";
import DateSelector from "./components/DateSelector/index.vue";
import GoodDetail from "@/components/GoodDetail/index.vue";
import GoodFacility from "./components/GoodFacility/index.vue";
import GoodPackage from "./components/GoodPackage/index.vue";
import { useSelectedDateStore } from "@/store";
const router = useRouter()
// 导航栏透明度 - 默认透明,随滚动变为不透明
const navOpacity = ref(0);
const calendarVisible = ref(false);
@@ -113,14 +117,14 @@ const goodsInfo = async (params) => {
}
if (goodsData.value.commodityStatus !== "1") {
uni.showModal({
showDialog({
title: "温馨提示",
content: "您选的商品暂不可预订,请重新选择。",
success: (res) => {
if (res.confirm) {
uni.navigateBack({ delta: 1 });
}
},
message: "您选的商品暂不可预订,请重新选择。",
}).then(() => {
router.back()
}).catch(() => {
});
return;
}
@@ -229,9 +233,12 @@ const handleDateSelect = (data) => {
// 跳转订购
const navigateToPay = ({ commodityId }) => {
uni.navigateTo({
url: `/pages-booking/index?commodityId=${commodityId}`,
});
router.push({
name: "booking",
query: {
commodityId,
},
})
};
</script>

View File

@@ -20,7 +20,7 @@
<script setup>
import { defineProps, defineEmits, computed } from "vue";
import { orderPayNow } from "@/api/order";
import { DebounceUtils } from "@/utils";
import { DebounceUtils } from "@/utils/DebounceUtils";
const props = defineProps({
orderData: {