初始化参股

This commit is contained in:
2026-01-27 18:06:04 +08:00
commit 2774a539bf
254 changed files with 33255 additions and 0 deletions

View File

@@ -0,0 +1,362 @@
<template>
<view class="page">
<scroll-view class="page-scroll" scroll-y>
<!-- 公司Logo和名称 -->
<view class="header-section">
<view class="company-logo">
<text class="logo-text">优艺家</text>
</view>
<text class="company-name">{{ companyInfo.name }}</text>
<text class="company-slogan">{{ companyInfo.slogan }}</text>
</view>
<!-- 公司介绍 -->
<view class="section">
<section-header title="公司简介"></section-header>
<view class="intro-card">
<text class="intro-text">{{ companyInfo.description }}</text>
</view>
</view>
<!-- 我们的优势 -->
<view class="section">
<section-header title="我们的优势"></section-header>
<view class="features-grid">
<view class="feature-item" v-for="item in companyInfo.features" :key="item.title">
<view class="feature-icon-bg">
<text class="feature-icon">✓</text>
</view>
<text class="feature-title">{{ item.title }}</text>
<text class="feature-desc">{{ item.desc }}</text>
</view>
</view>
</view>
<!-- 联系方式 -->
<view class="section">
<section-header title="联系我们"></section-header>
<view class="contact-card">
<view class="contact-item" @click="callPhone">
<view class="contact-icon-bg">
<text class="contact-icon">📞</text>
</view>
<view class="contact-info">
<text class="contact-label">客服电话</text>
<text class="contact-value">{{ companyInfo.phone }}</text>
</view>
<text class="contact-arrow"></text>
</view>
<view class="contact-item" @click="copyWechat">
<view class="contact-icon-bg">
<text class="contact-icon">💬</text>
</view>
<view class="contact-info">
<text class="contact-label">微信号</text>
<text class="contact-value">{{ companyInfo.wechat }}</text>
</view>
<text class="contact-arrow"></text>
</view>
<view class="contact-item" @click="openLocation">
<view class="contact-icon-bg">
<text class="contact-icon">📍</text>
</view>
<view class="contact-info">
<text class="contact-label">公司地址</text>
<text class="contact-value">{{ companyInfo.address }}</text>
</view>
<text class="contact-arrow"></text>
</view>
<view class="contact-item">
<view class="contact-icon-bg">
<text class="contact-icon">🕐</text>
</view>
<view class="contact-info">
<text class="contact-label">营业时间</text>
<text class="contact-value">{{ companyInfo.workTime }}</text>
</view>
</view>
</view>
</view>
<!-- 底部间距 -->
<view class="bottom-space"></view>
</scroll-view>
</view>
</template>
<script setup lang="uts">
import { getCompanyInfo } from '@/api/index.uts'
// 特色类型
type FeatureItem = {
title : string
desc : string
}
// 公司信息类型
type CompanyInfo = {
name : string
slogan : string
description : string
phone : string
wechat : string
address : string
workTime : string
features : FeatureItem[]
}
// 公司信息
const companyInfo = ref<CompanyInfo>({
name: '优艺家沙发翻新',
slogan: '让旧沙发焕发新生',
description: '',
phone: '400-888-8888',
wechat: 'youyijia2026',
address: '',
workTime: '',
features: []
})
// 获取公司信息
const fetchCompanyInfo = async () => {
try {
const res = await getCompanyInfo()
const data = res.data as UTSJSONObject
const featuresData = data['features'] as UTSJSONObject[]
const features = featuresData.map((item) : FeatureItem => {
return {
title: item['title'] as string,
desc: item['desc'] as string
} as FeatureItem
})
companyInfo.value = {
name: data['name'] as string,
slogan: data['slogan'] as string,
description: data['description'] as string,
phone: data['phone'] as string,
wechat: data['wechat'] as string,
address: data['address'] as string,
workTime: data['workTime'] as string,
features: features
} as CompanyInfo
} catch (e) {
console.error('获取公司信息失败', e)
}
}
// 拨打电话
const callPhone = () => {
uni.makePhoneCall({
phoneNumber: companyInfo.value.phone,
fail: () => {
uni.showToast({
title: '拨打电话失败',
icon: 'none'
})
}
})
}
// 复制微信号
const copyWechat = () => {
uni.setClipboardData({
data: companyInfo.value.wechat,
success: () => {
uni.showToast({
title: '微信号已复制',
icon: 'success'
})
}
})
}
// 打开位置
const openLocation = () => {
uni.openLocation({
latitude: 31.2,
longitude: 121.5,
name: companyInfo.value.name,
address: companyInfo.value.address,
fail: () => {
uni.showToast({
title: '无法打开地图',
icon: 'none'
})
}
})
}
onLoad(() => {
fetchCompanyInfo()
})
</script>
<style lang="scss">
.page {
flex: 1;
background-color: #f5f5f5;
}
.page-scroll {
flex: 1;
}
/* 头部区域 */
.header-section {
background: linear-gradient(135deg, #D4A574 0%, #B8895A 100%);
padding: 60rpx 32rpx;
align-items: center;
}
.company-logo {
width: 140rpx;
height: 140rpx;
background-color: #ffffff;
border-radius: 50%;
align-items: center;
justify-content: center;
margin-bottom: 24rpx;
}
.logo-text {
font-size: 36rpx;
font-weight: 600;
color: #D4A574;
}
.company-name {
font-size: 40rpx;
font-weight: 600;
color: #ffffff;
margin-bottom: 12rpx;
}
.company-slogan {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.8);
}
/* 通用section */
.section {
padding: 0 24rpx;
margin-bottom: 24rpx;
}
/* 公司介绍 */
.intro-card {
background-color: #ffffff;
border-radius: 16rpx;
padding: 32rpx;
}
.intro-text {
font-size: 28rpx;
color: #606266;
line-height: 48rpx;
}
/* 优势 */
.features-grid {
flex-direction: row;
flex-wrap: wrap;
background-color: #ffffff;
border-radius: 16rpx;
padding: 16rpx;
}
.feature-item {
width: 50%;
padding: 24rpx;
align-items: center;
}
.feature-icon-bg {
width: 80rpx;
height: 80rpx;
background-color: #D4A574;
border-radius: 50%;
align-items: center;
justify-content: center;
margin-bottom: 16rpx;
}
.feature-icon {
font-size: 40rpx;
color: #ffffff;
}
.feature-title {
font-size: 30rpx;
font-weight: 600;
color: #333333;
margin-bottom: 8rpx;
}
.feature-desc {
font-size: 24rpx;
color: #909399;
}
/* 联系方式 */
.contact-card {
background-color: #ffffff;
border-radius: 16rpx;
}
.contact-item {
flex-direction: row;
align-items: center;
padding: 32rpx;
border-bottom-width: 1rpx;
border-bottom-style: solid;
border-bottom-color: #EBEEF5;
}
.contact-item:last-child {
border-bottom-width: 0;
}
.contact-icon-bg {
width: 72rpx;
height: 72rpx;
background-color: #FDF6EE;
border-radius: 50%;
align-items: center;
justify-content: center;
margin-right: 24rpx;
}
.contact-icon {
font-size: 36rpx;
}
.contact-info {
flex: 1;
}
.contact-label {
font-size: 24rpx;
color: #909399;
margin-bottom: 8rpx;
}
.contact-value {
font-size: 30rpx;
color: #333333;
}
.contact-arrow {
font-size: 36rpx;
color: #909399;
}
/* 底部间距 */
.bottom-space {
height: 60rpx;
}
</style>

