feat: 20250919

This commit is contained in:
mac·ufutx 2025-09-19 19:01:15 +08:00
parent ddd8f091e0
commit 6853a60112
5 changed files with 311 additions and 153 deletions

2
components.d.ts vendored
View File

@ -9,6 +9,8 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
ElButton: typeof import('element-plus/es')['ElButton']
ElCarousel: typeof import('element-plus/es')['ElCarousel']
ElCarouselItem: typeof import('element-plus/es')['ElCarouselItem']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem']

122
src/data/caseImage.ts Normal file
View File

@ -0,0 +1,122 @@
export interface RealCase {
username: string
case: string // 案例描述
avatar: string // 用户头像
result: string // 案例结果
caseImage: string // 新增:案例相关图片(如场景图、产品使用图等)
}
export const newRealCasesWithImage: RealCase[] = [
{
username: '林婉清',
case: '高中班主任用「课堂专注度」功能分析学生听课数据AI识别走神高频时段并推送互动课件。商城买的护嗓麦克风收音清晰课后用「作业批改助手」自动统计错题率节省2小时工作时间。',
avatar: 'https://picsum.photos/id/601/200/200',
caseImage: 'https://images.health.ufutx.com/202509/18/f3ccdd27d2000e3f9255a7e3e2c48800.jpeg', // 课堂场景图(学生听课+课件屏幕)
result: '学生课堂走神率从35%降至12%班级月考平均分提升15分个人嗓子嘶哑频率减少80%。'
},
{
username: '陈浩宇',
case: '糖尿病患者用「血糖波动曲线」记录餐前餐后数据App智能提醒胰岛素注射时间。商城买的血糖仪免采血且误差0.5mmol/L同步推送低糖食谱和「餐后散步20分钟」计划。',
avatar: 'https://picsum.photos/id/602/200/200',
caseImage: 'https://images.health.ufutx.com/202509/18/156005c5baf40ff51a327f1c34f2975b.jpeg', // 血糖仪+低糖食谱场景图
result: '空腹血糖从8.5mmol/L降至6.2mmol/L低血糖发作次数从每月4次降至0次糖化血红蛋白从7.8%降至6.5%。'
},
{
username: '苏雨薇',
case: '留学生在海外用「翻译问诊」功能与当地医生沟通AI实时翻译症状描述。商城买的便携体温计支持多语言播报感冒时通过「海外药房导航」找到附近中文服务药店。',
avatar: 'https://picsum.photos/id/603/200/200',
caseImage: 'https://images.health.ufutx.com/202509/18/799bad5a3b514f096e69bbc4a7896cd9.jpeg', // 海外诊所+翻译App界面图
result: '就医沟通效率提升90%感冒康复周期缩短2天成功适配3种语言的医疗服务场景。'
},
{
username: '赵健峰',
case: '健身教练用「体成分分析」功能为学员定制增肌计划AI识别肌肉量薄弱部位。商城买的弹力带防滑且承重50kg通过「班级打卡」功能监督学员训练同步推送高蛋白食谱。',
avatar: 'https://picsum.photos/id/604/200/200',
caseImage: 'https://images.health.ufutx.com/202509/18/032b2cc936860b03048302d991c3498f.jpeg', // 健身房训练+弹力带使用图
result: '学员平均肌肉量增加3.2kg体脂率下降5.8%课程续费率从65%提升至92%。'
},
{
username: '吴欣怡',
case: '花店店主用「花粉过敏预警」功能调整进货品类避开高致敏花卉。商城买的保鲜喷雾延长玫瑰花期5天通过「会员健康标签」为过敏顾客推荐低敏花束减少退货率。',
avatar: 'https://picsum.photos/id/605/200/200',
caseImage: 'https://images.health.ufutx.com/202509/18/d0096ec6c83575373e3a21d129ff8fef.jpeg', // 花店陈列+低敏花束图
result: '顾客过敏投诉从每月8起降至1起花卉损耗率减少30%会员复购率提升45%。'
},
{
username: '马子轩',
case: '程序员用「久坐提醒」功能每50分钟弹窗拉伸提示App分析代码时长与颈椎压力关联。商城买的人体工学椅腰托可调节配合「眼疲劳监测」每2小时强制远眺同步记录睡眠质量。',
avatar: 'https://picsum.photos/id/606/200/200',
caseImage: 'https://images.health.ufutx.com/202509/18/28c03d3961c2e936cc6234f52d82e965.jpeg', // 程序员工位+工学椅+拉伸提醒图
result: '颈椎疼痛频率从每周5次降至1次视力从4.7恢复至4.9深度睡眠时间延长1.5小时。'
},
{
username: '郑思琪',
case: '哺乳期妈妈用「奶量预测」功能根据宝宝月龄调整吸奶时间AI识别供需失衡风险。商城买的吸奶器静音且吸力可调通过「辅食营养库」匹配宝宝过敏体质推送低敏食谱。',
avatar: 'https://picsum.photos/id/607/200/200',
caseImage: 'https://images.health.ufutx.com/202509/18/af9b82a1b9683d5734db895886c002c0.jpeg', // 母婴场景+吸奶器+辅食图
result: '奶量供需平衡率从60%提升至95%宝宝湿疹发作次数减少70%夜间连续睡眠时长从3小时增至6小时。'
},
{
username: '王浩明',
case: '退休老人用「语音记事」功能记录血压、用药时间App自动生成周健康报告。商城买的智能水杯提醒每日饮水1500ml通过「子女共享」功能让子女实时查看健康数据减少担忧。',
avatar: 'https://picsum.photos/id/608/200/200',
caseImage: 'https://images.health.ufutx.com/202509/18/59aa235ebd97e78a23210c5a618e3633.jpeg', // 老人居家+智能水杯+血压仪图
result: '每日饮水量达标率从40%提升至90%漏服药次数从每月6次降至0次子女远程关怀响应时间缩短至5分钟内。'
},
{
username: '李雨桐',
case: '舞蹈老师用「动作矫正」功能通过手机摄像头识别学员肢体角度AI标注错误动作。商城买的舞蹈把杆稳固且可折叠通过「课程回放」功能让学员课后复习重点标注纠错片段。',
avatar: 'https://picsum.photos/id/609/200/200',
caseImage: 'https://images.health.ufutx.com/202509/18/b89c4cc90e26a826ef04a7adfea8c40d.jpeg', // 舞蹈教室+把杆+手机矫正界面图
result: '学员动作标准率从55%提升至88%课程学习效率提升40%家长满意度评分从3.2分升至4.8分。'
},
{
username: '张博文',
case: '货车司机用「疲劳驾驶监测」通过方向盘震动提醒连续驾驶4小时强制休息。商城买的车载腰靠贴合腰椎曲线离线地图提前预警施工路段同步记录油耗与胎压数据。',
avatar: 'https://picsum.photos/id/610/200/200',
caseImage: 'https://images.health.ufutx.com/202509/18/734a4c7ff726c7e7aeadf15773e060ff.jpeg', // 货车驾驶舱+腰靠+导航界面图
result: '疲劳驾驶风险事件从每月3次降至0次油耗降低12%轮胎异常磨损率减少60%。'
},
{
username: '刘雅菲',
case: '鼻炎患者用「花粉浓度地图」避开高风险区域App推送每日鼻腔冲洗提醒。商城买的洗鼻器喷头柔软且水流可调通过「过敏原记录」识别尘螨过敏推荐防螨床品。',
avatar: 'https://picsum.photos/id/611/200/200',
caseImage: 'https://images.health.ufutx.com/202509/18/280a6001ab93c8eea794f0fdf899c4ad.jpeg', // 洗鼻器+花粉地图+防螨床品图
result: '鼻炎发作频率从每周4次降至1次打喷嚏次数从每日20次减至5次睡眠质量评分从60分升至85分。'
},
{
username: '陈宇轩',
case: '咖啡店店主用「员工健康打卡」功能记录体温、核酸状态,到期自动提醒体检。商城买的手部消毒机感应灵敏,通过「客流健康分析」调整高峰时段员工排班,减少交叉感染风险。',
avatar: 'https://picsum.photos/id/612/200/200',
caseImage: 'https://images.health.ufutx.com/202509/18/c7b9fe05ece7c3068309677eb788d520.jpeg', // 咖啡店吧台+消毒机+打卡界面图
result: '员工健康异常率从8%降至1%顾客投诉率减少50%门店卫生评分从B级升至A级。'
},
{
username: '高思远',
case: '备考公务员用「专注计时」功能屏蔽娱乐软件AI分析学习效率高峰时段。商城买的护眼台灯可调节色温通过「错题本同步」功能整理高频考点推送相似题型练习。',
avatar: 'https://picsum.photos/id/613/200/200',
caseImage: 'https://images.health.ufutx.com/202509/18/45fca35bf2e29ddb2e49305da1c813d6.jpeg', // 学习桌+护眼台灯+错题本App图
result: '日均有效学习时长从5小时增至7.5小时错题重复率从40%降至15%笔试成绩提升28分。'
},
{
username: '周雨薇',
case: '早产儿妈妈用「生长曲线监测」每周记录宝宝体重、身高AI对比标准曲线预警异常。商城买的婴儿体重秤精度达10g通过「母乳成分分析」调整饮食提升乳汁营养密度。',
avatar: 'https://picsum.photos/id/614/200/200',
caseImage: 'https://images.health.ufutx.com/202509/18/15a7bfcf08d4a38fad6d798e02461165.jpeg', // 婴儿体重秤+生长曲线App+母乳检测图
result: '宝宝生长达标率从65%提升至98%纠正月龄后体重追平同龄婴儿医生随访满意度达100%。'
},
{
username: '吴浩明',
case: '工地安全员用「噪音监测」功能记录施工环境分贝超过85dB自动推送防护提醒。商城买的防噪音耳塞降噪率达35dB通过「安全培训视频」定期考核工人同步记录防护装备佩戴情况。',
avatar: 'https://picsum.photos/id/615/200/200',
caseImage: 'https://images.health.ufutx.com/202509/18/e47f1edd5ca04227b115fe4b47786b85.jpeg', // 工地场景+噪音监测仪+耳塞图
result: '工人听力损伤风险降低70%安全考核通过率从75%升至96%工伤事故次数减少80%。'
},
{
username: '郑雅琳',
case: '瑜伽教练用「呼吸节奏监测」功能纠正学员呼吸与动作配合AI识别憋气时段。商城买的瑜伽垫防滑系数达0.8,通过「会员体态报告」生成个性化矫正计划,推送居家练习视频。',
avatar: 'https://picsum.photos/id/616/200/200',
caseImage: 'https://images.health.ufutx.com/202509/18/72eec5023144990dc4b80e31d8a0a6e0.jpeg', // 瑜伽馆+瑜伽垫+呼吸监测App图
result: '学员呼吸配合准确率从50%提升至90%体态不良改善率达85%私教课程续费率提升50%。'
}
]

