feat(report): 添加报告相关组件和 API

- 新增报告 API 文件,定义了多个后端请求方法
- 实现了多个报告相关组件,包括体型选择、基因风格、面部色彩维度等
- 添加了自定义 Swiper 组件和标签页组件
- 新增了一些工具函数,如数字转中文、获取最大元素等
This commit is contained in:
liguigong
2025-09-15 10:57:17 +08:00
parent 677123e6fd
commit 918d89eeae
23 changed files with 6199 additions and 0 deletions
+5
View File
@@ -6,3 +6,8 @@ https://doc.iocoder.cn/quick-start-front/#_2-uni-app-%E5%95%86%E5%9F%8E%E7%A7%BB
开源地址: https://github.com/yudaocode/yudao-mall-uniapp?tab=readme-ov-file 开源地址: https://github.com/yudaocode/yudao-mall-uniapp?tab=readme-ov-file
群公告
【金山文档 | WPS云文档】 【美搭小程序模板】
https://www.kdocs.cn/l/cbQW5YSrFJP7
+12
View File
@@ -32,6 +32,18 @@
"group": "商城" "group": "商城"
} }
}, },
{
"path": "pages/report/index",
"style": {
"navigationBarTitleText": "报告",
"enablePullDownRefresh": true
},
"meta": {
"sync": true,
"title": "报告",
"group": "商城"
}
},
{ {
"path": "pages/index/category", "path": "pages/index/category",
"style": { "style": {
+52
View File
@@ -0,0 +1,52 @@
import { request } from "../../http/index.js";
let baseUrl = "https://llshop.zglvling.com/soapi/";
// let baseUrl = "https://test.zglvling.com/soapi/";
// const praiseTextApi = (params) =>
// request.post(baseUrl + "ai/praiseTexttwo", params);
const userFaceInoApi = (params) =>
request.post(baseUrl + "userinfo/get_user_face_info", params);
const hairstyleListApi = (params) =>
request.post(
"https://puton.meidaojia.com/api/v1/test/getHairDetails",
params
);
const exchangeHairApi = (params) =>
request.post("https://puton.meidaojia.com/api/v1/test/exchangeHair", params);
const exchangeHairHistoryByTaskId = (params) =>
request.post(
"https://puton.meidaojia.com/api/v1/test/exchangeHairHistoryByTaskId",
params
);
const wardrobeListApi = (params) =>
request.post(baseUrl + "wardrobe/clothes_list", params);
const getStyleApi = (params) =>
request.post(baseUrl + "userinfo/get_user_face_info", params);
const hairStyleListApi = (params) =>
request.post(
"https://puton.meidaojia.com/api/v1/test/getHairDetails",
params
);
const addHairApi = (params) =>
request.post(baseUrl + "userinfo/add_hair_data", params);
const getHairApi = (params) =>
request.post(baseUrl + "userinfo/get_hair_data", params);
export {
praiseTextApi,
userFaceInoApi,
hairstyleListApi,
exchangeHairApi,
exchangeHairHistoryByTaskId,
wardrobeListApi,
getStyleApi,
// goWXtApi,
// clothesListApi,
hairStyleListApi,
addHairApi,
getHairApi,
};
@@ -0,0 +1,363 @@
<template>
<view class="uni-margin-wrap">
<view class="flex">
<text>搭配趋势</text>
<img src="./arrow-icon.png" class="arrow-icon" />
</view>
<swiper
@animationfinish="animationfinish"
class="swiper"
circular
:indicator-dots="false"
:autoplay="true"
>
<swiper-item v-for="item in props.items" :key="item.id">
<view class="swiper-item">
<view class="left" @click="toGoodsInfo(item[0])">
<img :src="item[0].file_name" class="left-img" />
</view>
<div class="right">
<view class="good-img">
<div class="good-img-box good-img-top"></div>
<div
class="good-img-box good-img-bottom"
@click="toGoodsInfo(item[4])"
>
<img :src="item[4].file_name" class="good-img-detail" />
</div>
</view>
<view class="btn" @click="toMini(item[4])">一键试穿</view>
</div>
</view>
</swiper-item>
</swiper>
<view class="dots">
<view
class="dot"
:class="{ active: index == current }"
v-for="(item, index) in props.items"
:key="item.id"
>
</view>
</view>
</view>
</template>
<script setup>
import { defineProps, ref, watchEffect } from "vue";
const autoplay = ref(true);
const props = defineProps({
items: {
type: Array,
default: [],
},
});
const current = ref(0);
const animationfinish = (e) => {
// console.log('e: ', e.detail.current);
current.value = e.detail.current;
};
// weixin://dl/business/?appid=wx32d8423809b8d7d&path=/pages/fitRoom/index&query=id=
const toMini = (item) => {
console.log(wx, "微信miniProgram");
wx.miniProgram.navigateTo({
url: `/pages/fitRoom/index?id=${item.id}`,
success: function () {
// alert('成功')
},
fail(error) {
// alert('错误', error)
console.log("navigateTo error: ", error);
},
});
/**小程序内跳转 */
// uni.webView.navigateTo({ url: `/pages/fitRoom/index&id=${item.id}` });
// location.href = `weixin://dl/business/?appid=wx32d8423809b8d7d&path=/pages/fitRoom/index&id=${item.id}`
};
const toGoodsInfo = (item) => {
wx.miniProgram.navigateTo({
url: "/pages/goods/goodsDetail/index?goods_id=" + item.goods_id,
});
//先去请求后端接口获取数据
// uni.request({
// url: "https://llshop.zglvling.com/soapi/text/ceshi1",
// method: "POST",
// data: {
// id: 123
// },
// header: {
// "Content-Type": "application/json"
// },
// success: (res) => {
// const config = res.data;
// // #ifdef H5
// // 1. 检查是否在微信浏览器中
// const isWechatBrowser = /MicroMessenger/i.test(navigator.userAgent);
// console.log("是否在微信浏览器", isWechatBrowser);
// if (!isWechatBrowser) {
// uni.showToast({
// title: "请在微信中打开",
// icon: "none"
// });
// }
// wx.config({
// debug: true, // 调试模式
// appId: config.appId,
// timestamp: config.timestamp,
// nonceStr: config.nonceStr,
// signature: config.signature,
// jsApiList: ["miniProgram"], // 需要用到的 API
// });
// wx.ready(() => {
// wx.miniProgram.navigateTo({
// path: "/pages/goods/goodsDetail/index",
// success: () => console.log("跳转成功"),
// fail: (err) => {
// console.error("跳转失败:", err);
// uni.showToast({
// title: "跳转失败",
// icon: "none"
// });
// },
// });
// });
// // #endif
// },
// fail: (err) => {
// console.error("请求失败:", err);
// uni.showToast({
// title: "网络错误",
// icon: "none"
// });
// },
// });
// 初始化微信 JS-SDK
// function initWechatSDK(config) {
// console.log("阿拉啦啦", wx);
// wx.config({
// debug: true, // 调试模式
// appId: config.appId,
// timestamp: config.timestamp,
// nonceStr: config.nonceStr,
// signature: config.signature,
// jsApiList: ["miniProgram"], // 需要用到的 API
// });
// wx.ready(() => {
// wx.miniProgram.navigateTo({
// path: "/pages/goods/goodsDetail/index",
// success: () => console.log("跳转成功"),
// fail: (err) => {
// console.error("跳转失败:", err);
// uni.showToast({
// title: "跳转失败",
// icon: "none"
// });
// },
// });
// });
// wx.error((err) => {
// console.error("微信 SDK 配置失败:", err);
// });
// }
// uni.request({
// url: 'https://llshop.zglvling.com/soapi/text/ceshi1', // 接口地址
// method: 'POST', // 请求方式(GET/POST/PUT/DELETE
// data: { id: 123 }, // 请求参数(GET 用 paramsPOST 用 data
// header: {
// 'Content-Type': 'application/json', // 请求头
// },
// success: (res) => {
// console.log('请求成功:', res.data);
// var config = res.data;
// const wx = window.wx;
// // 动态加载微信 JS-SDK
// if (typeof wx === 'undefined') {
// const script = document.createElement('script');
// script.src = 'https://res.wx.qq.com/open/js/jweixin-1.6.0.js';
// script.onload = () => {
// initWechatSDK(config); // 加载完成后初始化
// };
// document.body.appendChild(script);
// } else {
// initWechatSDK(config); // 如果已存在,直接初始化
// }
// wx.config({
// debug: true, // 调试模式
// appId: config.appId,
// timestamp: config.timestamp,
// nonceStr: config.nonceStr,
// signature: config.signature,
// jsApiList: ['miniProgram'], // 需要用到的 API
// });
// // 4. 配置完成后跳转小程序
// wx.ready(() => {
// wx.miniProgram.navigateTo({
// path: '/pages/home/index?id=123', // 小程序路径 + 参数
// success: () => console.log('跳转成功'),
// fail: (err) => {
// console.error('跳转失败:', err);
// uni.showToast({ title: '跳转失败,请在微信中打开', icon: 'none' });
// },
// });
// });
// },
// fail: (err) => {
// console.error('请求失败:', err);
// uni.showToast({ title: '网络错误', icon: 'none' });
// },
// complete: () => {
// console.log('请求完成');
// },
// });
// const appid = 'wx32d8423809b8d7d4';
// const path = `/pages/goods/goodsDetail/index`;
// const scheme = `weixin://dl/business/?appid=${appid}&path=${encodeURIComponent(path)}`;
// console.log('地址: ', scheme);
// // 方法1:动态创建 <a> 标签(兼容 iOS
// const a = document.createElement('a');
// a.href = scheme;
// a.style.display = 'none';
// document.body.appendChild(a);
// a.click();
// document.body.removeChild(a);
// location.href = scheme;
// // 可添加失败回调(通过监听页面跳转失败)
// setTimeout(() => {
// alert('跳转失败,请手动打开小程序');
// }, 1000);
// location.href = `weixin://dl/business/?appid=wx32d8423809b8d7d&path=/pages/fitRoom/index&id=${item.id}`
};
// }
</script>
<style scoped>
.uni-margin-wrap {
width: 100%;
/* height: 330px; */
padding: 16px 8px;
box-sizing: border-box;
}
.flex {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 14px;
color: #031c24;
width: 336px;
margin: 0 auto;
margin-bottom: 16px;
}
.flex .arrow-icon {
width: 24px;
}
.swiper {
width: 336px;
height: 330px;
margin: 0 auto;
}
.swiper-item {
width: 336px;
height: 330px;
display: flex;
justify-content: space-between;
}
.swiper-item .left {
width: 223px;
height: 330px;
overflow: hidden;
}
.swiper-item .left .left-img {
height: 330px;
/* width: 100%; */
object-fit: scale-down;
}
.swiper-item .right {
width: 102px;
height: 330px;
overflow: hidden;
display: flex;
flex-direction: column;
}
.swiper-item .right .good-img {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
margin-bottom: 15px;
}
.swiper-item .right .good-img .good-img-box {
height: 134px;
overflow: hidden;
/* background-color: red; */
}
.swiper-item .right .good-img .good-img-box .good-img-detail {
height: 100%;
object-fit: scale-down;
}
.swiper-item .right .btn {
height: 24px;
background-color: #031c24;
color: #ffffff;
font-size: 12px;
font-weight: 400;
display: flex;
align-items: center;
justify-content: center;
}
.dots {
height: 22px;
margin-top: 16px;
display: flex;
align-items: center;
justify-content: center;
}
.dots .dot {
width: 5px;
height: 2px;
background-color: #edf0f0;
margin-left: 5px;
}
.dots .dot:first-of-type {
margin-left: 0;
}
.dots .active {
width: 10px;
background-color: #031c24;
}
</style>
Binary file not shown.

After

Width:  |  Height:  |  Size: 343 B

@@ -0,0 +1,113 @@
<script setup>
import CheckBox from "./Checkbox.vue";
import { defineProps, ref, watch } from "vue";
const props = defineProps({
bodyType: {
type: String,
default: "",
},
gender: {
type: String,
default: "女",
},
});
const imageUrl = ref("");
const jsonWoman = ref([
{
value: "苹",
type: "苹果型(O型)",
pic: "https://oss.yz.zglvling.com/user/image/2025-07-24/1753325126711833.png"
},
{
value: "梨",
type: "梨型(A型)",
pic: "https://oss.yz.zglvling.com/user/image/2025-07-24/1753325141105794.png"
},
{
value: "沙",
type: "沙漏型(X型)",
pic: "https://oss.yz.zglvling.com/user/image/2025-07-24/1753325153784393.png"
},
{
value: "倒三角形(T型)",
type: "倒三角形(T型)",
pic: "https://oss.yz.zglvling.com/user/image/2025-07-24/1753325170869093.png"
},
{
value: "H",
type: "矩形(H型)",
pic: "https://oss.yz.zglvling.com/user/image/2025-07-24/1753325184784878.png"
},
]);
const jsonMan = ref([
{
value: "苹",
type: "苹果型(O型)",
pic: "https://oss.yz.zglvling.com/user/image/2025-07-24/1753324878640920.png",
},
{
value: "苹",
type: "梨型(A型)",
pic: "https://oss.yz.zglvling.com/user/image/2025-07-24/1753324934565088.png",
},
{
value: "H",
type: "矩形(H型)",
pic: "https://oss.yz.zglvling.com/user/image/2025-07-24/1753324951511967.png"
},
{
value: "倒",
type: "倒三角形(T型)",
pic: "https://oss.yz.zglvling.com/user/image/2025-07-24/1753324969423848.png"
},
]);
const json = ref([]);
watch(
[() => props.bodyType, () => props.gender],
([newVal, gender]) => {
console.log("props.bodyType newVal: xxxxxxxxxxxxx", newVal);
json.value = gender === "男" ? jsonMan.value : jsonWoman.value;
console.log("json.value: ", json.value);
json.value = json.value.map((item) => ({
...item,
checked: newVal.includes(item.type),
}));
imageUrl.value = json.value.find((item) => newVal.includes(item.type))?.pic;
},
{ deep: true, immediate: true }
);
</script>
<template>
<view class="container">
体型
<template v-for="item of json">
<CheckBox :checked="item.checked">{{ item.type }}</CheckBox>
</template>
<image class="figure" :src="imageUrl" mode="widthFix"></image>
</view>
</template>
<style scoped>
.container {
padding: 20px 26px;
}
.figure {
margin-top: 26px;
margin-bottom: 12px;
}
</style>
@@ -0,0 +1,98 @@
<script setup>
import { defineProps, ref, watch } from "vue";
import { styleJson } from "./json/style";
const styleImage = ref("");
const psychologicalStyleImage = ref("");
const props = defineProps({
styleType: {
type: String,
default: "",
},
psychologicalStyle: {
type: String,
default: "",
},
gender: {
type: String,
default: "女",
},
});
watch(
() => props.styleType,
(newVal) => {
styleImage.value = styleJson.find((item) => item.type === newVal)?.pic;
},
{ deep: true, immediate: true }
);
watch(
() => props.psychologicalStyle,
(newVal) => {
psychologicalStyleImage.value = styleJson.find(
(item) => item.type === newVal
)?.pic;
},
{ deep: true, immediate: true }
);
</script>
<template>
<view class="stylePicture">
<!-- 第一张和第二张都是9选一展示如果一样就显示一张就行-->
<template
v-if="styleImage === psychologicalStyleImage && props.gender == '女'"
>
<image class="bottomMap" :src="styleImage" mode="widthFix"></image>
</template>
<template v-else>
<template v-if="props.gender == '女'">
<image class="bottomMap" :src="styleImage" mode="widthFix"></image>
<view>
<view class="psychologicalStyle">心理风格</view>
<image
class="bottomMap"
:src="psychologicalStyleImage"
mode="widthFix"
></image>
</view>
</template>
</template>
</view>
<view class="">
<image
class="bottomMap"
:src="'https://llyz.oss-cn-beijing.aliyuncs.com/diagnostic-report-h5/bottomMap.png'"
mode="widthFix"
></image>
</view>
<view style="padding-left: 20px; margin-top: 56px">
人工智能大模型在计算中可能存在0.5%的误差
</view>
<view class="reportCompleted">
<image
class="bottomMap"
:src="'/static/reportCompleted.png'"
mode="widthFix"
></image>
</view>
</template>
<style scoped>
.psychologicalStyle {
color: #031c24;
padding-top: 23px;
padding-bottom: 10px;
}
.stylePicture {
padding: 20px;
}
.bottomMap {
width: 100%;
}
.reportCompleted {
padding: 0 20px;
padding-bottom: 50px;
}
</style>
@@ -0,0 +1,43 @@
<script>
export default {
name: "CheckBox",
props: {
checked: {
type: Boolean,
default: false,
},
},
};
</script>
<template>
<view class="checkbox">
<image
class="selectBox"
v-if="checked"
:src="'https://llyz.oss-cn-beijing.aliyuncs.com/diagnostic-report-h5/选中.svg'"
/>
<image
class="selectBox"
v-else
src="https://llyz.oss-cn-beijing.aliyuncs.com/diagnostic-report-h5/未选中.svg"
/>
<slot></slot>&nbsp;&nbsp;
</view>
</template>
<style scoped>
.checkbox {
display: inline;
vertical-align: middle;
line-height: 21px;
}
.selectBox {
width: 13px;
height: 13px;
padding-right: 1px;
position: relative;
top: 1px;
}
</style>
@@ -0,0 +1,117 @@
<script setup>
import {defineProps} from "vue";
import ProgressBar from "./ProgressBar.vue";
const props = defineProps({
hue: {
type: Number,
default: 0,
},
// 色度
chroma: {
type: Number,
default: 0,
},
// 亮度
luminance: {
type: Number,
default: 0,
},
});
</script>
<template>
<view class="container">
<view class="title">面部色彩维度</view>
<view class="title-en">COLOUR DIMENSIONS</view>
<view>
<view class="title">色相</view>
<view class="text-en">HUE</view>
<ProgressBar :step="Number(hue)"/>
<view class="desc">
<view class="flex">
<view>冷色</view>
<view>暖色</view>
</view>
<view class="flex flex-en">
<view>Cool</view>
<view>Warn</view>
</view>
</view>
</view>
<view>
<view class="title">亮度</view>
<view class="text-en">VALUE</view>
<ProgressBar :step="Number(chroma)"/>
<view class="desc">
<view class="flex">
<view>暗沉</view>
<view>白皙</view>
</view>
<view class="flex flex-en">
<view>Dark</view>
<view>White</view>
</view>
</view>
</view>
<view>
<view class="title">色度</view>
<view class="text-en">CHROMA</view>
<ProgressBar :step="Number(luminance)"/>
<view class="desc">
<view class="flex">
<view>柔和</view>
<view>鲜艳</view>
</view>
<view class="flex flex-en">
<view>Soft</view>
<view>Bright</view>
</view>
</view>
</view>
<!-- <ProgressBar :step="1" />-->
</view>
</template>
<style scoped>
.container {
padding: 20px;
text-align: center;
}
.title {
color: #031C24;
text-align: center;
}
.title-en {
margin-top: 4px;
color: #D8D8D8;
text-align: center;
}
.text-en {
margin-top: 6px;
color: #D8D8D8;
text-align: center;
font-size: 12px;
}
.flex {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
}
.flex-en {
font-size: 10px;
color: #D8D8D8;
margin-top: 6px;
}
.desc {
margin-top: 4px;
}
</style>
@@ -0,0 +1,64 @@
<script setup>
import { defineProps, ref, watch } from "vue";
import CheckBox from "./Checkbox.vue";
const props = defineProps({
type: {
type: String,
default: "静态型",
},
aiDesc: {
type: String,
default: '',
},
});
const json = ref( [
{
type: '0',
value: '动态型',
},
{
type: '1',
value: '动静适中',
},
{
type: '2',
value: '静态型',
}
])
watch(
() => props.type,
(newVal) => {
console.log('动静类型: props.type: ', newVal);
json.value = json.value.map((item) => ({
...item,
checked: item.value == newVal,
}));
},
{ deep: true, immediate: true }
);
</script>
<template>
<view class="container">
<view>
动静类型
<template v-for="item of json">
<CheckBox :checked="item.checked">{{ item.value }}</CheckBox>
</template>
<view class="desc">
{{ aiDesc }}
</view>
</view>
</view>
</template>
<style scoped>
.container {
padding: 0 26px;
}
.desc {
color: #4D4D4D;
margin-top: 6px;
}
</style>
@@ -0,0 +1,102 @@
<script setup>
import { defineProps, ref, watch } from "vue";
const map = {
"-5": 0,
"-4": 1,
"-3": 2,
"-2": 3,
"-1": 4,
0: 5,
1: 6,
2: 7,
3: 8,
4: 9,
5: 10,
};
const currentStep = ref(0);
const props = defineProps({
step: {
type: Number,
default: "0",
},
});
watch(
() => props.step,
(newVal) => {
console.log(map[newVal]);
currentStep.value = map[newVal];
},
{ deep: true, immediate: true }
);
</script>
<template>
<view class="progress">
<image
:src="'https://llyz.oss-cn-beijing.aliyuncs.com/diagnostic-report-h5/progress.svg'"
class="selectBox"
/>
<view :class="['bolus', `p${currentStep}`]"></view>
</view>
</template>
<style scoped>
.progress {
width: 100%;
height: 14px;
position: relative;
}
.selectBox {
width: 100%;
height: 14px;
}
.bolus {
width: 12px;
height: 12px;
border-radius: 50%;
position: absolute;
top: 2px;
left: 0;
background: #000;
}
.p0 {
left: 0;
}
.p1 {
left: calc((100% - 12px) / 10);
}
.p2 {
left: calc((100% - 12px) / 10 * 2);
}
.p3 {
left: calc((100% - 12px) / 10 * 3);
}
.p4 {
left: calc((100% - 12px) / 10 * 4);
}
.p5 {
left: calc((100% - 12px) / 10 * 5);
}
.p6 {
left: calc((100% - 12px) / 10 * 6);
}
.p7 {
left: calc((100% - 12px) / 10 * 7);
}
.p8 {
left: calc((100% - 12px) / 10 * 8);
}
.p9 {
left: calc((100% - 12px) / 10 * 9);
}
.p10 {
right: 0;
left: unset;
}
</style>
@@ -0,0 +1,66 @@
<script setup>
import { defineProps, ref, watch } from "vue";
import CheckBox from "./Checkbox.vue";
const props = defineProps({
type: {
type: String,
default: "曲直适中",
},
aiDesc: {
type: String,
default: '',
},
});
const json = ref( [
{
type: '0',
value: '曲线型',
desc: '面部五官线条(眉眼、嘴唇等)没有特别强烈的曲线感或直线硬朗感,面部轮廓、发型线条以及穿着的服装(V 领、简洁款式 )也呈现出既非极端曲线柔和,也非极端直线利落的状态,是较为平衡的直曲适中风格 。'
},
{
type: '1',
value: '曲直适中',
desc: '面部五官线条(眉眼、嘴唇等)没有特别强烈的曲线感或直线硬朗感,面部轮廓、发型线条以及穿着的服装(V 领、简洁款式 )也呈现出既非极端曲线柔和,也非极端直线利落的状态,是较为平衡的直曲适中风格 。'
},
{
type: '2',
value: '直线型',
desc: '面部五官线条(眉眼、嘴唇等)没有特别强烈的曲线感或直线硬朗感,面部轮廓、发型线条以及穿着的服装(V 领、简洁款式 )也呈现出既非极端曲线柔和,也非极端直线利落的状态,是较为平衡的直曲适中风格 。'
}
])
watch(
() => props.type,
(newVal) => {
json.value = json.value.map((item) => ({
...item,
checked: item.value === newVal,
}));
},
{ deep: true, immediate: true }
);
</script>
<template>
<view class="container">
<view>
轮廓类型
<template v-for="item of json">
<CheckBox :checked="item.checked">{{ item.value }}</CheckBox>
</template>
<view class="desc">
{{ aiDesc }}
</view>
</view>
</view>
</template>
<style scoped>
.container {
padding: 0 26px;
}
.desc {
color: #4D4D4D;
margin-top: 6px;
}
</style>
@@ -0,0 +1,63 @@
<script setup>
import {defineProps, ref, watch} from "vue";
import CheckBox from "./Checkbox.vue";
const props = defineProps({
skinColor: {
type: String,
default: '黄一白',
},
});
const json = ref( [
{
type: '0',
value: '粉一白'
},
{
type: '1',
value: '粉二白'
},
{
type: '2',
value: '粉三白'
},
{
type: '3',
value: '黄一白'
},
{
type: '4',
value: '黄二白'
},
{
type: '5',
value: '黄黑皮'
},
])
watch(
() => props.skinColor,
(newVal) => {
// console.log(json.value.find(item => item.value === newVal), '22');
if(!json.value.find(item => item.value === newVal)) {
// alert(`当前接口返回肤色与UI不一致${newVal}`)
}
json.value = json.value.map(item => ({...item, checked: item.value === newVal}));
},
{deep: true, immediate: true}
);
</script>
<template>
<view class="container">
肤色
<template v-for="item of json">
<CheckBox :checked="item.checked">{{ item.value }}</CheckBox>
</template>
</view>
</template>
<style scoped>
.container {
padding: 20px 26px;
}
</style>
@@ -0,0 +1,29 @@
<script setup>
import {defineProps, watchEffect} from "vue";
const props = defineProps({
html: {
type: String,
default: '',
},
});
watchEffect(() => {
})
</script>
<template>
<view class="container">
<rich-text :nodes="props.html"></rich-text>
</view>
</template>
<style scoped>
.container {
padding: 20px;
}
</style>
@@ -0,0 +1,65 @@
<script setup>
import { defineProps, ref, watch } from "vue";
import CheckBox from "./Checkbox.vue";
const props = defineProps({
type: {
type: String,
default: "中量感",
},
aiDesc: {
type: String,
default: '',
},
});
const json = ref( [
{
type: '0',
value: '小量感',
},
{
type: '1',
value: '中量感',
},
{
type: '2',
value: '大量感',
}
])
watch(
() => props.type,
(newVal) => {
console.log('量感类型:-----', props.type);
json.value = json.value.map((item) => ({
...item,
checked: item.value === newVal,
}));
},
{ deep: true, immediate: true }
);
</script>
<template>
<view class="container">
<view>
量感类型
<template v-for="item of json">
<CheckBox :checked="item.checked">{{ item.value }}</CheckBox>
</template>
<view class="desc">
{{ aiDesc }}
</view>
</view>
</view>
</template>
<style scoped>
.container {
padding: 20px 26px;
}
.desc {
color: #4D4D4D;
margin-top: 6px;
}
</style>
@@ -0,0 +1,145 @@
<script setup>
import { defineProps, ref, watch } from "vue";
import CheckBox from "./Checkbox.vue";
import { styleJson, manStyleJson } from "./json/style";
const props = defineProps({
styleType: {
type: String,
default: "",
},
picList: {
type: Array,
default: () => [], // 使用函数返回默认值
},
gender: {
type: String,
default: "",
},
});
const styleTypeDesc = ref("");
const styleTypeJson = ref();
const showPreview = ref(false);
const currentPreviewImage = ref("");
watch(
() => props.styleType,
(newVal) => {
const json = props.gender === "男" ? manStyleJson : styleJson;
styleTypeJson.value = json.map((item) => ({
...item,
checked: item.type === newVal,
}));
styleTypeDesc.value = styleTypeJson.value.find(
(item) => item.type === props.styleType
)?.value;
},
{ deep: true, immediate: true }
);
const previewImage = (imageUrl) => {
currentPreviewImage.value = imageUrl;
showPreview.value = true;
};
const closePreview = (e) => {
// 阻止事件冒泡
e.stopPropagation();
showPreview.value = false;
currentPreviewImage.value = "";
};
</script>
<template v-if="styleTypeJson">
<view class="title">基因风格</view>
<view class="container">
<template v-for="item of styleTypeJson">
<CheckBox :checked="item.checked">{{ item.type }}</CheckBox>
</template>
<view class="desc">
{{ styleTypeDesc }}
</view>
</view>
<view class="pic-container">
<image
v-for="(pic, index) in picList"
:key="index"
:src="pic"
class="pic"
mode="aspectFill"
@click="previewImage(pic)"
></image>
</view>
<!-- 图片预览遮罩层 -->
<view v-if="showPreview" class="preview-mask" @click="closePreview">
<view class="preview-container" @click.stop>
<image
:src="currentPreviewImage"
class="preview-image"
mode="widthFix"
@click="closePreview"
></image>
</view>
</view>
</template>
<style scoped>
.title {
padding-left: 20px;
line-height: 21px;
margin-bottom: 12px;
}
.container {
padding: 0 26px;
text-align: justify;
}
.desc {
color: #4e4e4e;
margin-top: 10px;
}
.pic-container {
display: flex;
padding: 20px 18px;
}
/* .pic {
width: 100px;
height: 100px;
background-color: #eee;
border-radius: 4px;
} */
/* 预览遮罩层样式 */
.preview-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 999;
}
.preview-container {
width: 90%;
height: 90%;
display: flex;
justify-content: center;
align-items: center;
}
.preview-image {
max-width: 100%;
max-height: 100%;
border-radius: 4px;
}
</style>
@@ -0,0 +1,123 @@
export const styleJson = [
{
type: "戏剧型",
gender: '女',
value:
"戏剧型风格以强烈的视觉冲击力为核心,强调夸张与大气。线条多为硬朗直线,剪裁夸张且富有张力,如 oversize 轮廓、立体结构。色彩偏好高对比度搭配,面料多选挺括或光泽感强的材质。装饰大胆醒目,如大色块、夸张配饰,整体传递出强烈的存在感与气场,适合追求个性与舞台感的形象。",
pic: '/static/dramaType.png'
},
{
type: "睿智型",
gender: '女',
value:
"睿智型风格突出知性与理性,注重细节的精致与思维的锐利感。线条简洁利落,剪裁合身不拖沓,多选用中性色或低饱和色调。面料偏好质感细腻的天然材质,装饰以简约金属饰件或几何图案为主。整体造型克制而不失格调,传递出冷静、聪慧且专业的气质,适合职场或需要展现思辨能力的场合。",
pic: '/static/wise.png'
},
{
type: "自然型",
gender: '女',
value:
"自然型风格强调随性与舒适,贴近生活本真。线条多为柔和的直线或自然曲线,剪裁宽松舒展,避免刻意修饰。色彩偏好大地色系、莫兰迪色等贴近自然的色调,面料多选棉、麻、针织等透气舒适的材质。装饰简约甚至无装饰,整体造型松弛自在,传递出亲切、质朴且不造作的气质。",
pic: '/static/natural.png'
},
{
type: "古典型",
gender: '女',
value:
"古典型风格以端庄得体为核心,注重规整与经典。线条多为直线或轻微曲线,剪裁合身严谨,无多余设计。色彩偏好黑白灰、藏蓝等中性色,搭配简洁有序。面料多选挺括细腻的羊毛、真丝等,装饰以小巧精致的金属扣、珍珠等为主。整体造型成熟稳重,传递出优雅、可靠且富有分寸感的形象,是经典与规范的代名词。",
pic: '/static/classical.png'
},
{
type: "优雅型",
gender: '女',
value:
"优雅型风格强调柔美的线条与温婉的气质,以曲线为主要视觉语言。剪裁贴合身体曲线但不紧绷,多为收腰、圆领等柔和设计。色彩偏好柔和的莫兰迪色、浅粉、米白等,面料多选真丝、雪纺等垂坠感强的材质。装饰以蕾丝、刺绣等精致元素为主,整体造型温柔婉约,传递出含蓄、高贵的女性魅力。",
pic: '/static/elegant.png'
},
{
type: "浪漫型",
gender: '女',
value:
"浪漫型风格以浓郁的女性化特质为核心,突出曲线与华丽感。线条多为夸张的 S 型曲线,剪裁紧身或蓬松,如鱼尾裙、泡泡袖。色彩偏好浓郁饱和的红、紫、酒红等,面料多选丝绒、缎面等光泽感强的材质。装饰繁复华丽,如蕾丝、水钻、大朵花卉图案,整体造型热情妩媚,传递出性感与奢华的气质。",
pic: '/static/romantic.png'
},
{
type: "前卫型",
gender: '女',
value:
"前卫型风格以创新与突破为核心,追求标新立异的设计。线条多为不规则几何形,剪裁大胆解构,如不对称、拼接、镂空等。色彩偏好撞色、荧光色或黑白极端对比,面料多选科技感材质或混搭组合。装饰充满未来感,如金属、PVC、夸张廓形配饰,整体造型颠覆传统,传递出叛逆、先锋的个性。",
pic: '/static/avantGarde.png'
},
{
type: "少女型",
gender: '女',
value:
"少女型风格突出甜美与活泼,充满青春朝气。线条多为圆润的曲线,剪裁短款、收腰,如娃娃领、A 字裙。色彩偏好马卡龙色、浅粉、鹅黄等明亮色调,面料多选纯棉、蕾丝、泡泡纱等轻盈材质。装饰以蝴蝶结、波点、卡通图案为主,整体造型娇俏可爱,传递出纯真、灵动的少女感。",
pic: '/static/girlShaped.png'
},
{
type: "少年型",
gender: '女',
value:
"少年型风格强调清爽与干练,带有中性化的利落感。线条多为直线,剪裁宽松平直,如衬衫、直筒裤、小西装。色彩偏好黑白灰、牛仔蓝等中性色调,面料多选牛仔、帆布、薄棉等挺括材质。装饰简约甚至无装饰,整体造型帅气洒脱,传递出率真、爽朗的中性气质,弱化女性化特质。",
pic: '/static/teenagerType.png'
},
]
export const manStyleJson = [
{
type: '戏剧型',
gender: '男',
value:'戏剧型风格以极具张力的视觉表现力为核心,凸显强悍气场与王者风范。线条多为极具力量感的硬朗直线,剪裁夸张且充满结构化设计,如超大廓形西装、立体剪裁的风衣。色彩偏好高饱和撞色或浓郁深沉的色调,面料多选挺括的厚羊毛、亮面皮革等富有质感的材质。装饰以金属质感的夸张配饰、独特纹样为主,整体传递出睥睨一切的霸气与掌控全场的魄力,是天生的焦点型人物。',
},
{
type: '睿智型',
gender: '男',
value:'睿智型风格彰显智慧与思辨,强调沉稳的气场与理性的锋芒。线条简洁利落且富有逻辑性,剪裁精准合体,杜绝冗余设计,如修身西装、简约衬衫。色彩以深灰、藏蓝、卡其等沉稳色调为主,搭配克制有序。面料多选高支棉、羊毛等质感上乘的天然材质,装饰仅以极简的金属袖口、几何纹领带点缀。整体造型散发着冷静、深邃的智者气息,是职场精英与思想者的标志性风格。',
},
{
type: '自然型',
gender: '男',
value:'自然型风格追求自在本真的状态,拒绝刻意雕琢。线条多为自然流畅的直线,剪裁宽松有度,如休闲款衬衫、直筒牛仔裤、棉麻质地的外套。色彩偏爱原木色、浅卡其、米白、牛仔蓝等贴近自然的色调,面料以棉、麻、粗针织、水洗牛仔为主,强调透气舒适。装饰极少甚至无装饰,整体传递出随性不羁的洒脱感,像行走的生活哲学家般亲切而有温度。',
},
{
type: '古典型',
gender: '男',
value:'古典型风格以经典庄重为核心,坚守永不过时的品味准则。线条以利落的直线为主,剪裁严谨合体,如标准款西装、牛津纺衬衫、合身西裤,处处体现传统男装的精髓。色彩以黑白灰、藏蓝、深棕等经典色系为主,搭配一丝不苟。面料多选顶级羊毛、纯棉、真丝等挺括细腻的材质,装饰仅以精致的袖扣、低调的领带夹点缀。整体造型成熟稳重,是可靠与教养的代名词。',
},
{
type: '优雅型',
gender: '男',
value:'优雅型风格散发从容温润的绅士风度,以内敛的魅力打动人心。线条多为流畅的弧线与适度的直线结合,柔和而不软弱,如微廓形西装、收腰款风衣。剪裁合体但不紧绷,强调舒适的廓形感。色彩偏爱柔和的米白、浅灰、豆沙绿等低饱和度色调,面料多选柔软垂坠的羊绒、天丝、细棉等。装饰以精致低调的格纹、暗纹或刺绣为主,整体传递出温润如玉的谦和气质,不经意间流露高级感。',
},
{
type: '浪漫型',
gender: '男',
value:'浪漫型风格充满艺术化的男性魅力,以深情与不羁为核心。线条多为流畅的曲线与柔和的直线交织,剪裁略带随性的宽松或贴合身形的流畅线条,如丝质垂感衬衫、微阔形丝绒西装。色彩偏好酒红、墨绿、宝蓝等富有故事感的浓郁色调,或柔和的暖棕色系。面料多选丝绒、桑蚕丝、高级针织等柔软且富有光泽的材质,装饰以复古纹样、刺绣细节、丝质围巾为主,整体散发着致命的吸引力与诗意情怀。',
},
{
type: '前卫型',
gender: '男',
value:'前卫型风格以颠覆与革新为灵魂,彰显先锋态度与实验精神。线条多为打破常规的不规则剪裁与几何线条,充满强烈的视觉冲突。剪裁大胆运用解构重组、oversize 与紧身的极端碰撞,如不对称夹克、拼接卫衣。色彩敢于尝试高饱和撞色、荧光色,或极致的黑白灰对比。面料常选用科技感面料与天然材质的混搭,如反光材质、再生纤维、做旧牛仔。装饰充满未来感与颠覆性,如机能风配件、抽象艺术印花,整体传递出挑战传统的叛逆个性。',
},
{
type: '少女型',
gender: '男',
value:'少女型风格在这里转化为洋溢着纯粹少年感的风格,传递青春活力与阳光气息。线条多为简洁明快的直线,略带圆润感,如休闲衬衫、连帽卫衣、直筒牛仔裤。剪裁合身偏宽松,舒适无束缚。色彩偏爱明亮的浅蓝、纯白、鹅黄、浅绿等清新色调,面料多选轻盈透气的纯棉、泡泡纱、薄针织。装饰以简约俏皮的条纹、小 logo、亮色配饰为主,整体造型干净清爽,像夏日阳光般治愈,充满未经打磨的纯真感。',
},
{
type: '少年型',
gender: '男',
value:'少年型风格突出清爽干练的中性化气质,彰显率真与洒脱。线条多为干脆利落的直线,棱角分明,如利落的短款夹克、合身衬衫、窄腿西裤。色彩以黑白灰、藏蓝、军绿等极具力量感的中性色为主,面料多选挺括耐磨的牛仔、帆布、厚棉布。装饰极简甚至无装饰,突出纯粹的线条感,整体传递出果断、飒爽的气场,充满少年特有的朝气与行动力。',
},
]
+273
View File
@@ -0,0 +1,273 @@
<template>
<view class="header">
<!-- 头像和信息区 -->
<view class="info-row">
<view class="avatar-stack">
<image
class="avatar-main"
:src="userFaceInfo?.avatar_img_url"
mode="aspectFill"
/>
<image
class="avatar-shadow"
:src="userFaceInfo?.avatar_img_url"
mode="aspectFill"
/>
</view>
</view>
<!-- 底部分栏信息 -->
<view class="">
<view class="info-main">
<view class="info-basic">
<text class="gender">{{ userFaceInfo.gender }}</text>
<text class="birthday">| {{ userFaceInfo.createTime }}</text>
</view>
<view class="info-desc">
{{ userFaceInfo.feature }}
<!-- {{
userFaceInfo.Facial_three_dimensionality
}} -->
{{ userFaceInfo.eyebrows }}{{ userFaceInfo.ocular_form }}
{{ userFaceInfo.neck_length }}
{{ userFaceInfo.nasolabial_folds }}
</view>
</view>
<view class="info-columns">
<view class="col-item">
<text class="col-label">身高</text>
<text class="col-value">{{ userFaceInfo.stature || "-" }}cm</text>
</view>
<view class="col-item">
<text class="col-label">体重</text>
<text class="col-value">{{ userFaceInfo.bodyWeight || "-" }}kg</text>
</view>
<view class="col-item">
<text class="col-label">修饰目标</text>
<text class="col-value">{{ userFaceInfo.purpose || "-" }}</text>
</view>
<view class="col-item">
<text class="col-label">局部问题</text>
<text class="col-value">{{ userFaceInfo.question || "-" }}</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { defineProps, watch, ref } from "vue";
const props = defineProps({
userFaceInfo: {
type: Object,
default: {},
},
});
const userFaceInfo = ref({});
watch(
() => props.userFaceInfo,
(newVal) => {
userFaceInfo.value = newVal;
},
{ deep: true }
);
</script>
<style scoped>
.header {
background: url("https://llyz.oss-cn-beijing.aliyuncs.com/diagnostic-report-h5/h5headerbg.png")
no-repeat center center;
color: #fff;
padding: 16px 20px 32px;
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
}
.status-bar-row {
display: flex;
justify-content: space-between;
align-items: center;
height: 32px;
padding: 0 12px;
background: #333;
}
.status-bar-title {
color: #aaa;
font-size: 14px;
}
.status-bar-icons {
display: flex;
align-items: center;
}
.status-bar-time {
font-size: 16px;
color: #fff;
margin-right: 16px;
}
.status-bar-right {
display: flex;
align-items: center;
gap: 4px;
}
.status-bar-icon {
width: 16px;
height: 16px;
background: #666;
border-radius: 3px;
}
.status-bar-icon.signal {
background: linear-gradient(90deg, #fff 60%, #666 100%);
}
.status-bar-icon.wifi {
background: linear-gradient(90deg, #fff 60%, #666 100%);
}
.status-bar-icon.battery {
background: #fff;
border-radius: 2px;
}
.header-action-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 12px 0 12px;
}
.back-btn {
width: 36px;
height: 36px;
background: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.back-arrow {
color: #222;
font-size: 22px;
font-weight: bold;
}
.header-icons {
display: flex;
gap: 10px;
}
.icon-box {
width: 24px;
height: 24px;
background: #444;
border-radius: 6px;
}
.icon-box.square {
border-radius: 4px;
}
.icon-box.dot {
border-radius: 50%;
width: 8px;
height: 8px;
background: #fff;
margin-top: 8px;
}
.icon-box.circle {
border-radius: 50%;
background: #fff;
border: 2px solid #444;
}
.info-row {
display: flex;
align-items: flex-start;
/* margin-top: 8px; */
/* padding: 0 24px; */
}
.avatar-stack {
position: relative;
width: 80px;
height: 80px;
display: flex;
}
.avatar-main {
width: 80px;
height: 80px;
border-radius: 50%;
/* border: 2px solid #fff; */
position: absolute;
background-color: #fff;
object-fit: cover;
left: 0;
top: 0;
z-index: 9;
}
.avatar-shadow {
width: 80px;
height: 80px;
border-radius: 50%;
background-color: gray;
/* opacity: 0.3; */
object-fit: cover;
position: absolute;
left: 82%;
z-index: 2;
/* 关键代码:图片变黑白 */
filter: grayscale(100%);
}
.info-main {
margin-top: 8px;
}
.info-basic {
font-size: 12px;
margin-bottom: 6px;
}
.gender {
margin-right: 8px;
}
.birthday {
color: #bbb;
}
.info-desc {
width: 336px;
font-size: 14px;
color: #ccc;
margin-bottom: 10px;
font-family: Molengo;
font-weight: 400;
font-size: 10px;
line-height: 100%;
letter-spacing: 0%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.info-columns {
display: flex;
justify-content: space-between;
align-items: flex-start;
/* padding: 0 24px; */
margin-top: 18px;
}
.col-item {
display: flex;
flex-direction: column;
align-items: flex-start;
}
.col-label {
color: #aaa;
font-size: 13px;
margin-bottom: 2px;
font-family: Molengo;
font-weight: 400;
font-size: 10px;
line-height: 10px;
letter-spacing: 0px;
}
.col-value {
color: #fff;
font-size: 15px;
font-family: Molengo;
font-weight: 400;
font-size: 12px;
line-height: 12px;
letter-spacing: 0px;
/* 超出隐藏并显示省略号 */
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
/* 可选:限制最大宽度(防止 flex 布局拉伸) */
max-width: 80px;
}
</style>
+66
View File
@@ -0,0 +1,66 @@
<template>
<view class="tabs">
<view class="tabs-header">
<view
v-for="(tab, index) in tabs"
:key="index"
:class="['tab-item', { active: index === activeTab }]"
@click="onTabClick(index)"
>
{{ tab.name }}
</view>
</view>
<view>
<slot></slot>
</view>
</view>
</template>
<script>
export default {
name: "Tabs",
props: {
tabs: {
type: Array,
required: true,
},
activeTab: {
type: Number,
default: 0,
},
},
emits: ["update:activeTab"],
methods: {
onTabClick(index) {
this.$emit("update:activeTab", index);
},
},
};
</script>
<style scoped>
.tabs {
width: 100%;
padding: 0 0 0 10px;
background-color: #fff;
}
.tabs-header {
display: flex;
/* border-bottom: 1px solid #eee; */
margin-bottom: 16px;
}
.tab-item {
padding: 10px 10px;
cursor: pointer;
color: #333;
font-size: 16px;
}
.tab-item.active {
color: rgba(3, 28, 36, 1);
border-bottom: 2px solid rgba(3, 28, 36, 1);
font-weight: bold;
}
/* .tabs-content {
padding-bottom: 16px;
} */
</style>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+21
View File
@@ -0,0 +1,21 @@
import request from '@/sheep/request';
const ReportApi = {
// 查询分类列表
getCategoryList: () => {
return request({
url: '/product/category/list',
method: 'GET',
});
},
// 查询分类列表,指定编号
getCategoryListByIds: (ids) => {
return request({
url: '/product/category/list-by-ids',
method: 'GET',
params: { ids },
});
},
};
export default ReportApi;
+56
View File
@@ -0,0 +1,56 @@
// 本地图片不通过input上传后端
function uploadFormData(formData) {
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.open("POST", "https://puton.meidaojia.com/api/v1/test/exchangeHair");
xhr.onload = function () {
if (xhr.status >= 200 && xhr.status < 300) {
try {
resolve(JSON.parse(xhr.responseText));
} catch (e) {
reject("返回内容不是有效JSON");
}
} else {
reject("上传失败,状态码:" + xhr.status);
}
};
xhr.onerror = function () {
reject("网络错误");
};
xhr.send(formData);
});
}
// url下载到本地
const downloadFile = (url) => {
return fetch(url).then((res) => {
return res.blob();
});
};
// 数字转中文
function numToChinese(num) {
const map = ["零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖"];
return map[num] !== undefined ? map[num] : num;
}
function getMaxElement(arr) {
const elements = ["金", "木", "水", "火", "土"];
arr = arr.map(Number);
const maxIndex = arr.indexOf(Math.max(...arr));
return elements[maxIndex];
}
function reorderByValue(arr, target) {
return [
...arr.filter((item) => item.typeName + "型" === target),
...arr.filter((item) => item.typeName + "型" !== target),
];
}
export {
uploadFormData,
downloadFile,
numToChinese,
getMaxElement,
reorderByValue,
};