This commit is contained in:
zpc 2026-02-23 19:19:35 +08:00
parent a48a4affba
commit 6b60e6fbb4
9 changed files with 223 additions and 103 deletions

View File

@ -21,12 +21,24 @@ public class CreatePlannerRequest
[MaxLength(500, ErrorMessage = "头像URL长度不能超过500个字符")] [MaxLength(500, ErrorMessage = "头像URL长度不能超过500个字符")]
public string Avatar { get; set; } = null!; public string Avatar { get; set; } = null!;
/// <summary>
/// 职称
/// </summary>
[MaxLength(200, ErrorMessage = "职称长度不能超过200个字符")]
public string? Title { get; set; }
/// <summary> /// <summary>
/// 简介 /// 简介
/// </summary> /// </summary>
[MaxLength(1000, ErrorMessage = "简介长度不能超过1000个字符")] [MaxLength(1000, ErrorMessage = "简介长度不能超过1000个字符")]
public string? Introduction { get; set; } public string? Introduction { get; set; }
/// <summary>
/// 标签(逗号分隔)
/// </summary>
[MaxLength(500, ErrorMessage = "标签长度不能超过500个字符")]
public string? Tags { get; set; }
/// <summary> /// <summary>
/// 咨询价格 /// 咨询价格
/// </summary> /// </summary>

View File

@ -20,11 +20,21 @@ public class PlannerDto
/// </summary> /// </summary>
public string Avatar { get; set; } = null!; public string Avatar { get; set; } = null!;
/// <summary>
/// 职称
/// </summary>
public string? Title { get; set; }
/// <summary> /// <summary>
/// 简介 /// 简介
/// </summary> /// </summary>
public string? Introduction { get; set; } public string? Introduction { get; set; }
/// <summary>
/// 标签(逗号分隔)
/// </summary>
public string? Tags { get; set; }
/// <summary> /// <summary>
/// 咨询价格 /// 咨询价格
/// </summary> /// </summary>

View File

@ -27,12 +27,24 @@ public class UpdatePlannerRequest
[MaxLength(500, ErrorMessage = "头像URL长度不能超过500个字符")] [MaxLength(500, ErrorMessage = "头像URL长度不能超过500个字符")]
public string Avatar { get; set; } = null!; public string Avatar { get; set; } = null!;
/// <summary>
/// 职称
/// </summary>
[MaxLength(200, ErrorMessage = "职称长度不能超过200个字符")]
public string? Title { get; set; }
/// <summary> /// <summary>
/// 简介 /// 简介
/// </summary> /// </summary>
[MaxLength(1000, ErrorMessage = "简介长度不能超过1000个字符")] [MaxLength(1000, ErrorMessage = "简介长度不能超过1000个字符")]
public string? Introduction { get; set; } public string? Introduction { get; set; }
/// <summary>
/// 标签(逗号分隔)
/// </summary>
[MaxLength(500, ErrorMessage = "标签长度不能超过500个字符")]
public string? Tags { get; set; }
/// <summary> /// <summary>
/// 咨询价格 /// 咨询价格
/// </summary> /// </summary>

View File

@ -144,7 +144,9 @@ public class PlannerService : IPlannerService
Id = p.Id, Id = p.Id,
Name = p.Name, Name = p.Name,
Avatar = p.Avatar, Avatar = p.Avatar,
Title = p.Title,
Introduction = p.Introduction, Introduction = p.Introduction,
Tags = p.Tags,
Price = p.Price, Price = p.Price,
Sort = p.Sort, Sort = p.Sort,
Status = p.Status, Status = p.Status,
@ -171,7 +173,9 @@ public class PlannerService : IPlannerService
{ {
Name = request.Name, Name = request.Name,
Avatar = request.Avatar, Avatar = request.Avatar,
Title = request.Title,
Introduction = request.Introduction, Introduction = request.Introduction,
Tags = request.Tags,
Price = request.Price, Price = request.Price,
Sort = request.Sort, Sort = request.Sort,
Status = request.Status, Status = request.Status,
@ -209,7 +213,9 @@ public class PlannerService : IPlannerService
planner.Name = request.Name; planner.Name = request.Name;
planner.Avatar = request.Avatar; planner.Avatar = request.Avatar;
planner.Title = request.Title;
planner.Introduction = request.Introduction; planner.Introduction = request.Introduction;
planner.Tags = request.Tags;
planner.Price = request.Price; planner.Price = request.Price;
planner.Sort = request.Sort; planner.Sort = request.Sort;
planner.Status = request.Status; planner.Status = request.Status;

View File

@ -41,7 +41,9 @@ public class PlannerService : IPlannerService
Id = p.Id, Id = p.Id,
Name = p.Name, Name = p.Name,
Avatar = p.Avatar, Avatar = p.Avatar,
Title = p.Title,
Introduction = p.Introduction, Introduction = p.Introduction,
Tags = p.Tags,
Price = p.Price Price = p.Price
}) })
.ToListAsync(); .ToListAsync();

