hookehuyr

refactor(icon): 替换NutIcon为IconFont组件并优化数据默认值处理

- 将所有页面中的NutIcon组件替换为IconFont组件
- 为API返回数据添加默认空数组处理
- 更新README文档说明项目功能和技术栈
- 修复部分页面样式问题
## 项目介绍
基于Taro4的微信小程序模版,集成了常用的功能,如登录、注册、列表、详情、购物车等。
本项目是将原有H5预约系统迁移至微信小程序的实现。基于 Taro4 + Vue3 + NutUI 开发,保留了原有的功能和UI风格。
## 功能模块
1. **预约流程**
* 日期与时间段选择 (`pages/booking/index`)
* 预约信息确认与提交 (`pages/submit/index`)
* 预约记录查看 (`pages/bookingList/index`)
* 预约详情 (`pages/bookingDetail/index`)
2. **参观者管理**
* 参观者列表 (`pages/visitorList/index`)
* 添加/编辑参观者 (`pages/addVisitor/index`)
* 支持身份证号校验与脱敏显示
3. **个人中心**
* 预约码展示 (`pages/bookingCode/index`)
* 我的页面 (`pages/me/index`)
* 邀请码/证件号查询 (`pages/search/index`)
4. **公共功能**
* 全局路由封装 (`hooks/useGo`)
* API 请求封装 (`utils/request`, `api/index`)
* 登录授权流程 (`pages/auth/index`)
## 技术栈
- Taro4
- Vue3
- TypeScript
- Pinia
- Less
* **框架**: Taro 4.x
* **UI库**: NutUI 4.x (Vue3)
* **语言**: JavaScript (Vue3 Setup 语法糖)
* **样式**: Less + TailwindCSS (部分)
* **状态管理**: Pinia
* **路由**: Taro Router + 自定义 Hooks
## 项目结构
- src
- api:请求接口
- assets:静态资源
- components:全局组件
- config:项目配置
- pages:页面
- stores:状态管理
- utils:工具函数
- app.config.js:项目配置
- app.js:应用入口
- app.less:全局样式
- taro.config.js:Taro配置
- tsconfig.json:TypeScript配置
- package.json:依赖配置
* `src/api`: 接口定义
* `src/assets`: 图片等静态资源
* `src/components`: 公共组件 (qrCode, qrCodeSearch, reserveCard等)
* `src/pages`: 页面文件
* `src/hooks`: 组合式函数 (useGo等)
* `src/utils`: 工具函数 (request, tools等)
## 项目运行
1. 安装依赖
```bash
pnpm install
```
```bash
npm install
```
2. 运行项目
2. 运行开发环境
```bash
pnpm dev:weapp
```
```bash
npm run dev:weapp
```
3. 打包构建
```bash
pnpm build:weapp
```
3. 打包项目
## 优化建议 (TODO)
```bash
npm run build:weapp
```
* [ ] 小程序授权流程有问题
* [ ] 完善支付流程(目前为模拟/H5跳转)
* [ ] 优化图片资源加载(考虑使用 CDN 或分包)
* [ ] 增强网络请求的错误处理与重试机制
* [ ] 补充单元测试
......
......@@ -14,7 +14,6 @@ declare module 'vue' {
NutDatePicker: typeof import('@nutui/nutui-taro')['DatePicker']
NutForm: typeof import('@nutui/nutui-taro')['Form']
NutFormItem: typeof import('@nutui/nutui-taro')['FormItem']
NutIcon: typeof import('@nutui/nutui-taro')['Icon']
NutInput: typeof import('@nutui/nutui-taro')['Input']
NutPopup: typeof import('@nutui/nutui-taro')['Popup']
Picker: typeof import('./src/components/time-picker-data/picker.vue')['default']
......
/*
* @Date: 2023-08-24 09:42:27
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2024-01-29 17:26:42
* @LastEditTime: 2026-01-06 21:08:32
* @FilePath: /xyxBooking-weapp/src/api/index.js
* @Description: 文件描述
*/
......@@ -100,55 +100,64 @@ export const payCallbackAPI = (params) => fn(fetch.post(Api.PAY_CALLBACK, params
export const billInfoAPI = (params) => fn(fetch.get(Api.BILL_INFO, params));
/**
* @description: 扫码核销二维码列表
* @returns
* @description: 预约单详情,参观者列表 - 免授权接口
* @param {String} pay_id 订单id
* @returns {String}
*/
export const onAuthBillInfoAPI = (params) => fn(fetch.get(Api.ON_AUTH_BILL_INFO, params));
/**
* @description: 预约码列表
* @returns {String}
*/
export const qrcodeListAPI = (params) => fn(fetch.get(Api.QRCODE_LIST, params));
/**
* @description: 扫码核销二维码状态
* @returns
* @description: 二维码使用状态
* @param {String} qr_code 二维码编号
* @returns {String} status 二维码状态 1=未激活(未支付),3=待使用(已支付),5=被取消,7=已使用
*/
export const qrcodeStatusAPI = (params) => fn(fetch.get(Api.QRCODE_STATUS, params));
/**
* @description: 预约单列表
* @returns
* @param {String}
* @returns {String}
*/
export const billListAPI = (params) => fn(fetch.get(Api.BILL_LIST, params));
/**
* @description: 退款
* @returns
* @description: 取消预约
* @param {String} pay_id
* @returns {String}
*/
export const icbcRefundAPI = (params) => fn(fetch.post(Api.ICBC_REFUND, params));
/**
* @description: 支付前准备(获取支付参数等)
* @returns
* @description: 预约单的参观者列表
* @param {String}
* @returns {String}
*/
export const billPrepareAPI = (params) => fn(fetch.post(Api.BILL_PREPARE, params));
export const billPersonAPI = (params) => fn(fetch.get(Api.BILL_PREPARE, params));
/**
* @description: 支付状态查询
* @returns
* 接口废弃
* @description: 刷新预约单支付状态
* @param {String}
* @returns {String}
*/
export const billPayStatusAPI = (params) => fn(fetch.get(Api.BILL_PAY_STATUS, params));
/**
* @description: 证件号查询二维码
* @returns
* @description: 身份证查询预约码
* @param {String}
* @returns {String}
*/
export const queryQrCodeAPI = (params) => fn(fetch.get(Api.QUERY_QR_CODE, params));
/**
* @description: 订单查询
* @returns
* @description: 查询订单号
* @param {String}
* @returns {String}
*/
export const icbcOrderQryAPI = (params) => fn(fetch.get(Api.ICBC_ORDER_QRY, params));
/**
* @description: 授权前订单详情查询
* @returns
*/
export const onAuthBillInfoAPI = (params) => fn(fetch.get(Api.ON_AUTH_BILL_INFO, params));
......
......@@ -15,7 +15,7 @@
<view class="booking-num">
<view class="num-body van-ellipsis">预约人数:<text>{{ reserve_info.total_qty }} 人</text>&nbsp;<text>({{ reserve_info.person_name }})</text></view>
<view v-if="(reserve_info.status === CodeStatus.SUCCESS || reserve_info.status === CodeStatus.USED || reserve_info.status === CodeStatus.CANCEL)">
<nut-icon name="rect-right" />
<IconFont name="rect-right" />
</view>
</view>
<view class="booking-price">支付金额:<text>¥ {{ reserve_info.total_amt }}</text></view>
......@@ -29,6 +29,7 @@
<script setup>
import { computed } from 'vue'
import { IconFont } from '@nutui/icons-vue-taro'
import { useGo } from '@/hooks/useGo'
const go = useGo();
......
......@@ -66,7 +66,7 @@ const formatId = (id) => replaceMiddleCharacters(id);
const loadList = async () => {
const { code, data } = await personListAPI({});
if (code) {
visitorList.value = data;
visitorList.value = data || [];
}
}
......
......@@ -109,7 +109,7 @@ useDidShow(async () => {
const { code, data } = await canReserveDateListAPI({ month: `${raw_date.getFullYear()}-${(raw_date.getMonth() + 1).toString().padStart(2, '0')}` });
if (code) {
// 日期列表
dates_list.value = data;
dates_list.value = data || [];
// 今日之前都不可约
dates_list.value.forEach((date) => {
if (dayjs(date.month_date).isBefore(dayjs())) {
......@@ -264,7 +264,7 @@ const onConfirm = async ({ selectedValue, selectedOptions }) => { // 选择日æœ
const { code, data } = await canReserveDateListAPI({ month: `${year}-${month}` });
if (code) {
// 日期列表
dates_list.value = data;
dates_list.value = data || [];
// 今日之前都不可约
dates_list.value.forEach((date) => {
if (dayjs(date.month_date).isBefore(dayjs())) {
......
......@@ -10,7 +10,7 @@
<view style="padding: 1rem;">
<qrCode></qrCode>
<view class="warning">
<view><nut-icon name="tips" />&nbsp;温馨提示</view>
<view><IconFont name="tips" />&nbsp;温馨提示</view>
<view style="margin-top: 0.5rem;">一人一码,扫码或识别身份证成功后进入</view>
<view style="height: 8rem;"></view>
</view>
......@@ -36,6 +36,7 @@
import { ref } from 'vue'
import Taro from '@tarojs/taro'
import qrCode from '@/components/qrCode';
import { IconFont } from '@nutui/icons-vue-taro'
import icon_3 from '@/assets/images/首页01@2x.png'
import icon_4 from '@/assets/images/二维码icon.png'
import icon_5 from '@/assets/images/我的01@2x.png'
......
......@@ -47,18 +47,19 @@ const loadData = async (isRefresh = false) => {
loading.value = false;
if (code) {
data.forEach(item => {
const list = data || [];
list.forEach(item => {
item.booking_time = item && formatDatetime(item);
item.order_time = item.created_time ? item.created_time.slice(0, -3) : '';
});
if (isRefresh) {
bookingList.value = data;
bookingList.value = list;
} else {
bookingList.value = bookingList.value.concat(data);
bookingList.value = bookingList.value.concat(list);
}
if (data.length < limit.value) {
if (list.length < limit.value) {
finished.value = true;
} else {
page.value++;
......
......@@ -6,7 +6,7 @@
{{ item.name }}
</view>
<view>
<nut-icon name="rect-right" size="1.2rem" />
<IconFont name="rect-right" size="1.2rem" />
</view>
</view>
<view class="index-nav">
......@@ -30,6 +30,7 @@
import { ref } from 'vue'
import Taro from '@tarojs/taro'
import { useGo } from '@/hooks/useGo'
import { IconFont } from '@nutui/icons-vue-taro'
import icon_3 from '@/assets/images/首页01@2x.png'
import icon_4 from '@/assets/images/二维码icon.png'
import icon_5 from '@/assets/images/我的02@2x.png'
......
......@@ -17,7 +17,7 @@
</view>
<view style="color:#A67939; font-size: 0.95rem; text-align: center;">
<view>
<nut-icon name="tips" />&nbsp;温馨提示
<IconFont name="tips" />&nbsp;温馨提示
</view>
<view style="margin-top: 0.5rem;">获取参观码,扫码或识别身份证成功进闸机</view>
</view>
......@@ -40,6 +40,7 @@
<script setup>
import { ref } from 'vue'
import Taro from '@tarojs/taro'
import { IconFont } from '@nutui/icons-vue-taro'
import qrCodeSearch from '@/components/qrCodeSearch';
import { useGo } from '@/hooks/useGo'
......
<!--
* @Date: 2024-01-15 16:25:51
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2024-01-30 15:18:58
* @FilePath: /xyxBooking-weapp/src/pages/submit/index.vue
* @LastEditTime: 2026-01-06 21:22:54
* @FilePath: /git/xyxBooking-weapp/src/pages/submit/index.vue
* @Description: 预约人员信息
-->
<template>
<view class="submit-page">
<view @tap="goToBooking" class="visit-time">
<view>参访时间</view>
<view><text style="font-size: 0.95rem;">{{ date }} {{ time }}</text>&nbsp;<nut-icon name="rect-right" /></view>
<view><text style="font-size: 0.95rem;">{{ date }} {{ time }}</text>&nbsp;<IconFont name="rect-right" /></view>
</view>
<view @tap="goToVisitor" class="add-visitors">
<view><nut-icon name="plus" /> 添加参观者</view>
<view><IconFont name="plus" /> 添加参观者</view>
</view>
<view v-if="visitorList.length" class="visitors-list">
<view v-for="(item, index) in visitorList" :key="index" @tap="addVisitor(item)" class="visitor-item">
......@@ -49,6 +49,7 @@
<script setup>
import { ref, computed } from 'vue'
import Taro, { useDidShow, useRouter as useTaroRouter } from '@tarojs/taro'
import { IconFont } from '@nutui/icons-vue-taro'
import { useGo } from '@/hooks/useGo'
import icon_check1 from '@/assets/images/多选01@2x.png'
import icon_check2 from '@/assets/images/多选02@2x.png'
......@@ -172,7 +173,7 @@ useDidShow(async () => {
if (date.value && time.value) {
const { code, data } = await personListAPI({ reserve_date: date.value, begin_time: time.value.split('-')[0], end_time: time.value.split('-')[1] });
if (code) {
visitorList.value = data;
visitorList.value = data || [];
}
}
});
......
......@@ -18,7 +18,7 @@
<view class="payment-amount">支付金额:<text>¥ {{ billInfo?.total_amt }}</text></view>
</view>
<view class="appointment-notice">
<view style="margin-bottom: 0.25rem;"><nut-icon name="tips" />&nbsp;温馨提示</view>
<view style="margin-bottom: 0.25rem;"><IconFont name="tips" />&nbsp;温馨提示</view>
<view style="font-size: 0.85rem;">1. 一人一码,或拿身份证,扫码或识别身份证成功后进入</view>
<view style="font-size: 0.85rem;">2. 若您无法按时参观,请提前在预约记录中取消您的预约</view>
</view>
......@@ -33,6 +33,7 @@
<script setup>
import { ref } from 'vue'
import Taro, { useDidShow, useRouter as useTaroRouter } from '@tarojs/taro'
import { IconFont } from '@nutui/icons-vue-taro'
import { useGo } from '@/hooks/useGo'
import { billInfoAPI } from '@/api/index'
import { formatDatetime } from '@/utils/tools';
......
<template>
<view class="me-page">
<view class="me-content">
<view class="visitor-list-page">
<view class="visitor-content">
<view class="title">
<view class="text">参观者信息</view>
</view>
<view @tap="() => { go('/pages/addVisitor/index') }" class="add-visitors">
<view><nut-icon name="plus" /> 添加参观者</view>
<view class="add-btn"><IconFont name="plus" /> 添加参观者</view>
</view>
<view v-if="visitorList.length" class="visitors-list">
<view v-for="(item, index) in visitorList" :key="index" class="visitor-item">
......@@ -44,6 +44,7 @@
<script setup>
import { ref } from 'vue'
import Taro, { useDidShow } from '@tarojs/taro'
import { IconFont } from '@nutui/icons-vue-taro'
import { useGo } from '@/hooks/useGo'
import { personListAPI, delPersonAPI } from '@/api/index'
import icon_3 from '@/assets/images/首页01@2x.png'
......@@ -77,15 +78,21 @@ function replaceMiddleCharacters(inputString) {
const formatId = (id) => replaceMiddleCharacters(id);
const loadList = async () => {
try {
const { code, data } = await personListAPI({});
if (code) {
visitorList.value = data;
visitorList.value = data || [];
}
} catch (err) {
console.error(err);
Taro.showToast({ title: '加载失败', icon: 'none' });
}
}
const removeItem = async (item) => {
const { confirm } = await Taro.showModal({ title: '提示', content: '确定删除该参观者吗?' });
if (confirm) {
try {
const { code, msg } = await delPersonAPI({ person_id: item.id });
if (code) {
Taro.showToast({ title: '删除成功' });
......@@ -93,6 +100,10 @@ const removeItem = async (item) => {
} else {
Taro.showToast({ title: msg || '删除失败', icon: 'none' });
}
} catch (error) {
console.error(error);
Taro.showToast({ title: '删除出错', icon: 'none' });
}
}
}
......@@ -102,12 +113,12 @@ useDidShow(() => {
</script>
<style lang="less">
.me-page {
.visitor-list-page {
min-height: 100vh;
background-color: #F6F6F6;
padding: 1rem;
.me-content {
.visitor-content {
.title {
.text {
font-size: 1.1rem;
......@@ -126,6 +137,11 @@ useDidShow(() => {
padding: .65rem 0;
margin: 1rem 0;
font-size: 1.15rem;
.add-btn {
display: flex;
align-items: center;
justify-content: center;
}
}
.visitors-list {
......@@ -171,6 +187,7 @@ useDidShow(() => {
display: flex;
flex-direction: column;
align-items: center;
font-size: 0.8rem;
}
}
}
......
......@@ -2,7 +2,7 @@
<view class="waiting-page">
<view class="waiting-content">
<view>
<nut-icon name="clock" size="40" color="#A67939" />
<IconFont name="clock" size="40" color="#A67939" />
</view>
<view style="margin: 1rem 0;">支付中</view>
<view>{{ current.seconds }} s</view>
......@@ -19,6 +19,7 @@
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import Taro, { useRouter } from '@tarojs/taro'
import { IconFont } from '@nutui/icons-vue-taro'
import { billPayStatusAPI } from '@/api/index'
import { useGo } from '@/hooks/useGo'
......