View File

@@ -0,0 +1,489 @@
<template>
<view class="page">
<scroll-view class="page-scroll" scroll-y>
<!-- 表单头部 -->
<view class="header-section">
<text class="header-title">预约翻新服务</text>
<text class="header-desc">填写以下信息,我们会尽快与您联系</text>
</view>
<!-- 表单内容 -->
<view class="form-section">
<!-- 姓名 -->
<view class="form-item">
<text class="form-label">
<text class="required">*</text>
您的姓名
</text>
<input
class="form-input"
type="text"
v-model="formData.userName"
placeholder="请输入您的姓名"
placeholder-class="placeholder"
/>
</view>
<!-- 电话 -->
<view class="form-item">
<text class="form-label">
<text class="required">*</text>
联系电话
</text>
<input
class="form-input"
type="number"
v-model="formData.phone"
placeholder="请输入您的手机号"
placeholder-class="placeholder"
maxlength="11"
/>
</view>
<!-- 地址 -->
<view class="form-item">
<text class="form-label">
<text class="required">*</text>
您的地址
</text>
<input
class="form-input"
type="text"
v-model="formData.address"
placeholder="请输入详细地址"
placeholder-class="placeholder"
/>
</view>
<!-- 沙发类型 -->
<view class="form-item">
<text class="form-label">沙发类型</text>
<view class="type-grid">
<view
class="type-item"
:class="{ 'type-active': formData.sofaType == item.id }"
v-for="item in sofaTypes"
:key="item.id"
@click="selectSofaType(item.id)"
>
<text
class="type-text"
:class="{ 'type-text-active': formData.sofaType == item.id }"
>{{ item.name }}</text>
</view>
</view>
</view>
<!-- 问题描述 -->
<view class="form-item">
<text class="form-label">问题描述</text>
<textarea
class="form-textarea"
v-model="formData.problem"
placeholder="请描述沙发的问题(如:皮面开裂、褪色、塌陷等)"
placeholder-class="placeholder"
maxlength="500"
></textarea>
<text class="textarea-count">{{ formData.problem.length }}/500</text>
</view>
<!-- 上传图片 -->
<view class="form-item">
<text class="form-label">上传图片(可选)</text>
<view class="upload-grid">
<view
class="upload-item"
v-for="(item, index) in imageList"
:key="index"
>
<image class="upload-image" :src="item" mode="aspectFill"></image>
<view class="upload-delete" @click="deleteImage(index)">
<text class="delete-icon">×</text>
</view>
</view>
<view class="upload-add" v-if="imageList.length < 9" @click="chooseImage">
<text class="add-icon">+</text>
<text class="add-text">添加图片</text>
</view>
</view>
<text class="upload-tip">最多可上传9张图片</text>
</view>
</view>
<!-- 底部间距 -->
<view class="bottom-space"></view>
</scroll-view>
<!-- 提交按钮 -->
<view class="submit-bar">
<view class="submit-btn" @click="handleSubmit">
<text class="submit-text">提交预约</text>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { submitBooking } from '@/api/index.uts'
import { SOFA_CATEGORIES } from '@/utils/config.uts'
// 表单类型
type FormData = {
userName : string
phone : string
address : string
sofaType : string
problem : string
}
// 沙发类型
type SofaType = {
id : string
name : string
}
// 表单数据
const formData = ref<FormData>({
userName: '',
phone: '',
address: '',
sofaType: '',
problem: ''
})
// 图片列表
const imageList = ref<string[]>([])
// 沙发类型列表
const sofaTypes = ref<SofaType[]>([])
// 提交中
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 selectSofaType = (id : string) => {
formData.value.sofaType = id
}
// 选择图片
const chooseImage = () => {
uni.chooseImage({
count: 9 - imageList.value.length,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
imageList.value = [...imageList.value, ...res.tempFilePaths]
}
})
}
// 删除图片
const deleteImage = (index : number) => {
imageList.value.splice(index, 1)
}
// 验证表单
const validateForm = () : boolean => {
if (formData.value.userName.trim() == '') {
uni.showToast({
title: '请输入您的姓名',
icon: 'none'
})
return false
}
const phone = formData.value.phone.trim()
if (phone == '') {
uni.showToast({
title: '请输入联系电话',
icon: 'none'
})
return false
}
// 简单验证手机号
if (phone.length != 11) {
uni.showToast({
title: '请输入正确的手机号',
icon: 'none'
})
return false
}
if (formData.value.address.trim() == '') {
uni.showToast({
title: '请输入您的地址',
icon: 'none'
})
return false
}
return true
}
// 提交预约
const handleSubmit = async () => {
if (!validateForm()) return
if (submitting.value) return
submitting.value = true
try {
const data = {
userName: formData.value.userName,
phone: formData.value.phone,
address: formData.value.address,
sofaType: formData.value.sofaType,
problem: formData.value.problem,
images: imageList.value
} as UTSJSONObject
const res = await submitBooking(data)
const result = res.data as UTSJSONObject
uni.showModal({
title: '预约成功',
content: result['message'] as string,
showCancel: false,
success: () => {
// 返回上一页或首页
uni.navigateBack({
fail: () => {
uni.switchTab({
url: '/pages/index/index'
})
}
})
}
})
} catch (e) {
console.error('提交预约失败', e)
uni.showToast({
title: '提交失败,请重试',
icon: 'none'
})
}
submitting.value = false
}
onLoad(() => {
initSofaTypes()
})
</script>
<style lang="scss">
.page {
flex: 1;
background-color: #f5f5f5;
}
.page-scroll {
flex: 1;
}
/* 头部 */
.header-section {
background: linear-gradient(135deg, #D4A574 0%, #B8895A 100%);
padding: 48rpx 32rpx;
align-items: center;
}
.header-title {
font-size: 40rpx;
font-weight: 600;
color: #ffffff;
margin-bottom: 12rpx;
}
.header-desc {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.8);
}
/* 表单区域 */
.form-section {
background-color: #ffffff;
margin: 24rpx;
border-radius: 16rpx;
padding: 32rpx;
}
.form-item {
margin-bottom: 32rpx;
}
.form-label {
font-size: 28rpx;
font-weight: 600;
color: #333333;
margin-bottom: 16rpx;
}
.required {
color: #F56C6C;
margin-right: 4rpx;
}
.form-input {
background-color: #f5f5f5;
border-radius: 12rpx;
padding: 24rpx;
font-size: 28rpx;
height:40px;
}
.placeholder {
color: #C0C4CC;
}
/* 沙发类型选择 */
.type-grid {
flex-direction: row;
flex-wrap: wrap;
}
.type-item {
padding: 16rpx 32rpx;
background-color: #f5f5f5;
border-radius: 8rpx;
margin-right: 16rpx;
margin-bottom: 16rpx;
}
.type-active {
background-color: #D4A574;
}
.type-text {
font-size: 26rpx;
color: #606266;
}
.type-text-active {
color: #ffffff;
}
/* 文本域 */
.form-textarea {
background-color: #f5f5f5;
border-radius: 12rpx;
padding: 24rpx;
font-size: 28rpx;
height: 200rpx;
width: 100%;
}
.textarea-count {
font-size: 24rpx;
color: #909399;
text-align: right;
margin-top: 8rpx;
}
/* 图片上传 */
.upload-grid {
flex-direction: row;
flex-wrap: wrap;
}
.upload-item {
width: 200rpx;
height: 200rpx;
margin-right: 16rpx;
margin-bottom: 16rpx;
position: relative;
}
.upload-image {
width: 100%;
height: 100%;
border-radius: 12rpx;
}
.upload-delete {
position: absolute;
top: -16rpx;
right: -16rpx;
width: 40rpx;
height: 40rpx;
background-color: #F56C6C;
border-radius: 50%;
align-items: center;
justify-content: center;
}
.delete-icon {
font-size: 28rpx;
color: #ffffff;
}
.upload-add {
width: 200rpx;
height: 200rpx;
background-color: #f5f5f5;
border-radius: 12rpx;
align-items: center;
justify-content: center;
border-width: 2rpx;
border-style: dashed;
border-color: #DCDFE6;
}
.add-icon {
font-size: 48rpx;
color: #909399;
}
.add-text {
font-size: 24rpx;
color: #909399;
margin-top: 8rpx;
}
.upload-tip {
font-size: 24rpx;
color: #909399;
margin-top: 16rpx;
}
/* 底部间距 */
.bottom-space {
height: 160rpx;
}
/* 提交按钮 */
.submit-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #ffffff;
padding: 24rpx 32rpx;
padding-bottom: 48rpx;
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.submit-btn {
background-color: #D4A574;
border-radius: 999rpx;
padding: 28rpx 0;
align-items: center;
}
.submit-text {
font-size: 32rpx;
font-weight: 600;
color: #ffffff;
}
</style>