View File

@ -30,12 +30,24 @@ public class Planner
[MaxLength(500)] [MaxLength(500)]
public string Avatar { get; set; } = null!; public string Avatar { get; set; } = null!;
/// <summary>
/// 职称
/// </summary>
[MaxLength(200)]
public string? Title { get; set; }
/// <summary> /// <summary>
/// 简介 /// 简介
/// </summary> /// </summary>
[MaxLength(1000)] [MaxLength(1000)]
public string? Introduction { get; set; } public string? Introduction { get; set; }
/// <summary>
/// 标签(逗号分隔)
/// </summary>
[MaxLength(500)]
public string? Tags { get; set; }
/// <summary> /// <summary>
/// 咨询价格 /// 咨询价格
/// </summary> /// </summary>

View File

@ -20,11 +20,21 @@ public class PlannerDto
/// </summary> /// </summary>
public string Avatar { get; set; } = null!; public string Avatar { get; set; } = null!;
/// <summary>
/// 职称
/// </summary>
public string? Title { get; set; }
/// <summary> /// <summary>
/// 简介 /// 简介
/// </summary> /// </summary>
public string? Introduction { get; set; } public string? Introduction { get; set; }
/// <summary>
/// 标签(逗号分隔)
/// </summary>
public string? Tags { get; set; }
/// <summary> /// <summary>
/// 价格 /// 价格
/// </summary> /// </summary>

View File

