16 Commits

Author SHA1 Message Date
0b33727815 feat: 调整 2026-03-26 17:17:57 +08:00
d7d50e1210 feat: 下单校验 2026-03-26 16:01:30 +08:00
1e01028ba8 feat:生成图片组件的调整 2026-03-26 15:36:12 +08:00
ba655834b9 feat: 生成照片的组件搭建 2026-03-26 15:13:33 +08:00
e72531cfb7 feat: 长文本组件的调试 2026-03-26 00:10:53 +08:00
00c58d47b9 feat: 长文本组件调整 2026-03-25 16:33:35 +08:00
14d2ad7b53 feat: 快速预定的处理 2026-03-20 00:25:12 +08:00
b7d9ebf816 feat: 样式调整 2026-03-17 20:23:27 +08:00
8696953f6f feat: 样式调整 2026-03-17 20:18:53 +08:00
be9e317284 feat: 图标的修改 2026-03-17 20:12:42 +08:00
51ce0ccd8b feat: 添加电话号码与微小调整 2026-03-17 20:09:19 +08:00
662f05d523 feat: 调整了快速预定的图片高度 2026-03-16 22:35:35 +08:00
40c37ea2e1 feat: 商品的核销逻辑 2026-03-16 22:28:24 +08:00
f5c3a3ca2d feat: 核销的样式的调整 2026-03-16 17:10:09 +08:00
87e7f7522f feat: 核销二维码的展示 2026-03-16 15:18:39 +08:00
70afd4d19f feat: 核销二维码组件 2026-03-13 16:54:10 +08:00
27 changed files with 612 additions and 134 deletions

View File

@@ -63,7 +63,7 @@ const openMap = () => {
// 拨打电话 // 拨打电话
const getPhoneNumber = () => { const getPhoneNumber = () => {
const o = props.orderData || {}; const o = props.orderData || {};
return o.commodityPhone || o.phone || o.contactPhone || ""; return o.commodityPhone || o.phone || o.contactPhone || "15608199221";
}; };
const callPhone = () => { const callPhone = () => {

View File

@@ -93,7 +93,7 @@ const props = defineProps({
}, },
loadingText: { loadingText: {
type: String, type: String,
default: "二维码生成中", default: "",
}, },
}); });

View File

@@ -24,9 +24,11 @@ export const CompName = {
// 调查问卷卡片 // 调查问卷卡片
callSurveyQuestionnaire: "callSurveyQuestionnaire", callSurveyQuestionnaire: "callSurveyQuestionnaire",
// 打开地图卡片 // 打开地图卡片
openMapCard: "openMapCard", mapCard: "mapCard",
// 回答卡片 // 回答卡片
answerCard: "answerCard", longTextCard: "longTextCard",
// 生成合成图片
generatorPhotoCard: "generatorPhotoCard",
}; };
/// 发送的指令类型 /// 发送的指令类型

View File