View File

@@ -0,0 +1,438 @@
<template>
<view class="page">
<scroll-view class="page-scroll" scroll-y>
<!-- 图片画廊 -->
<view class="gallery-section">
<swiper
class="gallery-swiper"
circular
indicator-dots
indicator-color="rgba(255,255,255,0.5)"
indicator-active-color="#ffffff"
>
<swiper-item v-for="(item, index) in caseDetail.afterImages" :key="index">
<image
class="gallery-image"
:src="item"
mode="aspectFill"
@click="previewImages(index)"
></image>
</swiper-item>
</swiper>
<view class="gallery-tag">
<text class="gallery-tag-text">{{ caseDetail.categoryName }}</text>
</view>
</view>
<!-- 基本信息 -->
<view class="info-section">
<text class="case-title">{{ caseDetail.title }}</text>
<view class="case-meta">
<text class="case-price">{{ caseDetail.price }}</text>
<view class="case-stats">
<text class="stat-text">👁 {{ caseDetail.views }}</text>
<text class="stat-text">❤ {{ caseDetail.likes }}</text>
</view>
</view>
</view>
<!-- 翻新前后对比 -->
<view class="compare-section">
<before-after
:beforeImage="caseDetail.beforeImages[0] || ''"
:afterImage="caseDetail.afterImages[0] || ''"
:showTitle="true"
></before-after>
</view>
<!-- 详细信息 -->
<view class="detail-section">
<view class="detail-header">
<text class="detail-title">翻新详情</text>
</view>
<view class="detail-list">
<view class="detail-item">
<text class="detail-label">使用材质</text>
<text class="detail-value">{{ caseDetail.material }}</text>
</view>
<view class="detail-item">
<text class="detail-label">翻新工期</text>
<text class="detail-value">{{ caseDetail.duration }}</text>
</view>
<view class="detail-item">
<text class="detail-label">完成日期</text>
<text class="detail-value">{{ caseDetail.createTime }}</text>
</view>
</view>
<view class="detail-desc">
<text class="desc-title">案例描述</text>
<text class="desc-content">{{ caseDetail.description }}</text>
</view>
</view>
<!-- 底部间距 -->
<view class="bottom-space"></view>
</scroll-view>
<!-- 底部操作栏 -->
<view class="bottom-bar">
<view class="bar-left">
<view class="bar-btn" @click="handleFavorite">
<text class="bar-icon">{{ isFavorite ? '❤️' : '🤍' }}</text>
<text class="bar-label">收藏</text>
</view>
<view class="bar-btn" @click="handleShare">
<text class="bar-icon">📤</text>
<text class="bar-label">分享</text>
</view>
</view>
<view class="bar-right">
<view class="contact-btn" @click="handleContact">
<text class="contact-text">在线咨询</text>
</view>
<view class="booking-btn" @click="goToBooking">
<text class="booking-text">立即预约</text>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { getCaseDetail } from '@/api/index.uts'
// 案例详情类型
type CaseDetail = {
id : string
title : string
category : string
categoryName : string
beforeImages : string[]
afterImages : string[]
description : string
material : string
duration : string
price : string
views : number
likes : number
createTime : string
}
// 案例ID
const caseId = ref('')
// 案例详情
const caseDetail = ref<CaseDetail>({
id: '',
title: '',
category: '',
categoryName: '',
beforeImages: [],
afterImages: [],
description: '',
material: '',
duration: '',
price: '',
views: 0,
likes: 0,
createTime: ''
})
// 是否收藏
const isFavorite = ref(false)
// 获取案例详情
const fetchCaseDetail = async () => {
try {
const res = await getCaseDetail(caseId.value)
const data = res.data as UTSJSONObject
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
})
} catch (e) {
console.error('获取案例详情失败', e)
}
}
// 预览图片
const previewImages = (index : number) => {
uni.previewImage({
current: index,
urls: caseDetail.value.afterImages
})
}
// 收藏
const handleFavorite = () => {
isFavorite.value = !isFavorite.value
uni.showToast({
title: isFavorite.value ? '已收藏' : '已取消收藏',
icon: 'none'
})
}
// 分享
const handleShare = () => {
// #ifdef MP-WEIXIN
// 微信小程序触发分享
// #endif
uni.showToast({
title: '分享功能开发中',
icon: 'none'
})
}
// 在线咨询
const handleContact = () => {
uni.makePhoneCall({
phoneNumber: '400-888-8888',
fail: () => {
uni.showToast({
title: '拨打电话失败',
icon: 'none'
})
}
})
}
// 预约
const goToBooking = () => {
uni.navigateTo({
url: '/pages/booking/index'
})
}
onLoad((options : OnLoadOptions) => {
caseId.value = options['id'] ?? ''
if (caseId.value != '') {
fetchCaseDetail()
}
})
</script>
<style lang="scss">
.page {
flex: 1;
background-color: #f5f5f5;
}
.page-scroll {
flex: 1;
}
/* 图片画廊 */
.gallery-section {
position: relative;
}
.gallery-swiper {
height: 500rpx;
}
.gallery-image {
width: 100%;
height: 100%;
}
.gallery-tag {
position: absolute;
top: 24rpx;
left: 24rpx;
background-color: rgba(212, 165, 116, 0.9);
padding: 8rpx 20rpx;
border-radius: 8rpx;
}
.gallery-tag-text {
font-size: 24rpx;
color: #ffffff;
}
/* 基本信息 */
.info-section {
background-color: #ffffff;
padding: 32rpx;
margin-bottom: 24rpx;
}
.case-title {
font-size: 36rpx;
font-weight: 600;
color: #333333;
margin-bottom: 24rpx;
}
.case-meta {
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.case-price {
font-size: 40rpx;
font-weight: 600;
color: #D4A574;
}
.case-stats {
flex-direction: row;
}
.stat-text {
font-size: 26rpx;
color: #909399;
margin-left: 24rpx;
}
/* 对比区域 */
.compare-section {
padding: 0 24rpx;
}
/* 详细信息 */
.detail-section {
background-color: #ffffff;
margin: 24rpx;
border-radius: 16rpx;
padding: 32rpx;
}
.detail-header {
margin-bottom: 24rpx;
}
.detail-title {
font-size: 32rpx;
font-weight: 600;
color: #333333;
}
.detail-list {
margin-bottom: 32rpx;
}
.detail-item {
flex-direction: row;
justify-content: space-between;
padding: 16rpx 0;
border-bottom-width: 1rpx;
border-bottom-style: solid;
border-bottom-color: #EBEEF5;
}
.detail-label {
font-size: 28rpx;
color: #909399;
}
.detail-value {
font-size: 28rpx;
color: #333333;
}
.detail-desc {
padding-top: 16rpx;
}
.desc-title {
font-size: 28rpx;
font-weight: 600;
color: #333333;
margin-bottom: 16rpx;
}
.desc-content {
font-size: 28rpx;
color: #606266;
line-height: 48rpx;
}
/* 底部间距 */
.bottom-space {
height: 160rpx;
}
/* 底部操作栏 */
.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
flex-direction: row;
align-items: center;
justify-content: space-between;
background-color: #ffffff;
padding: 16rpx 24rpx;
padding-bottom: 32rpx;
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.bar-left {
flex-direction: row;
}
.bar-btn {
align-items: center;
padding: 0 24rpx;
}
.bar-icon {
font-size: 40rpx;
}
.bar-label {
font-size: 22rpx;
color: #606266;
margin-top: 4rpx;
}
.bar-right {
flex-direction: row;
flex: 1;
justify-content: flex-end;
}
.contact-btn {
background-color: #f5f5f5;
padding: 20rpx 40rpx;
border-radius: 999rpx;
margin-right: 16rpx;
}
.contact-text {
font-size: 28rpx;
color: #606266;
}
.booking-btn {
background-color: #D4A574;
padding: 20rpx 48rpx;
border-radius: 999rpx;
}
.booking-text {
font-size: 28rpx;
font-weight: 600;
color: #ffffff;
}
</style>

View File

@@ -0,0 +1,272 @@
<template>
<view class="page">
<!-- 分类筛选 -->
<view class="category-bar">
<scroll-view class="category-scroll" scroll-x>
<view class="category-list">
<view
class="category-item"
:class="{ 'category-active': currentCategory == item.id }"
v-for="item in categories"
:key="item.id"
@click="selectCategory(item.id)"
>
<text
class="category-text"
:class="{ 'category-text-active': currentCategory == item.id }"
>{{ item.name }}</text>
</view>
</view>
</scroll-view>
</view>
<!-- 案例列表 -->
<scroll-view
class="case-scroll"
scroll-y
@scrolltolower="loadMore"
>
<view class="case-list">
<case-card
v-for="item in caseList"
:key="item.id"
:caseData="item"
@click="goToDetail"
></case-card>
</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 && caseList.length == 0">
<text class="empty-icon">📭</text>
<text class="empty-text">暂无相关案例</text>
</view>
<!-- 底部间距 -->
<view class="bottom-space"></view>
</scroll-view>
</view>
</template>
<script setup lang="uts">
import { getCaseList } from '@/api/index.uts'
import { SOFA_CATEGORIES, PAGE_SIZE } from '@/utils/config.uts'
// 分类类型
type CategoryItem = {
id : string
name : string
}
// 案例类型
type CaseItem = {
id : string
title : string
category : string
categoryName : string
coverImage : string
material : string
duration : string
price : string
views : number
likes : number
}
// 分类列表
const categories = ref<CategoryItem[]>([])
// 当前分类
const currentCategory = ref('all')
// 案例列表
const caseList = ref<CaseItem[]>([])
// 分页
const page = ref(1)
const total = ref(0)
// 加载状态
const loading = ref(false)
const noMore = ref(false)
// 初始化分类
const initCategories = () => {
categories.value = SOFA_CATEGORIES.map((item) : CategoryItem => {
return {
id: item.id,
name: item.name
} as CategoryItem
})
}
// 选择分类
const selectCategory = (id : string) => {
if (currentCategory.value == id) return
currentCategory.value = id
page.value = 1
caseList.value = []
noMore.value = false
fetchCaseList()
}
// 获取案例列表
const fetchCaseList = async () => {
if (loading.value || noMore.value) return
loading.value = true
try {
const params = {
category: currentCategory.value,
page: page.value,
pageSize: PAGE_SIZE
} 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
}
} catch (e) {
console.error('获取案例列表失败', e)
}
loading.value = false
}
// 加载更多
const loadMore = () => {
if (!noMore.value && !loading.value) {
page.value++
fetchCaseList()
}
}
// 跳转详情
const goToDetail = (id : string) => {
uni.navigateTo({
url: `/pages/cases/detail?id=${id}`
})
}
onLoad(() => {
initCategories()
fetchCaseList()
})
</script>
<style lang="scss">
.page {
flex: 1;
background-color: #f5f5f5;
}
/* 分类栏 */
.category-bar {
background-color: #ffffff;
padding: 24rpx 0;
border-bottom-width: 1rpx;
border-bottom-style: solid;
border-bottom-color: #EBEEF5;
}
.category-scroll {
flex-direction: row;
}
.category-scroll ::-webkit-scrollbar {
display: none;
}
.category-list {
flex-direction: row;
padding: 0 24rpx;
}
.category-item {
padding: 16rpx 32rpx;
margin-right: 16rpx;
border-radius: 999rpx;
background-color: #f5f5f5;
}
.category-active {
background-color: #D4A574;
}
.category-text {
font-size: 28rpx;
color: #606266;
}
.category-text-active {
color: #ffffff;
}
/* 案例列表 */
.case-scroll {
flex: 1;
}
.case-list {
padding: 24rpx;
}
/* 加载状态 */
.load-status {
padding: 32rpx 0;
align-items: center;
}
.load-text {
font-size: 26rpx;
color: #909399;
}
/* 空状态 */
.empty-state {
padding: 120rpx 0;
align-items: center;
}
.empty-icon {
font-size: 80rpx;
margin-bottom: 24rpx;
}
.empty-text {
font-size: 28rpx;
color: #909399;
}
/* 底部间距 */
.bottom-space {
height: 120rpx;
}
</style>