@ -57,9 +57,9 @@
@click="handleNavigationClick(item)" @click="handleNavigationClick(item)"
> >
<!-- 即将上线标签 --> <!-- 即将上线标签 -->
<view v-if="item.status === 2" class="coming-soon-tag"> <!-- <view v-if="item.status === 2" class="coming-soon-tag">
<text>即将上线</text> <text>即将上线</text>
</view> </view> -->
<image <image
:src="item.imageUrl" :src="item.imageUrl"
mode="aspectFit" mode="aspectFit"
@ -225,7 +225,7 @@ function handleNavigationClick(item) {
// 线 // 线
if (item.status === 2) { if (item.status === 2) {
uni.showToast({ uni.showToast({
title: '该功能暂未开放', title: '该功能即将上线',
icon: 'none', icon: 'none',
duration: 2000 duration: 2000
}) })

View File

@ -5,12 +5,14 @@
<!-- 页面内容 --> <!-- 页面内容 -->
<view class="page-content"> <view class="page-content">
<!-- 提示文字 -->
<view class="page-tip">点击选择您喜欢的规划师</view>
<!-- 加载状态 --> <!-- 加载状态 -->
<Loading v-if="pageLoading" type="page" :loading="true" /> <Loading v-if="pageLoading" type="page" :loading="true" />
<!-- 规划师列表 --> <!-- 规划师列表 -->
<view v-else class="planner-list"> <view v-else class="planner-list">
<!-- 有数据时显示列表 -->
<template v-if="plannerList.length > 0"> <template v-if="plannerList.length > 0">
<view <view
class="planner-card" class="planner-card"
@ -18,46 +20,58 @@
:key="item.id || index" :key="item.id || index"
@click="handlePlannerClick(item)" @click="handlePlannerClick(item)"
> >
<!-- 规划师头像 --> <!-- 上半部分头像 + 姓名职称 + 标签 -->
<view class="planner-avatar"> <view class="card-top">
<image <!-- 头像 -->
:src="item.avatar || item.photo || '/static/ic_empty.png'" <view class="planner-avatar">
mode="aspectFill" <image
class="avatar-image" :src="item.avatar || item.photo || '/static/ic_empty.png'"
@error="handleAvatarError(index)" mode="aspectFill"
/> class="avatar-image"
@error="handleAvatarError(index)"
/>
</view>
<!-- 右侧信息 -->
<view class="planner-info">
<!-- 姓名 | 职称 -->
<view class="planner-name-row">
<text class="planner-name">{{ item.name || '规划师' }}</text>
<text v-if="item.title" class="planner-title">|{{ item.title }}</text>
</view>
<!-- 标签列表 -->
<view class="planner-tags" v-if="parseTags(item.tags).length > 0">
<view
class="tag-item"
v-for="(tag, tagIndex) in parseTags(item.tags)"
:key="tagIndex"
>
{{ tag }}
</view>
</view>
<!-- 无标签时显示简介 -->
<view class="planner-intro" v-else-if="item.introduction">
{{ item.introduction }}
</view>
</view>
</view> </view>
<!-- 规划师信息 --> <!-- 下半部分价格 + 按钮 -->
<view class="planner-info"> <view class="card-bottom">
<!-- 姓名 -->
<view class="planner-name">{{ item.name || '规划师' }}</view>
<!-- 介绍 -->
<view class="planner-intro">{{ item.intro || item.introduction || '暂无介绍' }}</view>
<!-- 价格 -->
<view class="planner-price"> <view class="planner-price">
<text class="price-symbol">¥</text> <text class="price-symbol">¥</text>
<text class="price-value">{{ formatPrice(item.price) }}</text> <text class="price-value">{{ formatPrice(item.price) }}</text>
<text class="price-unit">/</text>
</view> </view>
</view> <view class="select-btn">点击选择</view>
<!-- 预约按钮 -->
<view class="planner-action">
<view class="book-btn">预约</view>
</view> </view>
</view> </view>
</template> </template>
<!-- 无数据时显示空状态 --> <!-- 空状态 -->
<view v-else class="empty-state"> <view v-else class="empty-state">
<image <image src="/static/ic_empty.png" mode="aspectFit" class="empty-icon" />
src="/static/ic_empty.png"
mode="aspectFit"
class="empty-icon"
/>
<text class="empty-text">暂无规划师</text> <text class="empty-text">暂无规划师</text>
</view> </view>
</view> </view>
@ -71,7 +85,7 @@
<script setup> <script setup>
/** /**
* 规划师列表页面 * 规划师列表页面
* 展示规划师卡片照片姓名介绍价格 * 展示规划师卡片照片姓名职称标签价格
*/ */
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { onPullDownRefresh } from '@dcloudio/uni-app' import { onPullDownRefresh } from '@dcloudio/uni-app'
@ -91,7 +105,6 @@ async function loadPlannerList() {
try { try {
const res = await getPlannerList() const res = await getPlannerList()
if (res && res.code === 0 && res.data) { if (res && res.code === 0 && res.data) {
//
if (Array.isArray(res.data)) { if (Array.isArray(res.data)) {
plannerList.value = res.data plannerList.value = res.data
} else if (res.data.list && Array.isArray(res.data.list)) { } else if (res.data.list && Array.isArray(res.data.list)) {
@ -102,27 +115,32 @@ async function loadPlannerList() {
} }
} catch (error) { } catch (error) {
console.error('加载规划师列表失败:', error) console.error('加载规划师列表失败:', error)
uni.showToast({ uni.showToast({ title: '加载失败,请稍后重试', icon: 'none' })
title: '加载失败,请稍后重试',
icon: 'none'
})
} finally { } finally {
pageLoading.value = false pageLoading.value = false
} }
} }
/**
* 解析标签字符串为数组
* @param {string} tags - 逗号分隔的标签字符串
* @returns {string[]}
*/
function parseTags(tags) {
if (!tags) return []
return tags.split(',').map(t => t.trim()).filter(Boolean)
}
/** /**
* 格式化价格 * 格式化价格
* @param {number|string} price - 价格 * @param {number|string} price - 价格
* @returns {string} 格式化后的价格 * @returns {string}
*/ */
function formatPrice(price) { function formatPrice(price) {
if (price === undefined || price === null) return '0' if (price === undefined || price === null) return '0'
const num = Number(price) const num = Number(price)
if (isNaN(num)) return '0' if (isNaN(num)) return '0'
//
if (Number.isInteger(num)) return num.toString() if (Number.isInteger(num)) return num.toString()
//
return num.toFixed(2) return num.toFixed(2)
} }
@ -134,10 +152,9 @@ function handleAvatarError(index) {
} }
/** /**
* 处理规划师点击 * 处理规划师点击 - 跳转到预约页面
*/ */
function handlePlannerClick(item) { function handlePlannerClick(item) {
//
uni.navigateTo({ uni.navigateTo({
url: `/pages/planner/book/index?plannerId=${item.id}&plannerName=${encodeURIComponent(item.name || '')}` url: `/pages/planner/book/index?plannerId=${item.id}&plannerName=${encodeURIComponent(item.name || '')}`
}) })
@ -162,73 +179,117 @@ onMounted(() => {
<style lang="scss" scoped> <style lang="scss" scoped>
@import '@/styles/variables.scss'; @import '@/styles/variables.scss';
//
$orange-color: #E8740E;
$orange-light: #FFF8F0;
$orange-gradient-start: #FFF5EB;
$orange-gradient-end: #FFFFFF;
.planner-list-page { .planner-list-page {
min-height: 100vh; min-height: 100vh;
background-color: $bg-color; background-color: $bg-color;
} }
//
.page-content { .page-content {
padding: $spacing-lg; padding: $spacing-lg;
padding-bottom: env(safe-area-inset-bottom); padding-bottom: env(safe-area-inset-bottom);
} }
//
.page-tip {
text-align: center;
font-size: $font-size-md;
color: $text-secondary;
margin-bottom: $spacing-lg;
}
// //
.planner-list { .planner-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: $spacing-md; gap: $spacing-lg;
} }
// // -
.planner-card { .planner-card {
display: flex; background: linear-gradient(135deg, $orange-gradient-start 0%, $orange-gradient-end 60%);
align-items: center; border-radius: $border-radius-xl;
background-color: $bg-white;
border-radius: $border-radius-lg;
padding: $spacing-lg; padding: $spacing-lg;
box-shadow: $shadow-sm; box-shadow: $shadow-sm;
border-left: 6rpx solid $orange-color;
&:active { &:active {
opacity: 0.9; opacity: 0.9;
} }
} }
// // +
.card-top {
display: flex;
align-items: flex-start;
}
//
.planner-avatar { .planner-avatar {
flex-shrink: 0; flex-shrink: 0;
width: 160rpx; width: 160rpx;
height: 160rpx; height: 180rpx;
border-radius: $border-radius-md; border-radius: $border-radius-md;
overflow: hidden; overflow: hidden;
margin-right: $spacing-md; margin-right: $spacing-md;
.avatar-image { .avatar-image {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
} }
// //
.planner-info { .planner-info {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
}
// +
.planner-name-row {
display: flex; display: flex;
flex-direction: column; align-items: baseline;
flex-wrap: wrap;
margin-bottom: $spacing-sm;
}
.planner-name {
font-size: $font-size-xl;
font-weight: $font-weight-bold;
color: $text-color;
}
.planner-title {
font-size: $font-size-sm;
color: $text-secondary;
margin-left: 8rpx;
}
//
.planner-tags {
display: flex;
flex-wrap: wrap;
gap: $spacing-xs; gap: $spacing-xs;
} }
// .tag-item {
.planner-name { display: inline-flex;
font-size: $font-size-lg; align-items: center;
font-weight: $font-weight-medium; padding: 6rpx 16rpx;
color: $text-color; font-size: $font-size-xs;
overflow: hidden; color: $orange-color;
text-overflow: ellipsis; background-color: rgba(232, 116, 14, 0.08);
border: 1rpx solid rgba(232, 116, 14, 0.2);
border-radius: $border-radius-sm;
white-space: nowrap; white-space: nowrap;
} }
// //
.planner-intro { .planner-intro {
font-size: $font-size-sm; font-size: $font-size-sm;
color: $text-secondary; color: $text-secondary;
@ -239,52 +300,47 @@ onMounted(() => {
line-height: 1.5; line-height: 1.5;
} }
// // +
.card-bottom {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: $spacing-md;
}
//
.planner-price { .planner-price {
display: flex; display: flex;
align-items: baseline; align-items: baseline;
margin-top: $spacing-xs;
.price-symbol { .price-symbol {
font-size: $font-size-sm; font-size: $font-size-lg;
color: $error-color; color: $orange-color;
font-weight: $font-weight-medium; font-weight: $font-weight-bold;
} }
.price-value { .price-value {
font-size: $font-size-xl; font-size: 44rpx;
color: $error-color; color: $orange-color;
font-weight: $font-weight-bold; font-weight: $font-weight-bold;
margin-left: 2rpx; margin-left: 2rpx;
} }
.price-unit {
font-size: $font-size-xs;
color: $text-placeholder;
margin-left: 4rpx;
}
} }
// //
.planner-action { .select-btn {
flex-shrink: 0; display: flex;
margin-left: $spacing-md; align-items: center;
justify-content: center;
.book-btn { padding: 16rpx 48rpx;
display: flex; background-color: $orange-color;
align-items: center; color: $text-white;
justify-content: center; font-size: $font-size-md;
width: 120rpx; font-weight: $font-weight-medium;
height: 64rpx; border-radius: $border-radius-round;
background-color: $primary-color;
color: $text-white; &:active {
font-size: $font-size-md; opacity: 0.85;
font-weight: $font-weight-medium;
border-radius: $border-radius-round;
&:active {
background-color: $primary-dark;
}
} }
} }
@ -295,13 +351,13 @@ onMounted(() => {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 200rpx 0; padding: 200rpx 0;
.empty-icon { .empty-icon {
width: 200rpx; width: 200rpx;
height: 200rpx; height: 200rpx;
margin-bottom: $spacing-lg; margin-bottom: $spacing-lg;
} }
.empty-text { .empty-text {
font-size: $font-size-md; font-size: $font-size-md;
color: $text-placeholder; color: $text-placeholder;