Compare commits

..

No commits in common. "00ab94b840a37028810e51cd90b0c121510909ce" and "892fccb4d81b0eed7cad9878ae4975a9c33de3df" have entirely different histories.

13 changed files with 1436 additions and 1533 deletions

47
App.vue
View File

@ -5,32 +5,32 @@ import { goLogin } from "@/hooks/useGoLogin";
import { goHome } from "@/hooks/useGoHome"; import { goHome } from "@/hooks/useGoHome";
onLaunch(async () => { onLaunch(async () => {
console.log("App Launch"); console.log("App Launch");
const token = uni.getStorageSync("token"); const token = uni.getStorageSync("token");
// token // token
if (!token) { if (!token) {
goLogin(); goLogin();
return; return;
} }
if (token) { if (token) {
const res = await checkPhone(); const res = await checkPhone();
if (res.data) { if (res.data) {
goHome(); goHome();
}
} }
}
}); });
onShow(() => { onShow(() => {
console.log("App Show"); console.log("App Show");
}); });
onHide(() => { onHide(() => {
console.log("App Hide"); console.log("App Hide");
}); });
</script> </script>
@ -41,18 +41,13 @@ onHide(() => {
page, page,
body, body,
#app { #app {
font-family: PingFang SC, PingFang SC; font-family: PingFang SC, PingFang SC;
background-color: #e9f3f7; background-color: #e9f3f7;
height: 100vh; height: 100vh;
width: 100vw; width: 100vw;
}
/*每个页面公共css */
::-webkit-scrollbar {
display: none;
} }
.mb12 { .mb12 {
margin-bottom: 12px; margin-bottom: 12px;
} }
</style> </style>

View File

@ -1,14 +1,11 @@
<template> <template>
<view class="container"> <view class="container">
<view class="chat-ai"> <view class="chat-ai">
<view class="loading-container" > <ChatMarkdown :key="textKey" :text="processedText" />
<image v-if="isLoading" class="loading-img" src="/static/msg_loading.svg" /> <slot name="content"></slot>
<ChatMarkdown :key="textKey" :text="processedText" />
</view>
<slot name="content"></slot>
</view>
<slot name="footer"></slot>
</view> </view>
<slot name="footer"></slot>
</view>
</template> </template>
<script setup> <script setup>
@ -16,14 +13,10 @@ import { defineProps, computed, ref, watch } from "vue";
import ChatMarkdown from "./ChatMarkdown.vue"; import ChatMarkdown from "./ChatMarkdown.vue";
const props = defineProps({ const props = defineProps({
text: { text: {
type: String, type: String,
default: "", default: "",
}, },
isLoading: {
type: Boolean,
default: false,
},
}); });
// key // key
@ -31,63 +24,50 @@ const textKey = ref(0);
// //
const processedText = computed(() => { const processedText = computed(() => {
if (!props.text) { if (!props.text) {
return ""; return "";
} }
// //
const textStr = String(props.text); const textStr = String(props.text);
//
if (textStr.includes("加载中") || textStr.includes("...")) {
return textStr;
}
//
if (textStr.includes("加载中") || textStr.includes("...")) {
return textStr; return textStr;
}
return textStr;
}); });
// text // text
watch( watch(
() => props.text, () => props.text,
(newText, oldText) => { (newText, oldText) => {
if (newText !== oldText) { if (newText !== oldText) {
textKey.value++; textKey.value++;
} }
}, },
{ immediate: true } { immediate: true }
); );
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.container { .container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
max-width: 100%; // max-width: 100%; //
overflow-x: hidden; // overflow-x: hidden; //
.chat-ai { .chat-ai {
margin: 6px 12px; margin: 6px 12px;
padding: 0 12px; padding: 0 12px;
min-width: 90px; min-width: 80px;
background: rgba(255, 255, 255, 0.4); background: rgba(255, 255, 255, 0.4);
box-shadow: 2px 2px 6px 0px rgba(0, 0, 0, 0.1); box-shadow: 2px 2px 6px 0px rgba(0, 0, 0, 0.1);
border-radius: 4px 20px 20px 20px; border-radius: 4px 20px 20px 20px;
border: 1px solid; border: 1px solid;
border-color: #ffffff; border-color: #ffffff;
} }
} }
</style>
.loading-container {
display: flex;
align-items: center;
padding: 4px 0;
}
.loading-img {
margin-left: -4px;
width: 32px;
height: 32px;
}
</style>

View File