View File

@@ -0,0 +1,418 @@
<template>
<view class="page">
<!-- 自定义导航栏 -->
<nav-bar title="优艺家沙发翻新"></nav-bar>
<scroll-view class="page-content" scroll-y>
<!-- 轮播图 -->
<view class="banner-section">
<swiper
class="banner-swiper"
circular
autoplay
indicator-dots
indicator-color="rgba(255,255,255,0.5)"
indicator-active-color="#ffffff"
>
<swiper-item v-for="item in bannerList" :key="item.id">
<image
class="banner-image"
:src="item.image"
mode="aspectFill"
@click="handleBannerClick(item)"
></image>
</swiper-item>
</swiper>
</view>
<!-- 服务入口 -->
<view class="service-section">
<view class="service-grid">
<service-card
v-for="item in serviceTypes"
:key="item.id"
:id="item.id"
:name="item.name"
:icon="item.icon"
@click="handleServiceClick"
></service-card>
</view>
</view>
<!-- 公司优势 -->
<view class="advantage-section">
<view class="advantage-list">
<view class="advantage-item" v-for="item in advantages" :key="item.title">
<text class="advantage-icon">{{ item.icon }}</text>
<view class="advantage-info">
<text class="advantage-title">{{ item.title }}</text>
<text class="advantage-desc">{{ item.desc }}</text>
</view>
</view>
</view>
</view>
<!-- 热门案例 -->
<view class="case-section">
<section-header
title="热门案例"
:showMore="true"
@more="goToCaseList"
></section-header>
<view class="case-list">
<case-card
v-for="item in hotCases"
:key="item.id"
:caseData="item"
@click="goToCaseDetail"
></case-card>
</view>
</view>
<!-- 预约入口 -->
<view class="booking-section" @click="goToBooking">
<view class="booking-content">
<view class="booking-left">
<text class="booking-title">免费上门评估</text>
<text class="booking-desc">专业师傅免费上门,为您的沙发量身定制翻新方案</text>
</view>
<view class="booking-btn">
<text class="booking-btn-text">立即预约</text>
</view>
</view>
</view>
<!-- 底部间距 -->
<view class="bottom-space"></view>
</scroll-view>
</view>
</template>
<script setup lang="uts">
import { getBanners, getHotCases } from '@/api/index.uts'
import { SERVICE_TYPES } from '@/utils/config.uts'
// 轮播图类型
type BannerItem = {
id : string
image : string
title : string
link : string
}
// 案例类型
type CaseItem = {
id : string
title : string
category : string
categoryName : string
coverImage : string
material : string
duration : string
price : string
views : number
likes : number
}
// 服务类型
type ServiceType = {
id : string
name : string
icon : string
}
// 优势类型
type AdvantageItem = {
icon : string
title : string
desc : string
}
// 轮播图数据
const bannerList = ref<BannerItem[]>([])
// 服务类型
const serviceTypes = ref<ServiceType[]>([])
// 热门案例
const hotCases = ref<CaseItem[]>([])
// 公司优势
const advantages = ref<AdvantageItem[]>([
{ icon: '👨‍🔧', title: '专业团队', desc: '10年+经验' },
{ icon: '✅', title: '品质保证', desc: '质保2年' },
{ icon: '🚗', title: '上门服务', desc: '免费评估' },
{ 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 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
})
} catch (e) {
console.error('获取轮播图失败', e)
}
}
// 获取热门案例
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
})
} catch (e) {
console.error('获取热门案例失败', e)
}
}
// 轮播图点击
const handleBannerClick = (item : BannerItem) => {
if (item.link != '') {
uni.navigateTo({
url: item.link,
fail: () => {
uni.switchTab({
url: item.link
})
}
})
}
}
// 服务点击
const handleServiceClick = (id : string) => {
uni.switchTab({
url: '/pages/service/index'
})
}
// 查看更多案例
const goToCaseList = () => {
uni.switchTab({
url: '/pages/cases/list'
})
}
// 查看案例详情
const goToCaseDetail = (id : string) => {
uni.navigateTo({
url: `/pages/cases/detail?id=${id}`
})
}
// 去预约
const goToBooking = () => {
uni.navigateTo({
url: '/pages/booking/index'
})
}
// API测试方法
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'
})
} catch (error) {
console.error('API测试失败:', error)
uni.showToast({
title: 'API测试失败',
icon: 'error'
})
}
}
onLoad(() => {
initServiceTypes()
fetchBanners()
fetchHotCases()
// 开发环境下自动进行API测试
// testAPI()
})
</script>
<style lang="scss">
.page {
flex: 1;
background-color: #f5f5f5;
}
.page-content {
flex: 1;
}
/* 轮播图 */
.banner-section {
padding: 24rpx;
}
.banner-swiper {
height: 320rpx;
border-radius: 16rpx;
overflow: hidden;
}
.banner-image {
width: 100%;
height: 100%;
}
/* 服务入口 */
.service-section {
padding: 0 24rpx 24rpx;
}
.service-grid {
flex-direction: row;
justify-content: space-between;
background-color: #ffffff;
border-radius: 16rpx;
padding: 24rpx 16rpx;
}
/* 公司优势 */
.advantage-section {
padding: 0 24rpx 24rpx;
}
.advantage-list {
flex-direction: row;
flex-wrap: wrap;
background-color: #ffffff;
border-radius: 16rpx;
padding: 24rpx;
}
.advantage-item {
width: 50%;
flex-direction: row;
align-items: center;
padding: 16rpx 0;
}
.advantage-icon {
font-size: 48rpx;
margin-right: 16rpx;
}
.advantage-info {
flex: 1;
}
.advantage-title {
font-size: 28rpx;
font-weight: 600;
color: #333333;
margin-bottom: 4rpx;
}
.advantage-desc {
font-size: 24rpx;
color: #909399;
}
/* 热门案例 */
.case-section {
padding: 0 24rpx;
}
.case-list {
padding-bottom: 24rpx;
}
/* 预约入口 */
.booking-section {
margin: 0 24rpx 24rpx;
background: linear-gradient(135deg, #D4A574 0%, #B8895A 100%);
border-radius: 16rpx;
padding: 32rpx;
}
.booking-content {
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.booking-left {
flex: 1;
}
.booking-title {
font-size: 34rpx;
font-weight: 600;
color: #ffffff;
margin-bottom: 8rpx;
}
.booking-desc {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.8);
}
.booking-btn {
background-color: #ffffff;
padding: 16rpx 32rpx;
border-radius: 999rpx;
margin-left: 24rpx;
}
.booking-btn-text {
font-size: 28rpx;
font-weight: 600;
color: #D4A574;
}
/* 底部间距 */
.bottom-space {
height: 120rpx;
}
</style>

View File

@@ -0,0 +1,537 @@
<template>
<view class="page">
<scroll-view class="page-scroll" scroll-y>
<!-- 顶部横幅 -->
<view class="banner">
<view class="banner-content">
<text class="banner-title">专业沙发翻新服务</text>
<text class="banner-desc">让旧沙发焕发新生</text>
</view>
</view>
<!-- 服务类型 -->
<view class="section">
<section-header title="服务类型"></section-header>
<view class="service-grid">
<view
class="service-item"
v-for="item in serviceTypes"
:key="item.id"
@click="handleServiceClick(item)"
>
<view class="service-icon-bg">
<text class="service-icon">{{ item.emoji }}</text>
</view>
<text class="service-name">{{ item.name }}</text>
<text class="service-desc">{{ item.desc }}</text>
</view>
</view>
</view>
<!-- 服务流程 -->
<view class="section">
<section-header title="服务流程"></section-header>
<view class="process-list">
<view class="process-item" v-for="(item, index) in processList" :key="item.step">
<view class="process-step">
<text class="step-num">{{ item.step }}</text>
</view>
<view class="process-content">
<text class="process-title">{{ item.title }}</text>
<text class="process-desc">{{ item.description }}</text>
</view>
<view class="process-line" v-if="index < processList.length - 1"></view>
</view>
</view>
</view>
<!-- 材质说明 -->
<view class="section">
<section-header title="材质说明"></section-header>
<view class="material-list">
<view class="material-item" v-for="item in materials" :key="item.name">
<view class="material-header">
<text class="material-name">{{ item.name }}</text>
<text class="material-price">{{ item.price }}</text>
</view>
<text class="material-desc">{{ item.desc }}</text>
<view class="material-tags">
<text class="material-tag" v-for="tag in item.tags" :key="tag">{{ tag }}</text>
</view>
</view>
</view>
</view>
<!-- 常见问题 -->
<view class="section">
<section-header title="常见问题"></section-header>
<view class="faq-list">
<view
class="faq-item"
v-for="(item, index) in faqList"
:key="index"
@click="toggleFaq(index)"
>
<view class="faq-header">
<text class="faq-question">{{ item.question }}</text>
<text class="faq-arrow">{{ item.expanded ? '' : '+' }}</text>
</view>
<view class="faq-answer" v-if="item.expanded">
<text class="faq-answer-text">{{ item.answer }}</text>
</view>
</view>
</view>
</view>
<!-- 底部间距 -->
<view class="bottom-space"></view>
</scroll-view>
<!-- 底部预约按钮 -->
<view class="bottom-bar">
<view class="bar-info">
<text class="bar-title">免费上门评估</text>
<text class="bar-desc">专业师傅为您量身定制方案</text>
</view>
<view class="bar-btn" @click="goToBooking">
<text class="bar-btn-text">立即预约</text>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { getServiceProcess } from '@/api/index.uts'
// 服务类型
type ServiceType = {
id : string
name : string
desc : string
emoji : string
}
// 流程类型
type ProcessItem = {
step : number
title : string
description : string
}
// 材质类型
type MaterialItem = {
name : string
price : string
desc : string
tags : string[]
}
// FAQ类型
type FaqItem = {
question : string
answer : string
expanded : boolean
}
// 服务类型数据
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 processList = ref<ProcessItem[]>([])
// 材质说明
const materials = ref<MaterialItem[]>([
{
name: '头层牛皮',
price: '¥800-1500/平',
desc: '采用进口头层牛皮,质地柔软,透气性好,使用寿命长',
tags: ['进口原料', '透气舒适', '耐用耐磨']
},
{
name: '二层牛皮',
price: '¥400-800/平',
desc: '经济实惠的选择,经过特殊处理后外观与头层相近',
tags: ['性价比高', '外观精美', '易打理']
},
{
name: '科技布',
price: '¥200-500/平',
desc: '新型环保面料,防水防污,清洁方便,触感舒适',
tags: ['防水防污', '易清洁', '环保健康']
},
{
name: '棉麻布艺',
price: '¥150-400/平',
desc: '天然面料,透气舒适,适合追求自然风格的客户',
tags: ['天然环保', '透气清爽', '风格多样']
}
])
// 常见问题
const faqList = ref<FaqItem[]>([
{
question: '翻新需要多长时间?',
answer: '根据沙发的大小和翻新程度不同一般需要3-10个工作日。局部修复1-3天整体翻新5-10天。',
expanded: false
},
{
question: '翻新后能保证多久?',
answer: '我们提供2年质保服务质保期内如有非人为损坏免费维修。头层牛皮正常使用可达10年以上。',
expanded: false
},
{
question: '可以上门取送吗?',
answer: '是的,我们提供免费上门取送服务(市区范围内),郊区会收取少量运输费用。',
expanded: false
},
{
question: '如何报价?',
answer: '我们提供免费上门评估服务,师傅会根据沙发的实际情况给出详细报价,价格透明无隐形消费。',
expanded: false
},
{
question: '定金和尾款如何支付?',
answer: '确认订单后支付30%定金,翻新完成验收满意后支付尾款。支持微信、支付宝、银行转账等多种方式。',
expanded: false
}
])
// 获取服务流程
const fetchServiceProcess = 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
})
} catch (e) {
console.error('获取服务流程失败', e)
}
}
// 切换FAQ展开
const toggleFaq = (index : number) => {
faqList.value[index].expanded = !faqList.value[index].expanded
}
// 服务点击
const handleServiceClick = (item : ServiceType) => {
uni.navigateTo({
url: '/pages/booking/index'
})
}
// 去预约
const goToBooking = () => {
uni.navigateTo({
url: '/pages/booking/index'
})
}
onLoad(() => {
fetchServiceProcess()
})
</script>
<style lang="scss">
.page {
flex: 1;
background-color: #f5f5f5;
}
.page-scroll {
flex: 1;
}
/* 顶部横幅 */
.banner {
height: 280rpx;
background: linear-gradient(135deg, #D4A574 0%, #B8895A 100%);
justify-content: center;
align-items: center;
}
.banner-content {
align-items: center;
}
.banner-title {
font-size: 40rpx;
font-weight: 600;
color: #ffffff;
margin-bottom: 16rpx;
}
.banner-desc {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.8);
}
/* 通用section */
.section {
padding: 0 24rpx;
margin-bottom: 24rpx;
}
/* 服务类型 */
.service-grid {
flex-direction: row;
flex-wrap: wrap;
background-color: #ffffff;
border-radius: 16rpx;
padding: 16rpx;
}
.service-item {
width: 50%;
padding: 24rpx;
align-items: center;
}
.service-icon-bg {
width: 100rpx;
height: 100rpx;
background-color: #FDF6EE;
border-radius: 50%;
align-items: center;
justify-content: center;
margin-bottom: 16rpx;
}
.service-icon {
font-size: 48rpx;
}
.service-name {
font-size: 30rpx;
font-weight: 600;
color: #333333;
margin-bottom: 8rpx;
}
.service-desc {
font-size: 24rpx;
color: #909399;
}
/* 服务流程 */
.process-list {
background-color: #ffffff;
border-radius: 16rpx;
padding: 32rpx;
}
.process-item {
flex-direction: row;
position: relative;
padding-bottom: 32rpx;
}
.process-step {
width: 48rpx;
height: 48rpx;
background-color: #D4A574;
border-radius: 50%;
align-items: center;
justify-content: center;
margin-right: 24rpx;
z-index: 1;
}
.step-num {
font-size: 24rpx;
font-weight: 600;
color: #ffffff;
}
.process-content {
flex: 1;
}
.process-title {
font-size: 30rpx;
font-weight: 600;
color: #333333;
margin-bottom: 8rpx;
}
.process-desc {
font-size: 26rpx;
color: #909399;
}
.process-line {
position: absolute;
left: 22rpx;
top: 56rpx;
width: 4rpx;
height: 60rpx;
background-color: #E8C9A8;
}
/* 材质说明 */
.material-list {
background-color: #ffffff;
border-radius: 16rpx;
}
.material-item {
padding: 32rpx;
border-bottom-width: 1rpx;
border-bottom-style: solid;
border-bottom-color: #EBEEF5;
}
.material-item:last-child {
border-bottom-width: 0;
}
.material-header {
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 12rpx;
}
.material-name {
font-size: 30rpx;
font-weight: 600;
color: #333333;
}
.material-price {
font-size: 28rpx;
color: #D4A574;
font-weight: 600;
}
.material-desc {
font-size: 26rpx;
color: #606266;
margin-bottom: 16rpx;
line-height: 40rpx;
}
.material-tags {
flex-direction: row;
flex-wrap: wrap;
}
.material-tag {
font-size: 22rpx;
color: #D4A574;
background-color: #FDF6EE;
padding: 8rpx 16rpx;
border-radius: 8rpx;
margin-right: 16rpx;
margin-bottom: 8rpx;
}
/* 常见问题 */
.faq-list {
background-color: #ffffff;
border-radius: 16rpx;
}
.faq-item {
padding: 32rpx;
border-bottom-width: 1rpx;
border-bottom-style: solid;
border-bottom-color: #EBEEF5;
}
.faq-item:last-child {
border-bottom-width: 0;
}
.faq-header {
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.faq-question {
font-size: 28rpx;
font-weight: 600;
color: #333333;
flex: 1;
}
.faq-arrow {
font-size: 36rpx;
color: #909399;
margin-left: 16rpx;
}
.faq-answer {
margin-top: 16rpx;
padding-top: 16rpx;
border-top-width: 1rpx;
border-top-style: dashed;
border-top-color: #EBEEF5;
}
.faq-answer-text {
font-size: 26rpx;
color: #606266;
line-height: 44rpx;
}
/* 底部间距 */
.bottom-space {
height: 160rpx;
}
/* 底部操作栏 */
.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index:2;
flex-direction: row;
align-items: center;
justify-content: space-between;
background-color: #ffffff;
padding: 20rpx 32rpx;
padding-bottom: 40rpx;
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.bar-info {
flex: 1;
}
.bar-title {
font-size: 30rpx;
font-weight: 600;
color: #333333;
}
.bar-desc {
font-size: 24rpx;
color: #909399;
margin-top: 4rpx;
}
.bar-btn {
background-color: #D4A574;
padding: 24rpx 56rpx;
border-radius: 999rpx;
}
.bar-btn-text {
font-size: 30rpx;
font-weight: 600;
color: #ffffff;
}
</style>

View File

@@ -0,0 +1,488 @@
<template>
<view class="page">
<scroll-view class="page-scroll" scroll-y>
<!-- 用户信息 -->
<view class="user-section">
<view class="user-card">
<view class="avatar-wrapper">
<image
class="user-avatar"
:src="userInfo.avatar || '/static/images/default-avatar.png'"
mode="aspectFill"
></image>
</view>
<view class="user-info">
<text class="user-name">{{ userInfo.nickName || '点击登录' }}</text>
<text class="user-phone" v-if="userInfo.phone">{{ userInfo.phone }}</text>
</view>
<view class="user-action" @click="handleLogin" v-if="!isLoggedIn">
<text class="action-text">登录</text>
</view>
</view>
</view>
<!-- 功能菜单 -->
<view class="menu-section">
<view class="menu-group">
<view class="menu-item" @click="goToBookingList">
<view class="menu-left">
<text class="menu-icon">📋</text>
<text class="menu-text">我的预约</text>
</view>
<view class="menu-right">
<text class="menu-badge" v-if="bookingCount > 0">{{ bookingCount }}</text>
<text class="menu-arrow"></text>
</view>
</view>
<view class="menu-item" @click="goToFavorites">
<view class="menu-left">
<text class="menu-icon">❤️</text>
<text class="menu-text">我的收藏</text>
</view>
<view class="menu-right">
<text class="menu-badge" v-if="favoriteCount > 0">{{ favoriteCount }}</text>
<text class="menu-arrow"></text>
</view>
</view>
</view>
<view class="menu-group">
<view class="menu-item" @click="goToAbout">
<view class="menu-left">
<text class="menu-icon">🏠</text>
<text class="menu-text">关于我们</text>
</view>
<view class="menu-right">
<text class="menu-arrow"></text>
</view>
</view>
<view class="menu-item" @click="callService">
<view class="menu-left">
<text class="menu-icon">📞</text>
<text class="menu-text">联系客服</text>
</view>
<view class="menu-right">
<text class="menu-value">400-888-8888</text>
<text class="menu-arrow"></text>
</view>
</view>
<view class="menu-item" @click="openFeedback">
<view class="menu-left">
<text class="menu-icon">💬</text>
<text class="menu-text">意见反馈</text>
</view>
<view class="menu-right">
<text class="menu-arrow"></text>
</view>
</view>
</view>
<view class="menu-group">
<view class="menu-item" @click="checkUpdate">
<view class="menu-left">
<text class="menu-icon">🔄</text>
<text class="menu-text">检查更新</text>
</view>
<view class="menu-right">
<text class="menu-value">v1.0.0</text>
<text class="menu-arrow"></text>
</view>
</view>
<view class="menu-item" @click="clearCache">
<view class="menu-left">
<text class="menu-icon">🧹</text>
<text class="menu-text">清除缓存</text>
</view>
<view class="menu-right">
<text class="menu-value">{{ cacheSize }}</text>
<text class="menu-arrow"></text>
</view>
</view>
</view>
</view>
<!-- 退出登录 -->
<view class="logout-section" v-if="isLoggedIn">
<view class="logout-btn" @click="handleLogout">
<text class="logout-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 UserInfo = {
id : string
nickName : string
avatar : string
phone : string
}
// 用户信息
const userInfo = ref<UserInfo>({
id: '',
nickName: '',
avatar: '',
phone: ''
})
// 是否登录
const isLoggedIn = ref(false)
// 预约数量
const bookingCount = ref(0)
// 收藏数量
const favoriteCount = ref(0)
// 缓存大小
const cacheSize = ref('0KB')
// 检查登录状态
const checkLoginStatus = () => {
const token = uni.getStorageSync(STORAGE_KEYS.TOKEN) as string
isLoggedIn.value = token != ''
if (isLoggedIn.value) {
// 获取用户信息
const info = uni.getStorageSync(STORAGE_KEYS.USER_INFO) as UTSJSONObject | null
if (info != null) {
userInfo.value = {
id: (info['id'] ?? '') as string,
nickName: (info['nickName'] ?? '') as string,
avatar: (info['avatar'] ?? '') as string,
phone: (info['phone'] ?? '') as string
} as UserInfo
}
}
}
// 获取缓存大小
const getCacheSize = () => {
uni.getStorageInfo({
success: (res) => {
const size = res.currentSize
if (size < 1024) {
cacheSize.value = `${size}KB`
} else {
cacheSize.value = `${(size / 1024).toFixed(2)}MB`
}
}
})
}
// 登录
const handleLogin = () => {
// #ifdef MP-WEIXIN
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'
})
},
fail: () => {
uni.showToast({
title: '登录失败',
icon: 'none'
})
}
})
// #endif
// #ifndef MP-WEIXIN
uni.showToast({
title: '请在微信小程序中登录',
icon: 'none'
})
// #endif
}
// 退出登录
const handleLogout = () => {
uni.showModal({
title: '提示',
content: '确定要退出登录吗?',
success: (res) => {
if (res.confirm) {
uni.removeStorageSync(STORAGE_KEYS.TOKEN)
uni.removeStorageSync(STORAGE_KEYS.USER_INFO)
isLoggedIn.value = false
userInfo.value = {
id: '',
nickName: '',
avatar: '',
phone: ''
} as UserInfo
uni.showToast({
title: '已退出登录',
icon: 'success'
})
}
}
})
}
// 跳转预约列表
const goToBookingList = () => {
uni.showToast({
title: '功能开发中',
icon: 'none'
})
}
// 跳转收藏列表
const goToFavorites = () => {
uni.showToast({
title: '功能开发中',
icon: 'none'
})
}
// 关于我们
const goToAbout = () => {
uni.navigateTo({
url: '/pages/about/index'
})
}
// 联系客服
const callService = () => {
uni.makePhoneCall({
phoneNumber: '400-888-8888',
fail: () => {
uni.showToast({
title: '拨打电话失败',
icon: 'none'
})
}
})
}
// 意见反馈
const openFeedback = () => {
// #ifdef MP-WEIXIN
// 微信小程序可以使用feedback
// #endif
uni.showToast({
title: '功能开发中',
icon: 'none'
})
}
// 检查更新
const checkUpdate = () => {
uni.showToast({
title: '已是最新版本',
icon: 'success'
})
}
// 清除缓存
const clearCache = () => {
uni.showModal({
title: '提示',
content: '确定要清除缓存吗?',
success: (res) => {
if (res.confirm) {
uni.clearStorage({
success: () => {
cacheSize.value = '0KB'
uni.showToast({
title: '清除成功',
icon: 'success'
})
}
})
}
}
})
}
onShow(() => {
checkLoginStatus()
getCacheSize()
})
</script>
<style lang="scss">
.page {
flex: 1;
background-color: #f5f5f5;
}
.page-scroll {
flex: 1;
}
/* 用户信息 */
.user-section {
background: linear-gradient(135deg, #D4A574 0%, #B8895A 100%);
padding: 48rpx 32rpx 64rpx;
}
.user-card {
flex-direction: row;
align-items: center;
}
.avatar-wrapper {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
overflow: hidden;
border-width: 4rpx;
border-style: solid;
border-color: rgba(255, 255, 255, 0.5);
}
.user-avatar {
width: 100%;
height: 100%;
}
.user-info {
flex: 1;
margin-left: 24rpx;
}
.user-name {
font-size: 36rpx;
font-weight: 600;
color: #ffffff;
}
.user-phone {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.8);
margin-top: 8rpx;
}
.user-action {
background-color: rgba(255, 255, 255, 0.2);
padding: 16rpx 32rpx;
border-radius: 999rpx;
}
.action-text {
font-size: 28rpx;
color: #ffffff;
}
/* 菜单区域 */
.menu-section {
margin-top: -32rpx;
padding: 0 24rpx;
}
.menu-group {
background-color: #ffffff;
border-radius: 16rpx;
margin-bottom: 24rpx;
overflow: hidden;
}
.menu-item {
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 32rpx;
border-bottom-width: 1rpx;
border-bottom-style: solid;
border-bottom-color: #EBEEF5;
}
.menu-item:last-child {
border-bottom-width: 0;
}
.menu-left {
flex-direction: row;
align-items: center;
}
.menu-icon {
font-size: 40rpx;
margin-right: 24rpx;
}
.menu-text {
font-size: 30rpx;
color: #333333;
}
.menu-right {
flex-direction: row;
align-items: center;
}
.menu-value {
font-size: 26rpx;
color: #909399;
margin-right: 8rpx;
}
.menu-badge {
background-color: #F56C6C;
color: #ffffff;
font-size: 22rpx;
padding: 4rpx 12rpx;
border-radius: 999rpx;
margin-right: 8rpx;
}
.menu-arrow {
font-size: 32rpx;
color: #909399;
}
/* 退出登录 */
.logout-section {
padding: 24rpx;
}
.logout-btn {
background-color: #ffffff;
border-radius: 16rpx;
padding: 28rpx 0;
align-items: center;
}
.logout-text {
font-size: 30rpx;
color: #F56C6C;
}
/* 底部间距 */
.bottom-space {
height: 120rpx;
}
</style>