feat:"完成页面接口的对接"

This commit is contained in:
2026-01-29 17:58:19 +08:00
parent 2774a539bf
commit 2b69da3c15
98 changed files with 9504 additions and 592 deletions

View File

@@ -55,25 +55,47 @@
/>
</view>
<!-- 沙发类型 -->
<!-- 服务类型 -->
<view class="form-item">
<text class="form-label">沙发类型</text>
<text class="form-label">
<text class="required">*</text>
服务类型
</text>
<view class="type-grid">
<view
class="type-item"
:class="{ 'type-active': formData.sofaType == item.id }"
v-for="item in sofaTypes"
:class="{ 'type-active': formData.serviceId == item.id }"
v-for="item in serviceList"
:key="item.id"
@click="selectSofaType(item.id)"
@click="selectService(item.id)"
>
<text
class="type-text"
:class="{ 'type-text-active': formData.sofaType == item.id }"
:class="{ 'type-text-active': formData.serviceId == item.id }"
>{{ item.name }}</text>
</view>
</view>
</view>
<!-- 预约时间 -->
<view class="form-item">
<text class="form-label">
<text class="required">*</text>
预约时间
</text>
<picker
mode="date"
:value="formData.appointmentDate"
:start="minDate"
@change="onDateChange"
>
<view class="picker-value">
<text class="picker-text" v-if="formData.appointmentDate">{{ formData.appointmentDate }}</text>
<text class="picker-placeholder" v-else>请选择预约日期</text>
</view>
</picker>
</view>
<!-- 问题描述 -->
<view class="form-item">
<text class="form-label">问题描述</text>
@@ -106,7 +128,7 @@
<text class="add-text">添加图片</text>
</view>
</view>
<text class="upload-tip">最多可上传9张图片</text>
<text class="upload-tip">提示:当前仅支持预览,图片暂不会上传到服务器</text>
</view>
</view>
@@ -124,22 +146,25 @@
</template>
<script setup lang="uts">
import { submitBooking } from '@/api/index.uts'
import { SOFA_CATEGORIES } from '@/utils/config.uts'
import { submitBooking, getActiveServices } from '@/api/index.uts'
// 表单类型
type FormData = {
userName : string
phone : string
address : string
sofaType : string
serviceId : number
appointmentDate : string
problem : string
}
// 沙发类型
type SofaType = {
id : string
// 服务类型
type ServiceItem = {
id : number
name : string
type : string
description : string
price : number
}
// 表单数据
@@ -147,32 +172,82 @@
userName: '',
phone: '',
address: '',
sofaType: '',
serviceId: 0,
appointmentDate: '',
problem: ''
})
// 图片列表
const imageList = ref<string[]>([])
// 沙发类型列表
const sofaTypes = ref<SofaType[]>([])
// 服务列表
const serviceList = ref<ServiceItem[]>([])
// 提交中
const submitting = ref(false)
// 初始化沙发类型
const initSofaTypes = () => {
sofaTypes.value = SOFA_CATEGORIES.filter((item) : boolean => item.id != 'all').map((item) : SofaType => {
return {
id: item.id,
name: item.name
} as SofaType
})
// 最小日期(今天)
const minDate = ref('')
// 加载服务列表
const loadServices = async () => {
try {
const res = await getActiveServices()
console.log('服务列表响应:', res)
if (res.code == 0 && res.data != null) {
const data = res.data as UTSJSONObject
// 兼容两种格式:直接数组 或 {list: [], total: n}
let list : UTSJSONObject[] = []
if (Array.isArray(data)) {
list = data as UTSJSONObject[]
} else {
list = data['list'] as UTSJSONObject[] || []
}
console.log('解析的服务列表:', list)
serviceList.value = list.map((item) : ServiceItem => {
const basePrice = item['basePrice'] as string || '0'
return {
id: item['id'] as number,
name: item['name'] as string,
type: item['type'] as string,
description: item['description'] as string,
price: parseFloat(basePrice)
} as ServiceItem
})
console.log('最终服务列表:', serviceList.value)
} else {
console.error('服务列表响应异常code:', res.code, 'data:', res.data)
}
} catch (e) {
console.error('加载服务列表失败', e)
}
}
// 选择沙发类型
const selectSofaType = (id : string) => {
formData.value.sofaType = id
// 初始化最小日期和默认日期(明天)
const initMinDate = () => {
const now = new Date()
const year = now.getFullYear()
const month = String(now.getMonth() + 1).padStart(2, '0')
const day = String(now.getDate()).padStart(2, '0')
minDate.value = `${year}-${month}-${day}`
// 设置默认预约时间为明天
const tomorrow = new Date()
tomorrow.setDate(tomorrow.getDate() + 1)
const tYear = tomorrow.getFullYear()
const tMonth = String(tomorrow.getMonth() + 1).padStart(2, '0')
const tDay = String(tomorrow.getDate()).padStart(2, '0')
formData.value.appointmentDate = `${tYear}-${tMonth}-${tDay}`
}
// 选择服务
const selectService = (id : number) => {
formData.value.serviceId = id
}
// 日期选择
const onDateChange = (e : any) => {
formData.value.appointmentDate = e.detail.value
}
// 选择图片
@@ -228,6 +303,22 @@
return false
}
if (formData.value.serviceId == 0) {
uni.showToast({
title: '请选择服务类型',
icon: 'none'
})
return false
}
if (formData.value.appointmentDate == '') {
uni.showToast({
title: '请选择预约时间',
icon: 'none'
})
return false
}
return true
}
@@ -239,21 +330,35 @@
submitting.value = true
try {
// 注意当前暂不支持图片上传如需上传图片请先将图片上传到图床获取URL
// 过滤掉微信临时路径后端需要永久URL
const validImages = imageList.value.filter((url) => {
return url.startsWith('http://') || url.startsWith('https://')
})
if (imageList.value.length > 0 && validImages.length === 0) {
console.log('警告:图片为微信临时路径,暂不支持上传')
}
// 构造后端需要的数据格式
const data = {
userName: formData.value.userName,
phone: formData.value.phone,
serviceId: formData.value.serviceId,
contactName: formData.value.userName,
contactPhone: formData.value.phone,
address: formData.value.address,
sofaType: formData.value.sofaType,
problem: formData.value.problem,
images: imageList.value
appointmentTime: formData.value.appointmentDate + 'T10:00:00.000Z', // 默认上午10点
requirements: formData.value.problem,
images: validImages // 只提交有效的URL
} as UTSJSONObject
console.log('提交预约数据:', data)
const res = await submitBooking(data)
const result = res.data as UTSJSONObject
console.log('预约提交结果:', res)
// request.uts 会自动处理失败情况并显示 toast这里只处理成功
uni.showModal({
title: '预约成功',
content: result['message'] as string,
content: '我们会尽快与您联系,请保持电话畅通',
showCancel: false,
success: () => {
// 返回上一页或首页
@@ -267,18 +372,16 @@
}
})
} catch (e) {
console.error('提交预约失败', e)
uni.showToast({
title: '提交失败,请重试',
icon: 'none'
})
console.error('提交预约异常:', e)
// request.uts 已经显示了错误 toast这里只需要记录日志
}
submitting.value = false
}
onLoad(() => {
initSofaTypes()
initMinDate()
loadServices()
})
</script>
@@ -347,6 +450,25 @@
color: #C0C4CC;
}
/* 日期选择器 */
.picker-value {
background-color: #f5f5f5;
border-radius: 12rpx;
padding: 24rpx;
height: 40px;
justify-content: center;
}
.picker-text {
font-size: 28rpx;
color: #303133;
}
.picker-placeholder {
font-size: 28rpx;
color: #C0C4CC;
}
/* 沙发类型选择 */
.type-grid {
flex-direction: row;

View File

@@ -40,7 +40,7 @@
<view class="compare-section">
<before-after
:beforeImage="caseDetail.beforeImages[0] || ''"
:afterImage="caseDetail.afterImages[0] || ''"
:afterImage="caseDetail.compareAfterImages[0] || ''"
:showTitle="true"
></before-after>
</view>
@@ -102,6 +102,7 @@
<script setup lang="uts">
import { getCaseDetail } from '@/api/index.uts'
import { getServiceTypeName } from '@/utils/config.uts'
// 案例详情类型
type CaseDetail = {
@@ -111,6 +112,8 @@
categoryName : string
beforeImages : string[]
afterImages : string[]
// 用于对比组件的专用后图(不使用 images 回退)
compareAfterImages : string[]
description : string
material : string
duration : string
@@ -131,6 +134,7 @@
categoryName: '',
beforeImages: [],
afterImages: [],
compareAfterImages: [],
description: '',
material: '',
duration: '',
@@ -147,38 +151,64 @@
const fetchCaseDetail = async () => {
try {
const res = await getCaseDetail(caseId.value)
const data = res.data as UTSJSONObject
if (res.code === 0 && res.data != null) {
// 处理成功的返回数据
const data = res.data as UTSJSONObject
// 适配后端返回的字段
const images = data['images'] as string[] || []
const beforeImages = data['beforeImages'] as string[] || []
const afterImages = data['afterImages'] as string[] || []
const createdAt = data['createdAt'] as string || ''
// 画廊展示如果afterImages为空使用images作为回退
const displayAfterImages = afterImages.length > 0 ? afterImages : images
// 对比组件before-after应只使用专用的 afterImages不回退到 images
const compareAfterImages = afterImages.length > 0 ? afterImages : []
caseDetail.value = {
id: data['id'] as string,
title: data['title'] as string,
category: data['category'] as string,
categoryName: data['categoryName'] as string,
beforeImages: data['beforeImages'] as string[],
afterImages: data['afterImages'] as string[],
description: data['description'] as string,
material: data['material'] as string,
duration: data['duration'] as string,
price: data['price'] as string,
views: data['views'] as number,
likes: data['likes'] as number,
createTime: data['createTime'] as string
} as CaseDetail
// 更新标题
uni.setNavigationBarTitle({
title: caseDetail.value.title
caseDetail.value = {
id: String(data['id']),
title: data['title'] as string,
category: data['serviceType'] as string || '',
categoryName: getServiceTypeName(data['serviceType'] as string),
beforeImages: beforeImages,
afterImages: displayAfterImages,
compareAfterImages: compareAfterImages,
description: data['description'] as string,
material: data['materials'] as string || '优质材料',
duration: (data['duration'] as number || 0) + '天',
price: data['price'] != null ? '¥' + data['price'] : '面议',
views: data['views'] as number || 0,
likes: data['likes'] as number || 0,
createTime: createdAt.split('T')[0] || ''
} as CaseDetail
// 更新标题
uni.setNavigationBarTitle({
title: caseDetail.value.title
})
} else {
uni.showToast({
title: res.message || '加载失败',
icon: 'none'
})
}
} catch (error) {
console.error('获取案例详情失败:', error)
uni.showToast({
title: '加载失败',
icon: 'none'
})
} catch (e) {
console.error('获取案例详情失败', e)
}
}
// 预览图片
const previewImages = (index : number) => {
const urls = caseDetail.value.afterImages || []
const current = urls[index] || urls[0] || ''
uni.previewImage({
current: index,
urls: caseDetail.value.afterImages
current: current,
urls: urls
})
}

View File

@@ -55,7 +55,7 @@
<script setup lang="uts">
import { getCaseList } from '@/api/index.uts'
import { SOFA_CATEGORIES, PAGE_SIZE } from '@/utils/config.uts'
import { SOFA_CATEGORIES, PAGE_SIZE, getServiceTypeName } from '@/utils/config.uts'
// 分类类型
type CategoryItem = {
@@ -111,6 +111,7 @@
page.value = 1
caseList.value = []
noMore.value = false
console.log(111)
fetchCaseList()
}
@@ -121,43 +122,56 @@
loading.value = true
try {
const params = {
category: currentCategory.value,
serviceType: currentCategory.value != 'all' ? currentCategory.value : undefined,
page: page.value,
pageSize: PAGE_SIZE
limit: PAGE_SIZE,
status: 'published'
} as UTSJSONObject
const res = await getCaseList(params)
const data = res.data as UTSJSONObject
// 适应后端返回格式:{ items: [], total: 0, page: 1, limit: 10 }
const list = data['items'] as UTSJSONObject[] || []
total.value = data['total'] as number || 0
const newList = list.map((item) : CaseItem => {
return {
id: item['id'] as string,
title: item['title'] as string,
category: item['category'] as string,
categoryName: item['categoryName'] as string,
coverImage: item['coverImage'] as string,
material: item['material'] as string,
duration: item['duration'] as string,
price: item['price'] as string,
views: item['views'] as number,
likes: item['likes'] as number
} as CaseItem
})
if (page.value == 1) {
caseList.value = newList
} else {
caseList.value = [...caseList.value, ...newList]
}
if (caseList.value.length >= total.value) {
noMore.value = true
if (res.code == 0 && res.data != null) {
const data = res.data as UTSJSONObject
// 适应后端返回格式:{ list: [], total: 0, page: 1, pageSize: 10 }
const list = data['list'] as UTSJSONObject[] || []
total.value = data['total'] as number || 0
const newList = list.map((item) : CaseItem => {
// 优先使用images其次afterImages最后beforeImages
const images = item['images'] as string[] || []
const afterImages = item['afterImages'] as string[] || []
const beforeImages = item['beforeImages'] as string[] || []
const coverImage = images.length > 0 ? images[0] : (afterImages.length > 0 ? afterImages[0] : (beforeImages.length > 0 ? beforeImages[0] : ''))
return {
id: String(item['id']),
title: item['title'] as string,
category: item['serviceType'] as string,
categoryName: getServiceTypeName(item['serviceType'] as string),
coverImage: coverImage,
material: item['materials'] as string || '暂无',
duration: (item['duration'] as number || 0) + '天',
price: item['price'] != null ? '¥' + item['price'] : '面议',
views: item['views'] as number || 0,
likes: item['likes'] as number || 0
} as CaseItem
})
if (page.value == 1) {
caseList.value = newList
} else {
caseList.value = [...caseList.value, ...newList]
}
if (caseList.value.length >= total.value) {
noMore.value = true
}
}
} catch (e) {
console.error('获取案例列表失败', e)
uni.showToast({
title: '加载失败',
icon: 'none'
})
}
loading.value = false
}

View File

@@ -90,8 +90,8 @@
</template>
<script setup lang="uts">
import { getBanners, getHotCases } from '@/api/index.uts'
import { SERVICE_TYPES } from '@/utils/config.uts'
import { getBanners, getHotCases, getActiveServices } from '@/api/index.uts'
import { getServiceTypeName } from '@/utils/config.uts'
// 轮播图类型
type BannerItem = {
@@ -146,30 +146,46 @@
{ icon: '💰', title: '价格透明', desc: '无隐形消费' }
])
// 初始化服务类型
const initServiceTypes = () => {
serviceTypes.value = SERVICE_TYPES.map((item) : ServiceType => {
return {
id: item.id,
name: item.name,
icon: item.icon
} as ServiceType
})
// 获取服务类型
const fetchServiceTypes = async () => {
try {
const res = await getActiveServices()
if (res.code === 0 && res.data != null) {
const data = res.data as UTSJSONObject
const list = data['list'] as UTSJSONObject[] || []
serviceTypes.value = list.map((item) : ServiceType => {
return {
id: String(item['id']),
name: item['name'] as string,
icon: item['icon'] as string || '/static/icons/default.png'
} as ServiceType
})
}
} catch (e) {
console.error('获取服务类型失败', e)
// 失败时使用默认数据
serviceTypes.value = [
{ id: 'repair', name: '局部修复', icon: '/static/icons/repair.png' },
{ id: 'refurbish', name: '整体翻新', icon: '/static/icons/refurbish.png' }
] as ServiceType[]
}
}
// 获取轮播图
const fetchBanners = async () => {
try {
const res = await getBanners()
const data = res.data as UTSJSONObject[]
bannerList.value = data.map((item) : BannerItem => {
return {
id: item['id'] as string,
image: item['image'] as string,
title: item['title'] as string,
link: item['link'] as string
} as BannerItem
})
if (res.code === 0 && res.data != null) {
const data = res.data as UTSJSONObject[]
bannerList.value = data.map((item) : BannerItem => {
return {
id: item['id'] as string,
image: item['image'] as string,
title: item['title'] as string,
link: item['link'] as string
} as BannerItem
})
}
} catch (e) {
console.error('获取轮播图失败', e)
}
@@ -179,23 +195,31 @@
const fetchHotCases = async () => {
try {
const res = await getHotCases()
const data = res.data as UTSJSONObject
// 适应后端返回格式:{ items: [], total: 0 }
const list = data['items'] as UTSJSONObject[] || []
hotCases.value = list.map((item) : CaseItem => {
return {
id: item['id'] as string,
title: item['title'] as string,
category: item['category'] as string,
categoryName: item['categoryName'] as string,
coverImage: item['coverImage'] as string,
material: item['material'] as string,
duration: item['duration'] as string,
price: item['price'] as string,
views: item['views'] as number,
likes: item['likes'] as number
} as CaseItem
})
if (res.code == 0 && res.data != null) {
const data = res.data as UTSJSONObject
// 适应后端返回格式:{ list: [], total: 0 }
const list = data['list'] as UTSJSONObject[] || []
hotCases.value = list.map((item) : CaseItem => {
// 优先使用images其次afterImages最后beforeImages
const images = item['images'] as string[] || []
const afterImages = item['afterImages'] as string[] || []
const beforeImages = item['beforeImages'] as string[] || []
const coverImage = images.length > 0 ? images[0] : (afterImages.length > 0 ? afterImages[0] : (beforeImages.length > 0 ? beforeImages[0] : ''))
return {
id: String(item['id']),
title: item['title'] as string,
category: item['serviceType'] as string || '',
categoryName: getServiceTypeName(item['serviceType'] as string),
coverImage: coverImage,
material: item['materials'] as string || '暂无',
duration: (item['duration'] as number || 0) + '天',
price: item['price'] != null ? '¥' + item['price'] : '面议',
views: item['views'] as number || 0,
likes: item['likes'] as number || 0
} as CaseItem
})
}
} catch (e) {
console.error('获取热门案例失败', e)
}
@@ -247,15 +271,10 @@
const testAPI = async () => {
try {
console.log('开始API对接测试...')
// 测试轮播图使用Mock数据
const bannerRes = await getBanners()
console.log('轮播图API响应:', bannerRes)
// 测试案例列表
const caseRes = await getCaseList()
console.log('案例列表API响应:', caseRes)
uni.showToast({
title: 'API测试完成请查看控制台',
icon: 'success'
@@ -270,7 +289,7 @@
}
onLoad(() => {
initServiceTypes()
fetchServiceTypes()
fetchBanners()
fetchHotCases()

View File

@@ -101,14 +101,16 @@
</template>
<script setup lang="uts">
import { getServiceProcess } from '@/api/index.uts'
import { getServiceProcess, getActiveServices } from '@/api/index.uts'
// 服务类型
type ServiceType = {
id : string
id : number
name : string
desc : string
emoji : string
type : string
basePrice : number
}
// 流程类型
@@ -134,15 +136,16 @@
}
// 服务类型数据
const serviceTypes = ref<ServiceType[]>([
{ id: 'repair', name: '局部修复', desc: '破损、划痕修复', emoji: '🔧' },
{ id: 'recolor', name: '改色翻新', desc: '皮面改色换新', emoji: '🎨' },
{ id: 'refurbish', name: '整体翻新', desc: '全面翻新升级', emoji: '✨' },
{ id: 'custom', name: '定制换皮', desc: '个性化定制', emoji: '💎' }
])
const serviceTypes = ref<ServiceType[]>([])
// 服务流程
const processList = ref<ProcessItem[]>([])
const processList = ref<ProcessItem[]>([
{ step: 1, title: '在线预约', description: '填写信息,预约上门时间' },
{ step: 2, title: '上门评估', description: '专业师傅免费上门勘察' },
{ step: 3, title: '确认方案', description: '沟通翻新方案和价格' },
{ step: 4, title: '取件翻新', description: '取回沙发进行专业翻新' },
{ step: 5, title: '送货验收', description: '送货上门,满意付款' }
])
// 材质说明
const materials = ref<MaterialItem[]>([
@@ -201,23 +204,45 @@
}
])
// 获取服务流程
const fetchServiceProcess = async () => {
// 获取服务列表
const fetchServices = async () => {
try {
const res = await getServiceProcess()
const data = res.data as UTSJSONObject[]
processList.value = data.map((item) : ProcessItem => {
return {
step: item['step'] as number,
title: item['title'] as string,
description: item['description'] as string
} as ProcessItem
})
const res = await getActiveServices()
if (res.code == 0 && res.data != null) {
const data = res.data as UTSJSONObject
const list = data['list'] as UTSJSONObject[] || []
// 服务类型emoji映射
const emojiMap = {
fabric: '🛋️',
leather: '💺',
cleaning: '✨',
repair: '🔧',
custom: '💎'
} as UTSJSONObject
serviceTypes.value = list.map((item) : ServiceType => {
const type = item['type'] as string
return {
id: item['id'] as number,
name: item['name'] as string,
desc: item['description'] as string,
emoji: emojiMap[type] as string || '🛋️',
type: type,
basePrice: item['basePrice'] as number
} as ServiceType
})
}
} catch (e) {
console.error('获取服务流程失败', e)
console.error('获取服务列表失败', e)
}
}
// 获取服务流程(暂时使用固定数据)
const fetchServiceProcess = async () => {
// 流程数据已在初始化时设置
}
// 切换FAQ展开
const toggleFaq = (index : number) => {
faqList.value[index].expanded = !faqList.value[index].expanded
@@ -238,6 +263,7 @@
}
onLoad(() => {
fetchServices()
fetchServiceProcess()
})
</script>

View File

@@ -0,0 +1,513 @@
<template>
<view class="page">
<!-- 状态筛选 -->
<view class="status-bar">
<scroll-view class="status-scroll" scroll-x>
<view class="status-list">
<view
class="status-item"
:class="{ 'status-active': currentStatus == item.value }"
v-for="item in statusList"
:key="item.value"
@click="selectStatus(item.value)"
>
<text
class="status-text"
:class="{ 'status-text-active': currentStatus == item.value }"
>{{ item.label }}</text>
</view>
</view>
</scroll-view>
</view>
<!-- 预约列表 -->
<scroll-view
class="list-scroll"
scroll-y
@scrolltolower="loadMore"
>
<view class="booking-list">
<view
class="booking-card"
v-for="item in bookingList"
:key="item.id"
@click="goToDetail(item.id)"
>
<view class="card-header">
<text class="booking-number">预约编号:{{ item.bookingNumber }}</text>
<view class="status-badge" :class="'status-' + item.status">
<text class="status-badge-text">{{ getStatusText(item.status) }}</text>
</view>
</view>
<view class="card-body">
<view class="info-row">
<text class="info-label">服务类型</text>
<text class="info-value">{{ item.serviceName }}</text>
</view>
<view class="info-row">
<text class="info-label">预约时间</text>
<text class="info-value">{{ formatDate(item.appointmentTime) }}</text>
</view>
<view class="info-row">
<text class="info-label">联系电话</text>
<text class="info-value">{{ item.contactPhone }}</text>
</view>
<view class="info-row">
<text class="info-label">服务地址</text>
<text class="info-value address">{{ item.address }}</text>
</view>
</view>
<view class="card-footer">
<text class="create-time">创建时间:{{ formatDate(item.createdAt) }}</text>
<view class="action-btns">
<view
class="action-btn cancel-btn"
v-if="item.status == 'pending' || item.status == 'confirmed'"
@click.stop="cancelBooking(item.id)"
>
<text class="btn-text">取消预约</text>
</view>
<view class="action-btn primary-btn" @click.stop="callService">
<text class="btn-text">联系客服</text>
</view>
</view>
</view>
</view>
</view>
<!-- 加载状态 -->
<view class="load-status">
<text class="load-text" v-if="loading">加载中...</text>
<text class="load-text" v-else-if="noMore">没有更多了</text>
</view>
<!-- 空状态 -->
<view class="empty-state" v-if="!loading && bookingList.length == 0">
<text class="empty-icon">📋</text>
<text class="empty-text">暂无预约记录</text>
<view class="empty-btn" @click="goToBooking">
<text class="empty-btn-text">立即预约</text>
</view>
</view>
<!-- 底部间距 -->
<view class="bottom-space"></view>
</scroll-view>
</view>
</template>
<script setup lang="uts">
import { getMyBookings, cancelBooking as cancelBookingApi } from '@/api/index.uts'
import { PAGE_SIZE } from '@/utils/config.uts'
// 预约状态类型
type StatusItem = {
value : string
label : string
}
// 预约项类型
type BookingItem = {
id : number
bookingNumber : string
serviceName : string
appointmentTime : string
contactPhone : string
address : string
status : string
createdAt : string
}
// 状态列表
const statusList = ref<StatusItem[]>([
{ value: 'all', label: '全部' },
{ value: 'pending', label: '待确认' },
{ value: 'confirmed', label: '已确认' },
{ value: 'in_progress', label: '进行中' },
{ value: 'completed', label: '已完成' },
{ value: 'cancelled', label: '已取消' }
])
// 当前状态
const currentStatus = ref('all')
// 预约列表
const bookingList = ref<BookingItem[]>([])
// 分页
const page = ref(1)
const total = ref(0)
// 加载状态
const loading = ref(false)
const noMore = ref(false)
// 选择状态
const selectStatus = (status : string) => {
if (currentStatus.value == status) return
currentStatus.value = status
page.value = 1
bookingList.value = []
noMore.value = false
fetchBookingList()
}
// 获取预约列表
const fetchBookingList = async () => {
if (loading.value || noMore.value) return
loading.value = true
try {
const params = {
page: page.value,
limit: PAGE_SIZE,
status: currentStatus.value != 'all' ? currentStatus.value : undefined
} as UTSJSONObject
const res = await getMyBookings(params)
if (res.code == 0 && res.data != null) {
const data = res.data as UTSJSONObject
const list = data['list'] as UTSJSONObject[] || []
total.value = data['total'] as number || 0
const newList = list.map((item) : BookingItem => {
const service = item['service'] as UTSJSONObject || {}
return {
id: item['id'] as number,
bookingNumber: item['bookingNumber'] as string,
serviceName: service['name'] as string || '',
appointmentTime: item['appointmentTime'] as string,
contactPhone: item['contactPhone'] as string,
address: item['address'] as string,
status: item['status'] as string,
createdAt: item['createdAt'] as string
} as BookingItem
})
if (page.value == 1) {
bookingList.value = newList
} else {
bookingList.value = [...bookingList.value, ...newList]
}
if (bookingList.value.length >= total.value) {
noMore.value = true
}
}
} catch (e) {
console.error('获取预约列表失败', e)
uni.showToast({
title: '加载失败',
icon: 'none'
})
}
loading.value = false
}
// 加载更多
const loadMore = () => {
if (!noMore.value && !loading.value) {
page.value++
fetchBookingList()
}
}
// 获取状态文本
const getStatusText = (status : string) : string => {
const map = {
pending: '待确认',
confirmed: '已确认',
in_progress: '进行中',
completed: '已完成',
cancelled: '已取消'
} as UTSJSONObject
return map[status] as string || status
}
// 格式化日期
const formatDate = (dateStr : string) : string => {
if (!dateStr) return ''
return dateStr.split('T')[0].replace(/-/g, '/')
}
// 取消预约
const cancelBooking = (id : number) => {
uni.showModal({
title: '提示',
content: '确定要取消这个预约吗?',
success: async (res) => {
if (res.confirm) {
try {
await cancelBookingApi(String(id))
uni.showToast({
title: '取消成功',
icon: 'success'
})
// 重新加载列表
page.value = 1
bookingList.value = []
noMore.value = false
fetchBookingList()
} catch (e) {
uni.showToast({
title: '取消失败',
icon: 'none'
})
}
}
}
})
}
// 跳转详情
const goToDetail = (id : number) => {
uni.showToast({
title: '详情功能开发中',
icon: 'none'
})
}
// 联系客服
const callService = () => {
uni.makePhoneCall({
phoneNumber: '400-888-8888',
fail: () => {
uni.showToast({
title: '拨打电话失败',
icon: 'none'
})
}
})
}
// 去预约
const goToBooking = () => {
uni.navigateTo({
url: '/pages/booking/index'
})
}
onLoad(() => {
fetchBookingList()
})
</script>
<style lang="scss">
.page {
flex: 1;
background-color: #f5f5f5;
}
/* 状态栏 */
.status-bar {
background-color: #ffffff;
padding: 24rpx 0;
border-bottom-width: 1rpx;
border-bottom-style: solid;
border-bottom-color: #EBEEF5;
}
.status-scroll {
width: 100%;
}
.status-list {
flex-direction: row;
padding: 0 32rpx;
}
.status-item {
margin-right: 32rpx;
padding: 12rpx 24rpx;
border-radius: 32rpx;
background-color: #f5f5f5;
}
.status-active {
background-color: #D4A574;
}
.status-text {
font-size: 28rpx;
color: #666666;
}
.status-text-active {
color: #ffffff;
}
/* 列表 */
.list-scroll {
flex: 1;
}
.booking-list {
padding: 24rpx 32rpx;
}
.booking-card {
background-color: #ffffff;
border-radius: 16rpx;
padding: 32rpx;
margin-bottom: 24rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
}
.card-header {
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
padding-bottom: 24rpx;
border-bottom-width: 1rpx;
border-bottom-style: solid;
border-bottom-color: #EBEEF5;
}
.booking-number {
font-size: 28rpx;
color: #333333;
font-weight: 500;
}
.status-badge {
padding: 8rpx 16rpx;
border-radius: 8rpx;
}
.status-pending {
background-color: #FFF3E0;
}
.status-confirmed {
background-color: #E3F2FD;
}
.status-in_progress {
background-color: #E8F5E9;
}
.status-completed {
background-color: #F3E5F5;
}
.status-cancelled {
background-color: #FFEBEE;
}
.status-badge-text {
font-size: 24rpx;
color: #666666;
}
.card-body {
margin-bottom: 24rpx;
}
.info-row {
flex-direction: row;
margin-bottom: 16rpx;
}
.info-label {
width: 140rpx;
font-size: 28rpx;
color: #999999;
}
.info-value {
flex: 1;
font-size: 28rpx;
color: #333333;
}
.address {
line-height: 1.6;
}
.card-footer {
padding-top: 24rpx;
border-top-width: 1rpx;
border-top-style: solid;
border-top-color: #EBEEF5;
}
.create-time {
font-size: 24rpx;
color: #999999;
margin-bottom: 16rpx;
}
.action-btns {
flex-direction: row;
justify-content: flex-end;
}
.action-btn {
padding: 12rpx 32rpx;
border-radius: 8rpx;
margin-left: 16rpx;
}
.cancel-btn {
background-color: #f5f5f5;
}
.primary-btn {
background-color: #D4A574;
}
.btn-text {
font-size: 28rpx;
color: #333333;
}
.primary-btn .btn-text {
color: #ffffff;
}
/* 加载状态 */
.load-status {
padding: 32rpx;
align-items: center;
}
.load-text {
font-size: 28rpx;
color: #999999;
}
/* 空状态 */
.empty-state {
padding: 120rpx 32rpx;
align-items: center;
}
.empty-icon {
font-size: 120rpx;
margin-bottom: 32rpx;
}
.empty-text {
font-size: 28rpx;
color: #999999;
margin-bottom: 48rpx;
}
.empty-btn {
padding: 16rpx 48rpx;
background-color: #D4A574;
border-radius: 48rpx;
}
.empty-btn-text {
font-size: 28rpx;
color: #ffffff;
}
.bottom-space {
height: 32rpx;
}
</style>

View File

@@ -0,0 +1,132 @@
<template>
<view class="page">
<!-- 收藏列表 -->
<scroll-view
class="list-scroll"
scroll-y
>
<view class="case-list">
<case-card
v-for="item in favoriteList"
:key="item.id"
:caseData="item"
@click="goToDetail"
></case-card>
</view>
<!-- 空状态 -->
<view class="empty-state" v-if="!loading && favoriteList.length == 0">
<text class="empty-icon">❤️</text>
<text class="empty-text">还没有收藏任何案例</text>
<view class="empty-btn" @click="goToCases">
<text class="empty-btn-text">去看看案例</text>
</view>
</view>
<!-- 底部间距 -->
<view class="bottom-space"></view>
</scroll-view>
</view>
</template>
<script setup lang="uts">
import { STORAGE_KEYS } from '@/utils/config.uts'
// 案例类型
type CaseItem = {
id : string
title : string
category : string
categoryName : string
coverImage : string
material : string
duration : string
price : string
views : number
likes : number
}
// 收藏列表
const favoriteList = ref<CaseItem[]>([])
// 加载状态
const loading = ref(false)
// 获取收藏列表
const fetchFavorites = () => {
loading.value = true
// 从本地存储获取收藏列表
const favorites = uni.getStorageSync(STORAGE_KEYS.FAVORITES) as string[] || []
// TODO: 这里应该根据收藏的ID列表从后端获取案例详情
// 暂时使用空列表
favoriteList.value = []
loading.value = false
}
// 跳转详情
const goToDetail = (id : string) => {
uni.navigateTo({
url: `/pages/cases/detail?id=${id}`
})
}
// 去案例列表
const goToCases = () => {
uni.navigateTo({
url: '/pages/cases/list'
})
}
onLoad(() => {
fetchFavorites()
})
</script>
<style lang="scss">
.page {
flex: 1;
background-color: #f5f5f5;
}
.list-scroll {
flex: 1;
}
.case-list {
padding: 24rpx 32rpx;
}
/* 空状态 */
.empty-state {
padding: 120rpx 32rpx;
align-items: center;
}
.empty-icon {
font-size: 120rpx;
margin-bottom: 32rpx;
}
.empty-text {
font-size: 28rpx;
color: #999999;
margin-bottom: 48rpx;
}
.empty-btn {
padding: 16rpx 48rpx;
background-color: #D4A574;
border-radius: 48rpx;
}
.empty-btn-text {
font-size: 28rpx;
color: #ffffff;
}
.bottom-space {
height: 32rpx;
}
</style>

View File

@@ -120,6 +120,7 @@
<script setup lang="uts">
import { STORAGE_KEYS } from '@/utils/config.uts'
import { wechatLogin } from '@/api/index.uts'
// 用户信息类型
type UserInfo = {
@@ -182,41 +183,92 @@
})
}
// 获取统计数据
const fetchStats = () => {
// 获取预约数量可以从后端API获取
bookingCount.value = 0
// 获取收藏数量
const favorites = uni.getStorageSync(STORAGE_KEYS.FAVORITES) as string[]
favoriteCount.value = favorites ? favorites.length : 0
}
// 登录
const handleLogin = () => {
// #ifdef MP-WEIXIN
// 必须在用户点击事件中直接调用 getUserProfile
uni.getUserProfile({
desc: '用于完善用户资料',
success: (res) => {
userInfo.value = {
id: '',
nickName: res.userInfo.nickName,
avatar: res.userInfo.avatarUrl,
phone: ''
} as UserInfo
// 保存用户信息
uni.setStorageSync(STORAGE_KEYS.USER_INFO, {
nickName: res.userInfo.nickName,
avatar: res.userInfo.avatarUrl
} as UTSJSONObject)
uni.setStorageSync(STORAGE_KEYS.TOKEN, 'mock_token_' + Date.now().toString())
isLoggedIn.value = true
uni.showToast({
title: '登录成功',
icon: 'success'
success: (profileRes) => {
// 在授权成功的回调中再调用 uni.login 获取 code
uni.login({
provider: 'weixin',
success: async (loginRes) => {
const code = loginRes.code
try {
// 调用后端微信登录接口
const res = await wechatLogin(code)
if (res.code === 0 && res.data != null) {
const data = res.data as UTSJSONObject
const token = data['access_token'] as string
const userDataObj = data['user'] as UTSJSONObject
// 保存 token
uni.setStorageSync(STORAGE_KEYS.TOKEN, token)
// 保存用户信息
userInfo.value = {
id: String(userDataObj['id']),
nickName: profileRes.userInfo.nickName,
avatar: profileRes.userInfo.avatarUrl,
phone: (userDataObj['phone'] ?? '') as string
} as UserInfo
uni.setStorageSync(STORAGE_KEYS.USER_INFO, {
id: userDataObj['id'],
nickName: profileRes.userInfo.nickName,
avatar: profileRes.userInfo.avatarUrl,
phone: userDataObj['phone']
} as UTSJSONObject)
isLoggedIn.value = true
uni.showToast({
title: '登录成功',
icon: 'success'
})
// 刷新统计数据
fetchStats()
} else {
throw new Error(res.message)
}
} catch (error) {
console.error('登录失败', error)
uni.showToast({
title: '登录失败,请重试',
icon: 'none'
})
}
},
fail: () => {
uni.showToast({
title: '登录失败',
icon: 'none'
})
}
})
},
fail: () => {
uni.showToast({
title: '登录失败',
title: '授权失败',
icon: 'none'
})
}
})
// #endif
// #ifndef MP-WEIXIN
uni.showToast({
title: '请在微信小程序中登录',
@@ -253,17 +305,29 @@
// 跳转预约列表
const goToBookingList = () => {
uni.showToast({
title: '功能开发中',
icon: 'none'
if (!isLoggedIn.value) {
uni.showToast({
title: '请先登录',
icon: 'none'
})
return
}
uni.navigateTo({
url: '/pages/user/booking-list'
})
}
// 跳转收藏列表
const goToFavorites = () => {
uni.showToast({
title: '功能开发中',
icon: 'none'
if (!isLoggedIn.value) {
uni.showToast({
title: '请先登录',
icon: 'none'
})
return
}
uni.navigateTo({
url: '/pages/user/favorites'
})
}