22
src/lib/device.ts Normal file
View File

@ -0,0 +1,22 @@
/**
*
* @returns boolean
*/
export const isMobile = (): boolean => {
// 1. UA 检测:匹配常见移动设备关键词
const mobileUA = /Android|iPhone|iPad|iPod|BlackBerry|Windows Phone|SymbianOS/i.test(navigator.userAgent)
// 2. 屏幕宽度辅助判断(适配部分平板/PC 小窗口场景)
const mobileWidth = window.innerWidth < 768
// 满足任一条件即视为移动端
return mobileUA || mobileWidth
}
/**
* H5
* @param mobileDomain 'm.xxx.com'
* @returns boolean
*/
export const isInMobilePage = (mobileDomain: string): boolean => {
return window.location.hostname === mobileDomain
}

View File

@ -94,7 +94,7 @@ const modules = [
icon: 'https://images.health.ufutx.com/202507/04/4a47a0db6e60853dedfcfdf08a5ca249.png'
}
]
const date = new Date()
// const date = new Date()
// 0
const isHover = ref<number>(0)

View File

@ -1,117 +1,104 @@
<template>
<section class="feedback-section">
<div class="feedback-title">真实客户案例</div>
<div class="feedback-desc">友福智能健康-用AI重塑人类健康未来</div>
<div class="feedback-list">
<Marquee
:duration="360"
:reverse="false"
repeatCount="3"
pause-on-hover
container-class="h-56 bg-white rounded-lg shadow-sm p-4"
:initial-offset="0"
<el-carousel
ref="carouselRef"
trigger="click"
height="460px"
arrow="always"
interval="5000"
@change="carouselChange"
>
<!-- 内容卡片直接作为Marquee的子元素不嵌套额外div -->
<div v-for="(item, index) in feedbackList[0]" :key="`${item.username}-${index}`" class="feedback-card">
<el-popover placement="right-end" :width="320" trigger="hover" popper-class="custom-popover">
<template #default>
<div class="_userinfo">
<div class="_userPic"><img :src="item.avatar" alt="avatar" class="avatar" /></div>
<div class="_username">{{ item.username }}</div>
<el-carousel-item v-for="(itemList, index) in feedbackList" :key="index" :name="index">
<div v-for="(item, idx) in itemList" :key="`${item.username}-${idx}`" class="feedback-card">
<div class="popover-container">
<div class="avatar-container">
<el-image
:ref="(el: any) => (imageRefs[idx] = el)"
:src="item.caseImage"
alt="certificate"
class="certificate-image"
:zoom-rate="1"
:preview-src-list="[item.caseImage]"
show-progress
preview-teleported
fit="contain"
:initial-index="idx"
/>
</div>
<div class="_comment">{{ item.case }}</div>
</template>
<template #reference>
<div class="popover-container">
<div class="avatar-container">
<img :src="item.avatar" alt="avatar" class="avatar" />
</div>
<div class="feedback-info">
<!-- <p class="username">{{ item.username }}</p>-->
<p class="comment">{{ item.result }}</p>
</div>
</div>
</template>
</el-popover>
</div>
</Marquee>
</div>
</div>
</el-carousel-item>
</el-carousel>
</div>
<div class="feedback-list" style="margin-top: 40px">
<Marquee
:duration="360"
:reverse="false"
repeatCount="3"
pause-on-hover
container-class="h-56 bg-white rounded-lg shadow-sm p-4"
:initial-offset="-300"
>
<!-- 内容卡片直接作为Marquee的子元素不嵌套额外div -->
<div v-for="(item, index) in feedbackList[1]" :key="`${item.username}-${index}`" class="feedback-card">
<el-popover placement="right-end" :width="320" trigger="hover" popper-class="custom-popover">
<template #default>
<div class="_userinfo">
<div class="_userPic"><img :src="item.avatar" alt="avatar" class="avatar" /></div>
<div class="_username">{{ item.username }}</div>
</div>
<div class="_comment">{{ item.case }}</div>
</template>
<template #reference>
<div class="popover-container">
<div class="avatar-container">
<img :src="item.avatar" alt="avatar" class="avatar" />
</div>
<div class="feedback-info">
<!-- <p class="username">{{ item.username }}</p>-->
<p class="comment">{{ item.result }}</p>
</div>
</div>
</template>
</el-popover>
</div>
</Marquee>
<div class="el-timeline__dot">
<div
v-for="(_, index) in feedbackList"
:key="index"
class="el-item__dot"
:class="{ 'el-item__dot--active': index === currentIndex }"
@click="handleSwitch(index)"
></div>
</div>
</section>
</template>
<script setup lang="ts">
// 使
// import TextGenerateEffect from '@/components/TextGenerateEffect.vue'
import Marquee from '@/components/Marquee.vue'
import { realCases } from '@/data/realCases.ts'
import { ref, reactive } from 'vue'
import type { CarouselInstance, ImageInstance } from 'element-plus'
import { newRealCasesWithImage } from '@/data/caseImage.ts'
// T
function splitArrayRandomly<T>(arr: T[], ratio = 0.5): [T[], T[]] {
//
//
type CaseItem = {
username: string
case: string
avatar: string
result: string
caseImage: string
}
//
const imageRefs = ref<(ImageInstance | null)[]>([])
// Carousel
const carouselRef = ref<CarouselInstance | null>(null)
//
const currentIndex = ref(0)
//
const handleSwitch = (target: number) => {
if (carouselRef.value) {
carouselRef.value.setActiveItem(target)
}
}
//
const carouselChange = (newIndex: number) => {
currentIndex.value = newIndex
}
// 3
function splitIntoGroupsOfThree<T>(arr: T[]): T[][] {
const shuffled = [...arr]
// Fisher-Yates
// Fisher-Yates
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
;[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]
}
// 50%
const splitIndex = Math.round(shuffled.length * ratio)
//
return [shuffled.slice(0, splitIndex), shuffled.slice(splitIndex)]
// 3
const groups: T[][] = []
for (let i = 0; i < shuffled.length; i += 3) {
groups.push(shuffled.slice(i, i + 3))
}
return groups
}
const feedbackList = reactive(splitArrayRandomly(realCases))
console.log('数组1:', feedbackList[0]) // : [3, 7, 2, 5]
//
const feedbackList = reactive<CaseItem[][]>(splitIntoGroupsOfThree(newRealCasesWithImage))
</script>
<style scoped lang="less">
//
@bg-color: #f9fbff;
@card-bg: #ffffff;
@radius: 12px;
@padding: 16px;
@gap: 50px;
@avatar-size: 60px;
@subtext-color: #999;
@transition: all 0.3s ease;
//
@bg-color: #f9fbff;
@card-bg: #ffffff;
@ -123,11 +110,11 @@ console.log('数组1:', feedbackList[0]) // 例如: [3, 7, 2, 5]
.feedback-section {
background-color: @bg-color;
padding: 0 0 100px 0; /* 改用标准padding写法避免自定义.px()可能的问题 */
padding: 0 0 100px 0;
text-align: center;
//background: bisque;
position: relative;
&&::before {
&::before {
content: '';
position: absolute;
top: 0;
@ -135,76 +122,87 @@ console.log('数组1:', feedbackList[0]) // 例如: [3, 7, 2, 5]
width: 60px;
z-index: 1;
pointer-events: none;
}
&&::before {
left: 100px;
background: linear-gradient(to right, #f9fbff, transparent);
}
.feedback-title {
font-size: 28px;
font-weight: 600;
padding: 100px 0 60px;
}
//
.feedback-title {
padding: 50px 0 0;
font-size: 32px;
font-weight: bold;
margin-bottom: 20px;
color: @text-color;
}
.feedback-desc {
font-size: 20px;
color: @text-color-secondary;
}
.el-timeline__dot {
width: auto;
display: inline-flex;
height: 36px;
padding: 8px 20px;
align-items: center;
gap: 20px;
border-radius: 100px;
background: #f5f7fe;
margin-top: 20px;
.el-item__dot {
width: 10px;
height: 10px;
border-radius: 100px;
opacity: 0.2;
background: var(--313-fa-8, #313fa8);
}
.el-item__dot--active {
width: 20px;
border-radius: 100px;
opacity: 1;
background: var(--313-fa-8, #313fa8);
}
}
.feedback-list {
//width: 100vw;
max-width: 1820px;
margin: 0 auto;
margin-left: 100px;
box-sizing: border-box;
//
padding: 10px;
display: flex;
flex-direction: row; /* 明确水平方向 */
flex-wrap: nowrap; /* 禁止换行 */
overflow: hidden; /* 隐藏超出容器的部分 */
flex-direction: row;
}
//
.feedback-card {
width: 362px; /* 固定宽度,确保能在一行放下多个 */
//max-width: 320px;
flex-shrink: 0; /* 禁止收缩,保证宽度不变 */
border-radius: 100px;
background: #f3f5f7;
//transition: @transition;
//box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); /* */
margin-right: 60px;
cursor: pointer; /* 提示可交互 */
transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); /* 卡片整体过渡 */
margin-top: 50px;
display: inline-block;
&:hover {
transform: translateY(-4px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
}
.popover-container {
padding: @padding;
display: flex; /* 卡片内部水平排列头像和文字 */
flex-direction: row; /* 明确水平方向 */
align-items: center;
width: 492px;
height: 352px;
display: inline-block;
margin-right: 30px;
}
.avatar-container {
flex-shrink: 0;
margin-right: 20px;
.avatar {
width: @avatar-size;
height: @avatar-size;
border-radius: 50%;
.avatar-container {
.certificate-image {
width: 100%;
height: 100%;
object-fit: cover;
border: 2px solid #f0f0f0;
}
}
.feedback-info {
text-align: left;
max-width: calc(100% - @avatar-size - 12px); /* 限制文本区域宽度 */
max-width: calc(100% - @avatar-size - 12px);
.username {
font-size: 16px;
font-weight: 500;
margin-bottom: 6px;
white-space: nowrap; /* 用户名不换行 */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
@ -214,18 +212,20 @@ console.log('数组1:', feedbackList[0]) // 例如: [3, 7, 2, 5]
color: @text-color;
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 2; /* 固定2行文本 */
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
/* 过渡动画重点控制max-height */
max-height: 42px; /* 14px*1.5*2=42px刚好2行高度 */
max-height: 42px;
transition: max-height 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
}
}
}
}
//
:deep(.el-image-viewer__canvas) {
height: 80vh;
}
@media (max-width: 768px) {
.feedback-card {
width: 100%;
@ -236,22 +236,17 @@ console.log('数组1:', feedbackList[0]) // 例如: [3, 7, 2, 5]
<style lang="less">
.custom-popover {
/* 修改圆角(核心) */
border-radius: 18px !important; /* 根据需求调整数值如8px、16px */
/* 可选:修改边框 */
border-radius: 18px !important;
border: 1px solid #e5e7eb !important;
/* 可选:修改阴影 */
box-shadow: 0 4px 16px rgba(79, 79, 79, 0.08) !important;
/* 可选:修改内部边距 */
padding: 22px !important;
._userinfo {
.flex();
display: flex;
align-items: center;
.mb(12px);
margin-bottom: 12px;
}
._userPic {
width: 62px;
height: 62px;
@ -259,16 +254,33 @@ console.log('数组1:', feedbackList[0]) // 例如: [3, 7, 2, 5]
overflow: hidden;
border: 4px solid #f3f5f7;
box-shadow: 0 0 16px rgba(79, 79, 79, 0.08);
.mr(10px);
margin-right: 10px;
}
._username {
font-size: 20px;
font-weight: 600;
color: @text-color;
}
._comment {
font-size: 16px;
color: @text-color-secondary;
}
}
.el-carousel {
width: 100%;
}
.demonstration {
color: var(--el-text-color-secondary);
}
.el-carousel__item h3 {
color: #475669;
opacity: 0.75;
margin: 0;
text-align: center;
}
</style>