@@ -20,19 +20,20 @@
<!-- 联系方式 --> <!-- 联系方式 -->
<view class="bg-white rounded-12 overflow-hidden"> <view class="bg-white rounded-12 overflow-hidden">
<view <view class="border-box border-bottom font-size-16 font-500 color-000 line-height-24 p-12">联系方式</view>
class="border-box border-bottom font-size-16 font-500 color-000 line-height-24 p-12"
>联系方式</view
>
<view class="flex flex-items-center border-box p-12"> <view class="flex flex-items-center border-box p-12">
<view class="font-size-14 font-500 color-525866 mr-12"> 联系手机 </view> <view class="font-size-14 font-500 color-525866 mr-12"> 联系人姓名 </view>
<view class="right"> <view class="right">
<input <input class="border-box px-4 py-2 font-size-15 color-000 line-height-20"
class="border-box px-4 py-2 font-size-15 color-000 line-height-20" v-model.trim="userFormList[0].visitorName" placeholder="请输入联系人" maxlength="20" />
v-model.trim="userFormList[0].contactPhone" </view>
placeholder="请输入联系手机" </view>
maxlength="11"
/> <view class="flex flex-items-center border-box p-12">
<view class="font-size-14 font-500 color-525866 mr-12"> 手机号码 </view>
<view class="right">
<input class="border-box px-4 py-2 font-size-15 color-000 line-height-20"
v-model.trim="userFormList[0].contactPhone" placeholder="请输入联系手机" maxlength="11" />
</view> </view>
</view> </view>
</view> </view>
@@ -50,7 +51,7 @@ const props = defineProps({
}, },
userFormList: { userFormList: {
type: Array, type: Array,
default: () => [{ contactPhone: "" }], default: () => [{ visitorName: "", contactPhone: "" }],
}, },
reservationDate: { reservationDate: {
type: String, type: String,

View File

@@ -197,7 +197,7 @@ const validateUserForms = () => {
}); });
if (invalidUsers.length) { if (invalidUsers.length) {
uni.showToast({ title: "请填写住客姓名", icon: "none" }); uni.showToast({ title: "请填写姓名", icon: "none" });
return false; return false;
} }
@@ -211,14 +211,12 @@ const handlePayClick = ThrottleUtils.createThrottle(async (goodsData) => {
try { try {
console.log("处理支付点击事件", userFormList.value); console.log("处理支付点击事件", userFormList.value);
// 判断是酒店类型
if (goodsData.commodityTypeCode === "0") {
// 校验用户姓名 // 校验用户姓名
if (!validateUserForms()) { if (!validateUserForms()) {
uni.hideLoading(); uni.hideLoading();
return; return;
} }
}
// 校验手机号 // 校验手机号
if (!PhoneUtils.validatePhone(userFormList.value[0].contactPhone)) { if (!PhoneUtils.validatePhone(userFormList.value[0].contactPhone)) {
uni.hideLoading(); uni.hideLoading();

View File

@@ -1,22 +1,106 @@
<template> <template>
<view <uni-popup
class="bg-white border-box rounded-12 flex flex-items-center flex-justify-center p-20 mb-12" ref="popupRef"
type="bottom"
:safe-area="false"
@maskClick="handleClose"
@change="handlePopupChange"
> >
<Qrcode <view class="refund-popup bg-F5F7FA border-box">
:size="size" <view class="border-box flex flex-items-center justify-between pt-12 pb-12 relative">
:unit="unit" <view class="flex flex-col flex-items-center flex-full ">
:val="val" <view class="flex-full font-size-16 text-color-900 text-center">核销凭证</view>
:loadMake="true" <view class="flex-full font-size-12 text-color-600 text-center mt-4">请向工作人员出示此码</view>
:onval="true"
/>
</view> </view>
<!-- 关闭按钮 -->
<uni-icons class="close absolute" type="close" size="20" color="#CACFD8" @click="handleClose" />
</view>
<view class="bg-white border-box rounded-12 flex flex-col flex-items-center flex-justify-center p-12 mb-12 mx-12">
<view class="flex w-full flex-row flex-items-center flex-justify-between py-4">
<view >
<view class="font-size-16 font-500 color-000 line-height-24 ellipsis-1">
{{ selectedVoucher.name }}
</view>
</view>
<view v-if="selectedVoucher" class="flex flex-row">
<view class="bg-F5F7FA text-color-600 font-size-12 p-4 rounded-4 mr-4">总计{{ selectedVoucher.count }}{{ selectedVoucher.unit }}
</view>
<view class="bg-theme-color-50 theme-color-500 font-size-12 p-4 rounded-4">{{ selectedVoucher.count - selectedVoucher.writeOffCount }}{{ selectedVoucher.unit }}可用</view>
</view>
</view>
<view class="flex flex-col w-full flex-items-center flex-justify-center border-box rounded-8 border pt-16 pb-32 mt-12">
<view class="flex flex-row flex-justify-between border-box w-full px-12">
<view class="bg-theme-color-50 theme-color-500 font-size-14 px-12 line-height-24 rounded-12">凭证{{ selectedVoucherList.length > 1 ? currentVoucherIndex + 1 : '' }}</view>
<view class="text-color-500 font-size-14 px-12 line-height-24 rounded-12">此码仅可核销{{ selectedVoucher.count - selectedVoucher.writeOffCount }}</view>
</view>
<view class="flex flex-items-center flex-justify-center mt-20 p-20 rounded-12 border borderColor"
:style="{ borderColor: selectedVoucher?.color }">
<Qrcode v-if="showQrcode" :size="props.size" :unit="props.unit" :val="qrcodeVal" :loadMake="true"
:onval="true" />
</view>
<view v-if="false" class="mt-20 bg-theme-color-50 theme-color-500 font-size-14 px-12 line-height-24 rounded-12">
{{ selectedVoucher.name }}
</view>
</view>
<view v-if="selectedVoucherList.length > 1" class="flex flex-row w-full flex-items-center flex-justify-between mt-16 mb-24">
<view class="actions-btn" @click.stop="upAction">
<uni-icons type="left" size="16" color="#171717" />
</view>
<view class="flex flex-col">
<view class="flex flex-row flex-justify-center flex-items-center">
<view class="theme-color-500 font-size-16">{{ currentVoucherIndex + 1 }}</view>
<view class="text-color-600 font-size-12">/{{ selectedVoucherList.length }}</view>
</view>
<view class="text-color-600 font-size-14">扫码后点击下一张</view>
</view>
<view class="actions-btn" @click.stop="downAction">
<uni-icons type="right" size="16" color="#171717" />
</view>
</view>
<view v-else class="mb-24"></view>
</view>
</view>
</uni-popup>
</template> </template>
<script setup> <script setup>
import { defineProps } from "vue";
import Qrcode from "@/components/Qrcode/index.vue"; import Qrcode from "@/components/Qrcode/index.vue";
import { defineProps, defineEmits } from "vue";
import { ref, watch, computed, nextTick } from "vue";
const props = defineProps({ const props = defineProps({
// 弹窗显示状态
modelValue: {
type: Boolean,
default: false,
},
orderData: {
type: Object,
required: true,
default: () => ({}),
},
selectedVoucher: {
type: Object,
required: false,
default: null,
},
selectedVoucherIndex: {
type: Number,
required: false,
default: 0,
},
size: { size: {
type: Number, type: Number,
default: 132, default: 132,
@@ -25,11 +109,133 @@ const props = defineProps({
type: String, type: String,
default: "px", default: "px",
}, },
val: {
type: String,
default: "text",
},
}); });
// Events定义
const emit = defineEmits(["close", "update:modelValue", "update:selectedVoucherIndex"]);
// 弹窗引用
const popupRef = ref(null);
// 控制二维码渲染
const showQrcode = ref(false);
// 当前选择的凭证索引
const currentVoucherIndex = ref(props.selectedVoucherIndex || 0);
const selectedVoucherList = computed(() => {
const list = props.orderData?.commodityPackageConfig || [];
if (!Array.isArray(list)) return [];
return list.filter((item) => item?.packageStatus === 0);
});
// 二维码内容
const selectedVoucher = computed(() => {
const list = selectedVoucherList.value || [];
if (!list.length) return null;
const idx = Math.min(Math.max(Number(currentVoucherIndex.value || 0), 0), list.length - 1);
return list[idx] || null;
});
const qrcodeVal = computed(() => {
const orderId = props.orderData?.orderId || "";
const voucherName = selectedVoucher.value?.name || "";
return orderId ? `${orderId}&${voucherName}` : "";
});
// 方法定义
const show = () => popupRef.value && popupRef.value.open();
const hide = () => popupRef.value && popupRef.value.close();
const handlePopupChange = ({ show }) => {
// popup 真实打开后再渲染二维码,避免 canvas 尺寸为 0 报错
if (show) {
nextTick(() => {
showQrcode.value = true;
});
} else {
showQrcode.value = false;
}
};
// 监听modelValue变化
watch(
() => props.modelValue,
(newVal) => {
if (newVal) {
showQrcode.value = false;
show();
} else {
showQrcode.value = false;
hide();
}
},
{ immediate: true }
);
// 同步外部 prop -> 内部 index初始化/外部变更)
watch(
() => props.selectedVoucherIndex,
(newIdx) => {
const len = selectedVoucherList.value.length;
let idx = Number(newIdx || 0);
if (len && idx >= len) idx = 0;
currentVoucherIndex.value = idx;
},
{ immediate: true }
);
// 当可用凭证列表变化时,确保 currentVoucherIndex 不越界
watch(
selectedVoucherList,
(list) => {
const len = list.length;
if (len === 0) {
if (currentVoucherIndex.value !== 0) {
currentVoucherIndex.value = 0;
}
return;
}
if (currentVoucherIndex.value >= len) {
currentVoucherIndex.value = 0;
}
},
{ immediate: true }
);
// 当内部索引变化时,通知父组件(避免无用循环)
watch(
() => currentVoucherIndex.value,
(val, oldVal) => {
if (val !== props.selectedVoucherIndex) {
emit("update:selectedVoucherIndex", val);
}
}
);
const handleClose = () => {
emit("update:modelValue", false);
emit("close");
};
const upAction = () => {
const len = selectedVoucherList.value.length;
if (!len) return;
let idx = Number(currentVoucherIndex.value || 0) - 1;
if (idx < 0) idx = len - 1;
currentVoucherIndex.value = idx;
};
const downAction = () => {
const len = selectedVoucherList.value.length;
if (!len) return;
let idx = Number(currentVoucherIndex.value || 0) + 1;
if (idx >= len) idx = 0;
currentVoucherIndex.value = idx;
};
</script> </script>
<style scoped lang="scss"></style> <style scoped lang="scss">
@import "./styles/index.scss";
</style>

View File

@@ -0,0 +1,23 @@
.refund-popup {
border-radius: 15px 15px 0 0;
padding-bottom: 40px;
}
.close {
top: 14px;
right: 12px;
}
.borderColor {
border-color: $theme-color-500;
}
.actions-btn {
width: 40px;
height: 40px;
border-radius: 20px;
background-color: #F5F5F5;
display: flex;
align-items: center;
justify-content: center;
}

View File

@@ -4,19 +4,48 @@
核销凭证列表 核销凭证列表
</text> </text>
<view class="flex flex-items-center flex-justify-between py-12 border-bottom"> <view class="flex flex-items-center flex-justify-between py-12"
:class="[index+1 === props.orderData.commodityPackageConfig.length ? '' : 'border-bottom']"
v-for="(item, index) in props.orderData.commodityPackageConfig" :key="item.name">
<view class="flex flex-col"> <view class="flex flex-col">
<text class="text-color-900 font-size-16">森系天幕租赁(3h)</text> <text class="text-color-900 font-size-16">{{ item.name }}</text>
<view class="flex flex-row mt-8"> <view class="flex flex-row mt-8">
<view class="bg-F5F7FA text-color-600 font-size-12 p-4 rounded-4 mr-4">总计2份</view> <view class="bg-F5F7FA text-color-600 font-size-12 p-4 rounded-4 mr-4">总计{{ item.count }}{{item.unit}}</view>
<view class="bg-theme-color-50 theme-color-500 font-size-12 p-4 rounded-4">2份可用</view> <view class="bg-theme-color-50 theme-color-500 font-size-12 p-4 rounded-4">{{ item.count - item.writeOffCount }}{{item.unit}}可用</view>
</view> </view>
</view> </view>
<view class="flex flex-items-center bg-theme-color-500 px-14 py-8 rounded-8" @click="emit('click')"> <view class="flex flex-items-center px-14 py-8 rounded-8" :class="[item.packageStatus === 0 ? 'bg-theme-color-500' : 'bg-gray']" @click="handleShowQrcode(item, index)">
<text class="color-white font-size-14">出示凭证</text> <text v-if="item.packageStatus === 0" class="font-size-14 color-white" >出示凭证</text>
<text v-else class="font-size-14 text-color-300">已核销</text>
</view> </view>
</view> </view>
</view> </view>
</template> </template>
<script setup>
import {defineProps, defineEmits } from "vue";
const emit = defineEmits(["selected"]);
const props = defineProps({
orderData: {
type: Object,
required: true,
default: () => ({}),
},
});
const handleShowQrcode = (item, index) => {
console.log("显示核销凭证二维码,凭证信息:", item);
if (item.packageStatus !== 0) {
uni.showToast({
title: "该凭证已核销使用",
icon: "none",
});
return;
}
emit("selected", { item, index });
};
</script>

View File

@@ -2,39 +2,33 @@
<view class="order-detail-page flex flex-col h-screen"> <view class="order-detail-page flex flex-col h-screen">
<TopNavBar titleAlign="center" :background="$theme-color-100" title="订单详情" /> <TopNavBar titleAlign="center" :background="$theme-color-100" title="订单详情" />
<view <view class="order-detail-wrapper border-box flex-full overflow-hidden scroll-y">
class="order-detail-wrapper border-box flex-full overflow-hidden scroll-y"
>
<OrderStatusInfo :orderData="orderData" /> <OrderStatusInfo :orderData="orderData" />
<OrderQrcode <VoucherList v-if="orderData.orderStatus === '2'" :orderData="orderData" @selected="handleSelectedVoucher" />
v-if="orderData.orderStatus === '2'"
size="132"
unit="px"
:val="orderData.orderId"
/>
<VoucherList/>
<AmtSection :orderData="orderData" @click="refundVisible = true" /> <AmtSection :orderData="orderData" @click="refundVisible = true" />
<GoodsInfo :orderData="orderData" /> <GoodsInfo :orderData="orderData" />
<UserInfo <UserInfo v-if="orderData.commodityTypeCode === '0'" :orderData="orderData" />
v-if="orderData.commodityTypeCode === '0'"
:orderData="orderData"
/>
<OrderInfo :orderData="orderData" /> <OrderInfo :orderData="orderData" />
</view> </view>
<FooterSection <FooterSection :orderData="orderData" @refund="handleRefundConfirm" @refresh="handlePaySuccess" />
:orderData="orderData"
@refund="handleRefundConfirm"
@refresh="handlePaySuccess"
/>
</view> </view>
<!-- 核销凭证 二维码 -->
<OrderQrcode
v-show="visbleQrcode"
v-model="visbleQrcode"
:orderData="orderData"
:selectedVoucher="selectedVoucher"
:selectedVoucherIndex="selectedVoucherIndex"
@close="handleClose"
/>
<!-- 退款状态显示 --> <!-- 退款状态显示 -->
<RefundPopup v-model="refundVisible" :orderData="orderData" /> <RefundPopup v-model="refundVisible" :orderData="orderData" />
</template> </template>
@@ -57,8 +51,27 @@ import RefundPopup from "@/components/RefundPopup/index.vue";
const refundVisible = ref(false); const refundVisible = ref(false);
const orderData = ref({}); const orderData = ref({});
const visbleQrcode = ref(false);
const selectedVoucher = ref(null);
const selectedVoucherIndex = ref(0);
onLoad(({ orderId }) => getOrderDetail(orderId)); onLoad(({ orderId }) => getOrderDetail(orderId));
// 处理选中核销凭证事件
const handleSelectedVoucher = (voucher) => {
console.log("选中的核销凭证:", voucher);
selectedVoucher.value = voucher.item;
selectedVoucherIndex.value = voucher.index;
visbleQrcode.value = true;
}
// 关闭核销凭证二维码弹窗
const handleClose = () => {
visbleQrcode.value = false;
selectedVoucher.value = null;
selectedVoucherIndex.value = 0;
};
// 获取订单详情 // 获取订单详情
const getOrderDetail = async (orderId) => { const getOrderDetail = async (orderId) => {
const res = await userOrderDetail({ orderId }); const res = await userOrderDetail({ orderId });

View File

@@ -1,5 +1,5 @@
.left { .left {
height: 107px; height: 80px;
width: 80px; width: 80px;
} }

View File

@@ -61,8 +61,12 @@ const tabList = ref([]);
// 处理Tab点击 // 处理Tab点击
const handleTabClick = (index) => { const handleTabClick = (index) => {
if (activeIndex.value === index) return; changeTabItem(index);
};
// 支持 force 参数,强制触发(即使 index 与当前相同)
const changeTabItem = (index, force = false) => {
if (!force && activeIndex.value === index) return;
activeIndex.value = index; activeIndex.value = index;
emit("change", { emit("change", {
@@ -70,7 +74,8 @@ const handleTabClick = (index) => {
item: tabList.value[index], item: tabList.value[index],
}); });
emit("update:modelValue", index); emit("update:modelValue", index);
}; }
// 监听tabs变化 // 监听tabs变化
watch( watch(
@@ -114,6 +119,12 @@ const getCommodityTypePageList = async () => {
} else { } else {
tabList.value = props.tabs; tabList.value = props.tabs;
} }
// 加载完成后强制选中第一个项并通知外部
if (tabList.value && tabList.value.length > 0) {
changeTabItem(0, true);
} else {
changeTabItem(props.defaultActive, true);
}
}; };
</script> </script>

View File

@@ -17,7 +17,7 @@
/* 不让子项拉伸,按内容宽度排列 */ /* 不让子项拉伸,按内容宽度排列 */
padding: 0 12px; padding: 0 12px;
/* 增加横向间距,便于触控 */ /* 增加横向间距,便于触控 */
min-width: 56px; min-width: 72px;
/* 保证可点击区域 */ /* 保证可点击区域 */
} }
@@ -42,9 +42,9 @@
&::before { &::before {
content: ""; content: "";
position: absolute; position: absolute;
left: 0; left: 4px;
top: 0; top: 0;
right: 0; right: 4px;
bottom: 0; bottom: 0;
background-color: #fff; background-color: #fff;
border-radius: 20px 20px 0 0; border-radius: 20px 20px 0 0;
@@ -76,9 +76,9 @@
left: 50%; left: 50%;
transform: translateX(-50%) scaleX(0.9); transform: translateX(-50%) scaleX(0.9);
height: 3px; height: 3px;
width: 20px; width: 24px;
background-color: $theme-color-500; background-color: $theme-color-500;
border-radius: 4px 4px 0 0; border-radius: 3px 3px 0 0;
opacity: 0; opacity: 0;
transition: opacity 0.2s ease, transform 0.2s ease; transition: opacity 0.2s ease, transform 0.2s ease;
z-index: 3; z-index: 3;

View File

@@ -7,7 +7,7 @@
<Tabs @change="handleTabChange"/> <Tabs @change="handleTabChange"/>
<!-- 选择入住离店日期 0:是酒店 --> <!-- 选择入住离店日期 0:是酒店 -->
<view v-if="currentType === '0' && dataList.length > 0" class="bg-white border-box flex flex-items-center p-12"> <view v-if="didSelectedTabItem && didSelectedTabItem.orderType === 0" class="bg-white border-box flex flex-items-center p-12">
<view class="in flex flex-items-center"> <view class="in flex flex-items-center">
<text class="font-size-11 font-500 color-99A0AE mr-4">入住</text> <text class="font-size-11 font-500 color-99A0AE mr-4">入住</text>
<text class="font-size-14 font-500 color-171717"> <text class="font-size-14 font-500 color-171717">
@@ -57,17 +57,22 @@ const selectedDate = ref({
}); });
const dataList = ref([]); const dataList = ref([]);
const paging = ref(null); const paging = ref(null);
const currentType = ref("0"); // 当前选中类型 // 当前选中
const didSelectedTabItem = ref(null);
const queryList = async (pageNum = 1, pageSize = 10) => { const queryList = async (pageNum = 1, pageSize = 10) => {
if (!didSelectedTabItem.value) {
paging.value.complete([]);
return;
}
try { try {
const params = { const params = {
commodityTypeCode: currentType.value, commodityTypeCode: didSelectedTabItem.value.typeCode,
size: pageSize, size: pageSize,
current: pageNum, current: pageNum,
}; };
if (currentType.value === "0") { if (didSelectedTabItem.value.orderType === 0) {
params.checkInDate = selectedDate.value.startDate; params.checkInDate = selectedDate.value.startDate;
params.checkOutDate = selectedDate.value.endDate; params.checkOutDate = selectedDate.value.endDate;
} }
@@ -92,12 +97,8 @@ const queryList = async (pageNum = 1, pageSize = 10) => {
}; };
const handleTabChange = ({ item }) => { const handleTabChange = ({ item }) => {
console.log("item typeCode: ", item.typeCode); console.log("选中的tab item: ", item);
currentType.value = item.typeCode; didSelectedTabItem.value = item;
if (currentType.value === "0") {
}
paging.value.reload(); paging.value.reload();
}; };

View File

@@ -33,6 +33,16 @@
"navigationBarTextStyle": "black" "navigationBarTextStyle": "black"
} }
} }
,
{
"path": "pages/long-answer/index",
"style": {
"navigationStyle": "custom",
"backgroundColor": "#FFFFFF",
"navigationBarBackgroundColor": "#FFFFFF",
"navigationBarTextStyle": "black"
}
}
], ],
"subPackages": [ "subPackages": [
{ {

View File

@@ -7,7 +7,7 @@
<view class="flex flex-items-start flex-col" <view class="flex flex-items-start flex-col"
v-for="(item, index) in goodsData.commodityPackageConfig" :key="index" v-for="(item, index) in goodsData.commodityPackageConfig" :key="index"
> >
<view class="title-row ml-4 mb-4"> <view class="title-row py-4 ml-4">
<text class="left font-size-14 color-171717">{{ item.name }}</text> <text class="left font-size-14 color-171717">{{ item.name }}</text>
<view class="sep" aria-hidden="true"></view> <view class="sep" aria-hidden="true"></view>
<text class="right font-size-14 color-171717">{{ item.count }}{{ item.unit }}</text> <text class="right font-size-14 color-171717">{{ item.count }}{{ item.unit }}</text>

View File

@@ -15,33 +15,37 @@
<view class="area-msg-list-content" v-for="item in chatMsgList" :key="item.msgId" :id="item.msgId"> <view class="area-msg-list-content" v-for="item in chatMsgList" :key="item.msgId" :id="item.msgId">
<template v-if="item.msgType === MessageRole.AI"> <template v-if="item.msgType === MessageRole.AI">
<ChatCardAI class="flex flex-justify-start" :key="`ai-${item.msgId}-${item.msg ? item.msg.length : 0}`" <ChatCardAI class="flex flex-justify-start" :key="`ai-${item.msgId}-${item.msg ? item.msg.length : 0}`"
:text="item.msg || ''" :isLoading="item.isLoading"> :text="item.finish && item.componentName ? '' : item.msg || ''" :isLoading="item.isLoading">
<template #content v-if="item.toolCall"> <template #content v-if="item.toolCall || item.componentName">
<QuickBookingComponent v-if="item.toolCall.componentName === CompName.quickBookingCard" /> <AnswerComponent v-if="
item.componentName === CompName.longTextCard
" :text="item.msg" :title="item.title" />
<QuickBookingComponent v-if="
item.toolCall && item.toolCall.componentName === CompName.quickBookingCard
" />
<DiscoveryCardComponent v-else-if=" <DiscoveryCardComponent v-else-if="
item.toolCall.componentName === CompName.discoveryCard item.toolCall && item.toolCall.componentName === CompName.discoveryCard
" /> " />
<CreateServiceOrder v-else-if=" <CreateServiceOrder v-else-if="
item.toolCall.componentName === CompName.callServiceCard item.toolCall && item.toolCall.componentName === CompName.callServiceCard
" :toolCall="item.toolCall" /> " :toolCall="item.toolCall" />
<OpenMapComponent v-else-if=" <OpenMapComponent v-else-if="
item.toolCall.componentName === CompName.openMapCard item.toolCall && item.toolCall.componentName === CompName.mapCard
" /> " />
<AnswerComponent v-else-if=" <GeneratorPhotoComponent v-else-if="
item.toolCall.componentName === CompName.answerCard item.toolCall && item.toolCall.componentName === CompName.generateMessageId
" /> " />
<Feedback v-else-if=" <Feedback v-else-if="
item.toolCall.componentName === CompName.feedbackCard item.toolCall && item.toolCall.componentName === CompName.feedbackCard
" :toolCall="item.toolCall" /> " :toolCall="item.toolCall" />
<DetailCardCompontent v-else-if=" <DetailCardCompontent v-else-if="
item.toolCall.componentName === item.toolCall && item.toolCall.componentName === CompName.pictureAndCommodityCard
CompName.pictureAndCommodityCard
" :toolCall="item.toolCall" /> " :toolCall="item.toolCall" />
<AddCarCrad v-else-if=" <AddCarCrad v-else-if="
item.toolCall.componentName === CompName.enterLicensePlateCard item.toolCall && item.toolCall.componentName === CompName.enterLicensePlateCard
" :toolCall="item.toolCall" /> " :toolCall="item.toolCall" />
<SurveyQuestionnaire v-else-if=" <SurveyQuestionnaire v-else-if="
item.toolCall.componentName === CompName.callSurveyQuestionnaire item.toolCall && item.toolCall.componentName === CompName.callSurveyQuestionnaire
" :toolCall="item.toolCall" /> " :toolCall="item.toolCall" />
</template> </template>
@@ -109,6 +113,7 @@ import AttachListComponent from "../../module/AttachListComponent/index.vue";
import DetailCardCompontent from "../../module/DetailCardCompontent/index.vue"; import DetailCardCompontent from "../../module/DetailCardCompontent/index.vue";
import OpenMapComponent from "../../module/OpenMapComponent/index.vue"; import OpenMapComponent from "../../module/OpenMapComponent/index.vue";
import AnswerComponent from "../../module/AnswerComponent/index.vue"; import AnswerComponent from "../../module/AnswerComponent/index.vue";
import GeneratorPhotoComponent from "../../module/GeneratorPhotoComponent/index.vue";
import CreateServiceOrder from "@/components/CreateServiceOrder/index.vue"; import CreateServiceOrder from "@/components/CreateServiceOrder/index.vue";
import Feedback from "@/components/Feedback/index.vue"; import Feedback from "@/components/Feedback/index.vue";
import AddCarCrad from "@/components/AddCarCrad/index.vue"; import AddCarCrad from "@/components/AddCarCrad/index.vue";
@@ -471,25 +476,32 @@ const handleWebSocketMessage = (data) => {
return; return;
} }
// 确保消息内容是字符串类型
if (data.content && typeof data.content !== "string") {
try {
data.content = JSON.stringify(data.content);
} catch (e) {
data.content = String(data.content);
}
}
// 优先使用 messageId 进行匹配
const msgId = data.messageId || data.id || data.msgId;
let aiMsgIndex = -1; let aiMsgIndex = -1;
if (msgId && pendingMap.has(msgId)) { if (currentSessionMessageId && pendingMap.has(currentSessionMessageId)) {
aiMsgIndex = pendingMap.get(msgId);
} else if (!msgId && currentSessionMessageId && pendingMap.has(currentSessionMessageId)) {
// 服务端未返回 messageId 的场景:优先使用当前会话的 messageId 映射
aiMsgIndex = pendingMap.get(currentSessionMessageId); aiMsgIndex = pendingMap.get(currentSessionMessageId);
if (aiMsgIndex >= 0 && aiMsgIndex < chatMsgList.value.length) {
const item = chatMsgList.value[aiMsgIndex];
if (item && item.msgType === MessageRole.AI &&
item.replyMessageId.length > 0 && data.replyMessageId &&
item.replyMessageId !== data.replyMessageId) {
// 已经存在对应的AI消息项继续使用
const aiMsg = {
msgId: `msg_${chatMsgList.value.length}`,
msgType: MessageRole.AI,
msg: "",
isLoading: false,
messageId: currentSessionMessageId,
replyMessageId: '',
componentName: "",
title: "",
finish: false,
};
chatMsgList.value.push(aiMsg);
aiMsgIndex = chatMsgList.value.length - 1;
}
}
} else { } else {
// 向后搜索最近的 AI 消息 // 向后搜索最近的 AI 消息(回退逻辑)
for (let i = chatMsgList.value.length - 1; i >= 0; i--) { for (let i = chatMsgList.value.length - 1; i >= 0; i--) {
if (chatMsgList.value[i] && chatMsgList.value[i].msgType === MessageRole.AI) { if (chatMsgList.value[i] && chatMsgList.value[i].msgType === MessageRole.AI) {
aiMsgIndex = i; aiMsgIndex = i;
@@ -502,6 +514,20 @@ const handleWebSocketMessage = (data) => {
} }
} }
// replyMessageId
if(data.replyMessageId) {
chatMsgList.value[aiMsgIndex].replyMessageId = data.replyMessageId;
}
// 确保消息内容是字符串类型
if (data.content && typeof data.content !== "string") {
try {
data.content = JSON.stringify(data.content);
} catch (e) {
data.content = String(data.content);
}
}
// 直接拼接内容到对应 AI 消息 // 直接拼接内容到对应 AI 消息
if (data.content) { if (data.content) {
if (chatMsgList.value[aiMsgIndex].isLoading) { if (chatMsgList.value[aiMsgIndex].isLoading) {
@@ -517,13 +543,22 @@ const handleWebSocketMessage = (data) => {
// 处理完成状态 // 处理完成状态
if (data.finish) { if (data.finish) {
chatMsgList.value[aiMsgIndex].finish = true;
const msg = chatMsgList.value[aiMsgIndex].msg; const msg = chatMsgList.value[aiMsgIndex].msg;
if (!msg || chatMsgList.value[aiMsgIndex].isLoading) { if (!msg || chatMsgList.value[aiMsgIndex].isLoading) {
chatMsgList.value[aiMsgIndex].msg = "未获取到内容,请重试"; // 如果服务器返回了 componentName 或 toolCall应保留空消息以供组件渲染否则显示错误占位
chatMsgList.value[aiMsgIndex].isLoading = false; if (data.toolCall || data.componentName) {
if (data.toolCall) {
chatMsgList.value[aiMsgIndex].msg = ""; chatMsgList.value[aiMsgIndex].msg = "";
} else {
chatMsgList.value[aiMsgIndex].msg = "未获取到内容,请重试";
} }
chatMsgList.value[aiMsgIndex].isLoading = false;
}
// 处理组件调用
if (data.componentName) {
chatMsgList.value[aiMsgIndex].title = data.content;
chatMsgList.value[aiMsgIndex].componentName = data.componentName;
} }
// 处理toolCall // 处理toolCall
@@ -550,6 +585,8 @@ const handleWebSocketMessage = (data) => {
isSessionActive.value = false; isSessionActive.value = false;
// 清理当前会话的 messageId避免保留陈旧 id // 清理当前会话的 messageId避免保留陈旧 id
resetMessageState(); resetMessageState();
nextTick(() => scrollToBottom());
} }
}; };
@@ -755,6 +792,10 @@ const sendChat = async (message, isInstruct = false) => {
msg: "加载中", msg: "加载中",
isLoading: true, isLoading: true,
messageId: currentSessionMessageId, messageId: currentSessionMessageId,
replyMessageId: '',
componentName: "",
title: "",
finish: false,
}; };
chatMsgList.value.push(aiMsg); chatMsgList.value.push(aiMsg);
// 添加AI消息后滚动到底部 // 添加AI消息后滚动到底部

View File

@@ -3,17 +3,17 @@
<!-- 占位撑开 --> <!-- 占位撑开 -->
<view class="w-vw"></view> <view class="w-vw"></view>
<view class="flex flex-col p-16 border-box"> <view class="flex flex-col p-16 border-box">
<view class="flex flex-row flex-items-center justify-center"> <view class="flex flex-row flex-items-start justify-center">
<uni-icons class="icon-active" type="fire-filled" size="18" color="opacity" /> <uni-icons class="icon-active" type="fire-filled" size="18" color="opacity" />
<text class="font-size-16 font-500 text-color-900 ml-6">游玩划重点</text> <text class="font-size-16 font-500 text-color-900 ml-6">游玩划重点</text>
</view> </view>
<!-- 文字内容最多显示3行 --> <!-- 文字内容最多显示3行 -->
<view class="answer-content font-size-12 font-color-600 mt-8"> <view class="answer-content font-size-12 font-color-600 mt-8">
{{ answerText }} <ChatMarkdown :key="textKey" :text="processedText" />
</view> </view>
<!-- 超过3行时显示...提示 --> <!-- 超过3行时显示...提示 -->
<view class="fles flex-row mt-8" v-if="isOverflow"> <view class="flex flex-row mt-8" v-if="isOverflow" @click="lookDetailAction">
<text class="font-size-12 font-400 theme-color-500 mr-4">查看完整攻略</text> <text class="font-size-12 font-400 theme-color-500 mr-4">查看详情</text>
<uni-icons class="icon-active" type="right" size="14" color="opacity"></uni-icons> <uni-icons class="icon-active" type="right" size="14" color="opacity"></uni-icons>
</view> </view>
</view> </view>
@@ -22,15 +22,52 @@
</template> </template>
<script setup> <script setup>
import { ref } from 'vue' import { defineProps, computed, ref, watch } from "vue";
const isOverflow = ref(true) import ChatMarkdown from "../../chat/ChatMarkdown/index.vue";
const isOverflow = ref(false)
// 直接根据文字长度判断超过约100个字符认为会溢出约3行 // 直接根据文字长度判断超过约100个字符认为会溢出约3行
const answerText = '建议一定要去「东华门」看角楼夕阳,那里是摄影师的私藏机位。御花园目前的玉兰花开得正好,别忘了抬头看看红墙上的猫。建议一定要去「东华门」看角楼夕阳,那里是摄影师的私藏机位。御花园目前的玉兰花开得正好,别忘了抬头看看红墙上的猫。' const props = defineProps({
title: {
type: String,
default: "",
},
text: {
type: String,
default: "",
}
});
// 简单判断12号字体3行约100个字符 // 用于强制重新渲染的key
isOverflow.value = answerText.length > 100 const textKey = ref(0);
// 处理文本内容(纯计算,不应有副作用)
const processedText = computed(() => {
if (!props.text) return "";
return String(props.text);
});
// 监听 text 变化:更新 textKey 并同步 isOverflow合并为单一响应函数避免冗余
watch(
() => props.text,
(newText, oldText) => {
const textStr = newText ? String(newText) : "";
isOverflow.value = textStr.length > 100;
if (newText !== oldText) {
textKey.value++;
}
},
{ immediate: true }
);
const lookDetailAction = () => {
const message = props.text ? String(props.text) : "";
uni.navigateTo({
url: `/pages/long-answer/index?message=${encodeURIComponent(message)}`,
});
}
</script> </script>

Binary file not shown.

After

Width:  |  Height:  |  Size: 532 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@@ -0,0 +1,34 @@
<template>
<view class="container w-full border-box border-ff overflow-hidden rounded-24 flex flex-col">
<!-- 占位撑开 -->
<view class="w-vw"></view>
<view class="content flex flex-col m-4 border-box rounded-24">
<view class="flex flex-col p-6 border-box flex-items-center justify-center">
<image class="w-full rounded-20" src="./images/g_image.png" mode="widthFix" />
<image class="g_title mt-20" src="./images/g_title.png" />
<text class="font-size-14 font-400 color-white my-12 text-center">
生成合照写真一键出片 \n
来到小七孔不打个卡再走吗
</text>
<view class="w-full border-box px-12 pb-8 flex flex-row">
<view class="btn-bg w-full border-box p-4 rounded-24 flex flex-row">
<view class="btn-bg-sub w-full border-box p-16 rounded-20 flex flex-row flex-items-center flex-justify-center color-white text-center font-size-18 font-800">
参加活动
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
</script>
<style lang="scss" scoped>
@import "./styles/index.scss";
</style>

View File

@@ -0,0 +1,25 @@
.container {
background: rgba(255, 255, 255, 0.2);
box-shadow: 0px 9px 34px 0px rgba(27, 9, 91, 0.07);
}
.content {
background: linear-gradient(180deg, $theme-color-100 0%, $theme-color-500 100%);
border-radius: 24px 24px 24px 24px;
border: 1px solid #FFFFFF;
}
.g_title {
width: 197px;
height: 42px;
}
.btn-bg {
background: rgba(255, 255, 255, 0.32);
border: 0.66px solid rgba(255, 255, 255, 0.32);
}
.btn-bg-sub {
background: $theme-color-500;
box-shadow: inset 0px 0px 41px 0px $theme-color-50, inset 0px 0px 15px 0px $theme-color-100;
}

View File

@@ -0,0 +1,19 @@
<template>
<ChatMarkdown :text="answerText" />
</template>
<script setup>
import ChatMarkdown from "../index/components/chat/ChatMarkdown/index.vue";
import { defineProps, onLoad } from "vue";
const props = defineProps({
answerText: {
type: String,
default: "",
}
});
onLoad(({ message = "" }) => {
props.answerText = decodeURIComponent(message);
});
</script>

View File

@@ -1,6 +1,6 @@
export const zniconsMap = { export const zniconsMap = {
"zn-wifi": "\ue681", "zn-wifi": "\ue681",
"zn-bath": "\ue682", "zn-bath": "\ue69a",
"zn-frame": "\ue683", "zn-frame": "\ue683",
"zn-shower-gel": "\ue684", "zn-shower-gel": "\ue684",
"zn-a-washingmachine": "\ue685", "zn-a-washingmachine": "\ue685",

View File

@@ -297,6 +297,30 @@
padding-bottom: 24px; padding-bottom: 24px;
} }
.p-32 {
padding: 32px;
}
.pt-32 {
padding-top: 32px;
}
.pb-32 {
padding-bottom: 32px;
}
.pl-32 {
padding-left: 32px;
}
.pr-32 {
padding-right: 32px;
}
.px-32 {
padding-left: 32px;
padding-right: 32px;
}
.py-32 {
padding-top: 32px;
padding-bottom: 32px;
}
.pb-safe-area { .pb-safe-area {
padding-bottom: Max(env(safe-area-inset-bottom), 12px); padding-bottom: Max(env(safe-area-inset-bottom), 12px);
} }

View File

@@ -31,6 +31,10 @@
border-radius: 20px; border-radius: 20px;
} }
.rounded-24 {
border-radius: 24px;
}
.rounded-50 { .rounded-50 {
border-radius: 50px; border-radius: 50px;
} }

View File

@@ -313,7 +313,7 @@ export default {
border:none; border:none;
text-align:center; text-align:center;
background-image:linear-gradient(to right,rgba(248,57,41,0),${themeColor},rgba(248,57,41,0)); background-image:linear-gradient(to right,rgba(248,57,41,0),${themeColor},rgba(248,57,41,0));
margin:4px 0; margin:10px 0;
`, `,
// 表格 // 表格
table: ` table: `

View File

@@ -215,7 +215,7 @@ export class WebSocketManager {
if (typeof messageData === "string") { if (typeof messageData === "string") {
// 处理心跳响应 // 处理心跳响应
if (MessageUtils.isPongMessage(messageData)) { if (MessageUtils.isPongMessage(messageData)) {
console.log("收到心跳响应:", messageData); // console.log("收到心跳响应:", messageData);
return; return;
} }
@@ -399,7 +399,7 @@ export class WebSocketManager {
}; };
this.sendMessage(heartbeatMessage); this.sendMessage(heartbeatMessage);
console.log("心跳消息已发送:", heartbeatMessage); // console.log("心跳消息已发送:", heartbeatMessage);
} catch (error) { } catch (error) {
console.error("发送心跳失败:", error); console.error("发送心跳失败:", error);
this.handleError(error); this.handleError(error);