@ -1,65 +1,62 @@
<template> <template>
<view class="input-area-wrapper"> <view class="input-area-wrapper">
<view v-if="!visibleWaveBtn" class="area-input"> <view v-if="!visibleWaveBtn" class="area-input">
<!-- 语音/键盘切换 --> <!-- 语音/键盘切换 -->
<view class="input-container-voice" @click="toggleVoiceMode"> <view class="input-container-voice" @click="toggleVoiceMode">
<image <image v-if="!isVoiceMode" src="/static/input_voice_icon.png"></image>
v-if="!isVoiceMode" <image v-else src="/static/input_keyboard_icon.png"></image>
src="/static/input_voice_icon.png" </view>
></image>
<image v-else src="/static/input_keyboard_icon.png"></image>
</view>
<!-- 输入框/语音按钮容器 --> <!-- 输入框/语音按钮容器 -->
<view class="input-button-container"> <view class="input-button-container">
<textarea <textarea
ref="textareaRef" ref="textareaRef"
v-if="!isVoiceMode" v-if="!isVoiceMode"
class="textarea" :class="['textarea', ios ? 'ios' : 'android']"
type="text" type="text"
cursor-spacing="20" cursor-spacing="65"
confirm-type="send" confirm-type="send"
v-model="inputMessage" v-model="inputMessage"
auto-height auto-height
:confirm-hold="true" :confirm-hold="true"
:placeholder="placeholder" :placeholder="placeholder"
:show-confirm-bar="false" :show-confirm-bar="false"
:hold-keyboard="holdKeyboard" :hold-keyboard="holdKeyboard"
:adjust-position="true" :adjust-position="true"
:disable-default-padding="true" :disable-default-padding="true"
maxlength="300" maxlength="300"
@confirm="sendMessage" @confirm="sendMessage"
@focus="handleFocus" @focus="handleFocus"
@blur="handleBlur" @blur="handleBlur"
@touchstart="handleTouchStart" @touchstart="handleTouchStart"
@touchend="handleTouchEnd" @touchend="handleTouchEnd"
/> />
<view <view
v-if="isVoiceMode" v-if="isVoiceMode"
class="hold-to-talk-button" class="hold-to-talk-button"
@longpress="handleVoiceTouchStart" @longpress="handleVoiceTouchStart"
@touchend="handleVoiceTouchEnd" @touchend="handleVoiceTouchEnd"
@touchcancel="handleVoiceTouchEnd" @touchcancel="handleVoiceTouchEnd"
> >
按住 说话 按住 说话
</view>
</view>
<view class="input-container-send">
<view class="input-container-send-btn" @click="sendMessage">
<image
v-if="props.isSessionActive"
src="/static/input_stop_icon.png"
></image>
<image v-else src="/static/input_send_icon.png"></image>
</view>
</view>
</view> </view>
</view>
<!-- 录音按钮 --> <view class="input-container-send">
<RecordingWaveBtn v-if="visibleWaveBtn" ref="recordingWaveBtnRef" /> <view class="input-container-send-btn" @click="sendMessage">
<image
v-if="props.isSessionActive"
src="/static/input_stop_icon.png"
></image>
<image v-else src="/static/input_send_icon.png"></image>
</view>
</view>
</view> </view>
<!-- 录音按钮 -->
<RecordingWaveBtn v-if="visibleWaveBtn" ref="recordingWaveBtnRef" />
</view>
</template> </template>
<script setup> <script setup>
@ -70,18 +67,18 @@ const plugin = requirePlugin("WechatSI");
const manager = plugin.getRecordRecognitionManager(); const manager = plugin.getRecordRecognitionManager();
const props = defineProps({ const props = defineProps({
modelValue: String, modelValue: String,
holdKeyboard: Boolean, holdKeyboard: Boolean,
isSessionActive: Boolean, isSessionActive: Boolean,
stopRequest: Function, stopRequest: Function,
}); });
const emit = defineEmits([ const emit = defineEmits([
"update:modelValue", "update:modelValue",
"send", "send",
"noHideKeyboard", "noHideKeyboard",
"keyboardShow", "keyboardShow",
"keyboardHide", "keyboardHide",
"sendVoice", "sendVoice",
]); ]);
const textareaRef = ref(null); const textareaRef = ref(null);
@ -93,139 +90,144 @@ const keyboardHeight = ref(0);
const isVoiceMode = ref(false); const isVoiceMode = ref(false);
const visibleWaveBtn = ref(false); const visibleWaveBtn = ref(false);
// iOS
const ios = computed(() => {
return uni.getSystemInfoSync().platform === "ios";
});
// //
watch( watch(
() => props.modelValue, () => props.modelValue,
(val) => { (val) => {
inputMessage.value = val; inputMessage.value = val;
} }
); );
// inputMessage // inputMessage
watch(inputMessage, (val) => { watch(inputMessage, (val) => {
// emit // emit
if (val !== props.modelValue) { if (val !== props.modelValue) {
emit("update:modelValue", val); emit("update:modelValue", val);
} }
}); });
// / // /
const toggleVoiceMode = () => { const toggleVoiceMode = () => {
isVoiceMode.value = !isVoiceMode.value; isVoiceMode.value = !isVoiceMode.value;
}; };
// //
const handleVoiceTouchStart = () => { const handleVoiceTouchStart = () => {
manager.start({ lang: "zh_CN" }); manager.start({ lang: "zh_CN" });
visibleWaveBtn.value = true; visibleWaveBtn.value = true;
// //
nextTick(() => { nextTick(() => {
if (recordingWaveBtnRef.value) { if (recordingWaveBtnRef.value) {
recordingWaveBtnRef.value.startAnimation(); recordingWaveBtnRef.value.startAnimation();
} }
}); });
}; };
// //
const handleVoiceTouchEnd = () => { const handleVoiceTouchEnd = () => {
manager.stop(); manager.stop();
// //
if (recordingWaveBtnRef.value) { if (recordingWaveBtnRef.value) {
recordingWaveBtnRef.value.stopAnimation(); recordingWaveBtnRef.value.stopAnimation();
} }
visibleWaveBtn.value = false; visibleWaveBtn.value = false;
}; };
// //
const initRecord = () => { const initRecord = () => {
manager.onRecognize = (res) => { manager.onRecognize = (res) => {
let text = res.result; let text = res.result;
inputMessage.value = text; inputMessage.value = text;
}; };
// //
manager.onStop = (res) => { manager.onStop = (res) => {
console.log(res, 37); console.log(res, 37);
let text = res.result; let text = res.result;
if (text == "") { if (text == "") {
console.log("没有说话"); console.log("没有说话");
return; return;
} }
inputMessage.value = text; inputMessage.value = text;
// //
emit("send", text); emit("send", text);
}; };
}; };
// //
onMounted(() => { onMounted(() => {
// //
uni.onKeyboardHeightChange((res) => { uni.onKeyboardHeightChange((res) => {
keyboardHeight.value = res.height; keyboardHeight.value = res.height;
if (res.height) { if (res.height) {
emit("keyboardShow", res.height); emit("keyboardShow", res.height);
} else { } else {
emit("keyboardHide"); emit("keyboardHide");
} }
}); });
initRecord(); initRecord();
}); });
const sendMessage = () => { const sendMessage = () => {
if (props.isSessionActive) { if (props.isSessionActive) {
// //
if (props.stopRequest) { if (props.stopRequest) {
props.stopRequest(); props.stopRequest();
}
} else {
//
if (!inputMessage.value.trim()) return;
emit("send", inputMessage.value);
//
if (props.holdKeyboard && textareaRef.value) {
nextTick(() => {
textareaRef.value.focus();
});
}
} }
} else {
//
if (!inputMessage.value.trim()) return;
emit("send", inputMessage.value);
//
if (props.holdKeyboard && textareaRef.value) {
nextTick(() => {
textareaRef.value.focus();
});
}
}
}; };
const handleFocus = () => { const handleFocus = () => {
isFocused.value = true; isFocused.value = true;
emit("noHideKeyboard"); emit("noHideKeyboard");
}; };
const handleBlur = () => { const handleBlur = () => {
isFocused.value = false; isFocused.value = false;
}; };
const handleTouchStart = () => { const handleTouchStart = () => {
emit("noHideKeyboard"); emit("noHideKeyboard");
}; };
const handleTouchEnd = () => { const handleTouchEnd = () => {
emit("noHideKeyboard"); emit("noHideKeyboard");
}; };
// //
const focusInput = () => { const focusInput = () => {
if (textareaRef.value) { if (textareaRef.value) {
textareaRef.value.focus(); textareaRef.value.focus();
} }
}; };
// //
const blurInput = () => { const blurInput = () => {
if (textareaRef.value) { if (textareaRef.value) {
textareaRef.value.blur(); textareaRef.value.blur();
} }
}; };
// //
@ -234,87 +236,92 @@ defineExpose({ focusInput });
<style scoped lang="scss"> <style scoped lang="scss">
.area-input { .area-input {
display: flex;
align-items: flex-end;
border-radius: 22px;
background-color: #ffffff;
box-shadow: 0px 0px 20px 0px rgba(52, 25, 204, 0.05);
margin: 0 12px;
margin-bottom: 8px;
.input-container-voice {
display: flex; display: flex;
align-items: center; align-items: center;
border-radius: 22px; justify-content: center;
width: 44px;
height: 44px;
image {
width: 22px;
height: 22px;
}
}
.input-button-container {
flex: 1;
position: relative;
}
.hold-to-talk-button {
width: 100%;
height: 44px;
color: #333333;
font-size: 16px;
display: flex;
justify-content: center;
align-items: center;
background-color: #ffffff; background-color: #ffffff;
box-shadow: 0px 0px 20px 0px rgba(52, 25, 204, 0.05); transition: all 0.2s ease;
margin: 0 12px; user-select: none;
margin-bottom: 8px; -webkit-user-select: none;
}
.input-container-voice { .input-container-send {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
align-self: flex-end; width: 44px;
width: 44px; height: 44px;
height: 44px;
image { .input-container-send-btn {
width: 22px; display: flex;
height: 22px; align-items: center;
} justify-content: center;
width: 100%;
height: 100%;
} }
.input-button-container { image {
flex: 1; width: 28px;
position: relative; height: 28px;
}
}
.textarea {
flex: 1;
box-sizing: border-box;
width: 100%;
max-height: 92px;
min-height: 44px;
font-size: 16px;
line-height: normal;
&.android {
padding: 12px 0;
} }
.hold-to-talk-button { &.ios {
width: 100%; padding: 6px 0;
height: 44px;
color: #333333;
font-size: 16px;
display: flex;
justify-content: center;
align-items: center;
background-color: #ffffff;
transition: all 0.2s ease;
user-select: none;
-webkit-user-select: none;
} }
.input-container-send { &::placeholder {
display: flex; color: #cccccc;
align-items: center; line-height: normal;
justify-content: center;
align-self: flex-end;
width: 44px;
height: 44px;
.input-container-send-btn {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
image {
width: 28px;
height: 28px;
}
} }
.textarea { &:focus {
flex: 1; outline: none;
box-sizing: border-box;
width: 100%;
max-height: 92px;
min-height: 22px;
font-size: 16px;
line-height: 22px;
margin: 6px 0;
&::placeholder {
color: #cccccc;
line-height: normal;
}
&:focus {
outline: none;
}
} }
}
} }
</style> </style>

File diff suppressed because it is too large Load Diff

View File

@ -50,6 +50,12 @@ const sendReply = (text) => {
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
padding-bottom: 12px; padding-bottom: 12px;
box-sizing: border-box; box-sizing: border-box;
/* 隐藏滚动条 */
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
} }
.more-tips-item { .more-tips-item {

View File

@ -1,143 +1,140 @@
<template> <template>
<view class="quick-access"> <view class="quick-access">
<view class="quick-access-scroll"> <view class="quick-access-scroll">
<view <view class="quick-access-item" v-for="(item, index) in itemList" :key="index" @click="sendReply(item)">
class="quick-access-item" <image class="quick-access-item-bg" src="/static/quick/quick_icon_bg.png" mode="aspectFill"></image>
v-for="(item, index) in itemList" <view class="quick-access-item-title">
:key="index" <image :src="item.icon"></image>
@click="sendReply(item)" <text>{{ item.title }}</text>
> </view>
<image <text class="quick-access-item-content">{{ item.content }}</text>
class="quick-access-item-bg" </view>
src="/static/quick/quick_icon_bg.png" </view>
mode="aspectFill" </view>
></image>
<view class="quick-access-item-title">
<image :src="item.icon"></image>
<text>{{ item.title }}</text>
</view>
<text class="quick-access-item-content">{{
item.content
}}</text>
</view>
</view>
</view>
</template> </template>
<script setup> <script setup>
import { onMounted, ref } from "vue"; import { onMounted, ref } from 'vue';
const itemList = ref([]); const itemList = ref([])
const emits = defineEmits(["replySent"]); const emits = defineEmits(['replySent']);
const sendReply = (item) => { const sendReply = (item) => {
emits("replySent", item); // emits('replySent', item); //
}; }
onMounted(() => { onMounted(() => {
initData(); initData()
}); })
const initData = () => { const initData = () => {
itemList.value = [ itemList.value = [
{ {
icon: "/static/quick/quick_icon_yuding.png", icon: '/static/quick/quick_icon_yuding.png',
title: "快速预定", title: '快速预定',
content: "预定门票、房间、餐食", content: '预定门票、房间、餐食',
type: "Command.quickBooking", type: 'Command.quickBooking'
}, },
{ {
icon: "/static/quick/quick_icon_find.png", icon: '/static/quick/quick_icon_find.png',
title: "探索发现", title: '探索发现',
content: "探索玩法、出片佳地", content: '探索玩法、出片佳地',
type: "Command.discovery", type: 'Command.discovery'
}, },
{ {
icon: "/static/quick/quick_icon_order.png", icon: '/static/quick/quick_icon_order.png',
title: "订单/工单", title: '订单/工单',
content: "我的订单/工单", content: '我的订单/工单',
type: "MyOrder", type: 'MyOrder'
}, },
{ {
icon: "/static/quick/quick_icon_call.png", icon: '/static/quick/quick_icon_call.png',
title: "呼叫服务", title: '呼叫服务',
content: "加水、客房服务等", content: '加水、客房服务等',
type: "Command.createWorkOrder", type: 'Command.createWorkOrder'
}, }
]; ]
}; }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.quick-access { .quick-access {
width: 100%; width: 100%;
&-scroll {
display: flex;
flex-direction: row;
overflow-x: auto;
white-space: nowrap;
-webkit-overflow-scrolling: touch;
/* 隐藏滚动条 */
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
.quick-access-item {
flex: 0 0 104px;
border-radius: 8px;
margin: 4px 4px 8px 4px;
box-shadow: 0 2px 5px 0px rgba(0,0,0,0.1);
padding: 12px;
display: inline-flex;
flex-direction: column;
position: relative;
&:first-child {
margin-left: 12px;
}
&:last-child {
margin-right: 12px;
}
.quick-access-item-bg {
position: absolute;
top: 0;
left: 0;
z-index: 0;
border-radius: 8px;
width: 128px;
height: 56px;
}
.quick-access-item-title {
display: flex;
align-items: center;
z-index: 1;
image {
width: 16px;
height: 16px;
margin-right: 4px;
}
text {
font-family: PingFang SC, PingFang SC;
font-weight: 500;
font-size: 12px;
color: #201F32;
line-height: 16px;
}
}
.quick-access-item-content {
z-index: 1;
margin-top: 4px;
font-family: PingFang SC, PingFang SC;
font-weight: 400;
font-size: 10px;
color: #678CAD;
line-height: 18px;
}
}
}
&-scroll {
display: flex;
flex-direction: row;
overflow-x: auto;
white-space: nowrap;
-webkit-overflow-scrolling: touch;
}
.quick-access-item {
flex: 0 0 104px;
border-radius: 8px;
margin: 4px 4px 8px 4px;
box-shadow: 0 2px 5px 0px rgba(0, 0, 0, 0.1);
padding: 12px;
display: inline-flex;
flex-direction: column;
position: relative;
&:first-child {
margin-left: 12px;
}
&:last-child {
margin-right: 12px;
}
.quick-access-item-bg {
position: absolute;
top: 0;
left: 0;
z-index: 0;
border-radius: 8px;
width: 128px;
height: 56px;
}
.quick-access-item-title {
display: flex;
align-items: center;
z-index: 1;
image {
width: 16px;
height: 16px;
margin-right: 4px;
}
text {
font-family: PingFang SC, PingFang SC;
font-weight: 500;
font-size: 12px;
color: #201f32;
line-height: 16px;
}
}
.quick-access-item-content {
z-index: 1;
margin-top: 4px;
font-family: PingFang SC, PingFang SC;
font-weight: 400;
font-size: 10px;
color: #678cad;
line-height: 18px;
}
}
}
</style> </style>

View File

@ -1,75 +1,70 @@
<template> <template>
<view class="top-bg-content"> <view class="top-bg-content">
<view class="top-item"> <view class="top-item" >
<!-- :style="backgroundStyle" --> <!-- :style="backgroundStyle" -->
<image <image class="top-item-left" :src="initPageImages.welcomeImageUrl"></image>
class="top-item-left" <image class="top-item-right" :src="initPageImages.logoImageUrl"></image>
:src="initPageImages.welcomeImageUrl" </view>
></image> <ChatCardAI v-if="welcomeContent.length" :text='welcomeContent'/>
<image </view>
class="top-item-right"
:src="initPageImages.logoImageUrl"
></image>
</view>
<ChatCardAI v-if="welcomeContent.length" :text="welcomeContent" />
</view>
</template> </template>
<script setup> <script setup>
import { defineProps, computed, ref } from "vue"; import { defineProps, computed, ref } from "vue";
import ChatCardAI from "./ChatCardAI.vue"; import ChatCardAI from './ChatCardAI.vue';
const props = defineProps({ const props = defineProps({
initPageImages: { initPageImages: {
type: Object, type: Object,
default: { default: {
backgroundImageUrl: "", backgroundImageUrl: '',
logoImageUrl: "", logoImageUrl: '',
welcomeImageUrl: "", welcomeImageUrl: ''
}, }
}, },
welcomeContent: { welcomeContent: {
type: String, type: String,
default: default: '查信息、预定下单、探索玩法、呼叫服务、我通通可以满足,快试试问我问题吧!'
"查信息、预定下单、探索玩法、呼叫服务、我通通可以满足,快试试问我问题吧!", }
}, })
});
//
const backgroundStyle = computed(() => {
return {
backgroundImage: `url(${props.initPageImages.backgroundImageUrl})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat'
}
})
//
const backgroundStyle = computed(() => {
return {
backgroundImage: `url(${props.initPageImages.backgroundImageUrl})`,
backgroundSize: "cover",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
};
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.top-bg-content { .top-bg-content {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
align-items: stretch; align-items: stretch;
flex-direction: column; flex-direction: column;
} }
.top-item { .top-item {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 0 32px; padding: 0 32px;
margin-bottom: -6px; margin-bottom: -8px;
} }
.top-item-left { .top-item-left {
width: 118px; width: 118px;
height: 52px; height: 52px;
} }
.top-item-right { .top-item-right {
width: 130px; width: 130px;
height: 130px; height: 130px;
} margin: -12px 0;
}
</style> </style>

View File

@ -49,6 +49,14 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.area-msg-list-content {
/* 隐藏滚动条 */
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
.message-item-ai { .message-item-ai {
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;
@ -132,6 +140,15 @@
align-items: center; align-items: center;
} }
} }
::-webkit-scrollbar {
display: none;
width: 0 !important;
height: 0 !important;
-webkit-appearance: none;
background: transparent;
color: transparent;
}
// 打字机光标闪烁动画 // 打字机光标闪烁动画
.typing-cursor { .typing-cursor {

View File

@ -1,214 +1,201 @@
<template> <template>
<view class="container"> <view class="container">
<ModuleTitle :title="commodityDTO.title" /> <ModuleTitle :title="commodityDTO.title" />
<view class="container-scroll"> <view class="container-scroll">
<view <view v-for="(item, index) in commodityDTO.commodityList" :key="`${item.commodityId}-${index}`">
v-for="(item, index) in commodityDTO.commodityList"
:key="`${item.commodityId}-${index}`"
>
<view class="mk-card-item" @click="placeOrderHandle(item)"> <view class="mk-card-item" @click="placeOrderHandle(item)">
<!-- <view class="card-badge">超值推荐</view> --> <!-- <view class="card-badge">超值推荐</view> -->
<image <image class="card-img" :src="item.commodityIcon" mode="aspectFill" />
class="card-img" <view class="card-content">
:src="item.commodityIcon" <view class="card-title-column">
mode="aspectFill" <text class="card-title">{{ item.commodityName }}</text>
/> <view class="card-tags" v-for="(tag) in item.commodityTradeRuleList" :key="tag">
<view class="card-content"> <text class="card-tag">{{ tag }}</text>
<view class="card-title-column"> </view>
<text class="card-title">{{ </view>
item.commodityName <template v-for="(serviceItem, index) in item.commodityServices" :key="serviceItem.serviceTitle">
}}</text> <view v-if="index < 3" class="card-desc">· {{ serviceItem.serviceTitle }}</view>
<view </template>
class="card-tags" <view class="card-bottom-row">
v-for="tag in item.commodityTradeRuleList" <view class="card-price-row">
:key="tag" <text class="card-price-fu"></text>
> <text class="card-price">{{ item.commodityPrice }}</text>
<text class="card-tag">{{ tag }}</text> <text class="card-unit">/</text>
</view> </view>
</view>
<template
v-for="(
serviceItem, index
) in item.commodityServices"
:key="serviceItem.serviceTitle"
>
<view v-if="index < 3" class="card-desc"
>· {{ serviceItem.serviceTitle }}</view
>
</template>
<view class="card-bottom-row">
<view class="card-price-row">
<text class="card-price-fu"></text>
<text class="card-price">{{
item.commodityPrice
}}</text>
<text class="card-unit">/</text>
</view>
<text class="card-btn">下单</text> <text class="card-btn">下单</text>
</view> </view>
</view> </view>
</view> </view>
</view> </view>
</view> </view>
</view> </view>
</template> </template>
<script setup> <script setup>
import ModuleTitle from "@/components/ModuleTitle/index.vue"; import ModuleTitle from '@/components/ModuleTitle/index.vue'
import { defineProps } from "vue"; import { defineProps } from 'vue'
const props = defineProps({ const props = defineProps({
commodityDTO: { commodityDTO: {
type: Object, type: Object,
default: {}, default: {}
}, }
}); })
///
const placeOrderHandle = (item) => {
uni.navigateTo({
url: `/pages/goods/index?commodityId=${item.commodityId}`,
})
}
///
const placeOrderHandle = (item) => {
uni.navigateTo({
url: `/pages/goods/index?commodityId=${item.commodityId}`,
});
};
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.container { .container {
.container-scroll {
display: flex;
flex-direction: row;
overflow-x: auto;
overflow-y: hidden;
margin: 4px 0;
.mk-card-item { .container-scroll {
position: relative; display: flex;
flex-direction: row;
overflow-x: auto;
overflow-y: hidden;
margin: 4px 0;
display: flex; /* 隐藏滚动条 */
flex-direction: column; scrollbar-width: none;
align-items: start; &::-webkit-scrollbar {
width: 188px; display: none;
// height: 244px; }
background-color: #ffffff;
border-radius: 10px;
margin-right: 8px;
padding-bottom: 12px;
.card-badge { .mk-card-item {
position: absolute; position: relative;
top: 8px;
left: 8px;
background: #ffe7b2;
color: #b97a00;
font-size: 12px;
padding: 2px 8px;
border-radius: 4px;
z-index: 2;
}
.card-img { display: flex;
width: 188px; flex-direction: column;
height: 114px; align-items: start;
border-radius: 10px; width: 188px;
object-fit: cover; /* 确保图片不变形,保持比例裁剪 */ // height: 244px;
flex-shrink: 0; /* 防止图片被压缩 */ background-color: #ffffff;
} border-radius: 10px;
margin-right: 8px;
padding-bottom: 12px;
.card-content { .card-badge {
box-sizing: border-box; position: absolute;
padding: 10px 12px 0 12px; top: 8px;
display: flex; left: 8px;
flex-direction: column; background: #ffe7b2;
justify-content: space-between; color: #b97a00;
align-items: start; font-size: 12px;
width: 100%; padding: 2px 8px;
flex: 1; /* 让内容区域占据剩余空间 */ border-radius: 4px;
overflow: hidden; /* 防止内容溢出 */ z-index: 2;
} }
.card-title-column { .card-img {
display: flex; width: 188px;
align-items: start; height: 114px;
flex-direction: column; border-radius: 10px;
width: 100%; object-fit: cover; /* 确保图片不变形,保持比例裁剪 */
} flex-shrink: 0; /* 防止图片被压缩 */
}
.card-title { .card-content {
font-size: 16px; box-sizing: border-box;
font-weight: bold; padding: 10px 12px 0 12px;
color: #222; display: flex;
width: 100%; flex-direction: column;
/* 限制标题最多显示两行 */ justify-content: space-between;
display: -webkit-box; align-items: start;
-webkit-box-orient: vertical; width: 100%;
-webkit-line-clamp: 2; flex: 1; /* 让内容区域占据剩余空间 */
overflow: hidden; overflow: hidden; /* 防止内容溢出 */
text-overflow: ellipsis; }
line-height: 1.4;
max-height: 2.8em; /* 2行的高度 */
}
.card-tags { .card-title-column {
display: flex; display: flex;
flex-direction: row; align-items: start;
align-items: start; flex-direction: column;
padding: 6px 0; width: 100%;
} }
.card-tag { .card-title {
background: #f5f5f5; font-size: 16px;
color: #ff6600; font-weight: bold;
font-size: 10px; color: #222;
border-radius: 4px; width: 100%;
padding: 0 6px; /* 限制标题最多显示两行 */
margin-left: 2px; display: -webkit-box;
border: 1px solid #ff6600; -webkit-box-orient: vertical;
} -webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.4;
max-height: 2.8em; /* 2行的高度 */
}
.card-desc { .card-tags {
font-size: 13px; display: flex;
color: #888; flex-direction: row;
margin-top: 2px; align-items: start;
} padding: 6px 0;
}
.card-bottom-row { .card-tag {
display: flex; background: #f5f5f5;
align-items: center; color: #ff6600;
justify-content: space-between; font-size: 10px;
margin-top: 8px; border-radius: 4px;
width: 100%; padding: 0 6px;
} margin-left: 2px;
border: 1px solid #ff6600;
}
.card-price-row { .card-desc {
.card-price-fu { font-size: 13px;
color: #ff6600; color: #888;
font-size: 11px; margin-top: 2px;
font-weight: normal; }
}
.card-price { .card-bottom-row {
color: #ff6600; display: flex;
font-size: 16px; align-items: center;
font-weight: bold; justify-content: space-between;
} margin-top: 8px;
.card-unit { width: 100%;
font-size: 11px; }
color: #888;
font-weight: normal;
margin-left: 2px;
}
}
.card-btn { .card-price-row {
background: #ff6600; .card-price-fu {
color: #fff; color: #ff6600;
font-size: 15px; font-size: 11px;
border-radius: 20px; font-weight: normal;
padding: 0 18px; }
height: 32px;
line-height: 32px; .card-price {
} color: #ff6600;
} font-size: 16px;
} font-weight: bold;
} }
.card-unit {
font-size: 11px;
color: #888;
font-weight: normal;
margin-left: 2px;
}
}
.card-btn {
background: #ff6600;
color: #fff;
font-size: 15px;
border-radius: 20px;
padding: 0 18px;
height: 32px;
line-height: 32px;
}
}
}
}
</style> </style>

View File

@ -1,214 +1,200 @@
<template> <template>
<view class="container"> <view class="container">
<ModuleTitle title="相关商品" /> <ModuleTitle title="相关商品" />
<view class="container-scroll"> <view class="container-scroll">
<view <view v-for="(item, index) in commodityList" :key="`${item.commodityId}-${index}`">
v-for="(item, index) in commodityList" <view class="mk-card-item" @click="placeOrderHandle(item)">
:key="`${item.commodityId}-${index}`" <image class="card-img" :src="item.commodityPhoto" mode="aspectFill" />
> <view class="card-content">
<view class="mk-card-item" @click="placeOrderHandle(item)"> <view class="card-title-column">
<image <text class="card-title">{{ item.commodityName }}</text>
class="card-img" <view class="card-tags" v-for="(tag) in item.commodityTradeRuleList" :key="tag">
:src="item.commodityPhoto" <text class="card-tag">{{ tag }}</text>
mode="aspectFill" </view>
/> </view>
<view class="card-content"> <template v-for="(serviceItem, index) in item.commodityServices" :key="serviceItem.serviceTitle">
<view class="card-title-column"> <view v-if="index < 3" class="card-desc">· {{ serviceItem.serviceTitle }}</view>
<text class="card-title">{{ </template>
item.commodityName <view class="card-bottom-row">
}}</text> <view class="card-price-row">
<view <text class="card-price-fu"></text>
class="card-tags" <text class="card-price">{{ item.specificationPrice }}</text>
v-for="tag in item.commodityTradeRuleList" <text class="card-unit">/</text>
:key="tag" </view>
> <text class="card-btn">下单</text>
<text class="card-tag">{{ tag }}</text> </view>
</view> </view>
</view>
<template
v-for="(
serviceItem, index
) in item.commodityServices"
:key="serviceItem.serviceTitle"
>
<view v-if="index < 3" class="card-desc"
>· {{ serviceItem.serviceTitle }}</view
>
</template>
<view class="card-bottom-row">
<view class="card-price-row">
<text class="card-price-fu"></text>
<text class="card-price">{{
item.specificationPrice
}}</text>
<text class="card-unit">/</text>
</view>
<text class="card-btn">下单</text>
</view>
</view>
</view> </view>
</view> </view>
</view> </view>
</view> </view>
</template> </template>
<script setup> <script setup>
import ModuleTitle from "@/components/ModuleTitle/index.vue"; import ModuleTitle from '@/components/ModuleTitle/index.vue'
import { defineProps } from "vue"; import { defineProps } from 'vue'
const props = defineProps({ const props = defineProps({
commodityList: { commodityList: {
type: Array, type: Array,
default: [], default: []
}, }
}); })
///
const placeOrderHandle = (item) => {
uni.navigateTo({
url: `/pages/goods/index?commodityId=${item.commodityId}`,
})
}
///
const placeOrderHandle = (item) => {
uni.navigateTo({
url: `/pages/goods/index?commodityId=${item.commodityId}`,
});
};
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.container { .container {
margin: 6px 0 0; margin: 6px 0 0;
.container-scroll { .container-scroll {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
overflow-x: auto; overflow-x: auto;
overflow-y: hidden; overflow-y: hidden;
margin: 4px 0; margin: 4px 0;
.mk-card-item { /* 隐藏滚动条 */
position: relative; scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
display: flex; .mk-card-item {
flex-direction: column; position: relative;
align-items: start;
width: 188px;
// height: 244px;
background-color: #ffffff;
border-radius: 10px;
margin-right: 8px;
padding-bottom: 12px;
.card-badge { display: flex;
position: absolute; flex-direction: column;
top: 8px; align-items: start;
left: 8px; width: 188px;
background: #ffe7b2; // height: 244px;
color: #b97a00; background-color: #ffffff;
font-size: 12px; border-radius: 10px;
padding: 2px 8px; margin-right: 8px;
border-radius: 4px; padding-bottom: 12px;
z-index: 2;
}
.card-img { .card-badge {
width: 188px; position: absolute;
height: 114px; top: 8px;
border-radius: 10px; left: 8px;
object-fit: cover; /* 确保图片不变形,保持比例裁剪 */ background: #ffe7b2;
flex-shrink: 0; /* 防止图片被压缩 */ color: #b97a00;
} font-size: 12px;
padding: 2px 8px;
border-radius: 4px;
z-index: 2;
}
.card-content { .card-img {
box-sizing: border-box; width: 188px;
padding: 10px 12px 0 12px; height: 114px;
display: flex; border-radius: 10px;
flex-direction: column; object-fit: cover; /* 确保图片不变形,保持比例裁剪 */
justify-content: space-between; flex-shrink: 0; /* 防止图片被压缩 */
align-items: start; }
width: 100%;
flex: 1; /* 让内容区域占据剩余空间 */
overflow: hidden; /* 防止内容溢出 */
}
.card-title-column { .card-content {
display: flex; box-sizing: border-box;
align-items: start; padding: 10px 12px 0 12px;
flex-direction: column; display: flex;
width: 100%; flex-direction: column;
} justify-content: space-between;
align-items: start;
width: 100%;
flex: 1; /* 让内容区域占据剩余空间 */
overflow: hidden; /* 防止内容溢出 */
}
.card-title { .card-title-column {
font-size: 16px; display: flex;
font-weight: bold; align-items: start;
color: #222; flex-direction: column;
width: 100%; width: 100%;
/* 限制标题最多显示两行 */ }
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.4;
max-height: 2.8em; /* 2行的高度 */
}
.card-tags { .card-title {
display: flex; font-size: 16px;
flex-direction: row; font-weight: bold;
align-items: start; color: #222;
padding: 6px 0; width: 100%;
} /* 限制标题最多显示两行 */
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.4;
max-height: 2.8em; /* 2行的高度 */
}
.card-tag { .card-tags {
background: #f5f5f5; display: flex;
color: #ff6600; flex-direction: row;
font-size: 10px; align-items: start;
border-radius: 4px; padding: 6px 0;
padding: 0 6px; }
margin-left: 2px;
.card-tag {
background: #f5f5f5;
color: #ff6600;
font-size: 10px;
border-radius: 4px;
padding: 0 6px;
margin-left: 2px;
border: 1px solid #ff6600; border: 1px solid #ff6600;
} }
.card-desc { .card-desc {
font-size: 13px; font-size: 13px;
color: #888; color: #888;
margin-top: 2px; margin-top: 2px;
} }
.card-bottom-row { .card-bottom-row {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
margin-top: 8px; margin-top: 8px;
width: 100%; width: 100%;
} }
.card-price-row { .card-price-row {
.card-price-fu { .card-price-fu {
color: #ff6600; color: #ff6600;
font-size: 11px; font-size: 11px;
font-weight: normal; font-weight: normal;
} }
.card-price { .card-price {
color: #ff6600; color: #ff6600;
font-size: 16px; font-size: 16px;
font-weight: bold; font-weight: bold;
} }
.card-unit { .card-unit {
font-size: 11px; font-size: 11px;
color: #888; color: #888;
font-weight: normal; font-weight: normal;
margin-left: 2px; margin-left: 2px;
} }
} }
.card-btn { .card-btn {
background: #ff6600; background: #ff6600;
color: #fff; color: #fff;
font-size: 15px; font-size: 15px;
border-radius: 20px; border-radius: 20px;
padding: 0 18px; padding: 0 18px;
height: 32px; height: 32px;
line-height: 32px; line-height: 32px;
} }
} }
}
} }
}
</style> </style>

View File

@ -1,67 +1,72 @@
<template> <template>
<view class="container"> <view class="container">
<ModuleTitle :title="recommendTheme.themeName" /> <ModuleTitle :title="recommendTheme.themeName" />
<view class="container-scroll"> <view class="container-scroll">
<view <view v-for="(item, index) in recommendTheme.recommendPostsList" :key="index">
v-for="(item, index) in recommendTheme.recommendPostsList"
:key="index"
>
<view class="mk-card-item" @click="sendReply(item)"> <view class="mk-card-item" @click="sendReply(item)">
<image :src="item.coverPhoto" mode="widthFix"></image> <image :src="item.coverPhoto" mode="widthFix"></image>
<text>{{ item.topic }}</text> <text>{{ item.topic }}</text>
</view> </view>
</view> </view>
</view> </view>
</view> </view>
</template> </template>
<script setup> <script setup>
import ModuleTitle from "@/components/ModuleTitle/index.vue"; import ModuleTitle from '@/components/ModuleTitle/index.vue'
import { RECOMMEND_POSTS_TITLE } from "@/constant/constant"; import { RECOMMEND_POSTS_TITLE } from '@/constant/constant'
import { defineProps } from "vue"; import { defineProps } from 'vue'
const props = defineProps({ const props = defineProps({
recommendTheme: { recommendTheme: {
type: Object, type: Object,
default: {}, default: {}
}, }
}); })
const sendReply = (item) => {
const topic = item.userInputContent || item.topic.replace(/^#/, '');
uni.$emit(RECOMMEND_POSTS_TITLE, topic);
}
const sendReply = (item) => {
const topic = item.userInputContent || item.topic.replace(/^#/, "");
uni.$emit(RECOMMEND_POSTS_TITLE, topic);
};
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.container { .container {
.container-scroll {
display: flex;
flex-direction: row;
overflow-x: auto;
margin-top: 4px;
.mk-card-item { .container-scroll {
display: flex; display: flex;
flex-direction: column; flex-direction: row;
align-items: start; overflow-x: auto;
width: 188px; margin-top: 4px;
height: 154px;
background-color: #ffffff;
border-radius: 10px;
margin-right: 8px;
image { /* 隐藏滚动条 */
width: 188px; scrollbar-width: none;
height: 112px; &::-webkit-scrollbar {
} display: none;
text { }
padding: 12px;
text-align: center; .mk-card-item {
font-weight: 500; display: flex;
font-size: 12px; flex-direction: column;
color: #333333; align-items: start;
} width: 188px;
} height: 154px;
} background-color: #ffffff;
} border-radius: 10px;
margin-right: 8px;
image {
width: 188px;
height: 112px;
}
text {
padding: 12px;
text-align: center;
font-weight: 500;
font-size: 12px;
color: #333333;
}
}
}
}
</style> </style>

View File

@ -44,6 +44,12 @@
overflow-x: auto; overflow-x: auto;
margin-top: 4px; margin-top: 4px;
/* 隐藏滚动条 */
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
.mk-card-item { .mk-card-item {
flex-shrink: 0; /* 关键:防止 flex 布局压缩子元素宽度 */ flex-shrink: 0; /* 关键:防止 flex 布局压缩子元素宽度 */
width: 142px; width: 142px;

View File

@ -1,49 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" width="100" height="100" style="shape-rendering: auto; display: block; background: transparent;" xmlns:xlink="http://www.w3.org/1999/xlink"><g><g transform="rotate(0 50 50)">
<rect fill="#00a6ff" height="12" width="6" ry="6" rx="3" y="24" x="47">
<animate repeatCount="indefinite" begin="-0.9166666666666666s" dur="1s" keyTimes="0;1" values="1;0" attributeName="opacity"></animate>
</rect>
</g><g transform="rotate(30 50 50)">
<rect fill="#00a6ff" height="12" width="6" ry="6" rx="3" y="24" x="47">
<animate repeatCount="indefinite" begin="-0.8333333333333334s" dur="1s" keyTimes="0;1" values="1;0" attributeName="opacity"></animate>
</rect>
</g><g transform="rotate(60 50 50)">
<rect fill="#00a6ff" height="12" width="6" ry="6" rx="3" y="24" x="47">
<animate repeatCount="indefinite" begin="-0.75s" dur="1s" keyTimes="0;1" values="1;0" attributeName="opacity"></animate>
</rect>
</g><g transform="rotate(90 50 50)">
<rect fill="#00a6ff" height="12" width="6" ry="6" rx="3" y="24" x="47">
<animate repeatCount="indefinite" begin="-0.6666666666666666s" dur="1s" keyTimes="0;1" values="1;0" attributeName="opacity"></animate>
</rect>
</g><g transform="rotate(120 50 50)">
<rect fill="#00a6ff" height="12" width="6" ry="6" rx="3" y="24" x="47">
<animate repeatCount="indefinite" begin="-0.5833333333333334s" dur="1s" keyTimes="0;1" values="1;0" attributeName="opacity"></animate>
</rect>
</g><g transform="rotate(150 50 50)">
<rect fill="#00a6ff" height="12" width="6" ry="6" rx="3" y="24" x="47">
<animate repeatCount="indefinite" begin="-0.5s" dur="1s" keyTimes="0;1" values="1;0" attributeName="opacity"></animate>
</rect>
</g><g transform="rotate(180 50 50)">
<rect fill="#00a6ff" height="12" width="6" ry="6" rx="3" y="24" x="47">
<animate repeatCount="indefinite" begin="-0.4166666666666667s" dur="1s" keyTimes="0;1" values="1;0" attributeName="opacity"></animate>
</rect>
</g><g transform="rotate(210 50 50)">
<rect fill="#00a6ff" height="12" width="6" ry="6" rx="3" y="24" x="47">
<animate repeatCount="indefinite" begin="-0.3333333333333333s" dur="1s" keyTimes="0;1" values="1;0" attributeName="opacity"></animate>
</rect>
</g><g transform="rotate(240 50 50)">
<rect fill="#00a6ff" height="12" width="6" ry="6" rx="3" y="24" x="47">
<animate repeatCount="indefinite" begin="-0.25s" dur="1s" keyTimes="0;1" values="1;0" attributeName="opacity"></animate>
</rect>
</g><g transform="rotate(270 50 50)">
<rect fill="#00a6ff" height="12" width="6" ry="6" rx="3" y="24" x="47">
<animate repeatCount="indefinite" begin="-0.16666666666666666s" dur="1s" keyTimes="0;1" values="1;0" attributeName="opacity"></animate>
</rect>
</g><g transform="rotate(300 50 50)">
<rect fill="#00a6ff" height="12" width="6" ry="6" rx="3" y="24" x="47">
<animate repeatCount="indefinite" begin="-0.08333333333333333s" dur="1s" keyTimes="0;1" values="1;0" attributeName="opacity"></animate>
</rect>
</g><g transform="rotate(330 50 50)">
<rect fill="#00a6ff" height="12" width="6" ry="6" rx="3" y="24" x="47">
<animate repeatCount="indefinite" begin="0s" dur="1s" keyTimes="0;1" values="1;0" attributeName="opacity"></animate>
</rect>
</g><g></g></g><!-- [ldio] generated by https://loading.io --></svg>

Before

Width:  |  Height:  |  Size: 3.3 KiB