first commit

This commit is contained in:
2026-02-26 09:32:03 +08:00
commit 36a8e4c51b
845 changed files with 116474 additions and 0 deletions
+103
View File
@@ -0,0 +1,103 @@
<script setup lang="ts">
import * as R from 'ramda'
import {useConfigStore} from "@/store";
import {setDayjsLocale, setWotDesignLocale} from "@/plugin";
const configStore = useConfigStore()
onLaunch(initConfig);
onShow(() => {
console.log('%c外卖用户端--用户端', 'background: #00A76D; color: white; padding: 3px; border-radius: 2px;');
console.log("App Show");
});
onHide(() => {
console.log("App Hide");
});
onError((error: string) => {
console.log('App Error', error)
})
function initApp() {
// #ifdef APP-PLUS
try {
plus.screen.lockOrientation('portrait-primary')
const color = plus.android.newObject("android.graphics.Color");
const ac = plus.android.runtimeMainActivity();
const c2int = plus.android.invoke(color, "parseColor", "#FFFFFF");
console.log("c2int===" + JSON.stringify(c2int))
const win = plus.android.invoke(ac, "getWindow");
console.log("win===" + JSON.stringify(win))
plus.android.invoke(win, "setNavigationBarColor", c2int);
} catch (e) {
console.log('error', e)
}
// #endif
}
// 初始化配置
function initConfig() {
// #ifdef APP-PLUS
setTimeout(()=> {
plus.navigator.closeSplashscreen();
}, 2000)
plus.screen.lockOrientation('portrait-primary')
// #endif
const {statusBarHeight, windowHeight, windowWidth, screenWidth, screenHeight, safeAreaInsets} =
uni.getWindowInfo()
const {deviceId} =
uni.getDeviceInfo()
const {appVersion} =
uni.getAppBaseInfo()
const {
uniPlatform,
osName,
osVersion,
} =
uni.getSystemInfoSync()
configStore.deviceId = deviceId
configStore.osName = osName
configStore.osVersion = osVersion
configStore.appVersion = appVersion
configStore.uniPlatform = uniPlatform
configStore.statusBarHeight = statusBarHeight ?? 0
configStore.windowHeight = windowHeight ?? 0
configStore.windowWidth = windowWidth ?? 0
configStore.screenWidth = screenWidth ?? 0
configStore.screenHeight = screenHeight ?? 0
configStore.safeAreaInsets = safeAreaInsets ?? {
bottom: 0,
top: 0,
}
setWotDesignLocale()
setDayjsLocale()
if (osName === 'android') {
initApp()
}
if(!configStore.isShowedLanguageSelectPage) {
console.log('未展示过语言选择页面,导航到语言选择页面')
uni.navigateTo({
url: '/pages-login/pages/choose-language/index',
success() {
configStore.isShowedLanguageSelectPage = true
console.log('导航到语言选择页面成功')
},
})
}
}
</script>
<style lang="scss">
@import "@/styles/index.scss";
</style>
+459
View File
@@ -0,0 +1,459 @@
<template>
<view class="flex items-center pb-36rpx">
<image
src="@img/chef/1330.png"
mode="aspectFill"
class="w-44rpx h-44rpx shrink-0 mr-12rpx"
/>
{{ t('common.comment') }}({{ props.tableTotal }})
</view>
<template v-if="dataList && dataList.length">
<view class="c_comment" v-for="(item1, index1) in dataList" :key="item1.id">
<!-- 一级评论 -->
<CommonComp
:data="item1"
@likeClick="() => likeClick({ item1, index1 })"
@replyClick="() => replyClick({ item1, index1 })"
@deleteClick="() => deleteClick({ item1, index1 })"
/>
<view class="children_item bg-#F5F7FB rounded-16rpx" v-if="item1.childrenShow && item1.childrenShow.length> 0">
<!-- 二级评论 -->
<CommonComp
v-for="(item2, index2) in item1.childrenShow"
:key="item2.id"
:data="item2"
:pData="item1"
@likeClick="() => likeClick({ item1, index1, item2, index2 })"
@replyClick="() => replyClick({ item1, index1, item2, index2 })"
@deleteClick="() => deleteClick({ item1, index1, item2, index2 })"
/>
</view>
<view class="flex items-center pl-100rpx text-#666 pt-10rpx" v-if="item1.children || item1.commentCount> 0">
<!-- 展开二级评论 -->
<view
class="flex items-center"
@click="expandReply(item1)"
>
<text>{{ t('pages-user.recipe.expandReply') }}</text>
<wd-icon name="chevron-down" class="mt-4rpx" size="22px"></wd-icon>
</view>
<!-- 折叠二级评论 -->
<view
class="flex items-center"
@click="shrinkReply(item1)"
>
<text>{{ t('pages-user.recipe.collapseReply') }}</text>
<wd-icon name="chevron-up" class="mt-4rpx" size="22px"></wd-icon>
</view>
</view>
</view>
</template>
<!-- 空盒子 -->
<view class="empty_box" v-else>
<view class="py-100rpx center">
<image class="w-250rpx h-250rpx" src="@img/chef/100.png"></image>
</view>
</view>
<!-- 评论弹窗 -->
<wd-popup ref="cPopupRef" v-model="cPopupShow" custom-style="border-radius:16rpx 16rpx 0 0;" position="bottom" @change="popChange">
<view class="w-full rounded-16rpx px-30rpx py-16rpx">
<view class="flex items-center">
<template v-if="Object.keys(replyTemp).length">
<text class="text_aid">{{ t('pages-user.recipe.replyTo') }}</text>
<image class="w-68rpx h-68rpx rounded-50% mx-10rpx" :src="replyTemp.item2 ? replyTemp.item2.user_avatar : replyTemp.item1.user_avatar" />
<text class="text_main">{{ replyTemp.item2 ? replyTemp.item2.user_name : replyTemp.item1.user_name }}</text>
</template>
</view>
<view class="w-full flex-center-sb gap-30rpx bg-white py-12rpx">
<view class="w-full h-74rpx center bg-#F6F6F6 rounded-16rpx px-28rpx">
<wd-input
no-border
clearable
:focus-when-clear="false"
confirm-type="send"
use-prefix-slot
custom-class="flex items-center !text-30rpx !bg-transparent flex-1"
placeholderStyle="font-size: 30rpx;color: #6D6D6D; font-weight: 500;"
@confirm="sendClick"
v-model="commentValue"
:placeholder="commentPlaceholder"
>
</wd-input>
</view>
<wd-button @click="sendClick" class="!h-74rpx !w-120rpx !rounded-16rpx !bg-#333 !text-white !text-30rpx">{{ t('common.send') }}</wd-button>
</view>
</view>
</wd-popup>
<wd-message-box/>
</template>
<script setup>
import CommonComp from "./componets/common";
import {appCommentDeleteCommentIdDelete, appCommentPublishCommentPost, appCommentReplyListPost} from "@/service";
import {useMessage} from "wot-design-uni";
import {formatTimestampWithMonthName} from "@/utils/utils";
const message = useMessage();
const { t } = useI18n();
const props = defineProps({
/** 登陆用户信息
* id: number // 登陆用户id
* user_name: number // 登陆用户名
* user_avatar: string // 登陆用户头像地址
*/
myInfo: {
type: Object,
default: () => {},
},
/** 文章作者信息
* id: number // 文章作者id
* user_name: number // 文章作者名
* user_avatar: string // 文章作者头像地址
*/
userInfo: {
type: Object,
default: () => {},
},
/** 评论列表
* id: number // 评论id
* parent_id: number // 父级评论id
* reply_id: number // 被回复人评论id
* reply_name: string // 被回复人名称
* user_name: string // 用户名
* user_avatar: string // 评论者头像地址
* user_content: string // 评论内容
* is_like: boolean // 是否点赞
* like_count: number // 点赞数统计
* create_time: string // 创建时间
*/
tableData: {
type: Array,
default: () => [],
},
// 评论总数
tableTotal: {
type: Number,
default: 0,
},
// 评论删除模式
// bind - 当被删除的一级评论存在回复评论, 那么该评论内容变更显示为[当前评论内容已被移除]
// only - 仅删除当前评论(后端删除相关联的回复评论, 否则总数显示不对)
// all - 删除所有评论包括回复评论
deleteMode: {
type: String,
default: "all",
},
});
const emit = defineEmits([
"update:tableTotal",
"likeFun", // 点赞事件
"replyFun", // 回复事件
"deleteFun", // 删除事件
]);
// 渲染数据(前端的格式)
let dataList = ref([]);
watch(
() => props.tableData,
(newVal) => {
if (newVal.length !== dataList.value.length) {
let temp = props.tableData;
dataList.value = treeTransForm(temp);
}
},
{ deep: true, immediate: true }
);
// 数据转换
function treeTransForm(data) {
let newData = JSON.parse(JSON.stringify(data));
let result = [];
let map = {};
newData.forEach((item, i) => {
item.owner = item.user_id === props.myInfo.user_id; // 是否为当前登陆用户 可以对自己的评论进行删除 不能回复
// item.author = item.user_id === props.userInfo.user_id; // 是否为作者 显示标记
map[item.id] = item;
});
newData.forEach((item) => {
let parent = map[item.parent_id];
if (parent) {
(parent.children || (parent.children = [])).push(item); // 所有回复
if (parent.children.length === 1) {
(parent.childrenShow = []).push(item); // 显示的回复
}
} else {
result.push(item);
}
});
return result;
}
// 点赞
let setLike = (item) => {
item.is_like = !item.is_like;
item.like_count = item.is_like ? item.like_count + 1 : item.like_count - 1;
};
function likeClick({ item1, index1, item2, index2 }) {
let item = item2 || item1;
setLike(item);
emit("likeFun", { params: item }, (res) => {
// 请求后端失败, 重置点赞
setLike(item);
});
}
// 回复
let cPopupRef = ref(null); // 弹窗实例
const cPopupShow = ref(false); // 弹窗显示状态
let replyTemp = reactive({}); // 临时数据
function replyClick({ item1, index1, item2, index2 }) {
replyTemp = JSON.parse(JSON.stringify({ item1, index1, item2, index2 }));
cPopupShow.value = true;
}
// 发起新评论
let isNewComment = ref(false); // 是否为新评论
defineExpose({ newCommentFun });
function newCommentFun() {
isNewComment.value = true;
cPopupShow.value = true;
}
// 评论弹窗
let focus = ref(false);
function popChange(e) {
// 关闭弹窗
if (!e.show) {
commentValue.value = ""; // 清空输入框值
replyTemp = {}; // 清空被回复人信息
isNewComment.value = false; // 恢复是否为新评论默认值
}
focus.value = e.show;
}
let commentValue = ref(""); // 输入框值
let commentPlaceholder = ref("说点什么..."); // 输入框占位符
// 发送评论
function sendClick({ item1, index1, item2, index2 } = replyTemp) {
console.log('replyTemp', replyTemp.item1)
console.log('replyTemp', replyTemp.item2)
const data = replyTemp.item2 || replyTemp.item1
appCommentPublishCommentPost({
body: {
topId: replyTemp.item2 ? replyTemp.item1.id : data.id,
parentId:data.id,
targetId: data.target_id,
targetType: 1,
content: commentValue.value,
}
}).then(res=> {
console.log(res)
emit("update");
cPopupShow.value = false;
commentValue.value = ""; // 清空输入框值
// shrinkReply(replyTemp.item1)
dataList.value.forEach((item, i) => {
if (item.id === replyTemp.item1.id) {
shrinkReply(item)
}
})
})
}
function expandReply(item1) {
// item1.childrenShow = item1.children
console.log(item1)
appCommentReplyListPost({
params: {
pageNum: 1,
pageSize: 100,
},
body: {
parentId: item1.id,
}
}).then(res=> {
console.log('回复列表', res)
item1.children = res.rows.map(item => {
let userInfo = {}
// 普通用户
if (+item.userPort === 1) {
userInfo.user_id = item.userVo.id
userInfo.user_name = `${item.userVo.firstName} ${item.userVo.surname}`
userInfo.user_avatar = item.userVo.avatar
} else {
userInfo.user_id = item.merchantVo.userId
userInfo.user_name = item.merchantVo.merchantName
userInfo.user_avatar = item.merchantVo.logo
}
return {
id: item.id,
topId: item.topId,
parent_id: null, // 评论父级的id
reply_id: null, // 被回复评论的id
reply_name: item.parentUserVo ? `${item.parentUserVo.firstName} ${item.parentUserVo.surname}` : null, // 被回复人名称
target_id: item1.target_id,
commentCount: item.commentCount,
user_id: userInfo.user_id, // 用户id
user_name: userInfo.user_name, // 用户名
user_avatar: userInfo.user_avatar, // 用户头像地址
user_content: item.content, // 用户评论内容
create_time: formatTimestampWithMonthName(item.createTime), // 创建时间
owner: userInfo.user_id === props.myInfo.user_id, // 是否为当前登陆用户 可以对自己的评论进行删除 不能回复
}
})
item1.childrenShow = item1.children
})
}
function shrinkReply(item1) {
item1.childrenShow = []
}
// 删除
const delPopupRef = ref(null);
let delTemp = reactive({}); // 临时数据
function deleteClick({ item1, index1, item2, index2 }) {
console.log('删123除', item1, index1, item2, index2)
message
.confirm({
title: t("common.prompt.system-prompt"),
msg: `${t("common.prompt.system-prompt-delete")}`,
confirmButtonText: t("common.yes"),
cancelButtonText: t("common.no"),
cancelButtonProps: {
customClass:
"!h-88rpx !w-258rpx !min-w-auto !text-30rpx !lh-42rpx !font-bold !border-#666666 !rounded-20rpx",
},
confirmButtonProps: {
customClass:
"!h-88rpx !w-258rpx !min-w-auto !text-30rpx !lh-42rpx !font-bold !bg-primary !rounded-20rpx",
},
})
.then(async () => {
appCommentDeleteCommentIdDelete({
params: {
id: item2.id ? item2.id : item1.id,
}
}).then(res=> {
console.log('删除成功', res)
emit("deleteFun");
shrinkReply(item1)
})
})
.catch(() => {
});
}
// 展开评论if
function expandTxtShow({ item1, index1 }) {
return item1.childrenShow?.length && item1.children.length - item1.childrenShow.length;
}
// 展开更多评论
function expandReplyFun({ item1, index1 }) {
let csLen = dataList.value[index1].childrenShow.length;
dataList.value[index1].childrenShow.push(
...dataList.value[index1].children.slice(csLen, csLen + 6) // 截取5条评论
);
}
// 收起评论if
function shrinkTxtShow({ item1, index1 }) {
return item1.childrenShow?.length >= 2 && item1.children.length - item1.childrenShow.length === 0;
}
// 收起更多评论
function shrinkReplyFun({ item1, index1 }) {
let csLen = dataList.value[index1].childrenShow.length;
dataList.value[index1].childrenShow = [];
dataList.value[index1].childrenShow.push(
...dataList.value[index1].children.slice(0, 1) // 截取1条评论
);
}
</script>
<style lang="scss" scoped>
////////////////////////
.center {
display: flex;
align-items: center;
}
////////////////////////
.c_total {
//padding: 20rpx 30rpx 0 30rpx;
font-size: 28rpx;
}
.empty_box {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
padding: 150rpx 10rpx;
font-size: 28rpx;
.txt {
//color: $uni-text-color-disable;
}
.click {
//color: $uni-color-primary;
}
}
.c_comment {
//padding: 20rpx 30rpx;
font-size: 28rpx;
.children_item {
padding: 20rpx 30rpx;
margin-top: 10rpx;
margin-left: 80rpx;
//background-color: $uni-bg-color-grey;
.expand_reply,
.shrink_reply {
margin-top: 10rpx;
margin-left: 80rpx;
.txt {
font-weight: 600;
//color: $uni-color-primary;
}
}
}
}
.c_popup_box {
background-color: #fff;
.reply_text {
@extend .center;
padding: 20rpx 20rpx 0 20rpx;
font-size: 26rpx;
.text_aid {
//color: $uni-text-color-grey;
margin-right: 5rpx;
}
.user_avatar {
width: 48rpx;
height: 48rpx;
border-radius: 50%;
margin-right: 6rpx;
margin-left: 12rpx;
}
.text_main {
}
}
.content {
@extend .center;
.text_area {
flex: 1;
padding: 20rpx;
}
.send_btn {
@extend .center;
justify-content: center;
width: 120rpx;
height: 60rpx;
border-radius: 20rpx;
font-size: 28rpx;
color: #fff;
//background-color: $uni-color-primary;
margin-right: 20rpx;
margin-left: 5rpx;
}
}
}
</style>
@@ -0,0 +1,158 @@
<template>
<view class="comment_item">
<view class="top">
<view class="top_left !text-#999 !text-26rpx">
<image v-if="+props.data.topId === 0" class="w-80rpx h-80rpx rounded-50% mr-24rpx" mode="aspectFill" :src="props.data.user_avatar" />
<text class="user_name">{{ props.data.user_name }}</text>
<text class="user_name">{{ cReplyName }}</text>
</view>
</view>
<view :class="[+props.data.topId === 0 ? '!pl-34rpx ml-70rpx' : '']" class="content !text-#333 !text-26rpx" @tap="replyClick(props.data)">
{{ c_content }}
</view>
<view :class="[+props.data.topId === 0 ? '!pl-100rpx' : '']" class="">
<text class="create_time !text-#999 !text-26rpx">{{ props.data.create_time }}</text>
<text v-if="props.data.owner" class="bg-#E6E6E6 rounded-4rpx px-8rpx text-22rpx text-#333 mx-10rpx" @tap="deleteClick(props.data)">{{ t('common.remove') }}</text>
<text class= "bg-#E6E6E6 rounded-4rpx px-8rpx text-22rpx text-#333 ml-10rpx">{{ t('common.reply') }}</text>
</view>
</view>
</template>
<script setup>
import { reactive, ref, onMounted, computed, watch, watchEffect } from "vue";
const { t } = useI18n();
const props = defineProps({
// 评论数据
data: {
type: Object,
default: () => {},
},
// 父级评论数据
pData: {
type: Object,
default: () => {},
},
});
// 被回复人名称
const cReplyName = computed(() => {
return props.data?.reply_name ? `` + props.data?.reply_name : "";
});
// 点赞数显示
const cLikeCount = computed(() => {
return props.data.like_count === 0 ? "" : props.data.like_count > 99 ? `99+` : props.data.like_count;
});
// 评论过长处理
let contentShowLength = 70; // 默认显示评论字符
let user_content = props.data.user_content;
let isShrink = ref(user_content.length > contentShowLength); // 是否收缩评论
let c_content = ref("");
watch(
() => isShrink.value,
(newVal) => {
c_content.value = newVal ? user_content.slice(0, contentShowLength + 1) : user_content;
},
{
immediate: true,
}
);
// 删除变更显示定制
watch(
() => props.data.user_content,
(newVal, oldVal) => {
if (newVal !== oldVal) {
c_content.value = newVal;
}
}
);
// 展开文字
function expandContentFun() {
isShrink.value = false;
}
// 收起文字
function shrinkContentFun() {
isShrink.value = true;
}
const emit = defineEmits(["likeClick", "replyClick", "deleteClick"]);
// 点赞
function likeClick(item) {
emit("likeClick", item);
}
// 回复
function replyClick(item) {
emit("replyClick", item);
}
// 删除
function deleteClick(item) {
emit("deleteClick", item);
}
</script>
<style lang="scss" scoped>
////////////////////////
.center {
display: flex;
align-items: center;
}
.ellipsis {
overflow: hidden; //超出文本隐藏
white-space: nowrap; //溢出不换行
text-overflow: ellipsis; //溢出省略号显示
}
////////////////////////
.comment_item {
font-size: 28rpx;
.top {
@extend .center;
justify-content: space-between;
.top_left {
display: flex;
align-items: center;
overflow: hidden;
.user_avatar {
width: 68rpx;
height: 68rpx;
border-radius: 50%;
margin-right: 12rpx;
}
.tag {
margin-right: 6rpx;
}
.user_name {
@extend .ellipsis;
max-width: 180rpx;
//color: $uni-text-color-grey;
}
}
.top_right {
@extend .center;
.like_count {
//color: $uni-text-color-grey;
&.active {
//color: $uni-color-primary;
}
}
}
}
.content {
padding: 10rpx 0;
//color: $uni-text-color;
&:active {
//background-color: $uni-bg-color-hover;
}
.shrink {
padding: 20rpx 20rpx 20rpx 0rpx;
//color: $uni-color-primary;
}
}
}
</style>
@@ -0,0 +1,29 @@
{
"compileType": "miniprogram",
"setting": {
"coverView": true,
"es6": true,
"postcss": true,
"minified": true,
"enhance": true,
"showShadowRootInWxmlPanel": true,
"packNpmRelationList": [],
"babelSetting": {
"ignore": [],
"disablePlugins": [],
"outputPath": ""
},
"ignoreUploadUnusedFiles": true
},
"condition": {},
"editorSetting": {
"tabIndent": "tab",
"tabSize": 2
},
"libVersion": "3.6.3",
"packOptions": {
"ignore": [],
"include": []
},
"appid": ""
}
@@ -0,0 +1,7 @@
{
"description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html",
"projectname": "cc-comment",
"setting": {
"compileHotReLoad": true
}
}
+140
View File
@@ -0,0 +1,140 @@
<script setup lang="ts">
import * as R from 'ramda'
import {debounce} from "throttle-debounce";
import Config from "@/config";
import {useConfigStore} from "@/store";
interface DayItem {
date: string
name: string
time: number
dataFormat: string
selected: boolean
}
const props = defineProps<{ date?: string }>();
const emits = defineEmits<{
success: [DayItem]
}>()
const {t} = useI18n();
const configStore = useConfigStore();
const $dayjs = inject('dayjs') as any
const show = ref(false);
// 选择的日期
const date = ref<{ fulldate: string } | null>(null)
watch(() => props.date, (value) => {
if (value) {
date.value = {fulldate: value}
}
}, {
immediate: true
})
function init() {
show.value = true;
}
function close() {
show.value = false;
}
function submit() {
close();
const weekdays = $dayjs.weekdaysShort()
const selectedDate: string = date.value?.fulldate || ''
let day = $dayjs(selectedDate)
emits('success', {
date: day.date(),
name: weekdays[day.day()],
time: day.valueOf(),
dataFormat: selectedDate,
selected: false,
})
}
const handleSubmit = debounce(Config.debounceLongTime, submit, {
atBegin: true
});
function handleCalendarChange(event: any) {
console.log('event', event)
date.value = event
}
defineOptions({
name: "ChooseDate",
});
defineExpose({
show,
init,
close,
});
</script>
<template>
<wd-popup
custom-class="rounded-t-30rpx"
position="bottom"
safe-area-inset-bottom
v-model="show"
@close="close"
>
<view class="box-border flex flex-col"
>
<view class="relative p-[40rpx+30rpx] flex items-center justify-between border-bottom">
<view class="text-42rpx text-primary font-bold">
{{ t('common.select-date') }}
</view>
<image class="absolute right-30rpx w-32rpx h-32rpx" src="@img-store/2106@2x.png" @click="close"></image>
</view>
<view class="mb-30rpx">
<wu-calendar
:todayDefaultStyle="false"
:showMonth="false"
:monthShowCurrentMonth="true"
:insert="true"
color="#333"
:itemHeight="52"
:startDate="$dayjs(configStore.serverTime).format('YYYY-MM-DD')"
@change="handleCalendarChange"></wu-calendar>
</view>
</view>
<fixed-bottom-large-btn class="" :text="t('common.confirm')"
@click="handleSubmit"
/>
</wd-popup>
</template>
<style scoped lang="scss">
:deep(.wu-calendar) {
.wu-calendar-item__weeks-lunar-text {
display: none;
}
.wu-calendar-item__weeks-box-item {
width: 43px !important;
border-radius: 16rpx !important;
}
.wu-calendar-item__weeks-box-item::after {
content: '';
display: block;
width: 8rpx;
height: 8rpx;
background-color: #fff;
border-radius: 50%;
}
}
</style>
@@ -0,0 +1,146 @@
<script lang="ts" setup>
// import {upload} from '@/utils/upload/alioss'
import { uploadToS3 } from '@/utils/upload/ymx'
const props = withDefaults(
defineProps<{
count?: number
isUpload?: boolean
}>(),
{
count: 1,
isUpload: true,
},
)
const emits = defineEmits(['change', 'update:modelValue'])
const {t} = useI18n()
const show = ref(false)
function init() {
show.value = true
}
function close() {
show.value = false
}
// 选择图片
function handleChooseImage(type: 'album' | 'camera') {
close()
chooseImage(type)
}
function chooseImage(type: 'album' | 'camera') {
// #ifdef H5
// uni.chooseImage({
// count: props.count,
// sizeType: ['original'],
// sourceType: type === 'album' ? ['album'] : ['camera'],
// success: async (res) => {
// console.log(res)
//
// let files: string | string[] = []
// const asyncList = []
//
// if (!props.isUpload) {
// return emits('change', res.tempFilePaths)
// }
//
// files = res.tempFilePaths
//
// await uni.showLoading({
// title: t('common.loading') + '...',
// mask: true,
// })
//
// for (let i = 0; i < files.length; i++) {
// // asyncList.push(upload(files[i]))
// asyncList.push(uploadToS3(files[i]))
// console.log(asyncList)
// }
//
// Promise.all(asyncList)
// .then((results) => {
// console.log(results)
// emits('change', results)
// })
// .finally(() => {
// uni.hideLoading()
// })
// },
// })
// #endif
// #ifdef APP-PLUS
uni.chooseMedia({
count: props.count,
mediaType: ['image'],
sourceType: type === 'album' ? ['album'] : ['camera'],
maxDuration: 30,
camera: 'back',
async success(res) {
let files: string | string[] = []
const asyncList = []
console.log('res.tempFiles', res.tempFiles)
if(res.tempFiles.length === 0) return
files = res.tempFiles.map((item) => item.tempFilePath)
if(props.count > 0) {
// 根据count截取数组长度
files = files.slice(0, props.count)
}
if (!props.isUpload) {
return emits('change', files)
}
await uni.showLoading({
title: t('common.loading') + '...',
mask: true,
})
for (let i = 0; i < files.length; i++) {
// asyncList.push(upload(files[i]))
asyncList.push(uploadToS3(files[i]))
console.log(asyncList)
}
Promise.all(asyncList)
.then((results) => {
console.log(results)
emits('change', results)
})
.finally(() => {
uni.hideLoading()
})
}
})
// #endif
}
defineExpose({
show,
init,
close,
})
</script>
<template>
<view>
<wd-popup position="bottom" safe-area-inset-bottom v-model="show" @close="close"
custom-style="border-radius: 30rpx 30rpx 0 0;">
<slot name="top"></slot>
<view class="p-[32rpx+20rpx] text-34rpx text-primary text-center font-bold" @click="handleChooseImage('album')">
<text>{{ t('common.select-from-album') }}</text>
</view>
<view class="p-[32rpx+20rpx] text-34rpx text-primary text-center font-bold" @click="handleChooseImage('camera')">
<text>{{ t('common.photograph') }}</text>
</view>
<view class="h-20rpx bg-common"></view>
<view class="p-[32rpx+20rpx] text-34rpx text-primary text-center" @click="close">{{ t('common.cancel') }}</view>
</wd-popup>
</view>
</template>
<style scoped lang="scss"></style>
+152
View File
@@ -0,0 +1,152 @@
<template>
<view class="collection-component" @click.stop="handleCollectionClick">
<view class="icon-container">
<!-- 未收藏图标 -->
<image
v-show="!isCollected"
:src="uncollectedIcon"
:class="['collection-icon', 'collected-icon', { 'slide-in': isAnimating && !isCollected }]"
mode="aspectFit"
/>
<!-- 已收藏图标 -->
<image
v-show="isCollected"
:src="collectedIcon"
:class="['collection-icon', 'uncollected-icon', { 'slide-out': isAnimating && isCollected }]"
mode="aspectFit"
/>
</view>
</view>
</template>
<script setup lang="ts">
import { throttle, debounce } from 'throttle-debounce'
// 定义组件属性
interface Props {
/** 是否已收藏 */
isCollected: boolean
/** 节流延迟时间(毫秒) */
throttleDelay?: number
/** 防抖延迟时间(毫秒) */
debounceDelay?: number
/** 是否禁用点击 */
disabled?: boolean
}
const props = withDefaults(defineProps<Props>(), {
isCollected: false,
throttleDelay: 1000,
debounceDelay: 1500,
disabled: false
})
// 定义事件
const emit = defineEmits<{
/** 收藏状态改变事件 */
'collection-change': [isCollected: boolean]
/** 点击事件 */
'click': [event: any]
}>()
// 图标路径
const uncollectedIcon = '/static/images/chef/117.png'
const collectedIcon = '/static/images/chef/118.png'
// 动画状态
const isAnimating = ref(false)
// 节流处理函数
const throttledEmit = throttle(props.throttleDelay, (isCollected: boolean) => {
emit('collection-change', isCollected)
})
// 防抖处理函数
const debouncedEmit = debounce(props.debounceDelay, (isCollected: boolean) => {
emit('collection-change', isCollected)
}, {
atBegin: true, // 立即触发
})
// 点击处理函数
const handleCollectionClick = (event: any) => {
if (props.disabled) {
return
}
// 触发点击事件
emit('click', event)
// 开始动画
isAnimating.value = true
// 立即触发状态变化
// throttledEmit(!props.isCollected)
debouncedEmit(!props.isCollected)
// 动画结束后重置状态
setTimeout(() => {
isAnimating.value = false
}, 300)
}
</script>
<style lang="scss" scoped>
.collection-component {
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
&:active {
transform: scale(0.95);
}
.icon-container {
position: relative;
width: 40rpx;
height: 40rpx;
overflow: hidden;
}
.collection-icon {
position: absolute;
top: 0;
left: 0;
width: 40rpx;
height: 40rpx;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&.uncollected-icon {
transform: translateY(0);
opacity: 1;
&.slide-out {
transform: translateY(-100%);
opacity: 0;
}
}
&.collected-icon {
transform: translateY(0);
opacity: 1;
&.slide-in {
animation: slide-in-from-bottom 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
}
}
}
@keyframes slide-in-from-bottom {
0% {
transform: translateY(100%);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
</style>
@@ -0,0 +1,51 @@
<script setup lang="ts">
import {useConfigStore} from "@/store";
const {proxy} = getCurrentInstance() as any
const {t} = useI18n()
const configStore = useConfigStore()
const isExpandAll = ref(false)
const isShowExpandBtn = ref(false)
onMounted(() => {
setTimeout(() => {
const selectorQuery = uni.createSelectorQuery().in(proxy)
selectorQuery.select('.expand-content').boundingClientRect((res) => {
console.log('rect', res)
console.log('res.height && res.height > uni.upx2px(10 * 42)', res.height && res.height > uni.upx2px(10 * 42))
if (res.height && res.height > uni.upx2px(20 * 42)) {
isShowExpandBtn.value = true
}
}).exec()
}, 200)
})
</script>
<template>
<view class="">
<view class="expand-content relative transition-all duration-150"
:style="[!isExpandAll? {
maxHeight: 20 * 42 + 'rpx',
overflow: 'hidden',
}:{}]"
>
<slot></slot>
</view>
<view class="center mt-28rpx" v-if="isShowExpandBtn" @click="isExpandAll=!isExpandAll">
<text class="text-24-bold" v-if="!isExpandAll">{{ t('common.see-all') }}</text>
<text class="text-24-bold" v-else>{{ t('common.hide-all') }}</text>
<image class="w-20rpx h-20rpx ml-10rpx shrink-0 transition-all" src="@img/2108@2x.png"
:class="[isExpandAll?'rotate-180':'']"
></image>
</view>
</view>
</template>
<style scoped lang="scss">
</style>
@@ -0,0 +1,88 @@
<script setup lang="ts">
import {appFiltersConfigGetFiltersGet} from "@/service";
const { t } = useI18n();
const emit = defineEmits(['applyPrice'])
const priceValue = ref<number[]>([])
const priceList = ref([ '$', '$$', '$$$', '$$$$' ])
function handleClickApply() {
if (priceValue.value.length === 0) {
emit('applyPrice', [])
handleClose()
return
} else {
// 获取选中的价格条件数组
const selectedConditions = priceValue.value.map(index =>
priceData.value[index]?.filterCondition || ''
).filter(condition => condition !== '')
emit('applyPrice', selectedConditions)
handleClose()
}
}
function changePrice(index: number) {
const currentIndex = priceValue.value.indexOf(index)
if (currentIndex > -1) {
// 如果已选中,则取消选中
priceValue.value.splice(currentIndex, 1)
} else {
// 如果未选中,则添加到选中列表
priceValue.value.push(index)
}
}
function handleClickReset() {
priceValue.value = []
emit('applyPrice', [])
handleClose()
}
const show = ref(false)
function onOpen() {
show.value = true
// 获取筛选条件的值
getPriceValue()
}
const priceData = ref([])
function getPriceValue() {
appFiltersConfigGetFiltersGet({}).then((res: any) => {
console.log('res', res)
if(res.data && res.data.price.length > 0) {
priceData.value = res.data.price
}
console.log('res', priceData.value)
})
}
function handleClose() {
show.value = false
}
defineExpose({
onOpen
})
</script>
<template>
<wd-popup v-model="show" position="bottom" @close="handleClose" safe-area-inset-bottom>
<view class="bg-white px-30rpx pt-50rpx pb-60rpx">
<view class="text-center text-40rpx lh-40rpx text-#333 font-bold">{{ t('components.filtrate-tool.price') }}</view>
<view class="flex-center-sb gap-18rpx mt-56rpx">
<template v-for="(item, index) in priceList">
<view @click="changePrice(index)" :class="[priceValue.includes(index) ? 'bg-#14181B text-#fff' : 'bg-#F2F2F2 text-#333']" class="flex-1 text-center text-30rpx h-76rpx rounded-38rpx lh-76rpx font-500 ">{{ item }}</view>
</template>
</view>
<view class="mt-88rpx">
<wd-button custom-class="!h-108rpx !bg-14181B !text-36rpx !lh-108rpx !text-#fff !rounded-16rpx" block
@click="handleClickApply">
{{ t('common.apply') }}
</wd-button>
<view @click="handleClickReset" class="text-center mt-52rpx text-36rpx lh-36rpx text-#333 font-500">{{ t('common.reset') }}</view>
</view>
</view>
</wd-popup>
</template>
<style scoped lang="scss">
</style>
@@ -0,0 +1,60 @@
<script setup lang="ts">
const { t } = useI18n();
const emit = defineEmits(['applyScore'])
const sliderValue = ref(1)
const sliderList = ref([ '3+', '3.5+', '4+', '4.5+', '5' ])
const scoreList = [
'3-3.5',
'3.5-4',
'4-4.5',
'4.5-5',
'5-10',
]
function handleClickApply() {
emit('applyScore', scoreList[sliderValue.value])
show.value = false
}
function handleClickReset() {
sliderValue.value = 1
emit('applyScore', null)
show.value = false
}
const show = ref(false)
function onOpen() {
show.value = true
}
function handleClose() {
show.value = false
}
defineExpose({
onOpen
})
</script>
<template>
<wd-popup v-model="show" position="bottom" @close="handleClose" safe-area-inset-bottom>
<view class="bg-white px-30rpx pt-50rpx pb-60rpx">
<view class="text-center text-40rpx lh-40rpx text-#333 font-bold">{{ t('components.filtrate-tool.score') }}</view>
<view class="mt-56rpx text-36rpx lh-36rpx text-#333 font-500">{{ t('components.filtrate-tool.over') }} {{ sliderList[sliderValue] }}</view>
<view class="flex-center-sb mt-52rpx mb-30rpx">
<template v-for="item in sliderList">
<text class="text-30rpx lh-30rpx text-#333 font-500">{{ item }}</text>
</template>
</view>
<wd-slider v-model="sliderValue" :min="0" :max="4" :step="1" hide-min-max hide-label active-color="#F6F6F6" inactive-color="#333" />
<view class="mt-62rpx">
<wd-button custom-class="!h-108rpx !bg-14181B !text-36rpx !lh-108rpx !text-#fff !rounded-16rpx" block
@click="handleClickApply">
{{ t('common.apply') }}
</wd-button>
<view @click="handleClickReset" class="text-center mt-52rpx text-36rpx lh-36rpx text-#333 font-500">{{ t('common.reset') }}</view>
</view>
</view>
</wd-popup>
</template>
<style scoped lang="scss">
</style>
+61
View File
@@ -0,0 +1,61 @@
<script setup lang="ts">
const { t } = useI18n();
const emit = defineEmits(["togglePickup", "toggleDiscount", "toggleScore", "togglePrice"]);
const isPickup = ref(false);
function togglePickup() {
isPickup.value = !isPickup.value;
emit("togglePickup", isPickup.value ? 1 : 2);
}
const isDiscount = ref(false);
function toggleDiscount() {
isDiscount.value = !isDiscount.value;
emit("toggleDiscount", isDiscount.value ? 1 : 2);
}
function handleScore() {
emit("toggleScore");
}
function handlePrice() {
emit("togglePrice");
}
</script>
<template>
<view>
<scroll-view :scroll-x="true">
<view class="flex items-center">
<view class="w-27rpx shrink-0"></view>
<view @click="togglePickup" :class="[isPickup ? 'bg-#333333 text-#fff' : 'bg-#F2F2F2 text-#333']" class="flex items-center transition-all mr-22rpx h-64rpx rounded-32rpx px-22rpx text-26rpx font-bold">
<image v-show="!isPickup" src="@img/chef/120.png" class="w-28rpx h-28rpx mr-10rpx"></image>
<image v-show="isPickup" src="@img/chef/122.png" class="w-28rpx h-28rpx mr-10rpx"></image>
{{ t('components.filtrate-tool.pickup') }}
</view>
<view @click="toggleDiscount" :class="[isDiscount ? 'bg-#333333 text-#fff' : 'bg-#F2F2F2 text-#333']" class="flex items-center transition-all mr-22rpx h-64rpx rounded-32rpx px-22rpx text-26rpx font-bold">
<image v-show="!isDiscount" src="@img/chef/121.png" class="w-28rpx h-28rpx mr-10rpx"></image>
<image v-show="isDiscount" src="@img/chef/123.png" class="w-28rpx h-28rpx mr-10rpx"></image>
{{ t('components.filtrate-tool.discount') }}
</view>
<view @click="handleScore" class="flex items-center mr-22rpx h-64rpx rounded-32rpx bg-#F2F2F2 px-22rpx text-26rpx text-#333 font-bold">
{{ t('components.filtrate-tool.score') }}
<image src="@img/chef/101.png" class="w-24rpx h-24rpx ml-10rpx"></image>
</view>
<view @click="handlePrice" class="flex items-center h-64rpx rounded-32rpx bg-#F2F2F2 px-22rpx text-26rpx text-#333 font-bold">
{{ t('components.filtrate-tool.price') }}
<image src="@img/chef/101.png" class="w-24rpx h-24rpx ml-10rpx"></image>
</view>
<view class="w-27rpx shrink-0 op-0">1</view>
</view>
</scroll-view>
</view>
</template>
<style scoped lang="scss">
</style>
@@ -0,0 +1,44 @@
<script setup lang="ts">
import {useConfigStore} from "@/store";
const props = withDefaults(defineProps<{
text: string,
fixed?: boolean
}>(), {
fixed: false
})
const emit = defineEmits<{
click: [event: any]
}>()
const configStore = useConfigStore()
function handleClick(event: any) {
emit('click', event)
}
</script>
<template>
<view class="">
<view class="h-128rpx" :style="[configStore.iosSafeBottomPlaceholder]" v-if="props.fixed"></view>
<view class="z-1 bg-#fff shadow-[0rpx_-6rpx_24rpx_rgba(0,0,0,0.16)]" :style="[props.fixed&&{
position: 'fixed',
bottom: '0',
left: '0',
right: '0',
}]">
<view class="h-128rpx p-[16rpx+30rpx] box-border">
<wd-button custom-class="!h-92rpx !text-30rpx !lh-42rpx !rounded-20rpx !bg-#14181B" block
@click="handleClick">
{{ props.text }}
</wd-button>
</view>
<view :style="[configStore.iosSafeBottomPlaceholder]"></view>
</view>
</view>
</template>
<style scoped lang="scss">
</style>
@@ -0,0 +1,85 @@
<script setup lang="ts">
import Config from '@/config'
import {throttle} from 'throttle-debounce'
const {t} = useI18n()
const props = withDefaults(
defineProps<{
modelValue?: string
disabled?: boolean
focus?: boolean
placeholder?: string
}>(),
{
modelValue: '',
disabled: false,
focus: false,
},
)
const emits = defineEmits<{
'update:modelValue': [value: string]
search: []
}>()
function search() {
emits('search')
}
const handleSearch = throttle(Config.throttleTime, search)
function handleInputUpdateModelValue(value: string) {
console.log(value)
emits('update:modelValue', value)
}
function handleClickLeft() {
uni.navigateBack()
}
defineOptions({
name: 'HeaderSearch',
})
</script>
<template>
<view
class="relative z-1 bg-#fff"
>
<status-bar/>
<view class="flex-center-sb px-30rpx h-88rpx">
<view class="shrink-0" @click="handleClickLeft">
<view class="i-fluent:ios-arrow-ltr-24-filled text-36rpx text-#333"></view>
</view>
<view
class="px-36rpx w-626rpx h-88rpx flex items-center bg-common rounded-44rpx "
>
<image src="@img/chef/100222.png" class="w-28rpx h-28rpx mr-18rpx"></image>
<wd-input
no-border
clearable
:focus-when-clear="false"
:disabled="disabled"
:focus="focus"
confirm-type="search"
use-prefix-slot
custom-class="flex items-center !text-30rpx !bg-transparent flex-1"
placeholderStyle="font-size: 30rpx;color: #6D6D6D; font-weight: 500;"
:modelValue="modelValue"
:placeholder="placeholder || t('common.prompt.please-enter-keyword-search')"
@update:modelValue="handleInputUpdateModelValue"
@confirm="handleSearch"
>
</wd-input>
</view>
</view>
</view>
</template>
<style scoped lang="scss">
:deep(.wd-input__clear) {
background: transparent !important;
}
</style>
+87
View File
@@ -0,0 +1,87 @@
<script setup lang="ts">
</script>
<template>
<view class="container-animation">
<view class="part one"></view>
<view class="part two"></view>
<view class="part three"></view>
<view class="part four"></view>
</view>
</template>
<style scoped lang="scss">
$border-width: 10px;
$border-color: #333;
.container-animation {
position: relative;
animation: rotate 2s linear infinite;
}
.part {
position: absolute;
height: 30px;
width: 30px;
box-sizing: border-box;
}
.one {
top: -30px;
left: -30px;
border-radius: 100% 0 0 0;
animation: animato1 1s linear 1s alternate infinite;
border-top: $border-width solid $border-color;
border-left: $border-width solid $border-color;
}
.two {
top: -30px;
border-radius: 0 100% 0 0;
animation: animato2 1s linear alternate infinite;
border-top: $border-width solid $border-color;
border-right: $border-width solid $border-color;
}
.three {
left: -30px;
border-radius: 0 0 0 100%;
animation: animato3 1s linear alternate infinite;
border-bottom: $border-width solid $border-color;
border-left: $border-width solid $border-color;
}
.four {
top: 0px;
left: 0px;
border-radius: 0 0 100% 0;
animation: animato4 1s linear 1s alternate infinite;
border-bottom: $border-width solid $border-color;
border-right: $border-width solid $border-color;
}
@keyframes rotate {
to {
transform: rotate(360deg);
background-color: orange;
}
}
@keyframes animato1 {
to {
width: 20rpx; height: 20rpx; top: -20rpx; left: -20rpx;
}
}
@keyframes animato2 {
to {
width: 20rpx; height: 20rpx; top: -20rpx;
}
}
@keyframes animato3 {
to {
width: 20rpx; height: 20rpx; left: -20rpx;
}
}
@keyframes animato4 {
to {
width: 20rpx; height: 20rpx;
}
}
</style>
+46
View File
@@ -0,0 +1,46 @@
<script setup lang="ts">
import * as R from 'ramda'
import type {Goods} from "@/service/index-data";
import {thumbnailImg} from "@/utils/utils";
const props = defineProps<{
item: Goods
}>()
const {t} = useI18n()
function navigateTo(url: string) {
uni.navigateTo({
url,
})
}
</script>
<template>
<view class="w-336rpx bg-#fff rounded-20rpx"
@click="navigateTo(`/pages-shop/pages/goods-detail/index?id=${item.id}`)"
>
<view class="flex">
<image class="w-336rpx h-336rpx rounded-t-20rpx" :src="thumbnailImg(item.coverImage)"></image>
</view>
<view class="p-[18rpx+16rpx+24rpx]">
<view class="text-26rpx text-primary lh-36rpx font-bold line-clamp-2">{{ item.name }}
</view>
<view class="mt-12rpx flex" v-if="item.goodsCategoryVo">
<view
class="h-32rpx px-10rpx center text-22rpx text-#999 lh-22rpx font-bold border-custom after:(!border-#D4D4D4 rounded-20rpx)">
{{ item.goodsCategoryVo?.name }}
</view>
</view>
<view class="mt-18rpx flex items-end justify-between">
<text class="text-30rpx text-#666 font-bold lh-42rpx">S${{ item.price }}</text>
</view>
</view>
</view>
</template>
<style scoped lang="scss">
</style>
+44
View File
@@ -0,0 +1,44 @@
<script setup lang="ts">
const props = withDefaults(defineProps<{
title?: string;
fixed?: boolean
showLeft?: boolean
customClass?: string
}>(), {
fixed: true,
showLeft: true
});
function handleClickLeft() {
uni.navigateBack()
}
</script>
<template>
<wd-navbar
:title="props.title"
safeAreaInsetTop
:fixed="props.fixed"
:placeholder="props.fixed"
:bordered="false"
:custom-class="props.customClass"
@click-left="handleClickLeft">
<template #left>
<view class="shrink-0" v-if="showLeft">
<view class="i-carbon:chevron-left text-50rpx text-primary ml-[-10rpx]"></view>
</view>
</template>
<template #right>
<slot name="right"></slot>
</template>
</wd-navbar>
</template>
<style scoped lang="scss">
:deep(.wd-navbar) {
z-index: 2 !important;
.wd-navbar__title {
font-weight: 500 !important;
}
}
</style>
@@ -0,0 +1,55 @@
<script setup lang="ts">
import type SetPassword from "@/components/set-password/set-password.vue";
import type PasswordInput from "@/components/password-input/password-input.vue";
import {useUserStore} from "@/store";
const emits = defineEmits<{
success: [password: string];
}>();
const userStore = useUserStore();
const setPasswordRef = ref<InstanceType<typeof SetPassword> | null>(null);
const passwordInputRef = ref<InstanceType<typeof PasswordInput> | null>(null);
function showSetPassword() {
setPasswordRef.value?.init();
}
function showPasswordInput() {
if (!userStore.userInfo?.payPwd) {
showSetPassword()
return
}
passwordInputRef.value?.init?.();
}
function handleSuccess(password: string) {
emits("success", password);
}
defineOptions({
name: "PasswordContainer",
});
defineExpose({
showSetPassword,
showPasswordInput,
});
</script>
<template>
<view class="">
<set-password ref="setPasswordRef"/>
<password-input ref="passwordInputRef" @success="handleSuccess"/>
</view>
</template>
<style scoped lang="scss">
</style>
@@ -0,0 +1,123 @@
<script setup lang="ts">
import {useConfigStore} from "@/store";
const emits = defineEmits<{
success: [password: string]
}>()
const {t} = useI18n()
const configStore= useConfigStore()
const show = ref(false)
const password = ref('')
const keyboardHeight = ref<number>(0)
const showKeyboard = ref<boolean>(false);
function init() {
console.log('init')
password.value = ''
show.value = true
}
function close() {
show.value = false
password.value = ''
}
function handleSubmit() {
console.log(password.value)
if (password.value.length === 6) {
emits('success', password.value)
close()
} else {
uni.showToast({
title: t('common.please-enter-6-digit-payment-password'),
icon: 'none',
})
}
}
const listener = function (res: any) {
console.log(res)
keyboardHeight.value = res.height
}
onMounted(() => {
// #ifdef APP-PLUS
uni.onKeyboardHeightChange(listener)
// #endif
})
onUnmounted(() => {
// #ifdef APP-PLUS
uni.offKeyboardHeightChange(listener)
// #endif
})
defineOptions({
name: 'PasswordInput',
styleIsolation: 'shared',
})
defineExpose({
init,
close,
})
</script>
<template>
<wd-popup
position="bottom"
safe-area-inset-bottom
v-model="show"
custom-class="rounded-t-30rpx !p-[40rpx+20rpx+48rpx]"
>
<view
class="mb-48rpx relative flex justify-center text-center text-34rpx text-primary font-bold"
>
<text>{{ t('common.please-enter-your-payment-password') }}</text>
</view>
<view class="relative">
<wd-password-input custom-class="flex-1 h-50px"
:length="6"
v-model="password" :focused="showKeyboard"/>
<input
type="number"
:maxlength="6"
:cursor-spacing="20"
v-model="password"
cursor-color="transparent"
class="absolute top-0 left-0 opacity-0 w-ful h-full"
@blur="showKeyboard = false"
@focus="showKeyboard = true"
/>
</view>
<view class="mt-60rpx">
<wd-button
custom-class="!w-full !h-88rpx !m-0 !text-30rpx !font-bold !rounded-20rpx"
@click="handleSubmit"
>{{ t('common.confirm') }}
</wd-button
>
</view>
<view class="" :style="[configStore.isIos?{
height: keyboardHeight + 'px',
transition: 'height 0.3s ease-in-out',
overflow: 'hidden',
}:{}]"></view>
</wd-popup>
</template>
<style scoped lang="scss">
:deep(.wd-password-input) {
margin: 0;
}
:deep(.wd-password-input__item) {
background-color: transparent;
}
</style>
@@ -0,0 +1,147 @@
<script lang="ts" setup>
import { getCaptcha } from '@/service'
const emits = defineEmits<{
success: [{ code: string; uuid: string }]
}>()
const imgCode = ref('')
const captcha = ref({
img: '',
uuid: '',
})
const show = ref(false)
function init() {
initData()
show.value = true
}
function close() {
show.value = false
imgCode.value = ''
captcha.value = {
img: '',
uuid: '',
}
}
function handleSubmit() {
if (!imgCode.value) {
return uni.showToast({ title: '请输入图形验证码', icon: 'none' })
}
emits('success', { code: imgCode.value, uuid: captcha.value.uuid })
close()
}
async function initData() {
try {
const res = await getCaptcha()
captcha.value = {
img: 'data:image/jpeg;base64,' + res?.data?.img,
uuid: res?.data?.uuid,
}
} catch (e) {}
}
defineOptions({
})
defineExpose({
init,
close,
})
</script>
<template>
<wd-popup :close-on-click-modal="false" custom-style="border-radius: 20rpx;" v-model="show">
<view class="content">
<view class="content__header">
<view class="content__title">请输入图形验证码</view>
<view class="content__close-icon" @click="close">
<image src="@/pages-login/static/images/icon_del@2x.png"></image>
</view>
</view>
<view class="content__body">
<view
class="flex items-center justify-between border border-solid border-gray-3 rounded-50rpx"
>
<wd-input
no-border
v-model.trim="imgCode"
placeholderStyle="font-size: 28rpx;line-height: 40rpx;color: #999999;"
placeholder="请输入"
custom-class="flex-1 p-[15rpx+30rpx] !bg-transparent "
></wd-input>
<view class="flex items-center">
<image :src="captcha.img" class="w-180rpx h-80rpx rounded-r-50rpx"></image>
</view>
</view>
<view class="content__tip" @click="initData">{{ $t('securityCode.cantSee') }}</view>
</view>
<view class="content__footer">
<wd-button block @click="handleSubmit">{{ $t('securityCode.confirm') }}</wd-button>
</view>
</view>
</wd-popup>
</template>
<style lang="scss" scoped>
.content {
box-sizing: border-box;
width: 630rpx;
padding: 30rpx 40rpx 40rpx;
background-color: #fff;
border-radius: 16rpx;
&__header {
position: relative;
text-align: center;
}
&__body {
margin: 42rpx 0 40rpx;
.uni-easyinput {
height: 90rpx;
display: flex;
align-items: center;
border-radius: $uni-border-radius-lg;
border: 1rpx solid #d1d1d1;
}
}
&__close-icon {
position: absolute;
right: 0;
top: 8rpx;
width: 32rpx;
height: 32rpx;
image {
width: 100%;
height: 100%;
}
}
&__title {
font-size: 32rpx;
font-weight: bold;
color: #333;
line-height: 48rpx;
letter-spacing: 2rpx;
}
&__tip {
margin-top: 10rpx;
text-align: right;
font-size: 24rpx;
font-weight: 400;
line-height: 33rpx;
color: $uni-color-primary;
letter-spacing: 2rpx;
}
}
</style>
@@ -0,0 +1,129 @@
<script setup lang="ts">
import * as R from 'ramda'
import {debounce} from "throttle-debounce";
import Config from "@/config";
import {useConfigStore, useUserStore} from "@/store";
import {getNailServiceDetail} from "@/service";
import type {Service} from "@/service/index-data";
import {thumbnailImg} from "@/utils/utils";
const emits = defineEmits<{
'success': [data: Service]
}>()
const {t} = useI18n();
const configStore = useConfigStore();
const userStore = useUserStore();
const info = ref<Service>({})
const show = ref(false);
const images = computed(() => {
const {images} = info.value
if (R.equals(R.type(images), 'String') && R.isNotEmpty(images)) {
return R.split(',', images as string)
}
return []
})
function init(id: string) {
initData(id)
show.value = true;
}
function close() {
show.value = false;
}
function handlePreview(current: string, urls: string[]) {
uni.previewImage({
current,
urls
})
}
function submit() {
if (!userStore.checkLogin()) {
return
}
close();
emits('success', info.value)
}
const handleSubmit = debounce(Config.debounceLongTime, submit, {atBegin: true});
async function initData(id: string) {
try {
const res = await getNailServiceDetail({
id: id
})
info.value = res.data || {}
} catch (e) {
}
}
defineOptions({
name: "ServiceDetail",
});
defineExpose({
show,
init,
close,
});
</script>
<template>
<wd-popup
custom-class="rounded-t-30rpx"
position="bottom"
safe-area-inset-bottom
v-model="show"
@close="close"
>
<view class="box-border h-1190rpx p-[40rpx+30rpx+0rpx] flex flex-col"
>
<view class="relative pb-20rpx pr-32rpx flex items-center justify-between">
<view class="text-34rpx text-primary font-bold lh-48rpx">
{{ info.name }}
</view>
<image class="absolute right-0 w-32rpx h-32rpx" src="@img/2106@2x.png" @click="close"></image>
</view>
<scroll-view scroll-y="true" class="mt-8rpx flex-1 overflow-hidden">
<view class="text-26rpx text-#999 font-bold lh-36rpx">{{
t('common.over')
}} {{ info.serviceTime }} {{ t('common.number-minutes') }}
</view>
<view class="mt-20rpx text-26rpx text-#666 font-bold lh-36rpx">S${{ info.serviceAmount }}</view>
<view class="mt-40rpx pb-30rpx text-24-bold">
{{ info.serviceDetail }}
</view>
<view class="mt-4rpx flex justify-between flex-wrap">
<view class="relative mt-20rpx flex shrink-0" v-for="(item, index) in images"
:key="item"
@click="handlePreview(item, images)"
>
<image class=" w-202rpx h-202rpx rounded-20rpx" :src="thumbnailImg(item)"></image>
</view>
<template v-if="images.length % 3 === 1">
<view class="w-202rpx h-202rpx"></view>
<view class="w-202rpx h-202rpx"></view>
</template>
<template v-if="images.length % 3 === 2">
<view class="w-202rpx h-202rpx"></view>
</template>
</view>
<view class="h-30rpx"></view>
</scroll-view>
</view>
<fixed-bottom-large-btn :text="t('common.add-to-booking')"
@click="handleSubmit"
/>
</wd-popup>
</template>
<style scoped lang="scss"></style>
@@ -0,0 +1,60 @@
<script lang="ts" setup>
const {t} = useI18n()
const show = ref(false)
function init() {
show.value = true
}
function close() {
show.value = false
}
function handleConfirm() {
close()
uni.navigateTo({
url: '/pages-user/pages/pay-password/set/index',
})
}
defineExpose({
show,
init,
close,
})
</script>
<template>
<wd-popup
custom-class="rounded-20rpx"
safe-area-inset-bottom
v-model="show"
@close="close"
>
<view class="w-630rpx box-border p-[40rpx+44rpx] flex flex-col">
<view class="mt-18rpx text-34-bold text-center">
{{ t('common.prompt.not-setup-payment-password') }}
</view>
<view class="mt-58rpx flex items-center justify-between">
<wd-button
custom-class="!h-88rpx !w-258rpx !min-w-auto !text-30rpx !lh-42rpx !font-bold !rounded-20rpx"
@click="close"
>
{{ t('common.cancel') }}
</wd-button>
<wd-button plain
custom-class="!h-88rpx !w-258rpx !min-w-auto !text-30rpx !lh-42rpx !font-bold !border-#666666 !rounded-20rpx"
@click="handleConfirm"
>
{{ t('common.go-to-settings') }}
</wd-button>
</view>
</view>
</wd-popup>
</template>
<style scoped lang="scss"></style>
+39
View File
@@ -0,0 +1,39 @@
<script setup lang="ts">
import {useConfigStore} from "@/store";
const configStore = useConfigStore()
function getSystemInfo(): any {
let systemInfo: any
// #ifdef MP-WEIXIN
try {
// const systemSetting = uni.getSystemSetting() // 暂时不需要
const deviceInfo = uni.getDeviceInfo()
const windowInfo = uni.getWindowInfo()
const appBaseInfo = uni.getAppBaseInfo()
systemInfo = {
...deviceInfo,
...windowInfo,
...appBaseInfo
}
} catch (error) {
console.warn('获取系统信息失败,降级使用uni.getSystemInfoSync:', error)
// 降级处理,使用原来的方法
systemInfo = uni.getSystemInfoSync()
}
// #endif
// #ifndef MP-WEIXIN
systemInfo = uni.getSystemInfoSync()
// #endif
return systemInfo
}
const { statusBarHeight } = getSystemInfo()
</script>
<template>
<view class="w-750rpx shrink-0" :style="[{ height: statusBarHeight + 'px' }]"></view>
</template>
<style scoped lang="scss">
</style>
@@ -0,0 +1,25 @@
<script setup lang="ts">
import type chooseLanguageVue from "@/components/choose-language/choose-language.vue";
const {t} = useI18n()
const chooseLanguageRef = ref<InstanceType<typeof chooseLanguageVue> | null>(null)
</script>
<template>
<view class="flex justify-end">
<view
class="flex items-center px-16rpx h-64rpx bg-primary rounded-l-50rpx shadow-[0rpx_6rpx_12rpx_rgba(19,72,173,0.24)]"
hover-class="!opacity-90"
@click.stop="chooseLanguageRef?.init()">
<image class="shrink-0 mr-6rpx w-32rpx h-32rpx" src="@img/17448@2x.png"></image>
<text class="mr-8rpx text-28rpx text-#fff font-bold">{{ t('common.switch-language') }}</text>
</view>
</view>
</template>
<style scoped lang="scss">
</style>
+86
View File
@@ -0,0 +1,86 @@
<script setup lang="ts">
import * as R from 'ramda'
import {debounce} from "throttle-debounce";
import Config from "@/config";
import {useConfigStore} from "@/store";
const emits = defineEmits<{
success: []
}>()
const {t} = useI18n();
const configStore = useConfigStore();
const verificationCode = ref('');
const show = ref(false);
function init(code: string) {
verificationCode.value = code;
show.value = true;
}
function close() {
show.value = false;
}
function submit() {
close();
emits('success')
}
const handleSubmit = debounce(Config.debounceLongTime, submit, {
atBegin: true
});
function handleCancel() {
close()
// navigateTo("/pages-shop/pages/booking-service/make-appointment/index")
}
function navigateTo(url: string) {
uni.navigateTo({
url,
})
}
defineOptions({
name: "UseCode",
});
defineExpose({
show,
init,
close,
});
</script>
<template>
<wd-popup
custom-class="!bg-transparent"
safe-area-inset-bottom
v-model="show"
@close="close"
>
<view class="box-border ">
<view class="p-[40rpx+40rpx+34rpx] bg-#fff rounded-20rpx"
>
<view class="text-44rpx text-primary font-bold lh-62rpx text-center">
{{ t('pages-store.order.useCode') }}
</view>
<view class="mt-40rpx center">
<uqrcode ref="uqrcode" canvas-id="qrcode" :value="verificationCode"
:size="512"
sizeUnit="rpx"
:options="{}"></uqrcode>
</view>
</view>
<view class="center mt-52rpx">
<image class="w-80rpx h-80rpx shrink-0" src="@img/20241@2x.png" @click="close"></image>
</view>
</view>
</wd-popup>
</template>
<style scoped lang="scss"></style>
+73
View File
@@ -0,0 +1,73 @@
<script setup lang="ts">
const { t } = useI18n();
const show = ref(false);
const pickerValue = ref(0)
const emits = defineEmits(['confirm'])
function onOpen(value: number) {
if (value) {
pickerValue.value = value;
}
show.value = true;
}
function handleClose() {
show.value = false;
}
function handleSubmit() {
emits('confirm', columns.value[pickerValue.value])
handleClose()
}
const columns = ref([{
value: 0,
label: t('components.visit.leaveItToMePersonally'),
}, {
value: 1,
label: t('components.visit.putItAtTheDoor'),
}])
defineExpose({
onOpen,
});
</script>
<template>
<wd-popup
v-model="show"
position="bottom"
@close="handleClose"
>
<view>
<view class="flex-center-sb h-102rpx bg-#F7F7F7 px-30rpx">
<view @click="handleClose" class="text-30rpx text-#999">{{ t('common.cancel') }}</view>
<view class="text-34rpx text-#333">{{ t('common.select') }}</view>
<view @click="handleSubmit" class="text-30rpx text-#FF6106">{{ t('common.confirm') }}</view>
</view>
<view class="bg-#fff px-54rpx py-56rpx">
<wd-picker-view :columns="columns" v-model="pickerValue"/>
</view>
</view>
</wd-popup>
</template>
<style scoped lang="scss">
:deep(.uni-picker-view-wrapper) {
& > uni-picker-view-column:first-of-type .uni-picker-view-group {
.uni-picker-view-indicator {
border-radius: 20rpx 0 0 20rpx !important;
&:after {
border: none;
}
&:before {
border: none;
}
}
}
}
:deep(.wd-picker-view-column__item) {
line-height: 94rpx !important;
}
:deep(.uni-picker-view-indicator) {
height: 94rpx !important;
}
</style>
+56
View File
@@ -0,0 +1,56 @@
const Config = {
appName: "CHEFLINK",
googleMapKey: "AIzaSyDvTA1j9_hPPg7kev4fzf6RlGpf_yYhdoo",
// stripeKey:
// "pk_test_51RcyzCQVf8HI8x55xHQ11F0ydksiLEscmWuut6o0eCHV8fCYOWI9F9VrddMlKarnux65EjSIQmFb8rTwrrjrRzgG00CyQVpyRQ",
stripeKey:
"pk_live_51Rcyz0HqArf2IYTZTxK8mXrmUoxLJuC6QpNYgG76CEGSD6D3pUi48QkIwuyEAtqklEwaLC6cHGP5vntuiAFWB7cY000m2o2AU1",
defaultLanguage: "en",
timezone: "Asia/Shanghai",
debounceLongTime: 1000,
debounceShortTime: 500,
throttleTime: 1000,
throttleShortTime: 500,
iosId: "6615065357",
weixinServiceUrl: `https://chatbot.weixin.qq.com/webapp/0USvjruTJTMczuq6ND6gVwAJj4hG6g?isFloat=false&robotName=CHEFLINK`,
weixinServiceName: "CHEFLINK",
// 登录页
loginPath: "/pages-login/pages/index",
// 首页
indexPath: "/pages/home/index",
// 引导页
guidePath: "/pages-login/pages/guide-page/location",
shareLink: "https://www.howhowfresh.com/h5/",
shareImage: "https://hanguomcn.oss-ap-northeast-2.aliyuncs.com/images/20250901105756_1756695476183_56ac07ac.png",
shareDesc: "分享专属二维码邀请新用户注册即邀请成功。一起来注册体验点餐功能。",
// 登录端口
userPort: 1,
// 手机区号数组
phoneCodeList: [
"+86", "+886", "+852", "+853", "+93", "+355", "+213", "+684", "+376", "+244",
"+1264", "+672", "+1268", "+54", "+374", "+297", "+61", "+43", "+994", "+973",
"+880", "+1246", "+375", "+32", "+501", "+229", "+1441", "+975", "+591", "+387",
"+267", "+55", "+1284", "+673", "+359", "+226", "+95", "+257", "+855", "+237",
"+1", "+238", "+1345", "+236", "+235", "+56", "+61", "+61", "+57", "+269", "+243",
"+242", "+682", "+506", "+225", "+385", "+53", "+357", "+420", "+45", "+253",
"+1767", "+1809", "+593", "+20", "+503", "+240", "+291", "+372", "+251", "+500",
"+298", "+679", "+358", "+33", "+594", "+689", "+241", "+995", "+49", "+233",
"+350", "+30", "+299", "+1473", "+590", "+1671", "+502", "+1481", "+224", "+245",
"+592", "+509", "+379", "+504", "+36", "+354", "+91", "+62", "+98", "+964", "+353",
"+972", "+39", "+1876", "+81", "+962", "+73", "+254", "+686", "+850", "+82", "+965",
"+996", "+856", "+371", "+961", "+266", "+231", "+218", "+423", "+370", "+352",
"+389", "+261", "+265", "+60", "+960", "+223", "+356", "+692", "+596", "+222",
"+230", "+269", "+52", "+691", "+373", "+377", "+976", "+1664", "+212", "+258",
"+264", "+674", "+977", "+31", "+599", "+687", "+64", "+505", "+227", "+234",
"+683", "+6723", "+1", "+47", "+968", "+92", "+680", "+507", "+675", "+595",
"+51", "+63", "+48", "+351", "+1809", "+974", "+262", "+40", "+7", "+250", "+290",
"+1869", "+1758", "+508", "+1784", "+685", "+378", "+239", "+966", "+221", "+381",
"+248", "+232", "+65", "+421", "+386", "+677", "+252", "+27", "+34", "+94", "+249",
"+597", "+47", "+268", "+46", "+41", "+963", "+992", "+255", "+66", "+1242", "+220",
"+228", "+690", "+676", "+1868", "+216", "+90", "+993", "+1649", "+688", "+256",
"+380", "+971", "+44", "+1", "+598", "+998", "+678", "+58", "+84", "+1340", "+681",
"+967", "+260", "+263"
]
};
export default Config;
+201
View File
@@ -0,0 +1,201 @@
/** 协议 */
export enum Agreement {
/** 用户须知及使用协议- */
USER_AGREEMENT = "chef_user_agreement",
/** 隐私政策 */
PRIVACY_POLICY = "chef_privacy_policy",
/** 钱包说明 */
BALANCE_EXPLANATION = "balance",
/** 平台协议 */
CHEF_PLATFORM_AGREEMENT = "chef_platform_agreement",
/** 关于我们 */
ABOUT_US = "ABOUT_US",
/** 积分说明 */
POINTS_EXPLANATION = "POINTS_EXPLANATION",
/** 会员说明 */
MEMBERSHIP_DESCRIPTION = "MEMBERSHIP_DESCRIPTION",
/** 会员支付协议 */
MEMBERSHIP_PAYMENT_AGREEMENT = "MEMBERSHIP_PAYMENT_AGREEMENT",
/** 支付说明 */
DEPOSIT_EXPLANATION = "DEPOSIT_EXPLANATION",
/** GST说明 */
GST_EXPLANATION = "GST_EXPLANATION",
}
/** 短信验证码类型 */
export enum SmsType {
/** 绑定手机号 */
USER_BIND_PHONE_NUMBER = 1,
/** 邮箱注册 */
USER_EMAIL_REGISTER = 2,
/** 忘记密码 */
USER_FORGET_PASSWORD = 3,
/** 忘记支付密码 */
USER_FORGET_PAYMENT_PASSWORD = 4,
/** 设置支付密码 */
USER_SET_PAYMENT_PASSWORD = 5
}
/** 登录类型 */
export enum LoginType {
/** 邮箱登录 */
EMAIL = 1,
/** 苹果登录 */
APPLE = 2,
/** 脸书登录 */
FACEBOOK = 3,
/** 谷歌登录 */
GOOGLE = 4,
/** 账号密码登录 */
ACCOUNT = 5,
}
/** 字典 */
export enum DictType {
/** 订单相关 */
ABOUT_ORDER = "about_order",
/** 平台相关 */
ABOUT_PLATFORM = "about_platform",
/** 价格范围 */
PRICE_RANGE = "price_range",
/** 购买须知 */
PURCHASE_NOTES = "purchase_notes",
/** 活动说明 */
ACTIVITY_DESCRIPTION = "activity_description",
/** 预约提示 */
APPOINTMENT_REMINDER = "appointment_reminder",
}
/** 字典 */
export enum DictValue {
/** 待付款订单取消时间 */
PENDING_PAYMENT_ORDER_CANCEL_TIME = "pending_payment_order_cancel_time",
/** 退款订单自动审核时间 */
REFUND_ORDER_AUTO_AUDIT_TIME = "refund_order_auto_audit_time",
/** 平台联系方式 */
CONTACT_METHOD = "contact_method",
/** 多人预约折扣提示 */
GROUP_DISCOUNT_EN = "group_discount_en",
/** 多人预约折扣提示 */
GROUP_DISCOUNT_ZH = "group_discount_zh",
/**
卸甲预约、补甲预约免费时长提示 */
FREE_REMOVAL_SERVICE_AVAILABLE_EN = "free_removal_service_available_en",
/**
卸甲预约、补甲预约免费时长提示 */
FREE_REMOVAL_SERVICE_AVAILABLE_ZH = "free_removal_service_available_zh"
}
/** 自定义事件 */
export enum EventEnum {
/** 裁剪头像 */
CROPPER_AVATAR = "CROPPER_AVATAR",
/** 展示星级筛选 */
STAR_RATING_FILTER = "show-star-rating-filter",
/** 展示价格筛选 */
PRICE_FILTER = "show-price-filter",
/** 选择地址 */
CHOOSE_ADDRESS = "CHOOSE_ADDRESS",
/** 选择预约时间 */
CHOOSE_APPOINTMENT_TIME = "CHOOSE_APPOINTMENT_TIME",
/** 选择支付方式 */
CHOOSE_PAYMENT_METHOD = "CHOOSE_PAYMENT_METHOD",
}
// 用户地址类型 地址类型(house,apartment,office,hotel,other)
export enum UserAddressType {
/** 家庭地址 */
HOUSE = "house",
/** 公寓地址 */
APARTMENT = "apartment",
/** 办公地址 */
OFFICE = "office",
/** 酒店地址 */
HOTEL = "hotel",
/** 其他地址 */
OTHER = "other",
}
/** 收藏对象类型(1-菜谱 2-菜品 3-配菜) */
export enum CollectionType {
/** 菜谱 */
RECIPE = 1,
/** 菜品 */
DISH = 2,
/** 配菜 */
SIDE_DISH = 3,
/** 店铺 */
STORE = 4,
}
// 订单状态
export enum OrderStatus {
/** 已退款 */
REFUNDED = -2,
/** 已取消 */
CANCELLED = -1,
/** 待付款 */
PENDING_PAYMENT = 1,
/** 已付款 */
HAS_PENDING_PAYMENT = 2,
/** 商家已接单 */
MERCHANT_ACCEPTED = 3,
/** 配送中 */
DELIVERING = 4,
/** 已核销(已送达) */
COMPLETED = 5,
/** 商家拒绝接单 */
MERCHANT_REJECTED = 6,
}
// 订单取消状态
export enum OrderCancelStatus {
/**
* 申请退款
*/
APPLIED = 1,
/**
* 商家同意
*/
APPROVED = 2,
/**
* 商家拒绝
*/
REJECTED = 3,
}
/** 消息类型 */
export enum MessageTypeEnum {
/** 有人评论了你 */
COMMENTED = 1,
/** 有人回复了你 */
REPLIED = 2,
/** 用户下单 */
ORDER_CREATED = 3,
/** 商家接单 */
MERCHANT_ACCEPTED = 4,
/** 商家同意退款 */
REFUND_AGREED = 5,
/** 商家拒绝退款 */
REFUND_REJECTED = 6,
/** 商家开始配送 */
DELIVERY_STARTED = 7,
/** 商家已送达 */
DELIVERY_ARRIVED = 8,
/** 订单已核销 */
ORDER_WRITTEN_OFF = 9,
/** 商家入驻审核通过 */
SETTLEMENT_APPROVED = 10,
/** 商家入驻审核未通过 */
SETTLEMENT_REJECTED = 11,
/** 评论审核通过 */
COMMENT_APPROVED = 13,
/** 评论审核未通过 */
COMMENT_REJECTED = 14,
}
+29
View File
@@ -0,0 +1,29 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import { DefineComponent } from 'vue'
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
const component: DefineComponent<{}, {}, any>
export default component
}
interface ImportMetaEnv {
/** 网站标题,应用名称 */
readonly VITE_APP_TITLE: string
/** 服务端口号 */
readonly VITE_SERVER_PORT: string
/** 后台接口地址 */
readonly VITE_SERVER_BASEURL: string
/** H5是否需要代理 */
readonly VITE_APP_PROXY: 'true' | 'false'
/** H5是否需要代理,需要的话有个前缀 */
readonly VITE_APP_PROXY_PREFIX: string // 一般是/api
/** 是否清除console */
readonly VITE_DELETE_CONSOLE: string
/** 是否是开发环境 */
readonly DEV: boolean
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
+13
View File
@@ -0,0 +1,13 @@
export default function useAreaCode() {
const defaultAreaCode = ref("+1")
function handlePageClick() {
}
return {
defaultAreaCode,
handlePageClick
}
}
+6
View File
@@ -0,0 +1,6 @@
export default function useEventEmit(eventName: string, callback: (result: any) => void) {
onLoad(() => uni.$on(eventName, callback))
onUnload(() => uni.$off(eventName))
}
+41
View File
@@ -0,0 +1,41 @@
import {appSmsSendPost} from "@/service";
// 获取短信验证码的自定义 Hook
export default function useGetMsgCode() {
const {t} = useI18n()
const isSend = ref(0)
async function getMsgCode({
type, phone, areaCode
}: {
type: string | number;
phone: string | number,
areaCode: string | number,
}) {
const res = await appSmsSendPost({
body: {
areaCode,
phone,
// 1 :用户端绑定手机号 2 :用户端邮箱注册 3 :用户端忘记密码 4 :用户端忘记支付密码 5 :用户端设置支付密码
type,
}
})
if (res) {
await uni.showToast({title: t('common.verification-code-sent-successfully'), icon: 'none'})
isSend.value = 60
const timer = setInterval(() => {
if (isSend.value === 0) {
clearInterval(timer)
} else {
isSend.value -= 1
}
}, 1000)
}
}
return {
isSend,
getMsgCode
}
}
+20
View File
@@ -0,0 +1,20 @@
// 监听网络状态变化的自定义 Hook
export default function useNetworkStatusChange(callback: () => void) {
const OnNetworkStatusChange = (res: any) => {
console.log('=>(useNetworkStatusChange)', res)
if (res.isConnected) {
callback && callback()
}
}
onLoad(() => {
uni.onNetworkStatusChange(OnNetworkStatusChange)
})
onUnload(() => {
uni.offNetworkStatusChange(OnNetworkStatusChange)
})
}
+42
View File
@@ -0,0 +1,42 @@
// 自定义分页 Hook
export default function usePage<T>(
queryFun: (pageNum: number, pageSize: number) => Promise<IResData<T>>,
callback?: (params?: any) => void,
) {
const paging = ref<ZPagingInstance<T> | null>(null)
const loading = ref(true)
const firstLoaded = ref(false)
const dataList = ref<T[]>([])
const totalRows = ref(0)
async function queryList(pageNum: number, pageSize: number) {
try {
const res = await queryFun(pageNum, pageSize)
console.log(res)
await paging.value?.complete(res?.rows)
totalRows.value = res?.total
firstLoaded.value = true
setTimeout(() => {
callback && callback({...res, pageNum, pageSize})
}, 100)
} catch (error) {
await paging.value?.complete(false)
setTimeout(() => {
callback && callback({pageNum, pageSize})
}, 100)
} finally {
setTimeout(() => {
loading.value = false
}, 100)
}
}
return {
paging,
firstLoaded,
loading,
dataList,
totalRows,
queryList,
}
}
+32
View File
@@ -0,0 +1,32 @@
import { ref, onMounted, onUnmounted } from 'vue'
import { throttle } from 'throttle-debounce'
export function useScrollThreshold(threshold = 250, delay = 150) {
const isThresholdReached = ref(false)
// 创建节流处理函数 (使用箭头函数保持this指向)
const throttledHandler = throttle(delay, (scrollTop: number) => {
const newState = scrollTop > threshold
// 仅当状态变化时更新
if (isThresholdReached.value !== newState) {
isThresholdReached.value = newState
console.log("阈值状态更新:", newState, "| 滚动位置:", scrollTop)
}
})
const handleScroll = (e: any) => {
// 传递滚动位置给节流函数
throttledHandler(e.scrollTop)
}
onMounted(() => {
uni.$on('page-scroll', handleScroll)
})
onUnmounted(() => {
uni.$off('page-scroll', handleScroll)
throttledHandler.cancel() // 清除节流函数残留任务
})
return isThresholdReached
}
+50
View File
@@ -0,0 +1,50 @@
import type { CustomRequestOptions } from '@/http/types'
import {http as httpUtils} from "@/utils/http";
export function http<T>(options: CustomRequestOptions) {
// 1. 返回 Promise 对象
return httpUtils<T>(options)
}
/**
* GET 请求
* @param url 后台地址
* @param query 请求query参数
* @param header 请求头,默认为json格式
* @returns
*/
export function httpGet<T>(url: string, query?: Record<string, any>, header?: Record<string, any>, options?: Partial<CustomRequestOptions>) {
return http<T>({
url,
query,
method: 'GET',
header,
...options,
})
}
/**
* POST 请求
* @param url 后台地址
* @param data 请求body参数
* @param query 请求query参数,post请求也支持query,很多微信接口都需要
* @param header 请求头,默认为json格式
* @returns
*/
export function httpPost<T>(url: string, data?: Record<string, any>, query?: Record<string, any>, header?: Record<string, any>, options?: Partial<CustomRequestOptions>) {
return http<T>({
url,
query,
data,
method: 'POST',
header,
...options,
})
}
http.get = httpGet
http.post = httpPost
// 支持与 alovaJS 类似的API调用
http.Get = httpGet
http.Post = httpPost
+33
View File
@@ -0,0 +1,33 @@
/**
* 在 uniapp 的 RequestOptions 和 IUniUploadFileOptions 基础上,添加自定义参数
*/
export interface CustomRequestOptions extends UniApp.RequestOptions {
query?: Record<string, any>
/** 出错时是否隐藏错误提示 */
hideErrorToast?: boolean
}
// 通用响应格式
export interface IResponse<T = any> {
code: number | string
data: T
message: string
status: string | number
}
// 分页请求参数
export interface PageParams {
page: number
pageSize: number
[key: string]: any
}
// 分页响应数据
export interface PageResult<T> {
list: T[]
total: number
page: number
pageSize: number
}
+30
View File
@@ -0,0 +1,30 @@
import type { CustomRequestOptions } from '@/http/types'
import { http } from './http'
/*
* openapi-ts-request 工具的 request 跨客户端适配方法
*/
export default function request<T = unknown>(
url: string,
options: Omit<CustomRequestOptions, 'url'> & {
params?: Record<string, unknown>
headers?: Record<string, unknown>
},
) {
const requestOptions = {
url,
...options,
}
if (options.params) {
requestOptions.query = requestOptions.params
delete requestOptions.params
}
if (options.headers) {
requestOptions.header = options.headers
delete requestOptions.headers
}
return http<T>(requestOptions)
}
+2
View File
@@ -0,0 +1,2 @@
export { routeInterceptor } from './route'
export { requestInterceptor } from './request'
+78
View File
@@ -0,0 +1,78 @@
import qs from 'qs'
import {useUserStore} from '@/store'
import {platform} from '@/utils/platform'
export type CustomRequestOptions = UniApp.RequestOptions & {
query?: Record<string, any>
/** 出错时是否隐藏错误提示 */
hideErrorToast?: boolean
}
// 请求基准地址
const baseUrl = import.meta.env.VITE_SERVER_BASEURL
const proxyPrefix = import.meta.env.VITE_APP_PROXY_PREFIX
// 拦截器配置
const httpInterceptor = {
// 拦截前触发
invoke(options: CustomRequestOptions) {
// 接口请求支持通过 query 参数配置 queryString
if (options.query) {
const queryStr = qs.stringify(options.query)
if (options.url.includes('?')) {
options.url += `&${queryStr}`
} else {
options.url += `?${queryStr}`
}
}
// 非 http 开头需拼接地址
if (!options.url.startsWith('http')) {
// #ifdef H5
// console.log(__VITE_APP_PROXY__)
if (JSON.parse(__VITE_APP_PROXY__)) {
// 啥都不需要做
options.url = proxyPrefix + options.url
} else {
options.url = baseUrl + options.url
}
// #endif
// 非H5正常拼接
// #ifndef H5
options.url = baseUrl + options.url
// #endif
// TIPS: 如果需要对接多个后端服务,也可以在这里处理,拼接成所需要的地址
}
// 1. 请求超时
options.timeout = 10000 // 10s
// 谷歌地图的请求不额外增加请求头
if (!options.url.startsWith('https://maps.googleapis.com')) {
// 2. (可选)添加小程序端请求头标识
const localeLanguages = uni.getLocale()
console.log(localeLanguages)
options.header = {
platform, // 可选,与 uniapp 定义的平台一致,告诉后台来源
'content-language': localeLanguages === 'zh-Hans' ? 'zh_CN' : 'en_US',
...options.header,
}
// console.log(options)
// 3. 添加 token 请求头标识
const userStore = useUserStore()
if (userStore.token) {
// options.header.Authorization = `Bearer ${userStore.token}`
options.header.token = userStore.token
}
}
},
}
export const requestInterceptor = {
install() {
// 拦截 request 请求
uni.addInterceptor('request', httpInterceptor)
// 拦截 uploadFile 文件上传
uni.addInterceptor('uploadFile', httpInterceptor)
},
}
+51
View File
@@ -0,0 +1,51 @@
import {useUserStore} from '@/store'
import Config from '@/config'
let loginPathPattern = ['/pages-user', '/pages/order', '/pages/invite', '/pages/scan-code']
let tabbarPaths = ['pages/index/index']
let prevPath = ''
let isDev = import.meta.env.MODE !== 'development'
// 黑登录拦截器 - (适用于大部分页面不需要登录,少部分页面需要登录)
const navigateToInterceptor = {
// 注意,这里的url是 '/' 开头的,如 '/pages/index/index',跟 'pages.json' 里面的 path 不同
invoke(result: any) {
console.log('navigateToInterceptor', result)
// if (url === prevPath) {
// return false
// }
// prevPath = url
// setTimeout(() => {
// prevPath = ''
// }, 500)
//
// const path = url.split('?')[0]
// console.log('path', path)
//
// const userStore = useUserStore()
//
// console.log(isDev)
// if (loginPathPattern.some((item) => path.includes(item))) {
// return userStore.checkLogin()
// }
// return true
},
}
const navigateBackInterceptor = {
invoke(result: any) {
console.log('navigateBackInterceptor', result)
return true
},
}
export const routeInterceptor = {
install() {
uni.addInterceptor('navigateTo', navigateToInterceptor)
uni.addInterceptor('redirectTo', navigateToInterceptor)
uni.addInterceptor('navigateBack', navigateBackInterceptor)
},
}
+67
View File
@@ -0,0 +1,67 @@
<template>
<wd-config-provider class="h-full" :themeVars="themeVars">
<view class="" v-if="!isConnected&&configStore.showNetworkAnomaly">
<navbar :fixed="false" :title="t('navbar-network-anomaly')"/>
</view>
<slot v-else/>
</wd-config-provider>
<wd-toast/>
<wd-message-box/>
</template>
<script lang="ts" setup>
import type {ConfigProviderThemeVars} from 'wot-design-uni'
import {useConfigStore} from "@/store";
const {t} = useI18n()
const configStore = useConfigStore()
const isConnected = ref(true)
const themeVars: ConfigProviderThemeVars = {
colorTheme: '#333',
buttonPrimaryBgColor: '#14181B',
}
const OnNetworkStatusChange = (res: any) => {
console.log('=>(useNetworkStatusChange)', res)
isConnected.value = !!res.isConnected
}
onLoad(() => {
uni.onNetworkStatusChange(OnNetworkStatusChange)
})
onShow(() => {
uni.getNetworkType({
success: function (res) {
console.log('networkType', res.networkType);
}
});
})
onUnload(() => {
if (configStore.showNetworkAnomaly) {
configStore.showNetworkAnomaly = false
}
uni.offNetworkStatusChange(OnNetworkStatusChange)
})
</script>
<style lang="scss">
/* #ifndef APP-NVUE */
page {
font-family: 'UberMove', sans-serif;
}
.poynter-oldstyle-text {
font-family: 'UberMove', sans-serif;
}
.helvetica-now-text {
font-family: 'UberMove', sans-serif;
}
/* #endif */
</style>
+706
View File
@@ -0,0 +1,706 @@
{
"agreement": {
"privacy-policy": "Privacy Policy",
"user-terms-conditions": "User Terms and Conditions"
},
"common": {
"addPicture": "Add Picture",
"app-marketplace": "Application Market",
"appleLoginFailed": "Apple sign-in failed. Please try again.",
"stock": "Stock",
"apply": "Apply",
"balance": "Balance",
"buildingType": "Building Type",
"cancel": "Cancel",
"close": "Close",
"comment": "Comment",
"confirm": "Confirm",
"continue": "Continue",
"delete": "Delete",
"distance": "Distance",
"email": "Email",
"empty-view-text": "No content yet",
"enter": "Enter",
"enterPassword": "Enter Password",
"evaluate": "Evaluate",
"expireTime": "Expire Time",
"failure": "Build failed",
"go-to-settings": "Go to Settings",
"goPay": "Go Pay",
"goSettle": "Go Settle",
"google-map": "Google Maps",
"googleLoginFailed": "Google sign-in failed. Please try again.",
"gotIt": "Got it",
"loading": "Loading",
"mile": "mile",
"minutes": "minutes",
"no": "No",
"obtain": "get",
"operation-success": "Operation successful",
"or": "OR",
"photograph": "photo",
"copyLink": "Copy Link",
"placeholder": {
"pleaseEnter": "Please enter",
"pleaseSelect": "Please select"
},
"please-enter-your-payment-password": "Please enter your payment password",
"pleaseEnterDeliveryReview": "Please enter delivery review content",
"pleaseEnterDishReview": "Please enter dish review content",
"pleaseLogin": "Please login",
"pleaseRateDelivery": "Please rate the delivery driver",
"pleaseRateDish": "Please rate the dish",
"point": "point",
"prompt": {
"authentication-failed-please-log-in": "Authentication failed. Please log in again",
"download-failed": "Download failed",
"getLocationFailed": "Failed to obtain location, please try again later",
"installed-specified-app": "Please select Map App",
"login-expired-please-log-in-again": "Your login has expired. Please log in again",
"not-setup-payment-password": "Have you not set a payment password yet?",
"phone-number-empty": "Number does not exist",
"picture-wrong-please-try-again": "Please select a file and try again",
"please-authorize-location-information": "Please authorize location information",
"please-carefully-read-and-agree": "Please carefully read and agree",
"please-enter-keyword-search": "Please enter a keyword to search",
"request-failed-please-try-again-later": "Network or server error please wait.",
"request-incorrect": "Request failed",
"save-failed": "Save failed",
"save-successfully": "Save successfully",
"system-prompt": "System prompts",
"system-prompt-delete": "Are you sure to delete it?",
"up-cross": "Loading...",
"up-failed": "Upload timed out, please try again later",
"update-failed": "Update failed",
"update-successfully": "Update successful",
"replication-successful": "Copy successful",
"claimCouponSuccessfully": "Coupon claimed successfully",
"stockInsufficient": "Stock insufficient"
},
"reEdit": "Re edit",
"recharge": "Recharge",
"remove": "Remove",
"reply": "Reply",
"reset": "Reset",
"sales": "Total sales",
"save": "Save",
"saveAndContinue": "Save and continue",
"search": "Search",
"select": "Select",
"select-from-album": "Select from mobile phone album",
"select-maps-app": "Select a map application",
"send": "Send",
"skip": "Skip",
"state": "State",
"submit": "Submit",
"unknownUser": "Unknown User",
"useCurrentLocation": "Use current location",
"useMyCurrentLocation": "use my current location",
"verification-code-sent-successfully": "Verification code sent successfully",
"yes": "Yes"
},
"components": {
"filtrate-tool": {
"discount": "Discount",
"over": "Over",
"pickup": "Pickup",
"price": "Price",
"score": "Score"
},
"noOpen": {
"title": "The cold chain function has not been opened yet, waiting to be opened later."
},
"placeholder": "Please enter",
"search": {
"placeholder": "Search CHEFLINK"
},
"searchSort": {
"comment": "Comment",
"thumbsUp": "Collection",
"time": "Time",
"title": "Sort",
"view": "Page view ranking"
},
"visit": {
"leaveItToMePersonally": "Leave it to me personally",
"putItAtTheDoor": "Put it at the door"
}
},
"navbar-change-password": "Change password",
"navbar-change-payment-password": "Modify payment password",
"navbar-customer-service": "Customer Service",
"navbar-forget-password": "Forgot password",
"navbar-forget-payment-password": "Forgot your payment password",
"navbar-invited-person": "My invitation",
"navbar-nickname": "Name",
"navbar-personal-information": "personal information",
"navbar-set-payment-password": "Set a payment password",
"navbar-settings": "Settings",
"orderStatus": {
"delivered": "Delivered",
"delivering": "Delivering",
"ordered": "Ordered",
"paid": "Paid",
"received": "Received"
},
"pages": {
"address": {
"apartment": {
"titleApartment": "Apartment/Unit/Floor (required)",
"titleApartmentTips": "e.g.1208",
"titleBuilding": "Building Name",
"titleBuildingTips": "e.g.Central Building",
"titleEntry": "Entry Code",
"titleEntryTips": "e.g.10150#"
},
"appTime": "Appointment delivery",
"appointmentTime": "Appointment time",
"choose-type": {
"apartment": "Apartment",
"apartmentDescription": "Multi-unit residential building",
"description": "Please inform us of your building type so that we can improve delivery accuracy.",
"hotel": "Hotel",
"hotelDescription": "Temporary residence, motel or resort",
"house": "House",
"houseDescription": "Single or multi- family home",
"navTitle": "Building Type",
"office": "Office",
"officeDescription": "Workplaces with entry restrictions",
"other": "Other",
"otherDescription": "Hospitals, parks, outdoors, etc",
"title": "Select building type"
},
"deliveryInstructions": "Delivery Instructions",
"deliveryPointInfo": "Delivery Point Information",
"hotel": {
"titleHotel": "Hotel Name (required)",
"titleHotelTips": "e.g.orange hotel",
"titleRoom": "Room/Floor",
"titleRoomTips": "e.g.1808"
},
"house": {
"tips": "e.g.house number or name"
},
"immediateDelivery": "Immediate delivery",
"moreOptions": "More options",
"office": {
"titleCorporate": "Corporate Name (required)",
"titleCorporateTips": "e.g.XXX Technology Co., Ltd",
"titleSuite": "Suite/Floor",
"titleSuiteTips": "e.g.18th floor"
},
"other": {
"titleApartment": "Apartment/Suite/Floor",
"titleApartmentTips": "e.g.orange hotel",
"titleCompany": "Company/Building Name",
"titleCompanyTips": "e.g.1808"
},
"otherDetails": "Other Details",
"pleaseTip": "e.g.don't knock on the door, don't ring the doorbell",
"reservation": "Reservation",
"reservationTime": {
"currentTimeExpired": "Current time has passed, automatically selected next business day",
"dateNotSelectable": "This date is not selectable",
"noAvailableTime": "No available time for current date, automatically selected next business day",
"notAvailable": "Not available",
"reservationSuccess": "Reservation successful",
"selectTimeSlot": "Please select a time slot"
},
"savedAddresses": "Saved addresses",
"title": "Address",
"titleDetail": "Address Details"
},
"browse": {
"titleCuisine": "Nearby Cuisine",
"titleRecipes": "Selected Recipes"
},
"home": {
"all-merchants": "All merchants",
"default-location": "Select the address",
"deliveryFee": "Shipping Fee",
"featured-on": "Featured on CHEFLINK",
"featured-dishes": "Featured Dishes",
"nearby-merchants": "Nearby Merchants"
},
"mine": {
"activity-description": "Activity description",
"activity-description-tip": "Share the QR code and invite new users to register successfully. Successful invitation",
"collection": "Collection",
"complaintsAndSuggestions": "Complaints and suggestions",
"customer-service-phone": "Customer service phone",
"dial": "Dial",
"discount": "Discount",
"help": "Help",
"invitation-code": "Invitation code",
"invite-friends": "Invite friends",
"inviteFriends": "Invite Friends",
"join": "Join",
"log-out-successfully": "Log out successfully",
"login-out-tip": "Are you sure you want to log out of your current login account?",
"member-desc": "Unlock members to enjoy more discounts and promotions.",
"member-title": "Free trial of CHEFLINK",
"order": "Order",
"platformAgreement": "Platform Agreement",
"privacyPolicy": "Privacy Policy",
"save-picture": "Save Image",
"set": "Set",
"storeSettled": "Store Settled",
"support": "Support",
"the-person-invited": "My invitation",
"wallet": "Wallet",
"work-time": "Work time",
"my-invitations": "My Invitations"
},
"order": {
"DEL": "DEL",
"PU": "PU",
"accept": "ACPT",
"all": "ALL",
"completed": "DCOMP",
"confirmReady": "CONF/READY",
"onTheWay": "OTW",
"title": "History"
},
"search": {
"hot-title": "Popular Categories",
"recently": "Recently",
"result": {
"food": "Food",
"recipe": "Recipe",
"result": "Result"
}
},
"shop": {
"description": "This feature is not yet available, please wait for it to be released later.",
"tips": "Tips"
}
},
"pages-login": {
"and": "and",
"choose-language": {
"confirm": "Confirm language",
"success": "Language set successfully",
"tip": "You can change this setting later in the 'My' section of the app",
"title": "Hello, welcome to log in"
},
"continuing-agree": "I have read it carefully and agreed",
"forget-password": {
"description": "Set a new password for Sign In",
"newPassword": "New password"
},
"guide-page": {
"location": {
"allowLocationAccess": "Allow Location Access",
"description": "We need to know your location in order to suggest nearby services.",
"selectLocation": "Select location",
"title": "What is Your Location?"
},
"notice": {
"activateNotification": "Activate notification",
"continueLater": "Continue later",
"description": "We'll update you on order statuses, limited-time deals, local store events and more.",
"title": "Stay in the know"
},
"welcome": {
"description": "Account created successfully.",
"next": "Next",
"title": "Hello"
}
},
"index": {
"apple-login": "Continue with Apple",
"description": "Create an account or log in to book and manage your appointments",
"facebook-login": "Continue with Facebook",
"google-login": "Continue with Google",
"input-placeholder": "Email Address/Phone",
"prompt": {
"confirm-email-verify": "The email address was inconsistent twice",
"email-address-verify": "Please enter the correct email address",
"first-name": "Please enter a name",
"last-name": "Please enter your last name",
"password": "Please enter your password",
"phone-number": "Please enter your phone number",
"phone-number-verify": "Please enter the correct mobile phone number"
},
"title": "Log in or sign up",
"wechat-login": "Continue with Wechat"
},
"login": {
"description": "Enter password to Sign In",
"forgotPassword": "Forgot password",
"title": "Sign In"
},
"login-successfully": "Login successfully",
"prompt": {
"first-name": "Please enter last name",
"last-name": "Please enter first name"
},
"sign-up": {
"confirm-email": "Confirm email",
"first-name": "First name",
"last-name": "Last name",
"password": "Password",
"phone-number": "Phone number",
"register-success": "Registration successful",
"title": "Sign Up"
},
"verify-code": {
"changeOne": "Change one",
"description": "For security reasons, please type characters to continue",
"title": "verification code"
}
},
"pages-store": {
"checkout": {
"addAddress": "Add address",
"addCoupon": "Add coupon",
"appointmentDelivery": "Appointment delivery",
"appointmentPickup": "Appointment pickup",
"chooseTime": "Choose the time",
"chooseTips": "Choose tips",
"contactPhone": "Contact phone",
"deliveryPreference": "Delivery preference",
"deliveryTime": "Delivery time",
"distance": "Distance",
"enableLocationForDistance": "Please enable location to get distance",
"immediateDelivery": "Immediate delivery",
"immediatePickup": "Immediate pickup",
"no": "No",
"notActivated": "Not activated",
"orderInfoSummary": "Order Information Summary",
"other": "Other",
"paymentSuccess": "Payment successful",
"pickupTime": "Pickup time",
"pleaseSelectAddress": "Please select address",
"pleaseSelectCreditCard": "Please select credit card",
"priceDetail": {
"desc": "These fees are only due to factors such as the number of shopping carts, and are used to pay for expenses related to your order, including platform services and delivery services.",
"memberDiscount": "Member discount",
"serviceFees": "Service fees and other expenses",
"taxation": "Taxation",
"title": "What content is included"
},
"tips": "Is weekly delivery available",
"tipsDesc": "Free delivery fee will be given for weekly delivery fees over 30, and delivery fees will be charged for orders below 30",
"title": "Checkout",
"yes": "Yes"
},
"order": {
"acceptanceTime": "Order acceptance time",
"autoCancellation": "Automatic cancellation after 30 minutes",
"cancellationReason": "Cancellation reason",
"cancellationReasonDesc": "30 minutes without processing automatically agreed by the system.",
"cancellationTime": "Cancellation time",
"cancellationTitle": "Merchant processing in progress",
"cancelled": "Cancelled",
"deliveryAddress": "Delivery address",
"deliveryPhotos": "Delivery of photos",
"deliveryTime": "Delivery time",
"estimatedDeliveryTime": "Estimated delivery time",
"orderInfo": "Ordering Information",
"orderNumber": "Order number",
"orderStatus": {
"agree": "The merchant has agreed to a refund",
"agreeRefund": "Refund agreed",
"cancelled": "Cancelled",
"completed": "Completed",
"delivered": "Delivered",
"delivering": "Delivering",
"hasPendingPayment": "Waiting for orders",
"merchantRejected": "Merchant Rejected",
"merchantRejectedDesc": "Merchant has rejected the order",
"ordered": "Ordered",
"paid": "Paid",
"pending": "Waiting for the merchant to process",
"pendingPayment": "Pending payment",
"ready": "Ready",
"received": "Received",
"refund": "Refund in progress",
"rejectRefund": "Refund Rejected"
},
"orderTime": "Order time",
"rejectReason": "The reason why the merchant refused",
"subtotal": "Subtotal",
"taxesAndOtherFees": "Taxes and other fees",
"total": "Total",
"upTime": "Estimated self-pickup time",
"useCode": "Use code",
"writeOff": "Meal Code"
},
"store": {
"addToCart": "Add to cart",
"appetizers": "Appetizers",
"merchantDiscounts": "Merchant discounts",
"claimNow": "Claim now",
"claimed": "Claimed",
"claimCoupon": "Claim coupon",
"couponOff": "Off",
"validDays": "Valid Days",
"days": "days",
"get": "Get",
"congratulations": "Congratulations",
"tips-1": "Order With Coupons",
"tips-2": "for Better Discounts",
"tips-3": "Tap Get",
"areaCode": {
"singapore": "Singapore"
},
"cancelOrder": {
"dontWant": "I dont want it anymore",
"forgotCoupon": "Forgot to use coupon",
"informationError": "Information filling error",
"wrongLess": "Wrong/Less"
},
"choose": "Please choose",
"delivery": "Delivery",
"deliveryTags": {
"carefulPackaging": "Careful packaging",
"fastDelivery": "Fast delivery",
"goodAttitude": "Good attitude",
"onTime": "On time",
"polite": "Polite",
"professional": "Professional"
},
"discount": "discount",
"earTime": "Earliest delivery time",
"members": "Member",
"miles": "miles",
"orderStatus": {
"delivered": "Delivered",
"delivering": "Delivering",
"ordered": "Ordered",
"paid": "Paid",
"received": "Received",
"rejected": "Rejected"
},
"pickup": "Pickup",
"pickupAddress": "Meal pickup address",
"pickupTime": "Meal pickup time",
"recommend": "Recommended for you",
"required": "Required",
"sales": "Sales",
"securityCode": {
"cantSee": "Can't see clearly? Change one",
"confirm": "Confirm"
},
"shareFunctionality": "Share functionality",
"tips": "We will be closed in",
"tips1": "minutes. Order now or consider other options.",
"tips2": "people have reordered",
"tips3": "membership to enjoy a ",
"tips4": "Enjoy delivery service over",
"tips5": "Delivery fee",
"title": "The minimum order amount for this store is",
"toast": {
"deliveryService": "This merchant does not provide delivery service",
"selfPickup": "This merchant does not provide pickup service"
},
"today": "Today",
"tomorrow": "Tomorrow",
"use": "Use",
"weekdays": {
"friday": "FRIDAY",
"monday": "MONDAY",
"saturday": "SATURDAY",
"sunday": "SUNDAY",
"thursday": "THURSDAY",
"tuesday": "TUESDAY",
"wednesday": "WEDNESDAY"
}
},
"view-reviews": {
"Score": "score",
"deliveryDriver": "Delivery Driver",
"goods": "Goods",
"viewTitle": "View reviews"
}
},
"pages-user": {
"balance": {
"all": "All",
"detail-list": "Detail list",
"month": "Month",
"month-filter": "Month filter",
"my-balance": "My balance",
"title": "Wallet",
"type": "Type",
"year": "Year",
"year-month-select": "Year month select"
},
"card": {
"add": "+ Add",
"desc": "After binding the card, payment can be made quickly and directly.",
"listCard": "Credit card list",
"title": "Bind the card first and make the payment later"
},
"cart": {
"addItems": "Add Items",
"addNote": "Add a note",
"items": "Items",
"requestTableware": "Request tableware, etc",
"title": "Cart",
"totalPrice": "Total price",
"viewCart": "View Shopping Cart",
"viewStore": "View Store"
},
"choosePaymethod": {
"creditCard": "Credit card payment",
"replace": "Replace",
"title": "Choose payment method",
"wallet": "Wallet payment"
},
"complaints": {
"contact-information": "Contact information",
"contact-information-tip": "Your contact information helps us communicate and solve problems, only visible to staff",
"description": "You are welcome to give us feedback on the use of our products and suggestions.",
"feedback-content": "Feedback content",
"feedback-content-placeholder": "Please fill in the feedback content",
"image": "Image",
"title": "Complaints and suggestions",
"validation": {
"contact-phone-invalid": "Please fill in the correct contact phone",
"contact-phone-required": "Please fill in the contact phone",
"content-max-length": "Feedback content cannot exceed 500 characters",
"content-min-length": "Feedback content must be at least 10 characters",
"content-required": "Please fill in the feedback content"
}
},
"coupon": {
"all-merchants": "Applicable to all merchants",
"expiry-date": "Expiry date: ",
"merchant-specific": "For specific merchant use",
"no-coupons": "You currently do not have any coupons",
"redeem-now": "Redeem now",
"search-placeholder": "Enter discount code",
"title": "Coupons"
},
"member": {
"annually": "Annually",
"autoSubscribe": "Auto subscribe",
"btn-text": "Subscribe to",
"creditCard": "credit card",
"desc": "It can save an additional $23.88 per year.",
"free": "free",
"free-trial": "Free trial",
"freeTrial": "Free trial weeks",
"join": "Join",
"month": "month",
"monthly": "Monthly",
"payTime": "Time of payment",
"paymentMethod": "Payment method",
"renewalNow": "Renewal now",
"weeks": "weeks",
"billedAnnually": "Billed at ${amount}/year",
"savingsPerYear": "It can save an additional ${amount} per year."
},
"member-center": {
"chooseLike": "Choose 3 items you like",
"likeDescription": "This information is used to provide you with a personalized service experience",
"notCurrentlyTrying": "Not currently trying CHEFLINK",
"subscribe": "Subscribe to CHEFLINK",
"temporarilySkip": "Temporarily skip",
"title": "Member Center",
"youWillMissOut": "You will miss out on discount offers for eligible meals and fresh meat orders."
},
"message": {
"readAll": "One-click read",
"title": "messages"
},
"password": {
"change-password-successfully": "Change password successfully",
"forget-password-successfully": "Forget password successfully"
},
"pay-password": {
"change-payment-password-successfully": "Modify payment password successfully",
"enter-6-digit-password": "Please enter a 6-digit password",
"forget-payment-password-successfully": "Forgot your payment password successfully",
"input-placeholder": {
"enter-new-password": "Please enter a new password",
"enter-new-password-again": "Please enter the new password again",
"enter-old-password": "Please enter the original password",
"enter-phone-number": "Please enter your mobile phone number",
"enter-verification-code": "Please enter the verification code"
},
"set-payment-password-successfully": "Setting the payment password successfully",
"two-passwords-inconsistent": "The password is inconsistent when entered twice"
},
"recharge": {
"amount": "Recharge amount",
"amount-invalid": "The recharge amount cannot be less than 0",
"description": "Please enter your recharge amount",
"pay-method": "Please select a payment method",
"success": "Recharge successful",
"title": "Recharge Balance"
},
"recipe": {
"collapseReply": "Collapse reply content",
"desc": "If you like it, give it a thumbs up",
"expandReply": "Expand reply",
"replyTo": "Reply to",
"title": "Recipe Details"
},
"setting": {
"cancelAccount": "Cancel account",
"cancelAccountConfirm": "Are you sure you want to cancel your account? All your data will be deleted and cannot be recovered.",
"cancelAccountSuccess": "Account cancellation successful",
"language": "Language switching",
"logout": "Logout",
"modification": "Change password",
"payPwd": "Payment password"
},
"store-settle-in": {
"address": "Address",
"audit": {
"pass": "Successful review",
"passDesc": "Congratulations on your successful entry into the platform!",
"passDesc1": "Come and upload your product information to start your money-making journey",
"reject": "Review failed",
"rejectDesc": "Reasons for failure",
"submitted": "Under review",
"submittedDesc": "The platform is under review. Please wait patiently..."
},
"businessLicense": "Business license",
"category": "Type",
"desc": "Just fill in the information and start your money-making journey now",
"detailedAddress": "Detailed address",
"idCardFront": "Picture of ID card",
"introduction": "Introduction",
"schema": {
"address": "Please select the address",
"businessLicense": "请上传营业执照",
"category": "Please select your type",
"description": "Please enter introduction",
"detailAddress": "Please enter detailed address",
"detailedAddress": "Please enter the detailed address",
"idCardBack": "请上传身份证背面",
"idCardFront": "请上传身份证正面",
"introduction": "Please enter the introduction",
"storeName": "Please enter the store name"
},
"storeName": "Shop name",
"title": "Shop entry"
},
"user-info": {
"head-portrait": "Head portrait",
"nickname": "Nickname",
"view-larger-image": "View larger image"
}
},
"pickupAddress": "address",
"tabBar": {
"browse": "Browse",
"global": "Global+",
"home": "Home",
"mine": "Profile",
"order": "History"
},
"toast": {
"addCartSuccess": "Add to cart successfully",
"commentSuccess": "Comment successfully",
"deleteSuccess": "Delete successfully",
"likeTips": "You can only select up to 3",
"orderNumberCopied": "Copy successfully",
"redemptionSuccessful": "Redemption successful",
"submitSuccess": "Submit success"
}
}
+18
View File
@@ -0,0 +1,18 @@
import {createI18n} from 'vue-i18n'// v9.x
import en from './en.json'
import zhHans from './zh-Hans.json'
import Config from "@/config";
const localeLanguage = uni.getLocale() || Config.defaultLanguage
const i18n = createI18n({
legacy: false,
locale: localeLanguage,// 获取已设置的语言
fallbackLocale: Config.defaultLanguage,
messages:{
en,
'zh-Hans': zhHans,
}
})
export {i18n}
+706
View File
@@ -0,0 +1,706 @@
{
"agreement": {
"privacy-policy": "隐私政策",
"user-terms-conditions": "用户须知及使用协议"
},
"common": {
"addPicture": "添加图片",
"app-marketplace": "应用市场",
"appleLoginFailed": "苹果登录授权失败,请重试",
"stock": "库存",
"apply": "确认",
"balance": "余额",
"buildingType": "建筑类型",
"cancel": "取消",
"close": "关闭",
"comment": "评论",
"confirm": "确认",
"continue": "继续",
"delete": "删除",
"distance": "距您",
"email": "邮箱",
"empty-view-text": "暂无内容",
"enter": "输入",
"enterPassword": "输入密码",
"evaluate": "评价",
"expireTime": "到期时间",
"failure": "生成失败",
"go-to-settings": "去设置",
"goPay": "去支付",
"goSettle": "去结算",
"google-map": "谷歌地图",
"googleLoginFailed": "谷歌登录授权失败,请重试",
"gotIt": "明白了",
"loading": "加载中",
"mile": "英里",
"minutes": "分钟",
"no": "否",
"obtain": "获取",
"operation-success": "操作成功",
"or": "或",
"photograph": "拍照",
"copyLink": "复制链接",
"placeholder": {
"pleaseEnter": "请输入",
"pleaseSelect": "请选择"
},
"please-enter-your-payment-password": "请输入支付密码",
"pleaseEnterDeliveryReview": "请输入配送员评价内容",
"pleaseEnterDishReview": "请输入菜品评价内容",
"pleaseLogin": "请登录",
"pleaseRateDelivery": "请为配送员评分",
"pleaseRateDish": "请为菜品评分",
"point": "分",
"prompt": {
"authentication-failed-please-log-in": "认证失败,请重新登录",
"download-failed": "下载失败",
"getLocationFailed": "获取定位失败,请稍后重试",
"installed-specified-app": "请选择地图App",
"login-expired-please-log-in-again": "登录过期,请重新登录",
"not-setup-payment-password": "您尚未设置支付密码?",
"phone-number-empty": "号码不存在",
"picture-wrong-please-try-again": "请选择一个文件后重试",
"please-authorize-location-information": "请授权位置信息",
"please-carefully-read-and-agree": "请已仔细阅读并同意",
"please-enter-keyword-search": "请输入搜索关键词",
"request-failed-please-try-again-later": "网络或服务器错误请稍后",
"request-incorrect": "请求失败",
"save-failed": "保存失败",
"save-successfully": "保存成功",
"system-prompt": "系统提示",
"system-prompt-delete": "是否确认删除?",
"up-cross": "上传中...",
"up-failed": "上传超时,请稍后重试",
"update-failed": "更新失败",
"update-successfully": "更新成功",
"replication-successful": "复制成功",
"claimCouponSuccessfully": "领取成功",
"stockInsufficient": "库存不足"
},
"reEdit": "重新编辑",
"recharge": "充值",
"remove": "删除",
"reply": "回复",
"reset": "重置",
"sales": "总销量",
"save": "保存",
"saveAndContinue": "保存并继续",
"search": "搜索",
"select": "选择",
"select-from-album": "从相册选择",
"select-maps-app": "选择地图应用",
"send": "发送",
"skip": "跳过",
"state": "州",
"submit": "提交",
"unknownUser": "未知用户",
"useCurrentLocation": "使用当前位置",
"useMyCurrentLocation": "使用我的当前位置",
"verification-code-sent-successfully": "验证码发送成功",
"yes": "是"
},
"components": {
"filtrate-tool": {
"discount": "折扣",
"over": "最低",
"pickup": "自取",
"price": "价格",
"score": "评分"
},
"noOpen": {
"title": "冷链功能尚未开启,等待稍后开启。"
},
"placeholder": "请输入",
"search": {
"placeholder": "搜索 CHEFLINK"
},
"searchSort": {
"comment": "评论排序",
"thumbsUp": "收藏量排序",
"time": "时间排序",
"title": "排序",
"view": "浏览量排序"
},
"visit": {
"leaveItToMePersonally": "让我自己来决定",
"putItAtTheDoor": "放在门口"
}
},
"navbar-change-password": "修改密码",
"navbar-change-payment-password": "修改支付密码",
"navbar-customer-service": "客服中心",
"navbar-forget-password": "忘记密码",
"navbar-forget-payment-password": "忘记支付密码",
"navbar-invited-person": "我的邀请",
"navbar-nickname": "昵称",
"navbar-personal-information": "个人信息",
"navbar-set-payment-password": "设置支付密码",
"navbar-settings": "设置",
"orderStatus": {
"delivered": "已送达",
"delivering": "配送中",
"ordered": "已下单",
"paid": "已支付",
"received": "已接收"
},
"pages": {
"address": {
"apartment": {
"titleApartment": "公寓/单元/楼层(必填)",
"titleApartmentTips": "请输入",
"titleBuilding": "建筑物名称",
"titleBuildingTips": "请输入",
"titleEntry": "进入代码",
"titleEntryTips": "请输入"
},
"appTime": "预约派送时间",
"appointmentTime": "预约时间",
"choose-type": {
"apartment": "公寓",
"apartmentDescription": "多单元住宅",
"description": "请告知我们您的建筑类型,以便我们提高配送准确性。",
"hotel": "酒店",
"hotelDescription": "临时住宅、酒店或度假村",
"house": "住宅",
"houseDescription": "单户或多户住宅",
"navTitle": "建筑类型",
"office": "办公室",
"officeDescription": "有进入限制的工作场所",
"other": "其他",
"otherDescription": "医院、公园、户外等",
"title": "选择建筑类型"
},
"deliveryInstructions": "配送说明",
"deliveryPointInfo": "交货点信息",
"hotel": {
"titleHotel": "酒店名称(必填)",
"titleHotelTips": "请输入",
"titleRoom": "房间/楼层",
"titleRoomTips": "请输入"
},
"house": {
"tips": "例如门牌号或名字"
},
"immediateDelivery": "立即派送",
"moreOptions": "更多选项",
"office": {
"titleCorporate": "公司名称(必填)",
"titleCorporateTips": "请输入",
"titleSuite": "套房/楼层",
"titleSuiteTips": "请输入"
},
"other": {
"titleApartment": "公寓/套房/楼层",
"titleApartmentTips": "请输入",
"titleCompany": "公司/建筑物名称",
"titleCompanyTips": "请输入"
},
"otherDetails": "其他信息",
"pleaseTip": "如不要敲门,不要按门铃",
"reservation": "预约",
"reservationTime": {
"currentTimeExpired": "当前时间段已过,已自动选择下一个营业日",
"dateNotSelectable": "该日期不可选择",
"noAvailableTime": "当前日期无可用时间,已自动选择下一个营业日",
"notAvailable": "不营业",
"reservationSuccess": "预约成功",
"selectTimeSlot": "请选择时间段"
},
"savedAddresses": "保存的地址",
"title": "地址管理",
"titleDetail": "地址详情"
},
"browse": {
"titleCuisine": "附近的美食",
"titleRecipes": "选择食谱"
},
"home": {
"all-merchants": "所有商家",
"default-location": "请选择地址",
"deliveryFee": "配送费",
"featured-on": "精选商家",
"featured-dishes": "精选菜品",
"nearby-merchants": "附近商家"
},
"mine": {
"activity-description": "活动说明:",
"activity-description-tip": "分享专属二维码邀请新用户注册即邀请成功。一起来注册体验点餐功能。",
"collection": "收藏",
"complaintsAndSuggestions": "投诉建议",
"customer-service-phone": "客服电话",
"dial": "拨号",
"discount": "优惠",
"help": "帮助",
"invitation-code": "邀请码",
"invite-friends": "邀请好友",
"inviteFriends": "邀请好友",
"join": "加入",
"log-out-successfully": "退出登录成功",
"login-out-tip": "是否确定退出当前登录账号",
"member-desc": "解锁会员,享受更多折扣和促销活动。",
"member-title": "CHEFLINK 免费试用",
"order": "订单",
"platformAgreement": "平台协议",
"privacyPolicy": "隐私政策",
"save-picture": "保存图片",
"set": "设置",
"storeSettled": "店铺入驻",
"support": "客服",
"the-person-invited": "我的邀请",
"wallet": "钱包",
"work-time": "工作时间",
"my-invitations": "我的邀请"
},
"order": {
"DEL": "配送",
"PU": "自取",
"accept": "接受",
"all": "全部",
"completed": "已完成",
"confirmReady": "确认/已准备",
"onTheWay": "在路上",
"title": "订单"
},
"search": {
"hot-title": "热搜",
"recently": "历史记录",
"result": {
"food": "美食",
"recipe": "菜谱",
"result": "条结果"
}
},
"shop": {
"description": "此功能暂未开放,请稍后体验",
"tips": "提示"
}
},
"pages-login": {
"and": "和",
"choose-language": {
"confirm": "确认语言",
"success": "语言设置成功",
"tip": "您可以在应用程序的'我的'部分稍后更改此设置",
"title": "您好,欢迎登录"
},
"continuing-agree": "我已认真阅读并同意",
"forget-password": {
"description": "设置新密码",
"newPassword": "新密码"
},
"guide-page": {
"location": {
"allowLocationAccess": "允许位置访问",
"description": "我们需要知道您的位置,以便为您提供附近的服务。",
"selectLocation": "选择位置",
"title": "您的位置是什么?"
},
"notice": {
"activateNotification": "激活通知",
"continueLater": "继续稍后",
"description": "我们将更新您的订单状态、限时优惠、本地商店活动等信息。",
"title": "保持了解"
},
"welcome": {
"description": "账户创建成功。",
"next": "下一步",
"title": "你好"
}
},
"index": {
"apple-login": "苹果登录",
"description": "创建账户或登录以预订和管理您的预约",
"facebook-login": "Facebook登录",
"google-login": "Google登录",
"input-placeholder": "邮箱地址/手机号",
"prompt": {
"confirm-email-verify": "两次邮箱不一致",
"email-address-verify": "请输入正确的邮箱地址",
"first-name": "请输入名字",
"last-name": "请输入姓氏",
"password": "请输入密码",
"phone-number": "请输入电话号码",
"phone-number-verify": "请输入正确的手机号"
},
"title": "登录或注册",
"wechat-login": "微信登录"
},
"login": {
"description": "输入密码以继续",
"forgotPassword": "忘记密码",
"title": "登录"
},
"login-successfully": "登录成功",
"prompt": {
"first-name": "请输入姓",
"last-name": "请输入名"
},
"sign-up": {
"confirm-email": "确认邮箱",
"first-name": "名",
"last-name": "姓",
"password": "密码",
"phone-number": "手机号",
"register-success": "注册成功",
"title": "注册"
},
"verify-code": {
"changeOne": "换一个",
"description": "出于安全原因,请键入验证码继续",
"title": "验证码"
}
},
"pages-store": {
"checkout": {
"addAddress": "新增地址",
"addCoupon": "添加优惠券",
"appointmentDelivery": "预约配送",
"appointmentPickup": "预约自取",
"chooseTime": "选择时间",
"chooseTips": "选择小费",
"contactPhone": "联系电话",
"deliveryPreference": "配送偏好",
"deliveryTime": "配送时间",
"distance": "距离",
"enableLocationForDistance": "请开启定位获取距离",
"immediateDelivery": "立即配送",
"immediatePickup": "立即自取",
"no": "否",
"notActivated": "商家暂未开通服务",
"orderInfoSummary": "订单信息摘要",
"other": "其他",
"paymentSuccess": "支付成功",
"pickupTime": "自取时间",
"pleaseSelectAddress": "请选择地址",
"pleaseSelectCreditCard": "请选择信用卡",
"priceDetail": {
"desc": "这些费用仅基于购物车数量等因素,并用于支付与您的订单相关的费用,包括平台服务和配送服务。",
"memberDiscount": "会员折扣",
"serviceFees": "服务费及其他费用",
"taxation": "税收",
"title": "包含哪些内容"
},
"tips": "是否需要每周送货",
"tipsDesc": "如您购买周配送服务,则本周30磅以上可以免配送费,30磅以下仍需要付配送费,不购买则按正常配送费计算",
"title": "结账",
"yes": "是"
},
"order": {
"acceptanceTime": "订单接受时间",
"autoCancellation": "30分钟后自动取消",
"cancellationReason": "取消原因",
"cancellationReasonDesc": "30分钟不处理,系统自动同意",
"cancellationTime": "取消时间",
"cancellationTitle": "商户处理中",
"cancelled": "已取消",
"deliveryAddress": "配送地址",
"deliveryPhotos": "送达照片",
"deliveryTime": "送达时间",
"estimatedDeliveryTime": "预计送达时间",
"orderInfo": "订单信息",
"orderNumber": "订单号",
"orderStatus": {
"agree": "商家已同意退款",
"agreeRefund": "已同意退款",
"cancelled": "已取消",
"completed": "已核销",
"delivered": "已完成",
"delivering": "配送中",
"hasPendingPayment": "待接单",
"merchantRejected": "已拒绝",
"merchantRejectedDesc": "商家已拒绝接单",
"ordered": "已下单",
"paid": "已付款",
"pending": "等待商家处理",
"pendingPayment": "待付款",
"ready": "已准备",
"received": "已接单",
"refund": "退款中",
"rejectRefund": "已拒绝退款"
},
"orderTime": "下单时间",
"rejectReason": "商家拒绝原因",
"subtotal": "小计",
"taxesAndOtherFees": "税金和其他费用",
"total": "总计",
"upTime": "预计自取时间",
"useCode": "核销码",
"writeOff": "核销"
},
"store": {
"addToCart": "加入购物车",
"appetizers": "开胃菜",
"merchantDiscounts": "商家折扣",
"claimNow": "立即领取",
"claimed": "已领取",
"claimCoupon": "领取优惠券",
"couponOff": "优惠",
"validDays": "有效天数",
"days": "天",
"get": "获取",
"congratulations": "恭喜!",
"tips-1": "使用优惠券下单",
"tips-2": "享受更多折扣",
"tips-3": "轻点获取",
"areaCode": {
"singapore": "新加坡"
},
"cancelOrder": {
"dontWant": "我不想要了",
"forgotCoupon": "忘记使用优惠券",
"informationError": "信息填写错误",
"wrongLess": "错误/缺少"
},
"choose": "请选择",
"delivery": "配送",
"deliveryTags": {
"carefulPackaging": "包装仔细",
"fastDelivery": "配送快速",
"goodAttitude": "态度良好",
"onTime": "准时送达",
"polite": "礼貌",
"professional": "专业"
},
"discount": "折扣",
"earTime": "最早交货时间",
"members": "会员",
"miles": "英里",
"orderStatus": {
"delivered": "已送达",
"delivering": "配送中",
"ordered": "已下单",
"paid": "已支付",
"received": "已接单",
"rejected": "商家拒绝"
},
"pickup": "取货",
"pickupAddress": "取餐地址",
"pickupTime": "取餐时间",
"recommend": "推荐给您",
"required": "必选项",
"sales": "销量",
"securityCode": {
"cantSee": "看不清?换一张",
"confirm": "确定"
},
"shareFunctionality": "分享功能",
"tips": "我们将在",
"tips1": "分钟后关闭,您可以选择现在订购或者其他时间下单。",
"tips2": "人已复购",
"tips3": "会员可享受",
"tips4": "最低起送金额",
"tips5": "配送费",
"title": "这家店的起送金额是",
"toast": {
"deliveryService": "该商家未开通配送服务",
"selfPickup": "该商家未开通取货服务"
},
"today": "今天",
"tomorrow": "明天",
"use": "使用",
"weekdays": {
"friday": "星期五",
"monday": "星期一",
"saturday": "星期六",
"sunday": "星期日",
"thursday": "星期四",
"tuesday": "星期二",
"wednesday": "星期三"
}
},
"view-reviews": {
"Score": "评分",
"deliveryDriver": "配送员",
"goods": "商品",
"viewTitle": "查看评价"
}
},
"pages-user": {
"balance": {
"all": "全部",
"detail-list": "明细列表",
"month": "月",
"month-filter": "月份筛选",
"my-balance": "我的余额",
"title": "钱包",
"type": "类型",
"year": "年",
"year-month-select": "年月筛选"
},
"card": {
"add": "添加信用卡",
"desc": "卡绑定后,可以快速直接付款",
"listCard": "信用卡列表",
"title": "先绑定卡,再付款"
},
"cart": {
"addItems": "添加商品",
"addNote": "添加备注",
"items": "项",
"requestTableware": "是否需要餐具",
"title": "购物车",
"totalPrice": "总计",
"viewCart": "查看购物车",
"viewStore": "查看店铺"
},
"choosePaymethod": {
"creditCard": "信用卡支付",
"replace": "替换",
"title": "选择支付方式",
"wallet": "余额支付"
},
"complaints": {
"contact-information": "联系电话",
"contact-information-tip": "您的联系方式有助于我们沟通解决问题,仅工作人员可见",
"description": "您好,欢迎您给我们反馈产品的使用感受和建议。",
"feedback-content": "反馈内容",
"feedback-content-placeholder": "请填写意见反馈内容",
"image": "图片",
"title": "投诉建议",
"validation": {
"contact-phone-invalid": "请填写正确的联系电话",
"contact-phone-required": "请填写联系电话",
"content-max-length": "反馈内容不能超过500个字符",
"content-min-length": "反馈内容至少需要10个字符",
"content-required": "请填写反馈内容"
}
},
"coupon": {
"all-merchants": "适用于所有商户使用",
"expiry-date": "到期日:",
"merchant-specific": "指定商户使用",
"no-coupons": "您目前没有任何优惠券",
"redeem-now": "立即兑换",
"search-placeholder": "输入优惠码",
"title": "优惠券"
},
"member": {
"annually": "每年",
"autoSubscribe": "是否自动续费",
"btn-text": "订阅",
"creditCard": "添加信用卡",
"desc": "它每年可以额外节省23.88美元",
"free": "免费",
"free-trial": "免费试用",
"freeTrial": "免费试用周数",
"join": "加入",
"month": "月",
"monthly": "每月",
"payTime": "支付时间",
"paymentMethod": "支付方式",
"renewalNow": "立即加入",
"weeks": "周",
"billedAnnually": "按${amount}/年 结算",
"savingsPerYear": "它每年可以额外节省${amount}"
},
"member-center": {
"chooseLike": "选择您喜欢的3个项目",
"likeDescription": "此信息用于为您提供个性化服务体验",
"notCurrentlyTrying": "目前未尝试CHEFLINK",
"subscribe": "订阅CHEFLINK",
"temporarilySkip": "暂时跳过",
"title": "会员中心",
"youWillMissOut": "您将错过符合条件的餐品和新鲜肉类的折扣优惠。"
},
"message": {
"readAll": "一键已读",
"title": "消息"
},
"password": {
"change-password-successfully": "修改密码成功",
"forget-password-successfully": "忘记密码成功"
},
"pay-password": {
"change-payment-password-successfully": "修改支付密码成功",
"enter-6-digit-password": "请输入6位密码",
"forget-payment-password-successfully": "忘记支付密码成功",
"input-placeholder": {
"enter-new-password": "请输入新密码",
"enter-new-password-again": "请再次输入新密码",
"enter-old-password": "请输入原密码",
"enter-phone-number": "请输入手机号",
"enter-verification-code": "请输入验证码"
},
"set-payment-password-successfully": "设置支付密码成功",
"two-passwords-inconsistent": "两次输入的密码不一致"
},
"recharge": {
"amount": "充值金额",
"amount-invalid": "充值金额不能小于0",
"description": "请输入您的充值金额",
"pay-method": "请选择支付方式",
"success": "充值成功",
"title": "余额充值"
},
"recipe": {
"collapseReply": "收起回复内容",
"desc": "如果您喜欢它,请收藏它",
"expandReply": "展开回复",
"replyTo": "回复给",
"title": "菜谱详情"
},
"setting": {
"cancelAccount": "注销账号",
"cancelAccountConfirm": "您确定注销账号吗?账号注销后,您的所有数据将被删除且无法恢复。",
"cancelAccountSuccess": "注销成功",
"language": "语言切换",
"logout": "退出登录",
"modification": "修改密码",
"payPwd": "支付密码"
},
"store-settle-in": {
"address": "地址",
"audit": {
"pass": "审核成功",
"passDesc": "恭喜您成功入驻平台!",
"passDesc1": "快来上传商品信息,开启赚钱之旅吧~",
"reject": "审核失败",
"rejectDesc": "失败原因",
"submitted": "审核中",
"submittedDesc": "平台审核中,耐心等待..."
},
"businessLicense": "食品操作处理证",
"category": "所属类型",
"desc": "简单填写信息,快来开启赚钱之旅",
"detailedAddress": "详细地址",
"idCardFront": "ID/护照",
"introduction": "介绍",
"schema": {
"address": "请选择地址",
"businessLicense": "请上传食品操作处理证",
"category": "请选择所属类型",
"description": "请输入介绍",
"detailAddress": "请输入详细地址",
"detailedAddress": "请输入详细地址",
"idCardBack": "请上传护照",
"idCardFront": "请上传ID",
"introduction": "请输入介绍",
"storeName": "请输入店铺名称"
},
"storeName": "店铺名称",
"title": "店铺入驻"
},
"user-info": {
"head-portrait": "头像",
"nickname": "昵称",
"view-larger-image": "查看大图"
}
},
"pickupAddress": "地址",
"tabBar": {
"browse": "浏览",
"global": "全球购",
"home": "首页",
"mine": "我的",
"order": "订单"
},
"toast": {
"addCartSuccess": "添加购物车成功",
"commentSuccess": "评论成功",
"deleteSuccess": "删除成功",
"likeTips": "最多只能选择3个",
"orderNumberCopied": "复制成功",
"redemptionSuccessful": "兑换成功",
"submitSuccess": "提交成功"
}
}
+24
View File
@@ -0,0 +1,24 @@
import {createSSRApp} from "vue";
import App from "./App.vue";
import {Pinia, store} from '@/store'
import {installAppPlugins} from '@/plugin'
import {requestInterceptor, routeInterceptor} from "@/interceptor";
import {i18n} from "@/locale";
// #ifndef APP-NVUE
import 'virtual:uno.css'
// #endif
export function createApp() {
const app = createSSRApp(App);
app.use(store)
app.use(requestInterceptor)
app.use(routeInterceptor)
app.use(i18n)
installAppPlugins(app)
return {
app,
Pinia,
};
}
+207
View File
@@ -0,0 +1,207 @@
{
"name" : "CHEFLINK delivery",
"appid" : "__UNI__06509BE",
"description" : "",
"versionName" : "1.0.11",
"versionCode" : 111,
"transformPx" : false,
/* 5+App */
"app-plus" : {
"usingComponents" : true,
"nvueStyleCompiler" : "uni-app",
"compilerVersion" : 3,
"checkPermissionDenied" : true,
"splashscreen" : {
"alwaysShowBeforeRender" : false,
"waiting" : false,
"autoclose" : false,
"delay" : 0
},
"safearea" : {
"background" : "#fff",
"bottom" : {
"offset" : "auto"
}
},
"optimization" : {
"subPackages" : true
},
"runmode" : "liberate",
// 开启分包优化后,必须配置资源释放模式
"compatible" : {
"ignoreVersion" : true
},
"screenOrientation" : [ "portrait-primary", "landscape-primary" ],
/* */
"modules" : {
"Geolocation" : {},
"Camera" : {},
"OAuth" : {},
"Share" : {},
"Push" : {}
},
/* */
"distribute" : {
/* android */
"android" : {
"permissions" : [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>",
"<uses-permission android:name=\"android.permission.CALL_PHONE\"/>"
],
"minSdkVersion" : 28,
"targetSdkVersion" : 35,
"schemes" : "naillover",
"abiFilters" : [ "armeabi-v7a", "arm64-v8a" ],
"excludePermissions" : [
"<uses-permission android:name=\"android.permission.READ_MEDIA_IMAGES\" />",
"<uses-permission android:name=\"android.permission.READ_MEDIA_VIDEO\"/>"
]
},
/* ios */
"ios" : {
"urltypes" : "naillover",
"idfa" : false,
"dSYMs" : false,
"privacyDescription" : {
"NSPhotoLibraryUsageDescription" : "上传用户头像等",
"NSPhotoLibraryAddUsageDescription" : "保存邀请海报图片等",
"NSCameraUsageDescription" : "上传用户头像等",
"NSLocationWhenInUseUsageDescription" : "更好的为您推荐商户",
"NSLocationAlwaysUsageDescription" : "更好的为您推荐商户",
"NSLocationAlwaysAndWhenInUseUsageDescription" : "更好的为您推荐商户"
}
},
/* SDK */
"sdkConfigs" : {
"geolocation" : {
"system" : {
"__platform__" : [ "ios", "android" ]
}
},
"maps" : {},
"payment" : {},
"ad" : {},
"oauth" : {
"apple" : {},
"google" : {
"clientid" : "455840300142-9bk9dd1u1b4bmgjc8n2c74jim89kdvph.apps.googleusercontent.com"
}
},
"share" : {},
"push" : {
"unipush" : {}
}
},
"icons" : {
"android" : {
"hdpi" : "unpackage/res/icons/72x72.png",
"xhdpi" : "unpackage/res/icons/96x96.png",
"xxhdpi" : "unpackage/res/icons/144x144.png",
"xxxhdpi" : "unpackage/res/icons/192x192.png"
},
"ios" : {
"appstore" : "unpackage/res/icons/1024x1024.png",
"ipad" : {
"app" : "unpackage/res/icons/76x76.png",
"app@2x" : "unpackage/res/icons/152x152.png",
"notification" : "unpackage/res/icons/20x20.png",
"notification@2x" : "unpackage/res/icons/40x40.png",
"proapp@2x" : "unpackage/res/icons/167x167.png",
"settings" : "unpackage/res/icons/29x29.png",
"settings@2x" : "unpackage/res/icons/58x58.png",
"spotlight" : "unpackage/res/icons/40x40.png",
"spotlight@2x" : "unpackage/res/icons/80x80.png"
},
"iphone" : {
"app@2x" : "unpackage/res/icons/120x120.png",
"app@3x" : "unpackage/res/icons/180x180.png",
"notification@2x" : "unpackage/res/icons/40x40.png",
"notification@3x" : "unpackage/res/icons/60x60.png",
"settings@2x" : "unpackage/res/icons/58x58.png",
"settings@3x" : "unpackage/res/icons/87x87.png",
"spotlight@2x" : "unpackage/res/icons/80x80.png",
"spotlight@3x" : "unpackage/res/icons/120x120.png"
}
}
},
"splashscreen" : {
"androidStyle" : "default",
"iosStyle" : "storyboard",
"ios" : {
"storyboard" : "CustomStoryboard.zip"
},
"android" : {
"hdpi" : "starter/drawable-hdpi/Untitled_design.png",
"xhdpi" : "starter/drawable-xhdpi/Untitled_design.png",
"xxhdpi" : "starter/drawable-xxhdpi/Untitled_design.png"
}
}
},
"locales" : {
"en" : {
// 英文
"ios" : {
"privacyDescription" : {
"NSCameraUsageDescription" : "Allow access to your camera to upload your avatar.",
"NSPhotoLibraryUsageDescription" : "Allow access to your photo library to select or save images.",
"NSPhotoLibraryAddUsageDescription" : "Allow saving images to your photo library, such as invitation posters.",
"NSLocationWhenInUseUsageDescription" : "Allow access to your location to recommend nearby merchants.",
"NSLocationAlwaysUsageDescription" : "Allow access to your location to recommend nearby merchants.",
"NSLocationAlwaysAndWhenInUseUsageDescription" : "Allow access to your location to recommend nearby merchants."
}
}
},
"zh" : {
// 中文(简体)
"ios" : {
"privacyDescription" : {
"NSCameraUsageDescription" : "允许访问相机以上传头像。",
"NSPhotoLibraryUsageDescription" : "允许访问相册以选择或保存图片。",
"NSPhotoLibraryAddUsageDescription" : "允许保存图片到相册,例如邀请海报。",
"NSLocationWhenInUseUsageDescription" : "允许访问您的位置以推荐附近商户。",
"NSLocationAlwaysUsageDescription" : "允许访问您的位置以推荐附近商户。",
"NSLocationAlwaysAndWhenInUseUsageDescription" : "允许访问您的位置以推荐附近商户。"
}
}
}
}
},
/* */
"quickapp" : {},
/* */
"mp-weixin" : {
"appid" : "",
"setting" : {
"urlCheck" : false
},
"usingComponents" : true
},
"mp-alipay" : {
"usingComponents" : true
},
"mp-baidu" : {
"usingComponents" : true
},
"mp-toutiao" : {
"usingComponents" : true
},
"uniStatistics" : {
"enable" : false
},
"vueVersion" : "3",
"locale" : "en"
}
+25
View File
@@ -0,0 +1,25 @@
{
"id": "cc-comment",
"name": "cc-comment 评论列表,回复,点赞,删除组件 vue3.2+uni-ui",
"displayName": "cc-comment 评论列表,回复,点赞,删除组件 vue3.2+uni-ui",
"version": "1.5",
"description": "支持多级评论, 点赞, 删除, 回复, 发起新评论; 注释详细方便二次开发; 组件功能高完成度可直接调用后端接口进行使用;",
"keywords": [
"评论",
"评论列表",
"留言板",
"回复点赞删除",
"一级二级多级评论"
],
"dcloudext": {
"type": "component-vue",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
}
}
}
@@ -0,0 +1,106 @@
<script lang="ts" setup>
import {getCaptcha} from "@/pages-login/service";
const {t} = useI18n();
const emit = defineEmits(["submit"]);
const show = ref(false);
const code = ref("");
const captchaData = ref({
img: "",
uuid: "",
code: "",
});
function onOpen() {
getCode();
show.value = true;
}
function getCode() {
code.value = "";
getCaptcha().then((res: any) => {
captchaData.value = res.data;
});
}
function handleClose() {
show.value = false;
}
function handleSubmit() {
emit("submit", {
code: code.value,
uuid: captchaData.value.uuid,
});
handleClose();
}
defineExpose({
onOpen,
});
</script>
<template>
<wd-popup
v-model="show"
custom-style="border-radius:20rpx;"
position="center"
@close="handleClose"
>
<view class="relative w-630rpx box-border px-36rpx py-40rpx">
<image
class="absolute top-34rpx right-36rpx w-32rpx h-32rpx"
src="@img/chef/100404.png"
@click="handleClose"
></image>
<view class="text-36rpx text-#333333 text-center lh-33rpx mb-22rpx">{{
t("pages-login.verify-code.title")
}}
</view>
<view class="text-28rpx text-#999999 lh-28rpx text-center mb-40rpx">{{
t("pages-login.verify-code.description")
}}
</view>
<view
class="mt-24rpx overflow-hidden flex pl-30rpx items-center justify-between h-90rpx border-2rpx border-solid border-#D1D1D1 rounded-20rpx"
>
<wd-input
v-model.trim="code"
:cursorSpacing="20"
:focus-when-clear="false"
:maxlength="20"
:placeholder="t('common.enter')"
clearable
custom-class="flex-1"
no-border
placeholderStyle="font-size: 28rpx;color: #999;line-height: 42rpx;"
type="number"
>
</wd-input>
<image
:src="`data:image/gif;base64,${captchaData.img}`"
class="w-120rpx h-88rpx"
></image>
</view>
<view
class="text-24rpx text-#FF6106 lh-24rpx text-right mt-16rpx"
@click="getCode"
>{{ t("pages-login.verify-code.changeOne") }}
</view
>
<view class="mt-54rpx">
<wd-button
block
custom-class="!h-90rpx !bg-#14181B !text-36rpx text-#fff font-bold !rounded-16rpx"
@click="handleSubmit"
>
{{ t("common.continue") }}
</wd-button>
</view>
</view>
</wd-popup>
</template>
<style lang="scss" scoped></style>
@@ -0,0 +1,242 @@
<script lang="ts" setup>
import {debounce} from "throttle-debounce"
import Config from "@/config"
import {useConfigStore} from "@/store"
import {setDayjsLocale, setWotDesignLocale} from "@/plugin"
const {t} = useI18n()
const {locale} = useI18n()
const configStore = useConfigStore()
// 语言选项 - 参考组件的语言配置
const languageOptions = ref([
{
name: "English",
systemValue: "en",
selected: false
},
{
name: "中文",
systemValue: "zh-Hans",
selected: false
}
])
// 初始化当前语言选择状态
const initLanguageSelection = () => {
const currentLocale = uni.getLocale()
languageOptions.value.forEach(option => {
option.selected = option.systemValue === currentLocale
})
}
// 选择语言
const selectLanguage = (selectedValue: string) => {
languageOptions.value.forEach(option => {
option.selected = option.systemValue === selectedValue
})
}
// 确认语言选择 - 参考组件的切换逻辑
const confirmLanguage = () => {
const selectedLanguage = languageOptions.value.find(option => option.selected)
if (selectedLanguage) {
const localeLanguages = uni.getLocale()
console.log(localeLanguages)
if (selectedLanguage.systemValue === uni.getLocale()) {
// 如果选择的语言和当前语言相同,直接跳转
uni.redirectTo({
url: '/pages-login/pages/index'
})
return
}
// 设置语言 - 使用组件相同的方法
uni.setLocale(selectedLanguage.systemValue)
locale.value = selectedLanguage.systemValue
setWotDesignLocale()
setDayjsLocale()
// #ifdef APP-PLUS
if (configStore.isIos) {
setTimeout(() => {
plus.runtime.restart()
}, 1000)
}
// #endif
// 显示成功提示
uni.showToast({
title: t('pages-login.choose-language.success'),
icon: 'none'
})
// 延迟跳转到登录页面
setTimeout(() => {
uni.switchTab({
url: '/pages/home/index'
})
}, 1500)
}
}
// 使用防抖处理确认操作
const handleConfirm = debounce(Config.debounceShortTime, confirmLanguage, {
atBegin: true,
})
// 页面加载时初始化语言选择
onMounted(() => {
initLanguageSelection()
})
</script>
<template>
<view class="language-container">
<!-- 欢迎文字 -->
<view class="welcome-section">
<text class="welcome-text">{{ t('pages-login.choose-language.title') }}</text>
</view>
<!-- 语言选择列表 -->
<view class="language-list">
<view
v-for="option in languageOptions"
:key="option.systemValue"
class="language-item"
@click="selectLanguage(option.systemValue)"
>
<view class="language-card">
<view v-show="option.selected" class="i-carbon:checkmark-filled text-primary"></view>
<view v-show="!option.selected" class="i-carbon:circle-outline text-#CCCCCC"></view>
<text :class="{ 'selected': option.selected }" class="language-name">{{ option.name }}</text>
</view>
</view>
</view>
<!-- 提示文字 -->
<view class="tip-section">
<text class="tip-text">{{ t('pages-login.choose-language.tip') }}</text>
</view>
<!-- 确认按钮 -->
<view class="confirm-section">
<view class="confirm-button" @click="handleConfirm">
<text class="confirm-text">{{ t('pages-login.choose-language.confirm') }}</text>
</view>
</view>
</view>
</template>
<style>
page {
background-color: white;
}
</style>
<style lang="scss" scoped>
.welcome-section {
padding: 286rpx 32rpx 0;
.welcome-text {
font-size: 52rpx;
font-weight: 500;
color: #333333;
line-height: 60rpx;
}
}
.language-list {
padding: 104rpx 32rpx 0;
.language-item {
margin-bottom: 28rpx;
&:last-child {
margin-bottom: 0;
}
.language-card {
width: 686rpx;
height: 90rpx;
background-color: #FFFFFF;
border: 2rpx solid #E8E8E8;
border-radius: 46rpx;
padding: 0 32rpx;
display: flex;
align-items: center;
gap: 24rpx;
.radio-button {
.radio-outer {
width: 32rpx;
height: 32rpx;
border: 2rpx solid #666666;
border-radius: 50%;
background-color: #FFFFFF;
display: flex;
align-items: center;
justify-content: center;
&.selected {
border-color: #000;
}
.radio-inner {
width: 20rpx;
height: 20rpx;
background-color: #000;
border-radius: 50%;
}
}
}
.language-name {
font-size: 26rpx;
color: #666666;
line-height: 36rpx;
&.selected {
color: #000;
font-weight: 500;
}
}
}
}
}
.tip-section {
padding: 324rpx 76rpx 0;
.tip-text {
font-size: 26rpx;
color: #666666;
line-height: 36rpx;
text-align: center;
display: block;
}
}
.confirm-section {
padding: 54rpx 32rpx 0;
.confirm-button {
width: 686rpx;
height: 92rpx;
background-color: #000;
border-radius: 46rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0px 8rpx 20rpx 0px rgba(0, 0, 0, 0.3);
.confirm-text {
font-size: 32rpx;
font-weight: 500;
color: #FFFFFF;
line-height: 44rpx;
}
}
}
</style>
@@ -0,0 +1,177 @@
<script lang="ts" setup>
import * as R from 'ramda'
import {z} from "zod";
import {useLogicStore} from "@/pages-login/store/module/logic";
import VerificationCode from "../../components/verification-code.vue";
import {debounce} from "throttle-debounce";
import Config from "@/config";
import {useUserStore} from "@/store";
import {appUserForgetPwdPost, appUserGetByEmailPost, appUserGetByPhonePost, appUserForgetPwdNotLoginPost} from "@/service";
const {t} = useI18n()
const logicStore = useLogicStore()
const userStore = useUserStore();
const verificationCodeRef = ref();
const FormSchema = z.string()
.min(1, {message: t('pages-login.prompt.password')})
function checkForm(): boolean {
const validateFormField = FormSchema.safeParse(logicStore.forgetPasswordForm.loginPwd)
console.log('validateFormField', validateFormField)
if (!validateFormField.success) {
const formatted = validateFormField.error.format()
R.path(['_errors', 0], formatted) && uni.showToast({
title: R.path(['_errors', 0], formatted),
icon: 'none',
})
}
return validateFormField.success;
}
function submit() {
verificationCodeRef.value.onOpen()
}
function codeSubmit(data) {
console.log(data)
// forgetPwdNotLogin
appUserForgetPwdNotLoginPost({
body: {
email: logicStore.forgetPasswordForm.email,
phone: logicStore.forgetPasswordForm.phone,
captcha: data.code,
uuid: data.uuid,
loginPwd: logicStore.forgetPasswordForm.loginPwd,
userPort: Config.userPort, // 登录端口2 商户端
}
}).then((res) => {
uni.showToast({
title: t('pages-login.prompt.reset-success'),
icon: 'none',
})
logicStore.reset()
uni.redirectTo({
url: '/pages-login/pages/index',
})
})
}
// 提交
const handleSubmit = R.when(checkForm, debounce(Config.debounceLongTime, submit, {
atBegin: true
}))
function navigateTo(url: string) {
uni.navigateTo({url});
}
// onLoad(R.pipe(() => getUserInfoByEmail(R.pick(['email'], logicStore.registerForm)), R.andThen((res) => {
// console.log(res)
// logicStore.forgetPasswordForm = {
// ...R.pick(['phone', 'areaCode'], res.data),
// email: logicStore.registerForm.email,
// loginPwd: '',
// captcha: "",
// }
// })))
onLoad(() => {
// 根据登录信息获取用户信息
if (logicStore.loginForm.type === 'email' && logicStore.loginForm.email) {
// 根据邮箱获取用户信息
appUserGetByEmailPost({
body: {
email: logicStore.loginForm.email,
userPort: Config.userPort, // 登录端口2 商户端
}
}).then((res: any) => {
console.log('根据邮箱获取用户信息', res)
if (res.data) {
logicStore.forgetPasswordForm.email = res.data.email
logicStore.forgetPasswordForm.phone = res.data.phone
}
})
} else if (logicStore.loginForm.type === 'phone' && logicStore.loginForm.phone) {
// 根据手机号获取用户信息
appUserGetByPhonePost({
body: {
phone: logicStore.loginForm.phone,
userPort: Config.userPort, // 登录端口2 商户端
}
}).then((res: any) => {
console.log('根据手机号获取用户信息', res)
if (res.data) {
logicStore.forgetPasswordForm.email = res.data.email
logicStore.forgetPasswordForm.phone = res.data.phone
}
})
}
})
</script>
<template>
<view>
<navbar/>
<view class="pt-64rpx px-27rpx">
<view class="text-50rpx lh-50rpx text-#14181B font-bold">
{{ t('pages-login.login.forgotPassword') }}
</view>
<view class="mt-30rpx text-28rpx lh-28rpx text-#999 font-bold">
{{ t('pages-login.forget-password.description') }}
</view>
<view class="mt-78rpx">
<view class="">
<view class="text-32rpx lh-32rpx text-#14181B">{{ t("common.email") }}</view>
<view
class="mt-24rpx flex px-30rpx items-center h-100rpx bg-#EFEFEF rounded-20rpx">
<image class="shrink-0 mr-12rpx w-30rpx h-30rpx" src="@img-login/103.png"></image>
<text class="text-28-bold">{{ logicStore.forgetPasswordForm.email }}</text>
</view>
</view>
<view class="mt-36rpx">
<view class="text-32rpx lh-32rpx text-#14181B">{{ t("pages-login.sign-up.phone-number") }}</view>
<view
class="mt-24rpx flex px-30rpx items-center h-100rpx bg-#EFEFEF rounded-20rpx">
<image class="shrink-0 mr-12rpx w-30rpx h-30rpx" src="@img-login/102.png"></image>
<text class="text-28-bold">{{ logicStore.forgetPasswordForm.phone }}</text>
</view>
</view>
<view class="mt-36rpx">
<view class="text-32rpx lh-32rpx text-#14181B">{{ t("pages-login.forget-password.newPassword") }}</view>
<view
class="mt-24rpx flex px-30rpx items-center h-100rpx border-2rpx border-solid border-#D4D4D4 rounded-20rpx">
<wd-input
v-model.trim="logicStore.forgetPasswordForm.loginPwd"
:cursorSpacing="20"
:focus-when-clear="false"
:maxlength="20"
:placeholder="t('common.enter')"
clearable
custom-class="flex-1"
no-border
placeholderStyle="font-size: 28rpx;color: #999;font-weight: bold;line-height: 42rpx;"
show-password
>
</wd-input>
</view>
</view>
<view class="mt-80rpx">
<wd-button block custom-class="!h-108rpx !text-36rpx font-bold !rounded-16rpx" @click="handleSubmit">
{{ t('common.continue') }}
</wd-button>
</view>
</view>
</view>
</view>
<verification-code ref="verificationCodeRef" @submit="codeSubmit"/>
</template>
<style lang="scss">
page {
background-color: #fff;
}
</style>
@@ -0,0 +1,90 @@
<script setup lang="ts">
import { useUserStore } from "@/store";
import useEventEmit from "@/hooks/useEventEmit";
import {EventEnum} from "@/constant/enums";
const userStore = useUserStore();
const { t } = useI18n();
// 允许位置访问
const allowLocationAccess = () => {
userStore.getLocation();
uni.navigateTo({
url: '/pages-login/pages/guide-page/notice'
})
};
// 手动选择位置
const selectLocation = () => {
uni.navigateTo({
url: '/pages-user/pages/search-address/index'
})
};
useEventEmit(EventEnum.CHOOSE_ADDRESS, (data) => {
console.log('搜索的地址信息', data)
if(data) {
userStore.userSetLocation = {
location: data.displayName,
longitude: data.location.lng,
latitude: data.location.lat,
}
setTimeout(()=> {
uni.navigateTo({
url: '/pages-login/pages/guide-page/notice'
})
}, 300)
}
})
</script>
<template>
<view class="bg-white flex flex-col">
<!-- 主要内容区域 -->
<view
class="flex-1 flex flex-col items-center justify-center px-40rpx pt-216rpx pb-120rpx"
>
<!-- 位置图标 -->
<image
src="@img-login/100218.png"
class="w-256rpx h-256rpx rounded-full"
/>
<!-- 标题 -->
<view class="text-center mt-84rpx">
<text class="text-50-bold">{{ t('pages-login.guide-page.location.title') }}</text>
</view>
<!-- 描述文本 -->
<view class="text-center mt-36rpx mb-86rpx">
<text class="text-30rpx lh-30rpx text-#999 leading-30rpx">
{{ t('pages-login.guide-page.location.description') }}
</text>
</view>
<!-- 主要按钮 -->
<view class="mb-46rpx w-full">
<button
class="w-full h-108rpx bg-#14181B rounded-16rpx border-none"
@click="allowLocationAccess"
>
<text class="text-white text-36rpx lh-108rpx"
>{{ t('pages-login.guide-page.location.allowLocationAccess') }}</text
>
</button>
</view>
<!-- 次要链接 -->
<view class="text-center">
<text class="text-36rpx text-#333" @click="selectLocation">
{{ t('pages-login.guide-page.location.selectLocation') }}
</text>
</view>
</view>
</view>
</template>
<style lang="scss">
page {
background-color: #ffffff;
}
</style>
@@ -0,0 +1,81 @@
<script setup lang="ts">
import { useUserStore } from "@/store";
const userStore = useUserStore();
const { t } = useI18n();
const allowLocationAccess = () => {
// 开启通知--获取通知权限
// #ifdef APP-PLUS
plus.push.getClientInfoAsync(
(info) => {
console.log('当前的推送信息', info)
},
(err) => {
console.log('err', err)
},
)
// #endif
uni.navigateTo({
url: '/pages-login/pages/guide-page/welcome'
})
};
// 手动选择位置
const selectLocation = () => {
uni.navigateTo({
url: '/pages-login/pages/guide-page/welcome'
})
};
</script>
<template>
<view class="bg-white flex flex-col">
<!-- 主要内容区域 -->
<view
class="flex-1 flex flex-col items-center justify-center px-40rpx pt-216rpx pb-120rpx"
>
<!-- 位置图标 -->
<image
src="@img-login/100219.png"
class="w-256rpx h-256rpx rounded-full"
/>
<!-- 标题 -->
<view class="text-center mt-84rpx">
<text class="text-50-bold">{{ t('pages-login.guide-page.notice.title') }}</text>
</view>
<!-- 描述文本 -->
<view class="text-center mt-36rpx mb-86rpx">
<text class="text-30rpx lh-30rpx text-#999 leading-30rpx">
{{ t('pages-login.guide-page.notice.description') }}
</text>
</view>
<!-- 主要按钮 -->
<view class="mb-46rpx w-full">
<button
class="w-full h-108rpx bg-#14181B rounded-16rpx border-none"
@click="allowLocationAccess"
>
<text class="text-white text-36rpx lh-108rpx"
>{{ t('pages-login.guide-page.notice.activateNotification') }}</text
>
</button>
</view>
<!-- 次要链接 -->
<view class="text-center">
<text class="text-36rpx text-#333" @click="selectLocation">
{{ t('pages-login.guide-page.notice.continueLater') }}
</text>
</view>
</view>
</view>
</template>
<style lang="scss">
page {
background-color: #ffffff;
}
</style>
@@ -0,0 +1,63 @@
<script setup lang="ts">
import { useUserStore, useConfigStore } from "@/store";
const userStore = useUserStore();
const configStore = useConfigStore()
const { t } = useI18n();
const allowLocationAccess = () => {
// 设置标记为已经加载过引导页
configStore.isShowedGuidePage = true
// uni.switchTab({
// url: '/pages/home/index'
// })
uni.navigateTo({
url: '/pages-user/pages/member-center/index'
})
};
</script>
<template>
<view class="bg-white flex flex-col">
<!-- 主要内容区域 -->
<view
class="flex-1 flex flex-col items-center justify-center px-40rpx pt-216rpx pb-120rpx"
>
<!-- 位置图标 -->
<image
src="@img-login/100220.png"
class="w-256rpx h-256rpx rounded-full"
/>
<!-- 标题 -->
<view class="text-center text-50-bold mt-84rpx">
<text class="mr-4rpx">{{ t('pages-login.guide-page.welcome.title') }},</text>
<text class="">{{ userStore.userInfo.firstName }} {{ userStore.userInfo.surname }}</text>
</view>
<!-- 描述文本 -->
<view class="text-center mt-36rpx mb-113rpx">
<text class="text-30rpx lh-30rpx text-#999 leading-30rpx">
{{ t('pages-login.guide-page.welcome.description') }}
</text>
</view>
<!-- 主要按钮 -->
<view class="w-full">
<button
class="w-full h-108rpx bg-#14181B rounded-16rpx border-none"
@click="allowLocationAccess"
>
<text class="text-white text-36rpx lh-108rpx"
>{{ t('pages-login.guide-page.welcome.next') }}</text
>
</button>
</view>
</view>
</view>
</template>
<style lang="scss">
page {
background-color: #ffffff;
}
</style>
+226
View File
@@ -0,0 +1,226 @@
<script setup lang="ts">
import * as R from 'ramda'
import {z} from "zod";
import {useLogicStore} from "@/pages-login/store/module/logic";
import {debounce} from "throttle-debounce";
import Config from "@/config";
import {appUserCheckEmailUniquePost, appUserCheckPhoneUniquePost} from '@/service'
import {useConfigStore} from "@/store";
import { isEmail } from '@/utils'
const areaCode = ref<string>('+1');
const columns = ref<string[]>(Config.phoneCodeList);
interface LoginMethod {
id: number;
method: string;
title: string;
icon: string;
}
const {t} = useI18n()
const logicStore = useLogicStore()
const configStore = useConfigStore()
const loginMethods = computed<LoginMethod[]>(() => {
const methods = [
{
id: 3,
method: 'Google',
title: t('pages-login.index.google-login'),
icon: '/static/app/images/100204.png',
},
// {
// id: 2,
// method: 'Facebook',
// title: t('pages-login.index.facebook-login'),
// icon: '/static/app/images/100207.png',
// },
{
id: 1,
method: 'Apple',
title: t('pages-login.index.apple-login'),
icon: '/static/app/images/100208.png',
},
// {
// id: 4,
// method: 'Wechat',
// title: t('pages-login.index.wechat-login'),
// icon: '/static/app/images/100209.png',
// },
]
if (R.and(configStore.isApp, configStore.isIos)) {
return methods
}
return methods.filter(item => item.method !== 'Apple')
})
const btnLoading = ref(false)
function handleSubmit() {
const {email} = logicStore.registerForm
let type = '';
console.log('email', email)
console.log('email', isEmail(email))
if (isEmail(email)) {
btnLoading.value = true
appUserCheckEmailUniquePost({
body: {
email,
userPort: Config.userPort, // 登录端口2 商户端
},
options: {}
}).then(res => {
console.log('email', res)
if (res.data) {
logicStore.loginForm.email = email;
logicStore.loginForm.type = 'email';
// 邮箱已存在
uni.navigateTo({url: '/pages-login/pages/login/index'})
} else {
// 邮箱未存在
type = 'email';
// 用户没注册
logicStore.registerForm.type = type;
logicStore.registerForm.confirmEmail = email;
uni.navigateTo({url: '/pages-login/pages/sign-up/index'})
}
}).finally(() => {
btnLoading.value = false
})
} else {
btnLoading.value = true
// 手机号登录
appUserCheckPhoneUniquePost({
body: {
areaCode: areaCode.value,
phone: email,
userPort: Config.userPort, // 登录端口2 商户端
},
options: {}
}).then(res => {
type = 'phone';
if (res.data) {
logicStore.loginForm.phone = email;
logicStore.loginForm.type = type;
logicStore.loginForm.areaCode = areaCode.value;
// 手机已存在
uni.navigateTo({url: '/pages-login/pages/login/index'})
} else {
// 用户没注册
logicStore.registerForm.type = type;
logicStore.registerForm.phone = email;
logicStore.registerForm.areaCode = areaCode.value;
logicStore.registerForm.email = '';
logicStore.registerForm.confirmEmail = '';
uni.navigateTo({url: '/pages-login/pages/sign-up/index'})
}
}).finally(() => {
btnLoading.value = false
})
}
}
function loginByMethod(item: LoginMethod) {
switch (item.method) {
case 'Apple': {
logicStore.appleLogin()
break
}
case 'Facebook': {
logicStore.facebookLogin()
break
}
case 'Google': {
logicStore.googleLogin()
// logicStore.testGoogleLogin()
break
}
case 'Wechat': {
logicStore.wechatLogin()
break
}
}
}
const handleLoginByMethod = debounce(Config.debounceLongTime, loginByMethod, {
atBegin: true
})
onUnload(() => {
logicStore.reset()
})
</script>
<template>
<view>
<status-bar/>
<view class="pt-120rpx px-30rpx">
<view class="text-50rpx lh-50rpx text-#14181B font-bold">
{{ t('pages-login.index.title') }}
</view>
<view class="mt-30rpx text-28rpx lh-28rpx text-#999 font-bold">
{{ t('pages-login.index.description') }}
</view>
<view class="mt-80rpx">
<view class="">
<view class="center h-88rpx border-2rpx border-solid border-#D4D4D4 rounded-20rpx"
:class="[index>0&&'mt-22rpx']" v-for="(item,index) in loginMethods"
:key="item.id"
@click="handleLoginByMethod(item)"
>
<image class="shrink-0 mr-16rpx w-40rpx h-40rpx" :src="item.icon"/>
<text class="text-28-bold">{{ item.title }}</text>
</view>
</view>
<view class="my-38rpx flex items-center justify-between">
<view class="w-308rpx h-0rpx border-bottom"></view>
<text class="text-28-bold">{{ t('common.or') }}</text>
<view class="w-308rpx h-0rpx border-bottom"></view>
</view>
<view class="">
<view class="flex px-24rpx items-center h-88rpx border-2rpx border-solid border-#D4D4D4 rounded-20rpx">
<view v-if="logicStore.registerForm.email && !isEmail(logicStore.registerForm.email)" class="text-28rpx">
<wd-picker v-model="areaCode" :columns="columns"/>
</view>
<wd-input
no-border
clearable
:focus-when-clear="false"
use-prefix-slot
:maxlength="255"
v-model.trim="logicStore.registerForm.email"
custom-class="flex-1"
placeholderStyle="font-size: 32rpx;color: #999;line-height: 32rpx;"
:placeholder="t('pages-login.index.input-placeholder')"
>
</wd-input>
</view>
</view>
<view class="mt-42rpx">
<wd-button custom-class="!h-108rpx !text-36rpx leading-36rpx !rounded-16rpx" block @click="handleSubmit" :loading="btnLoading" loading-color="#000">
{{ t('common.continue') }}
</wd-button>
</view>
</view>
</view>
</view>
</template>
<style lang="scss">
page {
background-color: #fff;
}
:deep(.wd-picker__cell) {
background-color: transparent !important;
padding: 20rpx 0 !important;
.wd-picker__value {
margin-right: 8rpx !important;
}
}
</style>
+134
View File
@@ -0,0 +1,134 @@
<script lang="ts" setup>
import * as R from 'ramda'
import {z} from "zod";
import {useLogicStore} from "@/pages-login/store/module/logic";
import {LoginType} from "@/constant/enums";
import {appLoginPost} from "@/service";
import {debounce} from "throttle-debounce";
import Config from "@/config";
import {useConfigStore, useUserStore} from "@/store";
const {t} = useI18n()
const userStore = useUserStore();
const configStore = useConfigStore()
const logicStore = useLogicStore()
const FormSchema = z.string()
.min(1, {message: t('pages-login.prompt.password')})
function checkForm(): boolean {
const validateFormField = FormSchema.safeParse(logicStore.loginForm.password)
console.log('validateFormField', validateFormField)
if (!validateFormField.success) {
const formatted = validateFormField.error.format()
R.path(['_errors', 0], formatted) && uni.showToast({
title: R.path(['_errors', 0], formatted),
icon: 'none',
})
}
return validateFormField.success;
}
const btnLoading = ref(false)
async function submit() {
btnLoading.value = true
try {
const params = {
email: '',
phone: '',
password: logicStore.loginForm.password,
type: logicStore.loginForm.type === 'email' ? LoginType.EMAIL : LoginType.ACCOUNT,
userPort: Config.userPort, // 商户端登录
areaCode: logicStore.loginForm.areaCode,
}
logicStore.loginForm.type === 'email' ? params.email = logicStore.loginForm.email : params.phone = logicStore.loginForm.phone
console.log('params42', params)
const res = await appLoginPost({
body: params,
})
btnLoading.value = false
console.log('res', res)
await uni.showToast({title: t('pages-login.login-successfully'), icon: "none"});
userStore.token = res.data.token;
nextTick(() => {
userStore.getUserInfo();
})
uni.switchTab(
{
url: Config.indexPath
})
} catch (err) {
btnLoading.value = false
}
}
// 提交
const handleSubmit = R.when(checkForm, debounce(Config.debounceLongTime, submit, {
atBegin: true
}))
function navigateTo(url: string) {
logicStore.resetRegisterForm()
uni.navigateTo({url});
}
</script>
<template>
<view>
<navbar/>
<view class="pt-60rpx px-27rpx">
<view class="text-50rpx lh-50rpx text-#14181B font-bold">
{{ t('pages-login.login.title') }}
</view>
<view class="mt-30rpx text-28rpx lh-28rpx text-#999 font-bold">
{{ t('pages-login.login.description') }}
</view>
<view class="mt-80rpx">
<view class="">
<view class="text-32rpx lh-32rpx text-#14181B">{{ t("pages-login.sign-up.password") }}</view>
<view
class="mt-24rpx flex px-30rpx items-center h-98rpx border-2rpx border-solid border-#D4D4D4 rounded-20rpx">
<wd-input
v-model.trim="logicStore.loginForm.password"
:cursorSpacing="20"
:focus-when-clear="false"
:maxlength="20"
:placeholder="t('common.enterPassword')"
clearable
custom-class="flex-1"
no-border
placeholderStyle="font-size: 32rpx;color: #999;"
show-password
>
</wd-input>
</view>
</view>
<view class="mt-40rpx">
<wd-button block custom-class="!h-108rpx !text-36rpx font-bold !rounded-16rpx" @click="handleSubmit" :loading="btnLoading" loading-color="#000">
{{ t('pages-login.login.title') }}
</wd-button>
</view>
<view class="flex-center-sb mt-60rpx">
<text class="text-32rpx text-#333 underline" @click="navigateTo('/pages-login/pages/sign-up/index')">
{{ t('pages-login.sign-up.title') }}
</text>
<text class="text-32rpx text-#CE7138 lh-34rpx underline"
@click="navigateTo('/pages-login/pages/forget-password/index')">
{{ t('pages-login.login.forgotPassword') }}
</text>
</view>
</view>
</view>
</view>
</template>
<style lang="scss">
page {
background-color: #fff;
}
</style>
+329
View File
@@ -0,0 +1,329 @@
<script lang="ts" setup>
import * as R from 'ramda'
import {z} from "zod";
import {useLogicStore} from "@/pages-login/store/module/logic";
import {Agreement} from "@/constant/enums";
import {debounce} from "throttle-debounce";
import Config from "@/config";
import VerificationCode from "../../components/verification-code.vue";
import {appUserRegisterPost} from "@/service";
import {useConfigStore, useUserStore} from "@/store";
const {t} = useI18n()
const userStore = useUserStore();
const configStore = useConfigStore()
const logicStore = useLogicStore()
const areaCode = ref<string>(logicStore.registerForm.areaCode || '+1');
const columns = ref<string[]>(Config.phoneCodeList);
const isAgreed = ref(false)
const FormSchema = z.object({
firstName: z.string().min(1, {message: t('pages-login.index.prompt.first-name')}),
surname: z.string().min(1, {message: t('pages-login.index.prompt.last-name')}),
phone: z.string().min(1, {message: t('pages-login.index.prompt.phone-number')}).regex(/^\d{6,11}$/, {message: t('pages-login.index.prompt.phone-number-verify')}),
loginPwd: z.string().min(1, {message: t('pages-login.index.prompt.password')}),
email: z.string().email({message: t('pages-login.index.prompt.email-address-verify')}),
confirmEmail: z.string().email({message: t('pages-login.index.prompt.email-address-verify')}),
}).refine((data) => data.email === data.confirmEmail, {
path: ['confirmEmail'],
message: t('pages-login.index.prompt.confirm-email-verify')
})
function checkForm(): boolean {
const validateFormField = FormSchema.safeParse(logicStore.registerForm)
if (!validateFormField.success) {
const fieldErrorMessage = validateFormField.error.flatten().fieldErrors
const errorMessage: string | undefined = R.path([0, 0], R.values(fieldErrorMessage))
errorMessage &&
uni.showToast({
title: errorMessage,
icon: 'none',
})
} else if (!isAgreed.value) {
uni.showToast({
title: `${t('common.prompt.please-carefully-read-and-agree')} ${t('agreement.user-terms-conditions')}${t('pages-login.and')}${t('agreement.privacy-policy')}`,
icon: 'none'
})
}
return validateFormField.success && isAgreed.value
}
const verificationCodeRef = ref()
const submit = () => {
// verificationCodeRef.value.onOpen()
codeSubmit()
}
// 验证码提交
const btnLoading = ref(false)
function codeSubmit() {
btnLoading.value = true
// console.log(data)
appUserRegisterPost({
body: {
...logicStore.registerForm,
phone: logicStore.registerForm.phone,
areaCode: areaCode.value,
// captcha: data.code,
// uuid: data.uuid,
userPort: Config.userPort, // 登录端口2 商户端
}
}).then((res) => {
userStore.token = res.data.token;
logicStore.reset()
uni.showToast({
title: t('pages-login.sign-up.register-success'),
icon: 'none',
})
nextTick(() => {
userStore.getUserInfo();
uni.navigateTo({
url: Config.guidePath
})
// uni.redirectTo({
// url: '/pages-login/pages/guide-page/location'
// })
// const pages = getCurrentPages()
// if(configStore.isShowedGuidePage) {
// if(pages.length > 2) {
// uni.navigateBack({delta: 2})
// } else {
// uni.switchTab({
// url: Config.indexPath
// })
// }
// } else {
// uni.navigateTo({
// url: Config.guidePath
// })
// }
})
}).finally(() => {
btnLoading.value = false
})
}
// 提交
const handleSubmit = R.when(checkForm, debounce(Config.debounceLongTime, submit, {
atBegin: true
}))
function navigateTo(url: string) {
uni.navigateTo({url});
}
const emailIsReadonly = ref(false)
const phoneIsReadonly = ref(false)
onMounted(() => {
const {email, type} = logicStore.registerForm
if (type === 'phone') {
// phoneIsReadonly.value = true
}
if (type === 'email') {
// emailIsReadonly.value = true
}
})
</script>
<template>
<view>
<navbar/>
<view class="pt-60rpx px-27rpx pb-80rpx">
<view class="mb-60rpx text-50rpx leading-50rpx font-bold text-#14181B">
{{ t('pages-login.sign-up.title') }}
</view>
<view class="">
<!-- 邮箱 -->
<view class="">
<view class="text-32rpx leading-32rpx text-#14181B mb-24rpx">{{ t("common.email") }}</view>
<view class="border-color px-30rpx flex items-center bg-#EFEFEF">
<wd-input
v-model.trim="logicStore.registerForm.email"
:cursorSpacing="20"
:focus-when-clear="false"
:maxlength="40"
:placeholder="t('common.enter')"
:readonly="emailIsReadonly"
clearable
custom-class="flex-1 !bg-transparent"
no-border
placeholderClass="!text-#999 !text-32rpx"
>
</wd-input>
</view>
</view>
<!-- Confirm email -->
<view class="mt-36rpx">
<view class="text-32rpx leading-32rpx text-#14181B mb-24rpx">{{
t("pages-login.sign-up.confirm-email")
}}
</view>
<view class="border-color px-30rpx flex items-center bg-#EFEFEF">
<wd-input
v-model.trim="logicStore.registerForm.confirmEmail"
:cursorSpacing="20"
:focus-when-clear="false"
:maxlength="40"
:placeholder="t('common.enter')"
:readonly="emailIsReadonly"
clearable
custom-class="flex-1 !bg-transparent"
no-border
placeholderClass="!text-#999 !text-32rpx"
>
</wd-input>
</view>
</view>
<!-- Password -->
<view class="mt-36rpx ">
<view class="text-32rpx leading-32rpx text-#14181B mb-24rpx">{{ t("pages-login.sign-up.password") }}</view>
<view class="border-color px-30rpx flex items-center bg-#EFEFEF">
<wd-input
v-model.trim="logicStore.registerForm.loginPwd"
:cursorSpacing="20"
:focus-when-clear="false"
:maxlength="20"
:placeholder="t('common.enterPassword')"
clearable
custom-class="flex-1 !bg-transparent"
no-border
placeholderClass="!text-#999 !text-32rpx"
showPassword
>
</wd-input>
</view>
</view>
<!-- First name -->
<view class="mt-36rpx ">
<view class="text-32rpx leading-32rpx text-#14181B mb-24rpx">{{ t("pages-login.sign-up.first-name") }}</view>
<view class="border-color px-30rpx flex items-center bg-#EFEFEF">
<wd-input
v-model.trim="logicStore.registerForm.firstName"
:cursorSpacing="20"
:focus-when-clear="false"
:maxlength="40"
:placeholder="t('common.enter')"
clearable
custom-class="flex-1 !bg-transparent"
no-border
placeholderClass="!text-#999 !text-32rpx"
>
</wd-input>
</view>
</view>
<!-- Last name -->
<view class="mt-36rpx ">
<view class="text-32rpx leading-32rpx text-#14181B mb-24rpx">{{ t("pages-login.sign-up.last-name") }}</view>
<view class="border-color px-30rpx flex items-center bg-#EFEFEF">
<wd-input
v-model.trim="logicStore.registerForm.surname"
:cursorSpacing="20"
:focus-when-clear="false"
:maxlength="40"
:placeholder="t('common.enter')"
clearable
custom-class="flex-1 !bg-transparent"
no-border
placeholderClass="!text-#999 !text-32rpx"
>
</wd-input>
</view>
</view>
<!-- Phone number -->
<view class="mt-36rpx ">
<view class="text-32rpx leading-32rpx text-#14181B mb-24rpx">{{
t("pages-login.sign-up.phone-number")
}}
</view>
<view class="border-color px-30rpx flex items-center bg-#EFEFEF">
<view class="pr-14rpx text-28rpx">
<wd-picker v-model="areaCode" :columns="columns"/>
</view>
<wd-input
v-model.trim="logicStore.registerForm.phone"
:cursorSpacing="20"
:focus-when-clear="false"
:maxlength="40"
:placeholder="t('common.enter')"
:readonly="phoneIsReadonly"
clearable
custom-class="flex-1 !bg-transparent"
no-border
placeholderClass="!text-#999 !text-32rpx"
>
</wd-input>
</view>
</view>
<view class="mt-54rpx">
<wd-button block custom-class="!h-108rpx !text-36rpx text-#fff font-bold !rounded-16rpx"
@click="handleSubmit" :loading="btnLoading" loading-color="#000">
{{ t('pages-login.sign-up.title') }}
</wd-button>
</view>
<view class="mt-36rpx flex items-start text-28rpx text-primary lh-42rpx" @click="isAgreed = !isAgreed">
<view class="shrink-0 center py-5rpx px-10rpx">
<image v-show="isAgreed" class=" w-28rpx h-28rpx"
src="@img-login/101.png"></image>
<image v-show="!isAgreed" class=" w-28rpx h-28rpx"
src="@img-login/100.png"></image>
</view>
<view class="">
<text class="">{{ t('pages-login.continuing-agree') }}</text>
<text class="text-#00A76D"
@click.stop="navigateTo(`/pages/agreement/index?code=${Agreement.USER_AGREEMENT}`)">
{{ t('agreement.user-terms-conditions') }}
</text>
<text class="text-#00A76D"
@click.stop="navigateTo(`/pages/agreement/index?code=${Agreement.PRIVACY_POLICY}`)">
{{ t('agreement.privacy-policy') }}
</text>
</view>
</view>
</view>
</view>
<verification-code ref="verificationCodeRef" @submit="codeSubmit"/>
</view>
</template>
<style lang="scss">
page {
background-color: #fff;
}
.border-color {
height: 98rpx;
border-radius: 16rpx;
border: 2rpx solid #D4D4D4;
}
:deep(.wd-input__clear) {
background-color: transparent !important;
}
:deep(.wd-input__icon) {
background-color: transparent !important;
}
:deep(.wd-picker__cell) {
background-color: transparent !important;
.wd-picker__value {
margin-right: 8rpx !important;
}
}
:deep(.wd-picker-view-column__item) {
line-height: 94rpx !important;
}
:deep(.uni-picker-view-indicator) {
height: 94rpx !important;
}
</style>
View File
+35
View File
@@ -0,0 +1,35 @@
import {http} from '@/utils/http'
// 获取图片验证码
export const getCaptcha = () => http.get<Record<string, any>>('/auth/code')
// 根据邮箱查询用户信息
export const getUserInfoByEmail = (data: Record<string, any>) => http.post<Record<string, any>>('/app/user/getByEmail', data)
// 根据手机号查询用户信息
export const getByPhone = (data: Record<string, any>) => http.post<Record<string, any>>('/app/user/getByPhone', data)
// 校验手机号是否被使用
export const checkPhoneUnique = (data: Record<string, any>) => http.post<Record<string, any>>('/app/user/checkPhoneUnique', data)
// 短信验证码
export const smsSend = (data: Record<string, any>) => http.post('/app/sms/send', data)
// 邮箱注册
export const register = (data: Record<string, any>) => http.post<{
token: string
}>('/app/user/register', data)
// 登录
export const login = (data: Record<string, any>) =>
http.post<{
token: string
}>('/app/login', data)
// 忘记密码
export const forgetPwd = (data: Record<string, any>) => http.post<{
token: string
}>('/app/user/forgetPwd', data)
Binary file not shown.

After

Width:  |  Height:  |  Size: 689 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File
+363
View File
@@ -0,0 +1,363 @@
import {defineStore} from "pinia";
import {login} from "@/pages-login/service";
import {useUserStore} from "@/store";
import * as R from "ramda";
import Config from "@/config";
import {LoginType} from "@/constant/enums";
export const useLogicStore = defineStore('login-logic', () => {
const {defaultAreaCode} = useAreaCode()
const registerForm = ref({
type: "",
email: '',
confirmEmail: '',
firstName: '',
lastName: '',
phone: '',
loginPwd: '',
areaCode: defaultAreaCode.value,
captcha: "",
})
const loginForm = ref({
type: "",
email: '',
areaCode: defaultAreaCode.value,
phone: '',
code: '',
password: '',
tripartiteLoginIdentify: '',
})
const forgetPasswordForm = ref({
email: '',
loginPwd: '',
captcha: "",
phone: '',
areaCode: defaultAreaCode.value,
})
function navigateTo(url: string) {
uni.navigateTo({
url,
})
}
function reset() {
registerForm.value = {
email: '',
firstName: '',
lastName: '',
phone: '',
loginPwd: '',
areaCode: defaultAreaCode.value,
captcha: "",
}
loginForm.value = {
type: "",
email: '',
areaCode: defaultAreaCode.value,
phone: '',
code: '',
password: '',
tripartiteLoginIdentify: '',
}
forgetPasswordForm.value = {
email: '',
loginPwd: '',
captcha: "",
phone: '',
areaCode: defaultAreaCode.value,
}
}
function resetRegisterForm() {
registerForm.value = {
type: "",
email: '',
confirmEmail: '',
firstName: '',
surname: '',
phone: '',
loginPwd: '',
areaCode: defaultAreaCode.value,
captcha: "",
}
}
const userStore = useUserStore()
const {t} = useI18n()
function appleLogin() {
return new Promise((resolve, reject) => {
uni.login({
provider: 'apple',
success: (res) => {
uni.getUserInfo({
provider: 'apple',
success: async (info) => {
// 获取用户信息成功, info.authResult保存用户信息
console.log('info', info)
const params = {
type: LoginType.APPLE,
email: info.userInfo?.email || '',
tripartiteLoginIdentify: info?.userInfo?.openId ?? '',
}
try {
const loginRes = await login(params)
console.log('loginRes', loginRes)
userStore.token = loginRes.data.token
nextTick(() => {
userStore.getUserInfo();
})
await uni.showToast({title: t('pages-login.login-successfully'), icon: "none"});
const pages = getCurrentPages()
setTimeout(R.ifElse(() => pages.length > 1, () => uni.navigateBack({delta: 1}), () => uni.switchTab(
{
url: Config.indexPath
}
)), 1000);
resolve(true)
} catch (err: any) {
console.log('err', err)
if (R.equals(+err.code, 711)) {
registerForm.value = {
email: info.userInfo?.email || '',
areaCode: '',
phone: '',
code: '',
password: '',
type: LoginType.APPLE,
tripartiteLoginIdentify: info?.userInfo?.openId ?? '',
}
navigateTo('/pages-login/pages/sign-up/index')
}
}
},
fail: (err: any) => {
console.log('err', err)
},
})
},
fail: (err: any) => {
console.log('err', err)
// 登录授权失败
uni.showToast({title: t('common.appleLoginFailed'), icon: 'none'})
},
})
})
}
function facebookLogin() {
return new Promise((resolve, reject) => {
uni.login({
provider: 'facebook',
success: (res) => {
uni.getUserInfo({
provider: 'facebook',
success: async (info) => {
// 获取用户信息成功, info.authResult保存用户信息
console.log('info', info)
const params = {
userType: 1,
type: LoginType.FACEBOOK,
openid: info?.userInfo?.openId ?? '',
}
try {
const loginRes = await login(params)
console.log('loginRes', loginRes)
userStore.token = loginRes.data.token
resolve(true)
} catch (err: any) {
console.log('err', err)
if (R.equals(+err.code, 711)) {
loginForm.value = {
email: info?.authResult?.email ?? '',
areaCode: '',
phone: '',
code: '',
password: '',
type: LoginType.FACEBOOK,
tripartiteLoginIdentify: info.authResult.user ?? '',
}
navigateTo('/pages-login/pages/enter-phone-number/index')
}
}
},
fail: (err: any) => {
console.log('err', err)
},
})
},
fail: (err: any) => {
console.log('err', err)
// 登录授权失败
uni.showToast({title: '登录授权失败', icon: 'none'})
},
})
})
}
function googleLogin() {
return new Promise((resolve, reject) => {
uni.login({
provider: 'google',
success: (res) => {
uni.getUserInfo({
provider: 'google',
success: async (info) => {
// 获取用户信息成功, info.authResult保存用户信息
console.log('info', info)
const params = {
userType: 1,
type: LoginType.GOOGLE,
email: info.userInfo.email,
tripartiteLoginIdentify: info.userInfo.openid,
}
try {
const loginRes = await login(params)
console.log('loginRes', loginRes)
userStore.token = loginRes.data.token
nextTick(() => {
userStore.getUserInfo();
})
await uni.showToast({title: t('pages-login.login-successfully'), icon: "none"});
const pages = getCurrentPages()
setTimeout(R.ifElse(() => pages.length > 1, () => uni.navigateBack({delta: 1}), () => uni.switchTab(
{
url: Config.indexPath
}
)), 1000);
resolve(true)
} catch (err: any) {
if (R.equals(+err.code, 711)) {
registerForm.value = {
email: info.userInfo.email,
areaCode: '+1',
phone: '',
code: '',
password: '',
type: LoginType.GOOGLE,
tripartiteLoginIdentify: info.userInfo.openid,
}
navigateTo('/pages-login/pages/sign-up/index')
}
}
},
fail: (err: any) => {
console.log('err', err)
},
})
},
fail: (err: any) => {
console.log('err', err)
// 登录授权失败
uni.showToast({title: t('common.googleLoginFailed'), icon: 'none'})
}
})
})
}
async function testGoogleLogin() {
let info = {
"headimgurl": "https://lh3.googleusercontent.com/a/ACg8ocLQF07FdGLYzv0wrEmPKb9v0_owckcC6I-YoX7YJY_rs8rQlaw",
"nickname": "gaofushuai",
"unionid": "101877608870663900115",
"openid": "101877608870663955115",
"email": "gfs0022097@gmail.com",
"openId": "101877608870663900215",
"nickName": "gaofushuai",
"avatarUrl": "https://lh3.googleusercontent.com/a/ACg8ocLQF07FdGLYzv0wrEmPKb9v0_owckcC6I-YoX7YJY_rs8rQlaw"
}
const params = {
userType: 1,
// 1 验证码 2 密码 3 一键登录 4 微信登录 5 小程序登录 6 苹果登录
type: LoginType.GOOGLE,
email: info.email,
tripartiteLoginIdentify: info.openid,
}
try {
const loginRes = await login(params)
console.log('testGoogleLogin', loginRes)
} catch (err: any) {
console.log('err22222222222', err)
if (R.equals(+err.code, 711)) {
registerForm.value = {
areaCode: '',
phone: '',
code: '',
password: '',
type: LoginType.GOOGLE,
email: info.email,
tripartiteLoginIdentify: info.openid,
}
navigateTo('/pages-login/pages/sign-up/index')
}
}
}
function wechatLogin() {
return new Promise((resolve, reject) => {
uni.login({
provider: 'weixin',
success: (res) => {
const {code} = res
uni.getUserInfo({
provider: 'weixin',
success: async (info) => {
// 获取用户信息成功, info.authResult保存用户信息
console.log('info', info)
const params = {
userType: 1,
// 1 验证码 2 密码 3 一键登录 4 微信登录 5 小程序登录 6 苹果登录
type: 6,
openid: info?.userInfo?.openId ?? '',
}
try {
const loginRes = await login(params)
console.log('loginRes', loginRes)
userStore.token = loginRes.data.token
resolve(true)
} catch (err: any) {
console.log('err', err)
}
},
fail: (err: any) => {
console.log('err', err)
},
})
},
fail: (err: any) => {
console.log('err', err)
// 登录授权失败
uni.showToast({title: '微信登录授权失败', icon: 'none'})
}
})
})
}
return {
registerForm,
loginForm,
forgetPasswordForm,
reset,
appleLogin,
facebookLogin,
googleLogin,
wechatLogin,
resetRegisterForm,
testGoogleLogin,
}
}
)
+69
View File
@@ -0,0 +1,69 @@
<script setup lang="ts">
import {getMarketingDishList} from "@/pages-store/service";
import {appMarketActivityListPost} from "@/service";
import {thumbnailImg} from "@/utils/utils";
const props = defineProps<{
id: string
}>()
function getList(pageNum: number, pageSize: number) {
return new Promise(resolve => {
getMarketingDishList(props.id).then(res => {
resolve(res)
})
})
}
const {paging, loading, firstLoaded, dataList, queryList} = usePage(getList)
onLoad(()=> {
// appMarketActivityListPost({
// options: {
// pageNum: 1,
// pageSize: 10,
// },
// }).then(res=> {
// console.log('', res)
// })
})
function handleClickDish(item: any) {
navigateTo(`/pages-store/pages/store/dishes?id=${item.id}&storeId=${item.merchantId}`)
}
function navigateTo(url: string) {
uni.navigateTo({ url })
}
</script>
<template>
<z-paging
ref="paging"
v-model="dataList"
@query="queryList"
bg-color="#fff"
>
<template #top>
<navbar />
</template>
<view class="grid grid-cols-2 gap-x-30rpx gap-y-46rpx px-32rpx">
<template v-for="(item,index) in dataList">
<view @click="handleClickDish(item)" class="w-330rpx overflow-hidden">
<image
:src="thumbnailImg(item?.dishImage?.split(',')[0])"
class="w-full h-186rpx rounded-24rpx mb-16rpx"
mode="aspectFill"
></image>
<text class="text-30rpx lh-30rpx text-#333 line-clamp-1 tracking-[.04em] font-500"
>{{ item.dishName }}</text>
</view>
</template>
</view>
</z-paging>
</template>
<style scoped lang="scss">
</style>
+102
View File
@@ -0,0 +1,102 @@
<script setup lang="ts">
import usePage from "@/hooks/usePage";
import {appMerchantRecommendListPost} from "@/service";
import {useUserStore} from "@/store";
import FiltrateTool from "@/components/filtrate-tool/index.vue";
import PriceChoose from "@/components/filtrate-tool/components/price-choose.vue";
import Score from "@/components/filtrate-tool/components/score.vue";
import FoodBox from "@/pages/home/components/tabbar-home/components/food-box/index.vue";
const userStore = useUserStore();
const {t} = useI18n()
const props = defineProps({
merchantCategoryIds: {
type: [Number, String],
default: null
}
})
const {paging, dataList, queryList} = usePage(getList)
function getList(pageNum: number, pageSize: number) {
return new Promise(resolve => {
appMerchantRecommendListPost({
params: {
pageNum: pageNum,
pageSize: pageSize,
},
body: {
lat: userStore.userLocation.latitude,
lng: userStore.userLocation.longitude,
selfPickup: selfPickup.value, // 是否自提
discount: discount.value, // 是否有折扣 1是 2 否
scoreRange: scoreRange.value || null, // 评分范围 比如 3-4
priceRange: price.value || null, // 价格范围 比如 10-30
merchantCategoryIds: props.merchantCategoryIds ? [props.merchantCategoryIds] : [], // 商家分类id集合(首页中间)
}
}).then(res => {
console.log(res)
resolve({rows: res.rows})
})
})
}
// 是否自提
const selfPickup = ref<number | null>(null)
function togglePickup(value: number) {
selfPickup.value = value;
paging.value.refresh()
}
// 是否有折扣
const discount = ref<number | null>(null)
function toggleDiscount(value: number) {
discount.value = value;
paging.value.refresh()
}
const scoreRef = ref<any>()
const scoreRange = ref<string | null>(null)
const priceChooseRef = ref<any>()
const price = ref<string | null>(null)
function toggleScore() {
if (scoreRef.value) {
scoreRef.value.onOpen();
}
}
function togglePrice() {
if (priceChooseRef.value) {
priceChooseRef.value.onOpen();
}
}
function applyScore(value: string) {
scoreRange.value = value;
}
function applyPrice(value: string) {
price.value = value;
}
</script>
<template>
<z-paging ref="paging" v-model="dataList" @query="queryList">
<template #top>
<view class="bg-white pb-24rpx">
<navbar :title="t('pages.home.featured-on')"/>
<!-- 筛选工具 -->
<filtrate-tool class="mt-32rpx" @togglePickup="togglePickup" @toggleDiscount="toggleDiscount" @toggleScore="toggleScore" @togglePrice="togglePrice" />
</view>
</template>
<view class="p-30rpx">
<template v-for="(item, index) in dataList" :key="index">
<food-box :item="item" />
</template>
</view>
</z-paging>
<score @applyScore="applyScore" ref="scoreRef" />
<price-choose @applyPrice="applyPrice" ref="priceChooseRef" />
</template>
<style scoped lang="scss">
</style>
+59
View File
@@ -0,0 +1,59 @@
<script setup lang="ts">
import {appMerchantRecommendListPost} from "@/service";
import FoodBox from "@/pages/home/components/tabbar-home/components/food-box/index.vue";
import {useUserStore} from "@/store";
const userStore = useUserStore();
const props = defineProps<{
id?: string
}>()
function getList(pageNum: number, pageSize: number) {
return new Promise(resolve => {
appMerchantRecommendListPost({
params: {
pageNum,
pageSize,
},
body: {
lat: userStore.userLocation.latitude,
lng: userStore.userLocation.longitude,
selfPickup: null, // 是否自提
discount: null, // 是否有折扣 1是 2 否
scoreRange: null, // 评分范围 比如 3-4
priceRange: null, // 价格范围 比如 10-30
merchantCategoryIds: [],
merchantLabelIds: props.id ? [props.id] : []
},
}).then(res => {
resolve(res)
})
})
}
const {paging, loading, firstLoaded, dataList, queryList} = usePage(getList)
</script>
<template>
<z-paging
ref="paging"
v-model="dataList"
@query="queryList"
bg-color="#fff"
>
<template #top>
<navbar />
</template>
<view class="p-32rpx">
<template v-for="(item, index) in dataList" :key="index">
<food-box :item="item" />
</template>
</view>
</z-paging>
</template>
<style scoped lang="scss">
</style>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,129 @@
<script setup lang="ts">
import {
onMounted,
getCurrentInstance
} from 'vue';
import Search from "@/pages/home/components/tabbar-home/components/search.vue";
import useEventEmit from "@/hooks/useEventEmit";
import {EventEnum} from "@/constant/enums";
const { t } = useI18n();
import {useAddressStore} from "@/pages/address/store/address";
import {appUserAddressListPost} from "@/service";
const addressStore = useAddressStore()
const addressesList = ref([])
const currentAddressId = ref('')
function getAddressList() {
appUserAddressListPost({
params: {
pageNum: 1,
pageSize: 100,
}
}).then(res => {
console.log('获取用户地址列表', res)
addressesList.value = res.rows
if(res.rows.length > 0) {
currentAddressId.value = res.rows[0].id
}
})
}
function handleClickSearch() {
uni.navigateTo({
url: '/pages-user/pages/search-address/index',
});
}
useEventEmit(EventEnum.CHOOSE_ADDRESS, (data) => {
console.log('搜索的地址信息', data)
if(data) {
addressStore.setAddressLocation({
displayName: data.displayName,
formattedAddress: data.formattedAddress,
longitude: data.location.lng,
latitude: data.location.lat,
})
setTimeout(()=> {
uni.navigateTo({
url: '/pages/address/choose-type'
})
}, 300)
}
})
onShow(()=> {
getAddressList()
})
onLoad((options: any)=> {
if(options.id) {
currentAddressId.value = options.id
}
})
function chooseAddress(item: any) {
if(String(item.id) === String(currentAddressId.value)) return
currentAddressId.value = item.id
eventChannel.emit('acceptDataFromOpenedPage', {
data: currentAddressId.value,
deliveryRemark: item.deliveryRemark,
deliveryType: item.deliveryType,
});
setTimeout(()=> {
uni.navigateBack()
}, 300)
}
let instance = null
let eventChannel = null
onMounted(()=> {
instance = getCurrentInstance().proxy
eventChannel = instance.getOpenerEventChannel();
})
onUnload(()=> {
instance = null
eventChannel = null
})
</script>
<template>
<view>
<navbar :title="t('pages.address.title')" />
<view class="mt-32rpx px-30rpx">
<search :is-auto-jump="false" @clickSearch="handleClickSearch" />
</view>
<view class="mt-64rpx text-40rpx lh-40rpx text-#333 font-bold pl-30rpx pb-24rpx">
{{ t('pages.address.savedAddresses') }}
</view>
<template v-for="item in addressesList">
<view @click="chooseAddress(item)" :class="[String(item.id) === String(currentAddressId) ? 'bg-#F3F3F3' : '']" class="w-full h-156rpx flex-center-sb px-30rpx">
<view class="flex items-center">
<image v-if="String(item.id) === String(currentAddressId)" src="@img/chef/143.png" class="w-44rpx h-44rpx shrink-0 mr-28rpx"></image>
<image v-else src="@img/chef/145.png" class="w-44rpx h-44rpx shrink-0 mr-28rpx"></image>
<view class="flex-1 h-156rpx pt-40rpx">
<view class="text-32rpx lh-32rpx text-#333 font-500 mb-16rpx line-clamp-1">{{ item.formattedAddress }}</view>
<view class="text-28rpx lh-28rpx text-#6D6D6D">{{ item.displayName || '' }}</view>
</view>
</view>
<image
:src="
String(item.id) === String(currentAddressId)
? '/static/images/chef/133.png'
: '/static/images/chef/134.png'
"
class="w-44rpx h-44rpx shrink-0 pl-30rpx"
mode="aspectFit"
/>
</view>
</template>
<template v-if="addressesList.length === 0">
<view class="py-100rpx center">
<image class="w-250rpx h-250rpx" src="@img/chef/100.png"></image>
</view>
</template>
</view>
</template>
<style>
page {
background-color: #fff;
}
</style>
@@ -0,0 +1,91 @@
<script setup lang="ts">
const { t } = useI18n();
import {useConfigStore} from "@/store";
const configStore = useConfigStore();
const emit = defineEmits(['confirm']);
const show = ref(false);
const sortOptions = [
{
label: t('pages-store.store.cancelOrder.informationError'),
value: 'time'
},
{
label: t('pages-store.store.cancelOrder.forgotCoupon'),
value: 'browse'
},
{
label: t('pages-store.store.cancelOrder.wrongLess'),
value: 'thumbsUp'
},
{
label: t('pages-store.store.cancelOrder.dontWant'),
value: 'comment'
}
];
const currentSort = ref(0);
function handleClick(index: number) {
// show.value = false;
currentSort.value = index;
}
function confirmCancel() {
console.log('取消订单', sortOptions[currentSort.value].label)
emit('confirm', sortOptions[currentSort.value].label);
}
function onOpen() {
show.value = true;
}
function handleClose() {
show.value = false;
}
defineExpose({
onOpen,
});
</script>
<template>
<wd-popup
v-model="show"
position="bottom"
@close="handleClose"
>
<view class="pl-30rpx pt-48rpx relative">
<image
@click="handleClose"
src="@img/chef/100404.png"
class="w-28rpx h-28rpx absolute top-30rpx right-30rpx"
mode="aspectFit"
/>
<view class="text-36rpx lh-36rpx text-#333 font-bold text-center mb-68rpx">{{ t('common.cancel') }}</view>
<template v-for="(item, index) in sortOptions">
<view @click="handleClick(index)" class="flex items-center mb-42rpx last:mb-0">
<view class="w-48rpx h-48rpx shrink-0 mr-20rpx">
<image
:src="
index === currentSort
? '/static/images/chef/152.png'
: '/static/images/chef/134.png'
"
class="w-40rpx h-40rpx"
mode="aspectFit"
/>
</view>
<view class="flex-1 text-30rpx lh-30rpx text-#333 flex items-center">{{ item.label }}</view>
</view>
</template>
</view>
<view class="mt-72rpx px-30rpx pb-64rpx">
<wd-button @click="confirmCancel" custom-class="!h-108rpx !text-36rpx !lh-108rpx !text-#fff !rounded-16rpx" block>
{{ t('common.submit') }}
</wd-button>
<view :style="[configStore.iosSafeBottomPlaceholder]"></view>
</view>
</wd-popup>
</template>
<style scoped lang="scss">
</style>
@@ -0,0 +1,88 @@
<script setup lang="ts">
import { useConfigStore } from "@/store";
const configStore = useConfigStore();
import Config from '@/config'
const { t } = useI18n()
const emit = defineEmits(['confirm']);
const areaCode = ref<string>('+1');
const columns = ref<string[]>(Config.phoneCodeList);
const localPhoneValue = ref('');
function confirmRide() {
emit('confirm', {
phone: localPhoneValue.value,
areaCode: areaCode.value,
})
handleClose();
}
const show = ref(false);
function onOpen(data: {phone: string, areaCode: string}) {
if(data.phone){
localPhoneValue.value = data.phone;
}
if(data.areaCode){
areaCode.value = data.areaCode;
}
show.value = true;
}
function handleClose() {
show.value = false;
}
defineExpose({
onOpen,
});
</script>
<template>
<wd-popup v-model="show" position="bottom" @close="handleClose">
<view class="w-full px-30rpx pt-58rpx">
<view class="">
<view class="flex items-center h-108rpx pr-28rpx rounded-12rpx b-1rpx bg-#F6F6F6">
<view class="pr-14rpx text-28rpx">
<wd-picker :columns="columns" v-model="areaCode" />
</view>
<wd-input v-model="localPhoneValue" @confirm="confirmRide" :placeholder="t('components.placeholder')" :no-border="true" placeholder-class="text-#A4A4A4 text-28rpx" custom-class="!w-full !bg-transparent"></wd-input>
</view>
</view>
<view class="mt-40rpx">
<wd-button
@click="confirmRide"
custom-class="!h-108rpx !text-36rpx !rounded-16rpx !bg-#14181B"
block
>
{{ t('common.save') }}
</wd-button>
</view>
<view :style="[configStore.iosSafeBottomPlaceholder]"></view>
</view>
</wd-popup>
</template>
<style scoped lang="scss">
:deep(.wd-picker__cell) {
background-color: transparent !important;
.wd-picker__value {
margin-right: 8rpx !important;
}
}
:deep(.uni-picker-view-wrapper) {
& > uni-picker-view-column:first-of-type .uni-picker-view-group {
.uni-picker-view-indicator {
border-radius: 20rpx 0 0 20rpx !important;
&:after {
border: none;
}
&:before {
border: none;
}
}
}
}
:deep(.wd-picker-view-column__item) {
line-height: 94rpx !important;
}
:deep(.uni-picker-view-indicator) {
height: 94rpx !important;
}
</style>
@@ -0,0 +1,366 @@
<template>
<view class="checkout-skeleton">
<!-- 页面标题骨架 -->
<view class="px-30rpx pt-20rpx">
<view class="page-title-skeleton skeleton-item mb-52rpx"></view>
<!-- 配送方式选择骨架 -->
<view class="delivery-method-skeleton skeleton-item mb-40rpx"></view>
</view>
<!-- 地址信息区域骨架 -->
<view class="mt-4rpx">
<!-- 地址信息项 -->
<view v-for="i in 3" :key="i" class="address-item-skeleton flex-center-sb border-bottom py-36rpx px-30rpx">
<view class="flex items-center">
<view class="address-icon-skeleton skeleton-item mr-28rpx"></view>
<view class="flex-1">
<view class="address-text-skeleton skeleton-item mb-8rpx"></view>
<view class="address-subtext-skeleton skeleton-item w-60%"></view>
</view>
</view>
<view class="arrow-icon-skeleton skeleton-item"></view>
</view>
<!-- 配送时间选择骨架 -->
<view class="flex-center-sb py-36rpx px-30rpx gap-22rpx">
<view class="delivery-time-card-skeleton skeleton-item"></view>
<view class="delivery-time-card-skeleton skeleton-item"></view>
</view>
</view>
<!-- 分隔线 -->
<view class="divider-skeleton skeleton-item"></view>
<!-- 订单信息摘要骨架 -->
<view class="pt-36rpx pb-36rpx">
<view class="px-30rpx">
<view class="order-summary-title-skeleton skeleton-item mb-10rpx"></view>
</view>
<!-- 折叠面板骨架 -->
<view class="collapse-skeleton px-30rpx">
<view class="collapse-header-skeleton flex-center-sb py-30rpx">
<view class="flex items-center">
<view class="store-avatar-skeleton skeleton-item mr-24rpx"></view>
<view>
<view class="store-name-skeleton skeleton-item mb-16rpx"></view>
<view class="store-items-skeleton skeleton-item"></view>
</view>
</view>
<view class="collapse-arrow-skeleton skeleton-item"></view>
</view>
<!-- 商品列表骨架 -->
<view v-for="i in 2" :key="i" class="order-item-skeleton flex-center-sb pl-46rpx pr-30rpx py-30rpx">
<view class="flex items-center">
<view class="item-quantity-skeleton skeleton-item mr-40rpx"></view>
<view>
<view class="item-name-skeleton skeleton-item mb-12rpx"></view>
<view class="item-options-skeleton skeleton-item"></view>
</view>
</view>
<view class="item-price-skeleton skeleton-item"></view>
</view>
</view>
</view>
<!-- 底部按钮区域骨架 -->
<view class="fixed bottom-0 left-0 right-0 bg-white">
<view class="membership-banner-skeleton skeleton-item"></view>
<view class="px-30rpx py-18rpx">
<view class="checkout-button-skeleton skeleton-item"></view>
</view>
<view class="safe-bottom-skeleton"></view>
</view>
</view>
</template>
<script setup lang="ts">
// 结账页面骨架屏组件
</script>
<style scoped lang="scss">
// 通用骨架屏样式
.skeleton-item {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
// 闪烁动画
@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
.checkout-skeleton {
background-color: #fff;
min-height: 100vh;
}
// 通用布局类
.flex-center-sb {
display: flex;
align-items: center;
justify-content: space-between;
}
.center {
display: flex;
align-items: center;
justify-content: center;
}
.border-bottom {
border-bottom: 1rpx solid #f0f0f0;
}
// 导航栏骨架
.navbar-skeleton {
width: 100%;
height: 88rpx;
border-radius: 0;
}
// 页面标题骨架
.page-title-skeleton {
width: 200rpx;
height: 46rpx;
border-radius: 8rpx;
}
// 配送方式选择骨架
.delivery-method-skeleton {
width: 100%;
height: 98rpx;
border-radius: 49rpx;
}
// 地址信息骨架
.address-icon-skeleton {
width: 44rpx;
height: 44rpx;
border-radius: 22rpx;
}
.address-text-skeleton {
width: 300rpx;
height: 32rpx;
border-radius: 8rpx;
}
.address-subtext-skeleton {
width: 150rpx;
height: 28rpx;
border-radius: 6rpx;
}
.arrow-icon-skeleton {
width: 32rpx;
height: 32rpx;
border-radius: 16rpx;
}
// 配送时间卡片骨架
.delivery-time-card-skeleton {
width: 100%;
height: 152rpx;
border-radius: 20rpx;
}
// 分隔线
.divider-skeleton {
width: 100%;
height: 10rpx;
border-radius: 0;
}
// 订单摘要标题骨架
.order-summary-title-skeleton {
width: 350rpx;
height: 32rpx;
border-radius: 8rpx;
}
// 折叠面板骨架
.store-avatar-skeleton {
width: 80rpx;
height: 80rpx;
border-radius: 40rpx;
}
.store-name-skeleton {
width: 200rpx;
height: 30rpx;
border-radius: 8rpx;
}
.store-items-skeleton {
width: 100rpx;
height: 28rpx;
border-radius: 6rpx;
}
.collapse-arrow-skeleton {
width: 32rpx;
height: 32rpx;
border-radius: 16rpx;
}
// 订单项骨架
.item-quantity-skeleton {
width: 48rpx;
height: 48rpx;
border-radius: 8rpx;
}
.item-name-skeleton {
width: 250rpx;
height: 32rpx;
border-radius: 8rpx;
}
.item-options-skeleton {
width: 120rpx;
height: 28rpx;
border-radius: 6rpx;
}
.item-price-skeleton {
width: 100rpx;
height: 32rpx;
border-radius: 8rpx;
}
// 优惠券骨架
.coupon-icon-skeleton {
width: 44rpx;
height: 44rpx;
border-radius: 22rpx;
}
.coupon-text-skeleton {
width: 150rpx;
height: 32rpx;
border-radius: 8rpx;
}
// 小费选择骨架
.tip-icon-skeleton {
width: 44rpx;
height: 44rpx;
border-radius: 22rpx;
}
.tip-title-skeleton {
width: 150rpx;
height: 32rpx;
border-radius: 8rpx;
}
.tip-option-skeleton {
width: 100%;
height: 72rpx;
border-radius: 20rpx;
}
// 配送周卡骨架
.weekly-title-skeleton {
width: 300rpx;
height: 32rpx;
border-radius: 8rpx;
}
.radio-option-skeleton {
width: 80rpx;
height: 32rpx;
border-radius: 8rpx;
}
.weekly-description-skeleton {
width: 100%;
height: 28rpx;
border-radius: 6rpx;
}
// 费用明细骨架
.cost-label-skeleton {
width: 150rpx;
height: 32rpx;
border-radius: 8rpx;
}
.cost-value-skeleton {
width: 100rpx;
height: 32rpx;
border-radius: 8rpx;
}
.total-label-skeleton {
width: 80rpx;
height: 32rpx;
border-radius: 8rpx;
}
.total-value-skeleton {
width: 120rpx;
height: 32rpx;
border-radius: 8rpx;
}
// 支付方式骨架
.payment-icon-skeleton {
width: 44rpx;
height: 44rpx;
border-radius: 22rpx;
}
.payment-text-skeleton {
width: 150rpx;
height: 32rpx;
border-radius: 8rpx;
}
.payment-type-skeleton {
width: 100rpx;
height: 32rpx;
border-radius: 8rpx;
}
// 底部区域骨架
.membership-banner-skeleton {
width: 100%;
height: 76rpx;
border-radius: 0;
}
.checkout-button-skeleton {
width: 100%;
height: 92rpx;
border-radius: 16rpx;
}
.safe-bottom-skeleton {
height: 34rpx;
}
// 响应式设计
@media (max-width: 750rpx) {
.page-title-skeleton {
width: 150rpx;
}
.order-summary-title-skeleton {
width: 280rpx;
}
.weekly-title-skeleton {
width: 250rpx;
}
}
</style>
@@ -0,0 +1,337 @@
<template>
<view class="order-detail-skeleton">
<!-- 页面标题和时间信息骨架 -->
<view class="px-30rpx pt-20rpx pb-50rpx">
<view class="delivery-time-skeleton skeleton-item mb-26rpx"></view>
<view class="order-time-skeleton skeleton-item mb-16rpx"></view>
<view class="cancel-time-skeleton skeleton-item"></view>
</view>
<!-- 订单进度骨架 -->
<view class="mb-52rpx px-30rpx">
<view class="progress-container">
<!-- 进度条背景 -->
<view class="progress-bg-skeleton skeleton-item"></view>
<!-- 进度步骤 -->
<view class="flex justify-between items-center">
<view v-for="i in 5" :key="i" class="flex flex-col items-center">
<!-- 步骤圆点 -->
<view class="progress-dot-skeleton skeleton-item mb-16rpx"></view>
<!-- 步骤标签 -->
<view class="progress-label-skeleton skeleton-item"></view>
</view>
</view>
</view>
</view>
<!-- 分隔线 -->
<view class="divider-skeleton skeleton-item"></view>
<!-- 商品列表骨架 -->
<view class="px-30rpx py-36rpx">
<!-- 商家信息骨架 -->
<view class="flex-center-sb h-80rpx mb-36rpx">
<view class="flex items-center">
<view class="store-avatar-skeleton skeleton-item mr-24rpx"></view>
<view class="store-name-skeleton skeleton-item"></view>
<view class="store-icon-skeleton skeleton-item ml-10rpx"></view>
</view>
<view class="collection-skeleton skeleton-item"></view>
</view>
<!-- 商品项骨架 -->
<view v-for="i in 3" :key="i" class="flex items-start mb-32rpx last:mb-0">
<!-- 商品图片 -->
<view class="product-image-skeleton skeleton-item mr-20rpx"></view>
<!-- 商品信息 -->
<view class="flex-1">
<view class="product-name-skeleton skeleton-item mb-20rpx"></view>
<view class="flex-center-sb mb-24rpx">
<view class="product-specs-skeleton skeleton-item"></view>
<view class="product-quantity-skeleton skeleton-item"></view>
</view>
<view class="product-price-skeleton skeleton-item"></view>
</view>
</view>
</view>
<!-- 分隔线 -->
<view class="divider-skeleton skeleton-item"></view>
<!-- 配送地址骨架 -->
<view class="pt-36rpx">
<view class="address-title-skeleton skeleton-item pl-30rpx mb-4rpx"></view>
<!-- 地址信息项 -->
<view v-for="i in 3" :key="i" class="flex items-center py-36rpx px-30rpx border-bottom">
<view class="address-icon-skeleton skeleton-item mr-28rpx"></view>
<view class="flex-1">
<view class="address-text-skeleton skeleton-item mb-8rpx"></view>
<view class="address-subtext-skeleton skeleton-item"></view>
</view>
</view>
</view>
<!-- 分隔线 -->
<view class="divider-skeleton skeleton-item"></view>
<!-- 订单信息骨架 -->
<view class="border-bottom px-30rpx py-36rpx">
<view class="order-info-title-skeleton skeleton-item mb-40rpx"></view>
<!-- 订单信息项 -->
<view v-for="i in 5" :key="i" class="flex-center-sb mb-40rpx last:mb-0">
<view class="order-info-label-skeleton skeleton-item"></view>
<view class="order-info-value-skeleton skeleton-item"></view>
</view>
</view>
<!-- 费用明细骨架 -->
<view class="border-bottom px-30rpx py-32rpx">
<!-- 费用项 -->
<view v-for="i in 3" :key="i" class="flex-center-sb mb-40rpx last:mb-0">
<view class="cost-label-skeleton skeleton-item"></view>
<view class="cost-value-skeleton skeleton-item"></view>
</view>
</view>
<!-- 底部安全区域 -->
<view class="safe-bottom-skeleton"></view>
</view>
</template>
<script setup lang="ts">
// 订单详情页面骨架屏组件
</script>
<style scoped lang="scss">
// 通用骨架屏样式
.skeleton-item {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
// 闪烁动画
@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
.order-detail-skeleton {
background-color: #fff;
min-height: 100vh;
}
// 通用布局类
.flex-center-sb {
display: flex;
align-items: center;
justify-content: space-between;
}
.border-bottom {
border-bottom: 1rpx solid #f0f0f0;
}
// 导航栏骨架
.navbar-skeleton {
width: 100%;
height: 88rpx;
border-radius: 0;
}
// 页面标题和时间信息骨架
.delivery-time-skeleton {
width: 500rpx;
height: 40rpx;
border-radius: 8rpx;
}
.order-time-skeleton {
width: 300rpx;
height: 28rpx;
border-radius: 6rpx;
}
.cancel-time-skeleton {
width: 400rpx;
height: 28rpx;
border-radius: 6rpx;
}
// 订单进度骨架
.progress-container {
position: relative;
}
.progress-bg-skeleton {
width: 100%;
height: 12rpx;
border-radius: 10rpx;
margin: 18rpx 0;
}
.progress-dot-skeleton {
width: 48rpx;
height: 48rpx;
border-radius: 50%;
}
.progress-label-skeleton {
width: 80rpx;
height: 24rpx;
border-radius: 6rpx;
}
// 分隔线
.divider-skeleton {
width: 100%;
height: 16rpx;
border-radius: 0;
}
// 商家信息骨架
.store-avatar-skeleton {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
}
.store-name-skeleton {
width: 200rpx;
height: 30rpx;
border-radius: 8rpx;
}
.store-icon-skeleton {
width: 32rpx;
height: 32rpx;
border-radius: 16rpx;
}
.collection-skeleton {
width: 60rpx;
height: 60rpx;
border-radius: 30rpx;
}
// 商品信息骨架
.product-image-skeleton {
width: 136rpx;
height: 136rpx;
border-radius: 16rpx;
flex-shrink: 0;
}
.product-name-skeleton {
width: 400rpx;
height: 30rpx;
border-radius: 8rpx;
}
.product-specs-skeleton {
width: 250rpx;
height: 24rpx;
border-radius: 6rpx;
}
.product-quantity-skeleton {
width: 60rpx;
height: 28rpx;
border-radius: 6rpx;
}
.product-price-skeleton {
width: 100rpx;
height: 30rpx;
border-radius: 8rpx;
}
// 地址信息骨架
.address-title-skeleton {
width: 200rpx;
height: 36rpx;
border-radius: 8rpx;
}
.address-icon-skeleton {
width: 44rpx;
height: 44rpx;
border-radius: 22rpx;
}
.address-text-skeleton {
width: 350rpx;
height: 32rpx;
border-radius: 8rpx;
}
.address-subtext-skeleton {
width: 200rpx;
height: 28rpx;
border-radius: 6rpx;
}
// 订单信息骨架
.order-info-title-skeleton {
width: 250rpx;
height: 36rpx;
border-radius: 8rpx;
}
.order-info-label-skeleton {
width: 150rpx;
height: 30rpx;
border-radius: 8rpx;
}
.order-info-value-skeleton {
width: 200rpx;
height: 30rpx;
border-radius: 8rpx;
}
// 费用明细骨架
.cost-label-skeleton {
width: 180rpx;
height: 32rpx;
border-radius: 8rpx;
}
.cost-value-skeleton {
width: 120rpx;
height: 32rpx;
border-radius: 8rpx;
}
// 底部安全区域
.safe-bottom-skeleton {
height: 100rpx;
}
// 响应式设计
@media (max-width: 750rpx) {
.delivery-time-skeleton {
width: 400rpx;
}
.product-name-skeleton {
width: 300rpx;
}
.address-text-skeleton {
width: 280rpx;
}
.order-info-value-skeleton {
width: 150rpx;
}
}
</style>
@@ -0,0 +1,129 @@
<script setup lang="ts">
import { computed } from 'vue'
const { t } = useI18n()
// 定义步骤类型
interface OrderStep {
label: string
value: number
}
const props = defineProps<{
steps?: OrderStep[]
currentStatus?: number
}>()
const steps = computed(() => props.steps ?? [
{ label: t('orderStatus.ordered'), value: 1 },
{ label: t('orderStatus.paid'), value: 2 },
{ label: t('orderStatus.received'), value: 3 },
{ label: t('orderStatus.delivering'), value: 4 },
{ label: t('orderStatus.delivered'), value: 5 },
])
const currentStatus = computed(() => props.currentStatus ?? 1)
// 计算每个步骤的激活状态
const processedSteps = computed(() => {
return steps.value.map(step => ({
...step,
active: currentStatus.value >= step.value
}))
})
// 计算进度条的宽度百分比
const progressWidth = computed(() => {
if (steps.value.length <= 1) return 0
// 找到当前状态在步骤数组中的位置
const currentStepIndex = steps.value.findIndex(step => step.value === props.currentStatus)
const activeStepIndex = currentStepIndex >= 0 ? currentStepIndex : -1
if (activeStepIndex < 0) return 0
const totalSteps = steps.value.length
const width = Math.min(activeStepIndex / (totalSteps - 1) * 100, 100)
if(width >= 50 && width < 100) {
return width - 3
}
return width
})
</script>
<template>
<view class="order-progress">
<view class="relative">
<!-- 进度条背景 -->
<view class="progress-bg w-full h-12rpx bg-#F2F2F2 rounded-10rpx absolute top-18rpx left-0"></view>
<!-- 进度条填充 -->
<view
class="progress-fill h-12rpx bg-#333333 rounded-10rpx absolute top-18rpx left-0 transition-all duration-300"
:style="{ width: `${progressWidth}%` }"
></view>
<!-- 进度步骤 -->
<view class="flex-center-sb relative z-1">
<view
v-for="(step, index) in processedSteps"
:key="step.value"
:class="[index === 0 ? '!items-start' : '', index === processedSteps.length - 1 ? '!items-end' : '']"
class="flex flex-col items-center step-item"
>
<!-- 步骤圆点 -->
<view
:class="[
'w-48rpx h-48rpx rounded-full transition-all duration-300',
index === processedSteps.length - 1 ? 'flex items-center justify-end' : 'center'
]"
>
<!-- 激活状态显示勾选图标 -->
<image
v-if="step.active"
src="@img/chef/1338.png"
mode="aspectFill"
class="w-48rpx h-48rpx"
/>
<!-- 未激活状态显示空心圆 -->
<image
v-else
src="@img/chef/1339.png"
mode="aspectFill"
class="w-36rpx h-36rpx"
/>
</view>
<!-- 步骤文字 -->
<view :class="[step.active ? 'text-#333 font-500' : 'text-#9E9E9E' ]" class="text-24rpx lh-24rpx mt-12rpx">{{ step.label }}</view>
</view>
</view>
</view>
</view>
</template>
<style scoped>
.order-progress {
position: relative;
}
.step-item {
position: relative;
z-index: 2;
}
.check-icon {
mask: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjciIGhlaWdodD0iMTgiIHZpZXdCb3g9IjAgMCAyNyAxOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTI2LjIyIDMuNDJMMTEuODIgMTcuODJDMTEuMSAxOC41NCA5LjkgMTguNTQgOS4xOCAxNy44Mkw5LjQyIDE3LjgyQzkuMTggMTcuODIgOC43IDE3LjU4IDguNDYgMTcuMzRMMC41NCA5LjQyQy0wLjE4IDguNyAtMC4xOCA3LjUgMC41NCA2LjU0QzEuMjYgNS44MiAyLjQ2IDUuODIgMy40MiA2LjU0TDEwLjYyIDEzLjc0TDIzLjgyIDAuNTM5OTk5QzI0LjU0IC0wLjE3OTk5OSAyNS43NCAtMC4xNzk5OTkgMjYuNDYgMC41Mzk5OTlMMjYuNyAwLjc4QzI2Ljk0IDEuNSAyNi45NCAyLjcgMjYuMjIgMy40MloiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=') no-repeat center;
mask-size: contain;
-webkit-mask: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjciIGhlaWdodD0iMTgiIHZpZXdCb3g9IjAgMCAyNyAxOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTI2LjIyIDMuNDJMMTEuODIgMTcuODJDMTEuMSAxOC41NCA5LjkgMTguNTQgOS4xOCAxNy44Mkw5LjQyIDE3LjgyQzkuMTggMTcuODIgOC43IDE3LjU4IDguNDYgMTcuMzRMMC41NCA5LjQyQy0wLjE4IDguNyAtMC4xOCA3LjUgMC41NCA2LjU0QzEuMjYgNS44MiAyLjQ2IDUuODIgMy40MiA2LjU0TDEwLjYyIDEzLjc0TDIzLjgyIDAuNTM5OTk5QzI0LjU0IC0wLjE3OTk5OSAyNS43NCAtMC4xNzk5OTkgMjYuNDYgMC41Mzk5OTlMMjYuNyAwLjc4QzI2Ljk0IDEuNSAyNi45NCAyLjcgMjYuMjIgMy40MloiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=') no-repeat center;
-webkit-mask-size: contain;
}
.progress-bg {
z-index: 0;
}
.progress-fill {
z-index: 1;
}
</style>
@@ -0,0 +1,95 @@
<script setup lang="ts">
import dayjs from 'dayjs'
const { t } = useI18n();
import {useConfigStore, useUserStore} from "@/store";
const configStore = useConfigStore();
const userStore = useUserStore();
// 用户会员状态是否已开通
const isUserMember = computed(()=> {
if(!userStore.userInfo.userMembershipVo) return false
if(userStore.userInfo.userMembershipVo && userStore.userInfo.userMembershipVo.expireTime ){
return dayjs().isBefore(dayjs(Number(userStore.userInfo.userMembershipVo.expireTime)))
} else {
return false
}
})
const show = ref(false);
const priceData = ref({})
const savings = ref(0)
const serviceFees = ref(0)
function onOpen(data: any, num: number) {
priceData.value = data;
savings.value = num;
// 计算服务费,使用分为单位避免浮点精度丢失
const original = Number(priceData.value?.actualAmount) || 0;
const ratio = Number(priceData.value?.merchantVo?.platformServiceFeeRatio) || 0;
const tip = Number(priceData.value?.tip) || 0;
const deliveryFee = Number(priceData.value?.deliveryFee) || 0;
console.log('原价', original)
console.log('服务费率', ratio)
console.log('小费', tip)
console.log('配送费', deliveryFee)
const toCents = (n: number) => Math.round(n * 100);
const serviceFeeCents = Math.round(toCents(original) * ratio);
const totalCents = toCents(tip) + toCents(deliveryFee);
serviceFees.value = totalCents / 100; // 保持为 number,模板显示时可格式化
show.value = true;
}
function handleClose() {
show.value = false;
}
defineExpose({
onOpen,
});
</script>
<template>
<wd-popup
v-model="show"
position="bottom"
@close="handleClose"
>
<view>
<view class="center h-102rpx bg-#F7F7F7 text-40rpx lh-40rpx font-bold text-#333">
{{ t('pages-store.checkout.priceDetail.title') }}?
</view>
<view class="border-bottom text-32rpx lh-32rpx font-500 text-#333 py-36rpx px-30rpx">
<view class="flex-center-sb mb-20rpx">
<view>{{ t('pages-store.checkout.priceDetail.serviceFees') }}</view>
<!-- <view>${{ (Number(priceData?.tip) + Number(priceData?.deliveryFee)).toFixed(2) }}</view>-->
<view>${{ serviceFees }}</view>
</view>
<view class="text-24rpx lh-28rpx text-#9E9E9E">
{{ t('pages-store.checkout.priceDetail.desc') }}
</view>
</view>
<view class="flex-center-sb border-bottom h-104rpx text-32rpx lh-32rpx font-500 text-#333 px-30rpx">
<view>{{ t('pages-store.checkout.priceDetail.memberDiscount') }}</view>
<view>-${{ isUserMember ? savings : 0 }}</view>
</view>
<view class="flex-center-sb border-bottom h-104rpx text-32rpx lh-32rpx font-500 text-#333 px-30rpx">
<view>{{ t('pages-store.checkout.priceDetail.taxation') }}</view>
<view>${{ Number(priceData?.tax) }}</view>
</view>
<view class="px-30rpx pt-40rpx">
<wd-button
@click="handleClose"
custom-class="!h-108rpx !text-36rpx !rounded-16rpx !bg-#14181B"
block
>
{{ t('common.gotIt') }}
</wd-button>
</view>
<view :style="[configStore.iosSafeBottomPlaceholder]"></view>
</view>
</wd-popup>
</template>
<style scoped lang="scss">
</style>
@@ -0,0 +1,105 @@
<script setup lang="ts">
interface Props {
// 当前评分值
modelValue: number
// 最大星级数量
maxStars?: number
// 星星大小
starSize?: number
// 激活颜色
activeColor?: string
// 未激活颜色
inactiveColor?: string
// 是否显示评分数值
showScore?: boolean
// 是否禁用交互
disabled?: boolean
// 自定义类名
customClass?: string
}
interface Emits {
(e: 'update:modelValue', value: number): void
(e: 'change', value: number): void
}
const props = withDefaults(defineProps<Props>(), {
maxStars: 5,
starSize: 44,
activeColor: '#F86F1F',
inactiveColor: '#D1D1D1',
showScore: true,
disabled: false,
customClass: ''
})
const emit = defineEmits<Emits>()
// 点击星星设置评分
const setRating = (rating: number) => {
if (props.disabled) return
emit('update:modelValue', rating)
emit('change', rating)
}
</script>
<template>
<view :class="['star-rating flex items-center', customClass]">
<!-- 星级评分 -->
<view class="flex items-center gap-20rpx">
<view
v-for="star in maxStars"
:key="star"
:class="[
'mr-8rpx last:mr-0 flex-center',
disabled ? '' : 'cursor-pointer'
]"
@click="setRating(star)"
>
<image
v-show="star <= modelValue"
src="@img/chef/155.png"
mode="aspectFill"
class="w-44rpx h-44rpx"
/>
<image
v-show="star > modelValue"
src="@img/chef/154.png"
mode="aspectFill"
class="w-44rpx h-44rpx"
/>
</view>
</view>
<!-- 评分数值 -->
<text
v-if="showScore"
class="text-28rpx lh-28rpx text-#333 font-500 ml-80rpx"
>
{{ modelValue }}.0
</text>
</view>
</template>
<style scoped lang="scss">
.star-rating {
.flex-center {
display: flex;
align-items: center;
justify-content: center;
}
.cursor-pointer {
cursor: pointer;
}
.bg-active {
transition: background-color 0.2s ease;
}
.bg-inactive {
transition: background-color 0.2s ease;
}
}
</style>
+481
View File
@@ -0,0 +1,481 @@
<script setup lang="ts">
import {appMerchantOrderDetailPost, appMerchantOrderEvaluateOrderPost, type MerchantOrderVo} from "@/service";
const {t} = useI18n()
import StarRating from './components/star-rating.vue'
import ChooseImage from "@/components/choose-image/choose-image.vue";
import {getDictFineList} from "@/pages-store/service";
// 配送员评分
const deliveryRating = ref(4)
// 商品评分
const goodsRating = ref(4)
// 评价文字
const reviewText = ref('')
const maxTextLength = 200
// 上传的图片列表
const uploadedImages = ref<string[]>([])
// 评价配送员图片
const deliveryUploadedImages = ref<string[]>([])
const maxImages = 3
// 评价标签
const deliveryTags = ref([])
// 计算剩余字数
const remainingChars = computed(() => {
return maxTextLength - reviewText.value.length
})
// 切换标签选中状态
const toggleTag = (item: any) => {
item.selected = !item.selected
}
// 删除图片
const removeImage = (index: number) => {
uploadedImages.value.splice(index, 1)
}
// 预览图片
const previewImage = (index: number) => {
uni.previewImage({
current: index,
urls: uploadedImages.value
})
}
// 提交评价
const submitReview = () => {
if (reviewText.value.trim() === '') {
uni.showToast({
title: '请输入评价内容',
icon: 'none'
})
return
}
const reviewData = {
deliveryRating: deliveryRating.value,
goodsRating: goodsRating.value,
reviewText: reviewText.value,
deliveryTags: deliveryTags.value.filter(t => t.selected).map(t => t.text),
goodsTags: goodsTags.value.filter(t => t.selected).map(t => t.text),
images: uploadedImages.value
}
console.log('提交评价:', reviewData)
uni.showToast({
title: '评价提交成功',
icon: 'none'
})
// 返回上一页
setTimeout(() => {
uni.navigateBack()
}, 1500)
}
// 返回上一页
const goBack = () => {
uni.navigateBack()
}
const chooseImageRef = ref()
// 点击上传图片的类型
const uploadType = ref(1)
const dishIndex = ref(0)
const handleChooseImage = (type: number, index?: number) => {
uploadType.value = type
if(type === 2) {
// 菜品图片上传
dishIndex.value = index
}
chooseImageRef.value.init()
}
function onImageChange(files: string[]) {
console.log(files)
// form.value.images = files.join(',')
switch (uploadType.value) {
case 1:
deliveryUploadedImages.value.push(...files)
break
case 2:
// uploadedImages.value.push(...files)
formData.value.merchantDishReviewList[dishIndex.value].uploadedImages.push(...files)
break
}
}
const orderId = ref('')
onLoad((options) => {
if(options.id) {
orderId.value = options.id;
appMerchantOrderDetail()
// 获取评价标签
getDeliveryTags()
}
})
const loading = ref(true)
const orderDetail = ref<MerchantOrderVo>()
function appMerchantOrderDetail() {
loading.value = true;
appMerchantOrderDetailPost({
params: {
orderId: orderId.value,
}
}).then((res: any)=> {
console.log('订单详情', res)
orderDetail.value = res.data
// 是自取订单还是配送订单 1-派送 2-自取 orderDetail.value.receiveMethod
if(res.data && res.data.merchantOrderDishVoList.length > 0) {
formData.value.merchantDishReviewList = res.data.merchantOrderDishVoList.map(item=> {
return {
dishId: item.dishId, // 菜品ID
merchantId: item.merchantId, // 菜品ID
merchantOrderDishId: item.id,//商家订单关联菜品ID
dishImage: item.merchantDishVo?.dishImage?.split(',')[0],
dishName: item.merchantDishVo?.dishName,
dishDescription: item.merchantDishVo?.dishDescription,
rating: 0,
content: '',
images: '',
uploadedImages: [],
}
})
}
}).finally(() => {
loading.value = false;
})
}
function getDeliveryTags() {
getDictFineList({
dictType: 'review_tag'
}).then(res=> {
console.log('评价标签', res)
if(res.data) {
deliveryTags.value = res.data.map((item) => {
return {
text: item.dictLabel,
selected: false
}
})
}
})
}
// 是自取订单还是配送订单
const isSelfPickup = computed(()=> {
return orderDetail.value?.receiveMethod === 2
})
const formData = ref({
deliveryReview: {
tags: '',
rating: 0,
content: '',
images: '',
}, // 配送员评价信息
merchantDishReviewList: [
{
dishId: '', // 菜品ID
dishImage: '',
dishName: '',
dishDescription: '',
rating: 0,
content: '',
images: '',
uploadedImages: [],
}
]
})
function submitForm() {
// 表单验证
if(!isSelfPickup.value) {
// 配送订单需要验证配送员评价
if(formData.value.deliveryReview.rating === 0) {
uni.showToast({
title: t('common.pleaseRateDelivery'),
icon: 'none'
})
return
}
if(!formData.value.deliveryReview.content || formData.value.deliveryReview.content.trim() === '') {
uni.showToast({
title: t('common.pleaseEnterDeliveryReview'),
icon: 'none'
})
return
}
}
// 验证菜品评价
for(let i = 0; i < formData.value.merchantDishReviewList.length; i++) {
const dish = formData.value.merchantDishReviewList[i]
if(dish.rating === 0) {
uni.showToast({
title: `${t('common.pleaseRateDish')} "${dish.dishName}"`,
icon: 'none'
})
return
}
if(!dish.content || dish.content.trim() === '') {
uni.showToast({
title: `${t('common.pleaseEnterDishReview')} "${dish.dishName}"`,
icon: 'none'
})
return
}
}
if(!isSelfPickup.value) {
// 配送订单
formData.value.deliveryReview.tags = deliveryTags.value.filter(t => t.selected).map(t => t.text).join(',')
formData.value.deliveryReview.images = deliveryUploadedImages.value.join(',')
}
formData.value.merchantDishReviewList.forEach(item=> {
item.images = item.uploadedImages.join(',')
})
console.log('提交评价', formData.value)
if(isSelfPickup.value) {
// 自取订单
formData.value.deliveryReview = null
}
appMerchantOrderEvaluateOrderPost({
body: {
orderId: orderId.value,
...formData.value,
}
}).then(res=> {
console.log('评价成功', res)
setTimeout(()=> {
uni.redirectTo({
url: '/pages-store/pages/order/view-reviews?id=' + orderId.value
})
},1000)
})
}
</script>
<template>
<view class="pb-100rpx">
<!-- 导航栏 -->
<navbar customClass="!bg-transparent" />
<view class="pl-30rpx pt-20rpx text-46rpx lh-46rpx font-bold text-#333 mb-52rpx">{{ t("common.evaluate") }}</view>
<!-- 配送员评价区域 -->
<view v-if="!isSelfPickup" class="bg-white px-30rpx py-40rpx mb-20rpx">
<text class="text-36rpx lh-36rpx text-#333 font-500 block mb-34rpx">{{ t('pages-store.view-reviews.deliveryDriver') }}</text>
<view class="bg-#F7F7F7 h-100rpx rounded-16rpx pl-16rpx flex items-center mb-28rpx">
<image
:src="orderDetail?.deliveryAvatar"
mode="aspectFill"
class="w-80rpx h-80rpx rounded-full mr-20rpx"
/>
<text class="text-28rpx lh-28rpx text-#333">{{ orderDetail?.deliveryFirstName }} {{ orderDetail?.deliverySurname }}</text>
</view>
<!-- 评分区域 -->
<view class="flex items-center mb-40rpx">
<text class="text-28rpx lh-28rpx text-#333 font-500 mr-66rpx">{{ t('pages-store.view-reviews.Score') }}</text>
<StarRating v-model="formData.deliveryReview.rating" />
</view>
<!-- 评价标签 -->
<view class="flex flex-wrap gap-16rpx">
<view
v-for="tag in deliveryTags"
:key="tag.id"
:class="[
'px-24rpx h-50rpx rounded-25rpx border-1rpx text-24rpx center transition-all',
tag.selected
? 'bg-#333333 text-white'
: 'bg-#F6F6F6 text-#999'
]"
@click="toggleTag(tag)"
>
{{ tag.text }}
</view>
</view>
<view
class="mt-30rpx min-h-206rpx box-border bg-#F6F6F6 rounded-20rpx overflow-hidden p-20rpx pb-40rpx relative"
>
<wd-textarea
v-model="formData.deliveryReview.content"
:maxlength="200"
custom-class="!bg-#F6F6F6"
custom-textarea-container-class="!bg-#F6F6F6"
no-border
auto-height
:placeholder="t('common.enter')"
/>
<view class="absolute bottom-26rpx right-20rpx text-28rpx lh-28rpx text-#999">{{ formData.deliveryReview.content.length }}/200</view>
</view>
<!-- 图片上传区域 -->
<view class="mt-34rpx">
<view class="flex flex-wrap gap-20rpx">
<!-- 已上传的图片 -->
<view
v-for="(image, index) in deliveryUploadedImages"
:key="index"
class="relative w-158rpx h-158rpx"
>
<image
:src="image"
mode="aspectFill"
class="w-full h-full rounded-16rpx bg-#F5F5F5"
@click="previewImage(index)"
/>
<!-- 删除按钮 -->
<image
@click="removeImage(index)"
src="@img/chef/113.png"
class="absolute top--10rpx right--10rpx z-1 w-36rpx h-36rpx"
></image>
</view>
<!-- 添加图片按钮 -->
<view
v-if="deliveryUploadedImages.length < maxImages"
class="w-158rpx h-158rpx bg-#F5F5F5 rounded-16rpx flex-center cursor-pointer"
@click="handleChooseImage(1)"
>
<image
src="@img/chef/112.png"
class="w-158rpx h-158rpx"
></image>
</view>
</view>
</view>
</view>
<!-- 商品评价区域 -->
<view class="pt-40rpx pb-32rpx bg-white">
<text class="pl-30rpx text-36rpx lh-36rpx text-#333 font-500 block mb-34rpx">{{ t('pages-store.view-reviews.goods') }}</text>
<template v-for="(item, index) in formData.merchantDishReviewList">
<view class="px-30rpx border-b-1rpx border-b-solid border-b-#D8D8D8 pb-32rpx mb-32rpx last:mb-0 last:border-none">
<view class="bg-#F7F7F7 h-100rpx rounded-16rpx pl-16rpx pr-24rpx flex items-center mb-28rpx">
<image
:src="item.dishImage"
mode="aspectFill"
class="w-80rpx h-80rpx rounded-16rpx mr-20rpx shrink-0"
/>
<view class="h-80rpx mt-10rpx">
<text class="text-28rpx text-#333 line-clamp-1">{{ item.dishName }}</text>
<view class="text-24rpx lh-24rpx text-#999 mt-12rpx line-clamp-1">{{ item.dishDescription }}</view>
</view>
</view>
<!-- 评分区域 -->
<view class="flex items-center mb-28rpx">
<text class="text-28rpx lh-28rpx text-#333 font-500 mr-66rpx">{{ t('pages-store.view-reviews.Score') }}</text>
<StarRating v-model="item.rating" />
</view>
<view
class="mt-30rpx min-h-206rpx box-border bg-#F6F6F6 rounded-20rpx overflow-hidden p-20rpx pb-40rpx relative"
>
<wd-textarea
v-model="item.content"
:maxlength="200"
custom-class="!bg-#F6F6F6"
custom-textarea-container-class="!bg-#F6F6F6"
no-border
auto-height
:placeholder="t('common.enter')"
/>
<view class="absolute bottom-26rpx right-20rpx text-28rpx lh-28rpx text-#999">{{ item.content.length }}/200</view>
</view>
<!-- 图片上传区域 -->
<view class="mt-34rpx">
<view class="flex flex-wrap gap-20rpx">
<!-- 已上传的图片 -->
<view
v-for="(image, index) in item.uploadedImages"
:key="index"
class="relative w-158rpx h-158rpx"
>
<image
:src="image"
mode="aspectFill"
class="w-full h-full rounded-16rpx bg-#F5F5F5"
@click="previewImage(index)"
/>
<!-- 删除按钮 -->
<image
@click="removeImage(index)"
src="@img/chef/113.png"
class="absolute top--10rpx right--10rpx z-1 w-36rpx h-36rpx"
></image>
</view>
<!-- 添加图片按钮 -->
<view
v-if="item.uploadedImages.length < maxImages"
class="w-158rpx h-158rpx bg-#F5F5F5 rounded-16rpx flex-center cursor-pointer"
@click="handleChooseImage(2, index)"
>
<image
src="@img/chef/112.png"
class="w-158rpx h-158rpx"
></image>
</view>
</view>
</view>
</view>
</template>
</view>
<!-- 底部提交按钮 -->
<fixed-bottom-large-btn
class="z-100"
:fixed="true"
:text="`${t('common.submit')}`"
@click="submitForm"
/>
<ChooseImage ref="chooseImageRef" @change="onImageChange" />
</view>
</template>
<style>
page {
background-color: #F6F6F6;
}
</style>
<style scoped lang="scss">
.evaluate-page {
padding-bottom: 200rpx; // 为底部按钮留出空间
}
.flex-center {
display: flex;
align-items: center;
justify-content: center;
}
.cursor-pointer {
cursor: pointer;
}
.transition-all {
transition: all 0.3s ease;
}
// 修复 textarea 在某些平台的样式问题
textarea {
box-sizing: border-box;
font-family: inherit;
}
</style>
+688
View File
@@ -0,0 +1,688 @@
<script setup lang="ts">
import {
appMerchantOrderDetailPost,
appCollectCollectPost,
type MerchantOrderVo,
appUserCardSelectDefaultPost, appMerchantOrderPayOrderPost, appMerchantOrderCancelOrderPost
} from "@/service";
import { debounce } from 'throttle-debounce'
const { t } = useI18n();
import OrderProgress from './components/order-progress.vue'
import OrderDetailSkeleton from './components/order-detail-skeleton.vue'
import Collection from "@/components/collection/index.vue";
import PriceDetail from "@/pages-store/pages/order/components/price-detail.vue";
import CancelOrder from "@/pages-store/pages/order/components/cancel-order.vue";
import {useConfigStore} from "@/store";
import {CollectionType, OrderStatus, EventEnum, OrderCancelStatus} from "@/constant/enums";
import {formatTimestampWithMonthName, formatTimestampShort, navGoogleMap, callPhone} from "@/utils/utils";
import useEventEmit from "@/hooks/useEventEmit";
const configStore = useConfigStore();
// 价格明细
const priceDetailRef = ref<InstanceType<typeof PriceDetail>>();
// 打开价格明细
const openPriceDetail = () => {
priceDetailRef.value?.onOpen({
tip: orderDetail.value.tip,
tax: orderDetail.value.tax,
deliveryFee: orderDetail.value.deliveryFee,
}, 0);
};
// 取消订单
const cancelOrderRef = ref<InstanceType<typeof CancelOrder>>();
// 打开取消订单
const openCancelOrder = () => {
cancelOrderRef.value?.onOpen();
};
function confirmCancel(reason: string) {
console.log('取消订单', reason)
appMerchantOrderCancelOrderPost({
body: {
orderId: orderDetail.value.id,
cancelReason: reason,
}
}).then(res=> {
uni.showToast({
title: '取消订单成功',
icon: 'none',
})
setTimeout(() => {
appMerchantOrderDetail()
}, 500)
})
}
// 订单步骤数据(配送订单)
const orderSteps = ref([
{ label: t('pages-store.order.orderStatus.ordered'), value: OrderStatus.PENDING_PAYMENT }, // 代付款
{ label: t('pages-store.order.orderStatus.paid'), value: OrderStatus.HAS_PENDING_PAYMENT }, // 已付款
{ label: t('pages-store.order.orderStatus.received'), value: OrderStatus.MERCHANT_ACCEPTED },// 商家已接单
{ label: t('pages-store.order.orderStatus.delivering'), value: OrderStatus.DELIVERING }, // 配送中
{ label: t('pages-store.order.orderStatus.delivered'), value: OrderStatus.COMPLETED }// 已送达
])
// 订单步骤数据(自取订单)
const orderStepsCancel = ref([
{ label: t('pages-store.order.orderStatus.ordered'), value: OrderStatus.PENDING_PAYMENT }, // 代付款
{ label: t('pages-store.order.orderStatus.ready'), value: OrderStatus.MERCHANT_ACCEPTED }, // 商家接单
{ label: t('pages-store.order.orderStatus.completed'), value: OrderStatus.COMPLETED },// 已核销
])
// 页面加载状态
const loading = ref(true);
const orderId = ref('')
onLoad((options: any)=> {
if(options.id) {
orderId.value = options.id;
// appMerchantOrderDetail()
// 查询用户默认信用卡
appUserCardSelectDefault()
}
})
onShow(()=> {
nextTick(()=> {
appMerchantOrderDetail()
})
})
const orderDetail = ref<MerchantOrderVo>()
function appMerchantOrderDetail() {
loading.value = true;
appMerchantOrderDetailPost({
params: {
orderId: orderId.value,
}
}).then((res: any)=> {
console.log('订单详情', res)
orderDetail.value = res.data
// 是自取订单还是配送订单 1-派送 2-自取
if(orderDetail.value) {
if(+orderDetail.value.receiveMethod === 2) {
orderSteps.value = orderStepsCancel.value
}
}
}).finally(() => {
loading.value = false;
})
}
// 订单状态
const orderStatus = computed(() => {
if(orderDetail.value) {
return orderDetail.value.orderStatus
}
return ''
})
// 复制订单号
const copyOrderNumber = (text: string) => {
if(!text) return
uni.setClipboardData({
data: text,
success: () => {
uni.showToast({
title: t('toast.orderNumberCopied'),
icon: 'none'
})
}
})
}
// 拨打电话
const callPhoneFn = (phone: string) => {
callPhone(phone)
}
// 收藏店铺
function handleCollectionClick() {
debouncedEmit(orderDetail.value?.merchantVo?.id, CollectionType.STORE, ()=> {
if (orderDetail.value?.merchantVo) {
orderDetail.value.merchantVo.isCollect = !orderDetail.value.merchantVo.isCollect
}
})
}
// 防抖处理函数
const debouncedEmit = debounce(1300, (id: any, type: CollectionType, callback: ()=> void) => {
// 收藏接口
appCollectCollectPost({
body: {
targetId: id,
targetType: type
}
}).then(res=> {
callback()
})
}, {
atBegin: true, // 立即触发
})
// 支付参数
const payMethodOptions = ref({
orderId: '',
cardId: '',
payMethod: 1, // 支付方式 1信用卡 2余额
payPassword: '',
})
useEventEmit(EventEnum.CHOOSE_PAYMENT_METHOD, (data) => {
if(data) {
if(data.payMethod === 1) {
payMethodOptions.value.cardId = data.cardId
payMethodOptions.value.cardNumber = data.cardNumber
payMethodOptions.value.payMethod = 1
} else {
payMethodOptions.value.payMethod = 2
}
}
})
function appUserCardSelectDefault() {
appUserCardSelectDefaultPost({}).then(res=> {
console.log('查询用户默认信用卡', res)
payMethodOptions.value.cardId = res.data?.cardId || ''
payMethodOptions.value.cardNumber = res.data?.cardNumber || ''
})
}
// 立即支付订单
function goPay() {
// 如果是余额支付,弹出支付密码弹窗
if(payMethodOptions.value.payMethod === 2) {
passwordInputRef.value?.showPasswordInput()
} else {
appMerchantOrderPayOrder()
}
}
function payPawSuccess(password: string) {
payMethodOptions.value.payPassword = password
appMerchantOrderPayOrder()
}
function appMerchantOrderPayOrder() {
appMerchantOrderPayOrderPost({
body: {
...payMethodOptions.value,
orderId: orderDetail.value.id,
}
}).then(res=> {
console.log('支付结果', res)
uni.showToast({
title: '支付成功',
icon: 'none'
})
setTimeout(()=> {
appMerchantOrderDetail()
}, 500)
})
}
function navGoogleMapFn() {
navGoogleMap(orderDetail.value?.merchantVo?.latitude + ',' + orderDetail.value?.merchantVo?.longitude)
}
const useCodeRef = ref()
function openQrCode() {
useCodeRef.value?.init(orderDetail.value?.orderNo || '')
}
function navigateTo(url: string) {
uni.navigateTo({ url })
}
</script>
<template>
<navbar />
<view
class="animate-in fade-in animate-ease-out animate-duration-300"
v-show="loading"
>
<!-- 骨架屏 -->
<OrderDetailSkeleton />
</view>
<view
class="animate-in fade-in animate-ease-in animate-duration-300"
v-if="!loading"
>
<!-- 已取消-成功取消 -->
<template v-if="orderStatus === OrderStatus.CANCELLED">
<view class="px-30rpx pt-20rpx pb-50rpx">
<view class="text-40rpx lh-36rpx text-#333 font-500">{{ t('pages-store.order.cancelled') }}</view>
<view class="text-28rpx lh-28rpx text-#7D7D7D mt-26rpx">
{{ t('pages-store.order.cancellationTime') }}: {{ formatTimestampShort(orderDetail?.cancelTime) }}
</view>
</view>
</template>
<!-- 已取消-等待商家同意 -->
<template v-else-if="orderDetail?.refundStatus === OrderCancelStatus.APPLIED">
<view class="px-30rpx pt-20rpx pb-50rpx">
<view class="text-40rpx lh-36rpx text-#333 font-500">{{ t('pages-store.order.cancellationTitle') }}</view>
<view class="text-28rpx lh-28rpx text-#7D7D7D mt-26rpx">
{{ t('pages-store.order.cancellationReasonDesc') }}
</view>
<view class="text-28rpx lh-28rpx text-#7D7D7D mt-26rpx">{{ t('pages-store.order.cancellationReason') }}{{ orderDetail?.cancelReason }}</view>
</view>
</template>
<!-- 商家拒绝订单 -->
<template v-if="orderStatus === OrderStatus.MERCHANT_REJECTED">
<view class="px-30rpx pt-20rpx pb-50rpx">
<view class="text-40rpx lh-36rpx text-#333 font-500">{{ t('pages-store.order.orderStatus.merchantRejected') }}</view>
<view class="text-28rpx lh-28rpx text-#7D7D7D mt-26rpx">{{ t('pages-store.order.orderStatus.merchantRejectedDesc') }}</view>
</view>
</template>
<template v-else>
<view class="px-30rpx pt-20rpx pb-50rpx">
<view class="text-40rpx lh-36rpx text-#333 font-500">
<template v-if="orderDetail?.receiveMethod === 1">
{{ t('pages-store.order.estimatedDeliveryTime') }}
</template>
<template v-else>{{ t('pages-store.order.upTime') }}</template>
:
<!-- 订单预计送达时间-->
{{ formatTimestampShort(orderDetail?.endScheduledTime) }}
</view>
<view class="text-28rpx lh-28rpx text-#7D7D7D mt-26rpx">
<view>{{ t('pages-store.order.orderTime') }}: {{ formatTimestampShort(orderDetail?.createTime) }}</view>
<!-- 订单30分钟自动取消--待支付 -->
<view v-if="orderStatus === OrderStatus.PENDING_PAYMENT">{{ t('pages-store.order.autoCancellation') }}</view>
</view>
<view class="text-28rpx lh-28rpx text-#7D7D7D mt-26rpx" v-if="orderDetail?.refundStatus === OrderCancelStatus.REJECTED">
{{ t('pages-store.order.rejectReason') }}{{ orderDetail?.rejectReason }}
</view>
</view>
<!-- 订单进度 -->
<view v-if="orderDetail?.refundStatus !== OrderCancelStatus.APPLIED || orderDetail?.refundStatus !== OrderCancelStatus.APPROVED" class="mb-52rpx px-30rpx">
<OrderProgress
:steps="orderSteps"
:current-status="orderStatus"
/>
</view>
<!-- 自取订单展示核销码按钮-->
<view class="px-30rpx pb-52rpx" v-if="orderDetail?.receiveMethod === 2 && orderStatus === OrderStatus.MERCHANT_ACCEPTED">
<wd-button block
custom-class="w-full !h-98rpx !text-30rpx !lh-42rpx !font-bold !rounded-20rpx !bg-#14181B"
@click="openQrCode">
{{ t('pages-store.order.writeOff') }}
</wd-button>
</view>
<!-- 自取订单展示核销码按钮-->
<!-- <OrderProgress-->
<!-- :steps="orderSteps"-->
<!-- :current-status="1"-->
<!-- />-->
</template>
<!-- 分隔线 -->
<view class="w-full h-16rpx bg-#F6F6F6"></view>
<!-- 配送员信息 -->
<view v-if="
orderDetail?.receiveMethod === 1 && (
orderStatus === OrderStatus.DELIVERING ||
orderStatus === OrderStatus.COMPLETED)
" class="pt-36rpx">
<view class="text-36rpx lh-36rpx text-#333 font-500 pl-30rpx">{{ t('pages-store.view-reviews.deliveryDriver') }}</view>
<view @click="callPhoneFn(orderDetail?.deliveryPhone)" class="flex-center-sb py-36rpx px-30rpx">
<view class="flex items-center">
<image
:src="orderDetail?.deliveryAvatar"
mode="aspectFill"
class="w-80rpx h-80rpx rounded-full shrink-0"
/>
<view class="ml-24rpx">
<text class="text-30rpx lh-30rpx font-500 text-#333 block mb-6rpx">
{{ orderDetail?.deliveryFirstName }} {{ orderDetail?.deliverySurname }}
</text>
<text class="text-24rpx lh-24rpx text-#7D7D7D">{{ t('pages-store.order.acceptanceTime') }}: {{ formatTimestampWithMonthName(orderDetail?.startDeliveryTime) }}
</text>
</view>
</view>
<image
src="@img/chef/153.png"
mode="aspectFill"
class="w-66rpx h-66rpx shrink-0"
/>
</view>
<!-- 送达信息-已送达 -->
<view v-if="orderStatus === OrderStatus.COMPLETED" class="px-30rpx pb-36rpx">
<view class="text-30rpx lh-30rpx text-#333">
<text class="mr-30rpx">{{ t('pages-store.order.deliveryTime') }}</text>
<text>{{ formatTimestampWithMonthName(orderDetail?.deliveryTime) }}</text>
</view>
<view class="text-30rpx lh-30rpx text-#333 mb-24rpx mt-36rpx">{{ t('pages-store.order.deliveryPhotos') }}</view>
<view class="flex items-center gap-20rpx">
<template v-for="item in orderDetail?.deliveryPhotos?.split(',')">
<wd-img width="158rpx" height="158rpx" radius="16rpx" mode="aspectFill" :src="item" :enable-preview="true" />
</template>
</view>
</view>
<view class="w-full h-16rpx bg-#F6F6F6"></view>
</view>
<!-- 商品列表 -->
<view class="px-30rpx py-36rpx">
<!-- 商家信息 -->
<view @click="navigateTo('/pages-store/pages/store/index?id=' + orderDetail?.merchantVo?.id)" class="flex-center-sb h-80rpx mb-36rpx">
<view class="flex items-center">
<image
:src="orderDetail?.merchantVo?.logo"
mode="aspectFill"
class="w-80rpx h-80rpx rounded-full bg-#F2F2F2 mr-24rpx shrink-0"
/>
<text class="text-30rpx lh-30rpx text-#333 font-500">{{ orderDetail?.merchantVo?.merchantName }}</text>
<image
src="@img/chef/142.png"
class="w-32rpx h-32rpx shrink-0 ml-10rpx"
></image>
</view>
<collection :is-collected="orderDetail?.merchantVo?.isCollect" @collectionChange="handleCollectionClick" />
</view>
<view
v-for="item in orderDetail?.merchantOrderDishVoList"
:key="item.id"
class="flex items-start mb-32rpx last:mb-0"
>
<!-- 商品图片 -->
<view class="w-136rpx h-136rpx rounded-16rpx overflow-hidden mr-20rpx shrink-0">
<image
:src="item.merchantDishVo?.dishImage?.split(',')[0]"
mode="aspectFill"
class="w-full h-full bg-#F2F2F2"
/>
</view>
<!-- 商品信息 -->
<view class="flex-1">
<text class="text-30rpx lh-30rpx text-#333 font-500 block mb-20rpx">{{ item.merchantDishVo?.dishName }}</text>
<view class="flex-center-sb text-24rpx lh-24rpx text-#7D7D7D mb-24rpx">
<text class="line-clamp-1">{{ item.merchantSideDishVo?.sideDishName }} {{ item.merchantSideDishItemVo?.name }}</text>
<!-- 数量 -->
<view class="shrink-0 text-28rpx lh-28rpx text-#333 text-right font-500">
X {{ item.count }}
</view>
</view>
<text class="text-30rpx lh-30rpx text-#333 font-500">{{ `$${item.merchantDishVo?.discountPrice}` }}</text>
</view>
</view>
</view>
<!-- 分隔线 -->
<view class="w-full h-16rpx bg-#F6F6F6"></view>
<view v-if="orderDetail?.receiveMethod === 1" class="pt-36rpx">
<view class="text-36rpx lh-36rpx text-#333 font-500 pl-30rpx mb-4rpx">{{ t('pages-store.order.deliveryAddress') }}</view>
<!-- 收货地址 -->
<view class="flex items-center border-bottom py-36rpx px-30rpx">
<image
src="@img/chef/145.png"
mode="aspectFill"
class="w-44rpx h-44rpx relative z-1 shrink-0"
/>
<!-- 地址信息 -->
<view class="ml-28rpx">
<text
class="text-32rpx lh-32rpx text-#333 block mb-8rpx line-clamp-1"
>{{ orderDetail?.merchantOrderUserAddressVo?.formattedAddress }}</text
>
<text class="text-28rpx lh-28rpx text-#6D6D6D">{{ orderDetail?.merchantOrderUserAddressVo?.displayName }}</text>
</view>
</view>
<!-- 配送偏好 -->
<view
class="flex items-center border-bottom py-36rpx px-30rpx"
>
<image
src="@img/chef/1333.png"
mode="aspectFill"
class="w-44rpx h-44rpx relative z-1"
/>
<view class="ml-28rpx text-32rpx lh-32rpx text-#333">
{{ orderDetail?.deliveryMethod }}
</view>
</view>
<!-- 联系电话 -->
<view
class="flex items-center py-36rpx px-30rpx"
>
<image
src="@img/chef/1334.png"
mode="aspectFill"
class="w-44rpx h-44rpx relative z-1"
/>
<view class="ml-28rpx text-32rpx lh-32rpx text-#333">
{{ orderDetail?.areaCode }} {{ orderDetail?.phone }}
</view>
</view>
</view>
<!-- 分隔线 -->
<view v-else class="px-30rpx pt-36rpx">
<view class="text-36rpx lh-36rpx text-#333 font-bold">{{ t('pickupAddress') }}</view>
<view class="flex-center-sb py-40rpx">
<view class="flex items-center">
<image
src="@img/chef/145.png"
mode="aspectFill"
class="w-44rpx h-44rpx shrink-0 mr-28rpx"
/>
<view>
<view class="text-30rpx lh-30rpx text-#333 mb-18rpx line-clamp-1">{{ orderDetail?.merchantVo?.merchantAddress }}</view>
<view class="text-28rpx lh-28rpx text-#6D6D6D">{{ orderDetail?.merchantVo?.merchantName }}</view>
</view>
</view>
<view @click="navGoogleMapFn" class="w-66rpx h-66rpx ml-20rpx center shrink-0 rounded-50% border-#333 border-solid border-1px">
<image
src="@img/chef/127.png"
mode="aspectFill"
class="w-62rpx h-62rpx"
/>
</view>
</view>
</view>
<view class="w-full h-16rpx bg-#F6F6F6"></view>
<view class="border-bottom px-30rpx py-36rpx text-36rpx lh-36rpx text-#333 ">
<view class="font-500 text-36rpx lh-36rpx mb-40rpx">{{ t('pages-store.order.orderInfo') }}</view>
<!-- 订单编号 -->
<view @click="copyOrderNumber(orderDetail?.orderNo)" class="flex-center-sb text-30rpx lh-30rpx mb-40rpx">
<text>{{ t('pages-store.order.orderNumber') }}</text>
<view class="flex items-center">
{{ orderDetail?.orderNo }}
<image
src="@img/chef/1340.png"
mode="aspectFill"
class="w-20rpx h-20rpx shrink-0 ml-10rpx"
/>
</view>
</view>
<!-- 下单时间 mb-40rpx根据支付状态显示 -->
<view class="flex-center-sb text-30rpx lh-30rpx" :class="[orderStatus !== OrderStatus.PENDING_PAYMENT ? 'mb-40rpx' : '']">
<text>{{ t('pages-store.order.orderTime') }}</text>
<text>{{ formatTimestampWithMonthName(orderDetail?.createTime) }}</text>
</view>
<!-- 待支付 -->
<template v-if="orderStatus !== OrderStatus.PENDING_PAYMENT">
<!-- 支付方式 -->
<view class="flex-center-sb text-30rpx lh-30rpx mb-40rpx">
<text>{{ t('pages-user.member.paymentMethod') }}</text>
<view class="flex items-center">
<image
src="@img/chef/138.png"
mode="aspectFill"
class="w-44rpx h-44rpx shrink-0 mr-10rpx"
/>
<text>{{ orderDetail?.payMethod === 1 ? t('pages-user.choosePaymethod.creditCard') : t('pages-user.choosePaymethod.wallet') }}</text>
</view>
</view>
<!-- 支付时间 -->
<view class="flex-center-sb text-30rpx lh-30rpx">
<text>{{ t('pages-user.member.payTime') }}</text>
<text>{{ formatTimestampWithMonthName(orderDetail?.payTime) }}</text>
</view>
</template>
</view>
<!-- 费用明细 -->
<view
:class="[ orderStatus === OrderStatus.PENDING_PAYMENT ? '' : 'pb-68rpx' ]"
class="px-30rpx pt-32rpx text-32rpx lh-32rpx text-#333">
<view class="flex-center-sb mb-40rpx">
<text>{{ t('pages-store.order.subtotal') }}</text>
<text>${{ orderDetail?.actualPrice }}</text>
</view>
<view class="flex-center-sb mb-40rpx">
<view @click="openPriceDetail" class="flex items-center">
<text>{{ t('pages-store.order.taxesAndOtherFees') }}</text>
<image
src="@img/chef/1336.png"
class="w-28rpx h-28rpx shrink-0 ml-10rpx mt-4rpx"
mode="aspectFit"
/>
</view>
<view>
<text>${{ (Number(orderDetail?.tax) + Number(orderDetail?.tip) + Number(orderDetail?.deliveryFee)).toFixed(2) }}</text>
</view>
</view>
<view class="flex-center-sb">
<text>{{ t('pages-store.order.total') }}</text>
<text>${{ orderDetail.paidAmount }}</text>
</view>
</view>
<!-- 订单支付 -->
<view
v-if="orderStatus === OrderStatus.PENDING_PAYMENT"
@click="navigateTo('/pages-user/pages/choose-paymethod/index')"
class="flex-center-sb text-32rpx lh-32rpx font-500 text-#333 py-36rpx px-30rpx pb-68rpx"
>
<view class="flex items-center">
<image
src="@img/chef/138.png"
class="w-44rpx h-44rpx mr-28rpx shrink-0"
mode="aspectFit"
/>
<text class="">
<template v-if="payMethodOptions.payMethod === 1">{{ t('pages-user.choosePaymethod.creditCard') }}</template>
<template v-else>{{ t('pages-user.choosePaymethod.wallet') }}</template>
</text>
</view>
<view class="flex items-center">
<view class="mr-12px">
<template v-if="payMethodOptions.payMethod === 1">
<!--判断用户是否有信用卡-->
<text v-if="!payMethodOptions.cardId">{{ t("pages-user.member.creditCard") }}</text>
<text v-else>{{ payMethodOptions.cardNumber }}</text>
</template>
<template v-else-if="payMethodOptions.payMethod === 2">
<text>{{ t('pages-user.choosePaymethod.wallet') }}</text>
</template>
</view>
<image
src="@img/chef/142.png"
class="w-32rpx h-32rpx shrink-0"
mode="aspectFit"
/>
</view>
</view>
<!-- 操作按钮 -->
<view class="px-30rpx pb-30rpx">
<!--申请取消订单中.已同意取消订单-->
<template v-if="+orderDetail.refundStatus === OrderCancelStatus.APPLIED || +orderDetail.refundStatus === OrderCancelStatus.APPROVED">
<template v-if="+orderDetail.refundStatus === OrderCancelStatus.APPLIED">
<wd-button custom-class="!h-108rpx !bg-transparent !text-36rpx !font-500 !lh-108rpx !text-#333 !rounded-16rpx !border-#666666 !border-solid !border-1rpx" block>
{{ t('pages-store.order.orderStatus.pending') }}
</wd-button>
</template>
<template v-if="+orderDetail.refundStatus === OrderCancelStatus.APPROVED">
<wd-button custom-class="!h-108rpx !bg-transparent !text-36rpx !font-500 !lh-108rpx !text-#333 !rounded-16rpx !border-#666666 !border-solid !border-1rpx" block>
{{ t('pages-store.order.orderStatus.agree') }}
</wd-button>
</template>
</template>
<template v-else>
<!-- 待支付 -->
<template v-if="orderStatus === OrderStatus.PENDING_PAYMENT">
<wd-button @click="goPay" custom-class="!h-108rpx !bg-14181B !text-36rpx !lh-108rpx !text-#fff !rounded-16rpx" block>
{{ t('common.goPay') }}
</wd-button>
<view @click="openCancelOrder" class="text-center mt-52rpx text-36rpx lh-36rpx text-#333 font-500">
{{ t('common.cancel') }}
</view>
</template>
<!-- 配送订单 -->
<template v-if="orderDetail?.receiveMethod === 1">
<!-- 待接单/已接单/配送中 -->
<template v-if="
orderStatus === OrderStatus.HAS_PENDING_PAYMENT ||
orderStatus === OrderStatus.MERCHANT_ACCEPTED ||
orderStatus === OrderStatus.DELIVERING
">
<!-- 已经被拒绝过不能在申请了 -->
<wd-button v-if="orderDetail?.refundStatus !== OrderCancelStatus.REJECTED" @click="openCancelOrder" custom-class="!h-108rpx !bg-transparent !text-36rpx !font-500 !lh-108rpx !text-#333 !rounded-16rpx !border-#666666 !border-solid !border-1rpx" block>
{{ t('common.cancel') }}
</wd-button>
</template>
<!--已完成,评价-->
<template v-else-if="orderStatus === OrderStatus.COMPLETED">
<template v-if="orderDetail?.dishReviewVoList.length > 0">
<wd-button @click="navigateTo('/pages-store/pages/order/view-reviews?id=' + orderDetail.id)" custom-class="!h-108rpx !bg-transparent !text-36rpx !font-500 !lh-108rpx !text-#333 !rounded-16rpx !border-#666666 !border-solid !border-1rpx" block>
{{ t('pages-store.view-reviews.viewTitle') }}
</wd-button>
</template>
<template v-else>
<wd-button @click="navigateTo('/pages-store/pages/order/evaluate?id=' + orderDetail.id)" custom-class="!h-108rpx !bg-14181B !text-36rpx !lh-108rpx !text-#fff !rounded-16rpx" block>
{{ t('common.evaluate') }}
</wd-button>
</template>
</template>
</template>
<template v-else>
<template v-if="
orderStatus === OrderStatus.HAS_PENDING_PAYMENT || orderStatus === OrderStatus.MERCHANT_ACCEPTED
">
<!-- 已经被拒绝过不能在申请了 -->
<wd-button v-if="orderDetail?.refundStatus !== OrderCancelStatus.REJECTED" @click="openCancelOrder" custom-class="!h-108rpx !bg-transparent !text-36rpx !font-500 !lh-108rpx !text-#333 !rounded-16rpx !border-#666666 !border-solid !border-1rpx" block>
{{ t('common.cancel') }}
</wd-button>
</template>
<template v-if="orderStatus === OrderStatus.COMPLETED">
<template v-if="orderDetail?.dishReviewVoList.length > 0">
<wd-button @click="navigateTo('/pages-store/pages/order/view-reviews?id=' + orderDetail.id)" custom-class="!h-108rpx !bg-transparent !text-36rpx !font-500 !lh-108rpx !text-#333 !rounded-16rpx !border-#666666 !border-solid !border-1rpx" block>
{{ t('pages-store.view-reviews.viewTitle') }}
</wd-button>
</template>
<template v-else>
<wd-button @click="navigateTo('/pages-store/pages/order/evaluate?id=' + orderDetail.id)" custom-class="!h-108rpx !bg-14181B !text-36rpx !lh-108rpx !text-#fff !rounded-16rpx" block>
{{ t('common.evaluate') }}
</wd-button>
</template>
</template>
</template>
</template>
<!--申请取消订单中-->
<view :style="[configStore.iosSafeBottomPlaceholder]"></view>
</view>
<!-- 价格明细 -->
<price-detail ref="priceDetailRef" />
<!-- 取消订单 -->
<cancel-order @confirm="confirmCancel" ref="cancelOrderRef" />
<!-- 支付订单 -->
<password-container @success="payPawSuccess" ref="passwordInputRef" />
<!-- 核销订单 -->
<use-code ref="useCodeRef" />
</view>
</template>
<style>
page {
background-color: #fff;
}
</style>
<style scoped>
</style>
@@ -0,0 +1,166 @@
<script setup lang="ts">
import {appMerchantOrderDetailPost, type MerchantOrderVo} from "@/service";
import {formatTimestampWithMonthName} from "@/utils/utils";
const {t} = useI18n()
const orderId = ref('')
onLoad((options) => {
if(options.id) {
orderId.value = options.id;
appMerchantOrderDetail()
}
})
const loading = ref(true)
const orderDetail = ref<MerchantOrderVo>()
function appMerchantOrderDetail() {
loading.value = true;
appMerchantOrderDetailPost({
params: {
orderId: orderId.value,
}
}).then((res: any)=> {
console.log('订单详情', res)
orderDetail.value = res.data
// 是自取订单还是配送订单 1-派送 2-自取 orderDetail.value.receiveMethod
if(res.data && res.data.merchantOrderDishVoList.length > 0) {
merchantDishReviewList.value = orderDetail.value?.dishReviewVoList?.map((review, index) => {
// 获取对应索引的merchantDishVo(数组一一对应,索引相同)
const dish = orderDetail.value?.merchantOrderDishVoList?.[index]?.merchantDishVo || {};
// 映射目标字段
return {
dishId: review.dishId || dish.id || '', // 优先取评价中的dishId,无则取商品的id
dishImage: dish.dishImage?.split(',')[0] || '', // 商品图片(来自merchantDishVo
dishName: dish.dishName || '', // 商品名称(来自merchantDishVo
dishDescription: dish.dishDescription || '', // 商品描述(来自merchantDishVo
rating: review.rating || 0, // 评价分数(来自dishReviewVo
content: review.content || '', // 评价内容(来自dishReviewVo
images: review.images || '', // 评价图片(来自dishReviewVo
createTime: formatTimestampWithMonthName(review.createTime) || '',
};
})
console.log('merchantDishReviewList', merchantDishReviewList.value)
}
}).finally(() => {
loading.value = false;
})
}
// 是自取订单还是配送订单
const isSelfPickup = computed(()=> {
return orderDetail.value?.receiveMethod === 2
})
const merchantDishReviewList = ref([
{
dishId: '', // 菜品ID
dishImage: '',
dishName: '',
dishDescription: '',
rating: 0,
content: '',
images: '',
createTime: '',
}
])
</script>
<template>
<view>
<!-- 导航栏 -->
<navbar customClass="!bg-transparent" />
<view class="pl-30rpx pt-20rpx text-46rpx lh-46rpx font-bold text-#333 mb-52rpx">{{ t('pages-store.view-reviews.viewTitle') }}</view>
<!-- 配送员评价信息 -->
<view v-if="!isSelfPickup" class="bg-white mb-20rpx px-30rpx py-40rpx">
<text class="text-36rpx lh-36rpx text-#333 font-500 block mb-34rpx">{{ t('pages-store.view-reviews.deliveryDriver') }}</text>
<view class="bg-#F7F7F7 h-100rpx rounded-16rpx pl-16rpx flex items-center mb-28rpx">
<image
:src="orderDetail?.deliveryAvatar"
mode="aspectFill"
class="w-80rpx h-80rpx rounded-full mr-20rpx"
/>
<text class="text-28rpx lh-28rpx text-#333">{{ orderDetail?.deliveryFirstName }} {{ orderDetail?.deliverySurname }}</text>
</view>
<!-- 评分 -->
<view>
<view v-if="orderDetail?.deliveryReviewVo?.rating" class="flex items-center">
<wd-rate v-model="orderDetail.deliveryReviewVo.rating" size="24rpx" space="10rpx" color="#D1D1D1" active-color="#333" />
<text class="text-24rpx lh-24rpx text-#333 font-500 ml-10rpx pt-4rpx">{{ orderDetail?.deliveryReviewVo?.rating }}{{ t('common.point') }}</text>
</view>
<view class="text-22rpx lh-22rpx text-#999 mt-14rpx">{{ formatTimestampWithMonthName(orderDetail?.deliveryReviewVo?.createTime) }}</view>
</view>
<!-- 标签 -->
<view class="flex flex-wrap gap-16rpx mt-24rpx mb-20rpx">
<view
v-for="tag in orderDetail?.deliveryReviewVo?.tags.split(',')"
:key="tag"
class="bg-#00A76D/18 h-42rpx rounded-42rpx px-16rpx center text-24rpx text-#333"
>
{{ tag }}
</view>
</view>
<view class="text-28rpx lh-28rpx text-#333">
{{ orderDetail?.deliveryReviewVo?.content }}
</view>
<!-- 图片 -->
<view class="flex items-center gap-32rpx mt-20rpx">
<template v-for="item in orderDetail?.deliveryReviewVo?.images.split(',')">
<wd-img width="202rpx" height="202rpx" radius="12rpx" mode="aspectFill" :src="item" :enable-preview="true" />
</template>
</view>
</view>
<view class="bg-white py-40rpx">
<text class="px-30rpx text-36rpx lh-36rpx text-#333 font-500 block mb-34rpx">{{ t('pages-store.view-reviews.goods') }}</text>
<template v-for="item in merchantDishReviewList">
<view class="mb-20rpx pb-30rpx px-30rpx last:mb-0 border-b-1rpx border-b-solid border-b-#D8D8D8 last:border-none">
<view class="bg-#F7F7F7 h-100rpx rounded-16rpx pl-16rpx pr-24rpx flex items-center mb-28rpx">
<image
:src="item.dishImage"
mode="aspectFill"
class="w-80rpx h-80rpx rounded-16rpx mr-20rpx shrink-0"
/>
<view class="h-80rpx mt-10rpx">
<text class="text-28rpx text-#333 line-clamp-1">{{ item.dishName }}</text>
<view class="text-24rpx lh-24rpx text-#999 mt-12rpx line-clamp-1">{{ item.dishDescription }}</view>
</view>
</view>
<!-- 评分 -->
<view>
<view class="flex items-center">
<wd-rate v-model="item.rating" size="24rpx" space="10rpx" color="#D1D1D1" active-color="#333" />
<text class="text-24rpx lh-24rpx text-#333 font-500 ml-10rpx pt-4rpx">{{ item.rating }}{{ t('common.point') }}</text>
</view>
<view class="text-22rpx lh-22rpx text-#999 mt-14rpx">{{ item.createTime }}</view>
</view>
<view class="text-28rpx lh-28rpx text-#333 mt-28rpx">
{{ item.content }}
</view>
<!-- 图片 -->
<view class="flex items-center gap-32rpx mt-20rpx">
<template v-for="img in item.images.split(',')">
<wd-img width="202rpx" height="202rpx" radius="12rpx" mode="aspectFill" :src="img" :enable-preview="true" />
</template>
</view>
</view>
</template>
</view>
</view>
</template>
<style>
page {
background-color: #F6F6F6;
}
</style>
@@ -0,0 +1,104 @@
<script setup lang="ts">
import {useConfigStore} from "@/store";
import {receiveCouponApi} from "@/pages-user/service";
const configStore = useConfigStore()
const {t} = useI18n()
const emit = defineEmits(['confirm'])
const props = defineProps({
couponList: {
type: Array,
default: () => []
}
})
const show = ref(false)
function init() {
show.value = true
}
function handleClose() {
show.value = false
}
function confirmCoupon(item: any) {
receiveCouponApi(item.id).then(res => {
if (res.code === 200) {
uni.showToast({
title: t('common.prompt.claimCouponSuccessfully'),
icon: 'none'
})
emit('confirm')
}
})
}
defineExpose({
init
});
</script>
<template>
<wd-popup v-model="show" custom-style="border-radius:0;" position="bottom" @close="handleClose">
<view class="bg-#F5F5F5 px-32rpx pt-30rpx">
<view class="text-40rpx lh-40rpx text-#333 font-bold text-center mb-60rpx">{{ t('pages-store.store.claimCoupon') }}</view>
<scroll-view scroll-y class="h-1000rpx">
<template v-for="item in couponList">
<view class="coupon-item h-328rpx flex flex-col mb-30rpx last:mb-0">
<view class="flex-1 pt-40rpx px-58rpx">
<view class="line-clamp-1 text-34rpx lh-34rpx text-#333 font-bold">
<!-- couponType 1-折扣券, 2-满减券-->
<template v-if="item.couponType === 1">
{{ Number(item.discountValue * 100).toFixed(0) }}% {{ t('pages-store.store.couponOff') }}
</template>
<template v-else>
${{ item.discountValue }} {{ t('pages-store.store.couponOff') }}
</template>
</view>
<view class="text-24rpx lh-32rpx text-#999 my-18rpx">
{{ t('pages-store.store.validDays') }}: {{ item.validDays }} {{ t('pages-store.store.days') }}
</view>
<view class="text-24rpx lh-32rpx text-#999 my-18rpx">
{{ item.nameZh }}
</view>
<view class="text-28rpx lh-28rpx text-#333 flex items-center">
<image
class="w-36rpx h-36rpx shrink-0 mr-10rpx"
src="@img/chef/106.png"
></image>
{{ t('pages-store.store.get') }} <template v-if="item.couponType === 1">
{{ Number(item.discountValue * 100).toFixed(0) }}%
</template>
<template v-else>
${{ item.discountValue }}
</template> {{ t('pages-store.store.couponOff') }}
</view>
</view>
<template v-if="item.userCouponVo">
<view class="h-86rpx bg-#e6e6e6 lh-84rpx text-center text-28rpx text-#fff rounded-br-18rpx rounded-bl-18rpx">
{{ t('pages-store.store.claimed') }}
</view>
</template>
<template v-else>
<view @click="confirmCoupon(item)" class="h-84rpx lh-84rpx text-center text-28rpx text-#fff">
{{ t('pages-store.store.claimNow') }}
</view>
</template>
</view>
</template>
</scroll-view>
<view :style="[configStore.iosSafeBottomPlaceholder]" />
</view>
</wd-popup>
</template>
<style scoped lang="scss">
.coupon-item {
background-image: url('@img/chef/103.png');
background-size: 100% 100%;
background-repeat: no-repeat;
background-position: center;
}
</style>
@@ -0,0 +1,225 @@
<template>
<view class="dishes-skeleton">
<!-- 顶部图片区域 -->
<view class="relative h-562rpx">
<!-- 状态栏 -->
<view class="fixed top-0 left-0 z-9 w-full pt-6rpx">
<view class="flex-center-sb px-30rpx">
<view class="back-button-skeleton skeleton-item"></view>
<view class="flex">
<view class="action-button-skeleton skeleton-item mr-20rpx"></view>
<view class="action-button-skeleton skeleton-item"></view>
</view>
</view>
</view>
<!-- 主图骨架 -->
<view class="main-image-skeleton skeleton-item w-750rpx h-562rpx absolute top-0 left-0"></view>
</view>
<!-- 菜品信息区域 -->
<view class="px-30rpx pt-40rpx pb-32rpx">
<!-- 菜品标题 -->
<view class="dish-title-skeleton skeleton-item mb-16rpx"></view>
<!-- 菜品描述 -->
<view class="dish-description-skeleton skeleton-item mb-12rpx"></view>
<view class="dish-description-skeleton skeleton-item mb-12rpx w-80%"></view>
<view class="dish-description-skeleton skeleton-item mb-12rpx w-60%"></view>
<!-- 价格信息 -->
<view class="flex items-center gap-16rpx">
<view class="current-price-skeleton skeleton-item"></view>
<view class="original-price-skeleton skeleton-item"></view>
<view class="member-price-skeleton skeleton-item"></view>
</view>
</view>
<!-- 分隔线 -->
<view class="divider-skeleton skeleton-item mb-36rpx"></view>
<!-- 选择调料区域 -->
<view class="px-30rpx">
<!-- 调料标题区域 -->
<view class="flex-center-sb mb-22rpx">
<view class="flex-1">
<view class="sauce-title-skeleton skeleton-item mb-12rpx"></view>
<view class="sauce-subtitle-skeleton skeleton-item"></view>
</view>
<view class="required-tag-skeleton skeleton-item"></view>
</view>
<!-- 调料选项列表 -->
<view class="">
<view
v-for="i in 4"
:key="i"
class="sauce-option-skeleton p-30rpx flex-center-sb border-b-1rpx border-#dfdfdf"
>
<view class="sauce-name-skeleton skeleton-item"></view>
<view class="radio-button-skeleton skeleton-item"></view>
</view>
</view>
</view>
<!-- 底部按钮区域 -->
<view class="fixed bottom-0 left-0 right-0 px-30rpx">
<view class="add-to-cart-skeleton skeleton-item mb-20rpx"></view>
<view class="safe-bottom-skeleton"></view>
</view>
</view>
</template>
<script setup lang="ts">
// 菜品详情页面骨架屏组件
</script>
<style scoped lang="scss">
// 通用骨架屏样式
.skeleton-item {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
// 闪烁动画
@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
.dishes-skeleton {
background-color: #fff;
min-height: 100vh;
}
// 通用布局类
.flex-center-sb {
display: flex;
align-items: center;
justify-content: space-between;
}
.center {
display: flex;
align-items: center;
justify-content: center;
}
// 状态栏区域
.back-button-skeleton {
width: 48rpx;
height: 48rpx;
border-radius: 24rpx;
}
.action-button-skeleton {
width: 68rpx;
height: 68rpx;
border-radius: 34rpx;
}
// 主图区域
.main-image-skeleton {
border-radius: 0;
}
// 菜品信息区域
.dish-title-skeleton {
width: 400rpx;
height: 40rpx;
border-radius: 8rpx;
}
.dish-description-skeleton {
width: 100%;
height: 24rpx;
border-radius: 6rpx;
}
// 价格区域
.current-price-skeleton {
width: 120rpx;
height: 32rpx;
border-radius: 8rpx;
}
.original-price-skeleton {
width: 100rpx;
height: 28rpx;
border-radius: 6rpx;
}
.member-price-skeleton {
width: 170rpx;
height: 28rpx;
border-radius: 14rpx;
}
// 分隔线
.divider-skeleton {
width: 100%;
height: 10rpx;
border-radius: 0;
}
// 调料选择区域
.sauce-title-skeleton {
width: 300rpx;
height: 40rpx;
border-radius: 8rpx;
}
.sauce-subtitle-skeleton {
width: 100rpx;
height: 24rpx;
border-radius: 6rpx;
}
.required-tag-skeleton {
width: 150rpx;
height: 64rpx;
border-radius: 8rpx;
}
// 调料选项
.sauce-option-skeleton {
.sauce-name-skeleton {
width: 200rpx;
height: 32rpx;
border-radius: 8rpx;
}
.radio-button-skeleton {
width: 48rpx;
height: 48rpx;
border-radius: 24rpx;
}
}
// 底部按钮
.add-to-cart-skeleton {
width: 100%;
height: 98rpx;
border-radius: 16rpx;
}
.safe-bottom-skeleton {
height: 34rpx;
}
// 响应式设计
@media (max-width: 750rpx) {
.dish-title-skeleton {
width: 300rpx;
}
.sauce-title-skeleton {
width: 250rpx;
}
}
</style>
@@ -0,0 +1,308 @@
<template>
<view class="store-skeleton">
<!-- 顶部图片区域 -->
<view class="relative h-562rpx">
<!-- 状态栏 -->
<view class="fixed top-0 left-0 z-9 w-full pt-6rpx">
<view class="flex-center-sb px-30rpx">
<view class="back-button-skeleton skeleton-item"></view>
<view class="flex">
<view class="action-button-skeleton skeleton-item mr-20rpx"></view>
<view class="action-button-skeleton skeleton-item mr-20rpx"></view>
<view class="action-button-skeleton skeleton-item"></view>
</view>
</view>
</view>
<!-- 主图骨架 -->
<view class="main-image-skeleton skeleton-item w-750rpx h-562rpx absolute top-0 left-0"></view>
</view>
<!-- 店铺信息区域 -->
<view class="px-30rpx pt-40rpx pb-42rpx">
<!-- 店铺名称 -->
<view class="text-center">
<view class="store-name-skeleton skeleton-item mx-auto mb-16rpx"></view>
<!-- 评分和信息 -->
<view class="center mb-16rpx">
<view class="rating-skeleton skeleton-item mr-20rpx"></view>
<view class="chef-link-skeleton skeleton-item mr-20rpx"></view>
<view class="distance-skeleton skeleton-item"></view>
</view>
<!-- 描述信息 -->
<view class="description-skeleton skeleton-item mx-auto mb-16rpx"></view>
<view class="description-skeleton skeleton-item mx-auto mb-16rpx"></view>
<!-- 标签 -->
<view class="tag-skeleton skeleton-item mx-auto"></view>
</view>
<!-- 配送方式切换 -->
<view class="delivery-switch-skeleton skeleton-item mt-40rpx"></view>
<!-- 配送信息卡片 -->
<view class="delivery-info-skeleton skeleton-item mt-36rpx"></view>
</view>
<!-- 标签页导航 -->
<view class="tabs-skeleton">
<view class="flex">
<view
v-for="i in 5"
:key="i"
class="tab-item-skeleton skeleton-item mr-40rpx"
></view>
</view>
<view class="tabs-divider skeleton-item"></view>
</view>
<!-- 商品列表区域 -->
<view class="px-30rpx">
<!-- 分类标题 -->
<view class="section-title-skeleton skeleton-item my-36rpx"></view>
<!-- 商品网格 -->
<view class="grid grid-cols-2 gap-30rpx">
<view
v-for="i in 6"
:key="i"
class="product-item-skeleton"
>
<!-- 商品图片 -->
<view class="product-image-skeleton skeleton-item mb-28rpx"></view>
<!-- 商品名称 -->
<view class="product-name-skeleton skeleton-item mb-12rpx"></view>
<!-- 价格行 -->
<view class="flex-center-sb mb-12rpx">
<view class="price-skeleton skeleton-item"></view>
<view class="member-price-skeleton skeleton-item"></view>
</view>
<!-- 原价和销量 -->
<view class="flex-center-sb">
<view>
<view class="original-price-skeleton skeleton-item mb-8rpx"></view>
<view class="sales-skeleton skeleton-item"></view>
</view>
<view class="add-button-skeleton skeleton-item"></view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
// Store页面骨架屏组件
</script>
<style scoped lang="scss">
// 通用骨架屏样式
.skeleton-item {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
// 闪烁动画
@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
.store-skeleton {
background-color: #fff;
min-height: 100vh;
}
// 通用布局类
.flex-center-sb {
display: flex;
align-items: center;
justify-content: space-between;
}
.center {
display: flex;
align-items: center;
justify-content: center;
}
// 状态栏区域
.status-bar-skeleton {
width: 100%;
height: 44rpx;
margin-bottom: 20rpx;
}
.back-button-skeleton {
width: 48rpx;
height: 48rpx;
border-radius: 24rpx;
}
.action-button-skeleton {
width: 68rpx;
height: 68rpx;
border-radius: 34rpx;
}
// 主图区域
.main-image-skeleton {
border-radius: 0;
}
// 店铺信息区域
.store-name-skeleton {
width: 400rpx;
height: 40rpx;
border-radius: 8rpx;
}
.rating-skeleton {
width: 120rpx;
height: 24rpx;
border-radius: 6rpx;
}
.chef-link-skeleton {
width: 100rpx;
height: 24rpx;
border-radius: 6rpx;
}
.distance-skeleton {
width: 80rpx;
height: 24rpx;
border-radius: 6rpx;
}
.description-skeleton {
width: 500rpx;
height: 24rpx;
border-radius: 6rpx;
}
.tag-skeleton {
width: 300rpx;
height: 48rpx;
border-radius: 8rpx;
}
// 配送方式
.delivery-switch-skeleton {
width: 318rpx;
height: 60rpx;
border-radius: 30rpx;
}
.delivery-info-skeleton {
width: 100%;
height: 164rpx;
border-radius: 20rpx;
}
// 标签页导航
.tabs-skeleton {
padding: 0 30rpx;
.tab-item-skeleton {
width: 120rpx;
height: 30rpx;
border-radius: 6rpx;
}
.tabs-divider {
width: 100%;
height: 10rpx;
margin-top: 32rpx;
border-radius: 0;
}
}
// 分类标题
.section-title-skeleton {
width: 200rpx;
height: 40rpx;
border-radius: 8rpx;
}
// 商品项
.product-item-skeleton {
.product-image-skeleton {
width: 100%;
height: 248rpx;
border-radius: 24rpx;
}
.product-name-skeleton {
width: 80%;
height: 30rpx;
border-radius: 6rpx;
}
.price-skeleton {
width: 100rpx;
height: 30rpx;
border-radius: 6rpx;
}
.member-price-skeleton {
width: 170rpx;
height: 28rpx;
border-radius: 14rpx;
}
.original-price-skeleton {
width: 80rpx;
height: 28rpx;
border-radius: 6rpx;
}
.sales-skeleton {
width: 100rpx;
height: 28rpx;
border-radius: 6rpx;
}
.add-button-skeleton {
width: 64rpx;
height: 64rpx;
border-radius: 32rpx;
}
}
// 分隔线
.divider-skeleton {
width: 100%;
height: 10rpx;
border-radius: 0;
}
// 底部提示
.bottom-tip-skeleton {
height: 96rpx;
border-radius: 0;
}
// 响应式设计
@media (max-width: 750rpx) {
.grid {
gap: 20rpx;
}
.product-item-skeleton {
.product-image-skeleton {
height: 200rpx;
}
}
}
</style>
+608
View File
@@ -0,0 +1,608 @@
<script setup lang="ts">
import {
appMerchantDishDetailGet,
appCollectCollectPost,
type MerchantDishVo,
appMerchantCartAddCartPost,
type MerchantCartVo,
appMerchantCartListByMerchantIdPost,
appMerchantDetailMerchantIdGet,
type MerchantVo
} from "@/service";
import { debounce } from 'throttle-debounce'
const { t } = useI18n();
import { useScrollThreshold } from "@/hooks/useScrollThreshold";
import Config from "@/config";
import {useConfigStore, useUserStore} from "@/store";
import DishesSkeleton from "./components/dishes-skeleton.vue";
import {CollectionType} from "@/constant/enums";
import {getDistanceInMiles} from "@/utils/utils";
const configStore = useConfigStore();
const userStore = useUserStore();
// 响应式数据
const loading = ref(true); // 加载状态
const selectedSauce = ref([]); // 默认选择第一个
const selectSauce = (item, id: string) => {
if(!item.selectedIds) {
item.selectedIds = [];
}
if(item.selectedIds.includes(id)) {
// 如果已选中,则取消选择
item.selectedIds = item.selectedIds.filter(selectedId => selectedId !== id)
} else {
// 单选逻辑:清空之前的选择,只保留当前选择
item.selectedIds = [id]
}
};
function handleSubmit() {
console.log('加入购物车', dishDetailData.value)
console.log('加入购物车', dishDetailData.value.merchantSideDishVoList)
const stock = Number(dishDetailData.value?.stock)
if (!Number.isNaN(stock) && stock === 0) {
uni.showToast({
title: t('common.prompt.stockInsufficient'),
icon: 'none'
})
return
}
// 查看所有必选的配菜是否已经选择了
try {
for(const item of dishDetailData.value.merchantSideDishVoList) {
if(+item.isRequired === 1) {
if(!item.selectedIds || item.selectedIds.length === 0) {
uni.showToast({
title: `${t('common.placeholder.pleaseSelect')} ${item.sideDishName}`,
icon: 'none'
})
return;
}
}
}
const merchantCartSideDishBoList = dishDetailData.value.merchantSideDishVoList
.filter(item => {
// 只包含必选配菜或已选择的非必选配菜
return +item.isRequired === 1 || (item.selectedIds && item.selectedIds.length > 0)
})
.map(item => ({
sideDishId: item.id, // 配菜ID
sideDishItemId: item.selectedIds[0] // 配菜子项ID(取第一个选中项)
}));
console.log('点击加入购物车', merchantCartSideDishBoList)
appMerchantCartAddCartPost({
body: {
merchantId: storeId.value,
dishId: dishDetailData.value.id, // 菜品ID
count: 1,
merchantCartSideDishBoList,
}
}).then(res=> {
uni.showToast({
title: t('toast.addCartSuccess'),
icon: 'none'
})
getCartInfo()
})
} catch (error) {
console.log('点击加入购物车', error)
}
}
const addCart = debounce(1000, handleSubmit, {
atBegin: true, // 立即触发
});
const showStatusBar = useScrollThreshold();
onPageScroll((e) => {
uni.$emit("page-scroll", e);
});
function navigateBack() {
uni.navigateBack({
delta: 1,
});
}
const storeId = ref('')
onLoad((options: any) => {
loading.value = true;
if(options.id) {
getDishDetail(options.id)
}
if(options.storeId) {
storeId.value = options.storeId
getCartInfo()
getStoreDetail()
}
})
// 获取菜品详情
const dishDetailData = ref<MerchantDishVo>(null);
function getDishDetail(id: string) {
appMerchantDishDetailGet({
params: {
dishId: id
}
}).then((res: any)=> {
console.log('获取菜品详情', res)
dishDetailData.value = res.data;
}).finally(()=> {
loading.value = false;
})
}
// 收藏菜品
function handleDishCollectionClick() {
debouncedEmit(dishDetailData.value.isCollect, dishDetailData.value.id, CollectionType.DISH, ()=> {
dishDetailData.value.isCollect = !dishDetailData.value.isCollect
})
}
// 防抖处理函数
const debouncedEmit = debounce(1300, (isCollected: boolean, id: string, type: CollectionType, callback: ()=> void) => {
// 收藏接口
appCollectCollectPost({
body: {
targetId: id,
targetType: type
}
}).then(res=> {
callback()
})
}, {
atBegin: true, // 立即触发
})
function handleShare() {
uni.shareWithSystem({
summary: '',
href: `${Config.shareLink}pages-store/pages/store/dishes?id=${dishDetailData.value.id}&storeId=${storeId.value}`,
success(){
// 分享完成,请注意此时不一定是成功分享
},
fail(){
// 分享失败
}
})
}
const totalPrice = computed(() => {
if (!dishDetailData.value) return 0;
let basePrice = Number(dishDetailData.value.discountPrice) || 0;
let sidePrice = 0;
if (dishDetailData.value.merchantSideDishVoList) {
dishDetailData.value.merchantSideDishVoList.forEach(item => {
if (item.selectedIds && item.selectedIds.length > 0) {
const selectedId = item.selectedIds[0]; // 单选逻辑,只取一个
const selected = item.merchantSideDishItemVoList.find(s => s.id === selectedId);
if (selected) {
sidePrice += Number(selected.price) || 0;
}
}
});
}
return (basePrice + sidePrice).toFixed(2);
});
const cartDataList = ref<MerchantCartVo[]>([])
function getCartInfo() {
if(!userStore.isLogin) return
appMerchantCartListByMerchantIdPost({
params: {
merchantId: storeId.value,
}
}).then((res: any)=> {
console.log('购物车列表', res)
cartDataList.value = res.data
})
}
function navigateToCart() {
// uni.navigateTo({
// url: '/pages-user/pages/cart/store-cart?storeId=' + storeId.value + '&storeName=' + storeDetail.value.merchantName,
// });
uni.navigateTo({
url:
'/pages-user/pages/cart/store-cart'
+ '?storeId=' + storeId.value
+ '&storeName=' + encodeURIComponent(storeDetail.value.merchantName),
})
}
// 获取商家详情信息
const storeDetail = ref<MerchantVo>({})
function getStoreDetail() {
appMerchantDetailMerchantIdGet({
params: {
merchantId: storeId.value,
}
}).then((res: any) => {
console.log('商家详情', res)
storeDetail.value = res.data as MerchantVo
}).catch((error) => {
console.error('获取商家详情失败:', error);
})
}
</script>
<template>
<view
class="animate-in fade-in animate-ease-out animate-duration-300"
v-show="loading"
>
<!-- 骨架屏 -->
<DishesSkeleton />
</view>
<view
class="animate-in fade-in animate-ease-in animate-duration-300"
v-show="!loading"
>
<!-- 页面内容 -->
<view class="dish-detail">
<!-- 顶部图片区域 -->
<status-bar />
<view class="h-562rpx">
<wd-swiper height="562rpx" :show-controls="false" :list="dishDetailData?.dishImage.split(',')" autoplay></wd-swiper>
<!-- 状态栏 -->
<view
class="fixed top-0 left-0 z-9 w-full transition-all pt-6rpx"
:class="[showStatusBar ? 'bg-#fff' : '']"
>
<status-bar />
<view class="flex-center-sb px-30rpx">
<image
@click="navigateBack"
src="@img/chef/1327.png"
mode="aspectFill"
class="w-48rpx h-48rpx relative z-1"
/>
<view class="flex items-center">
<view @click.stop="handleDishCollectionClick" class="w-68rpx h-68rpx mr-20rpx">
<image
v-if="!dishDetailData?.isCollect"
src="@img-store/1334.png"
mode="aspectFill"
class="w-full h-full"
/>
<image
v-else
src="@img-store/1337.png"
mode="aspectFill"
class="w-full h-full"
/>
</view>
<image
@click="handleShare"
src="@img-store/1336.png"
mode="aspectFill"
class="w-68rpx h-68rpx relative z-1"
/>
</view>
</view>
</view>
</view>
<!-- 菜品信息区域 -->
<view class="dish-info">
<!-- 菜品标题 -->
<text class="dish-title">{{ dishDetailData?.dishName }}</text>
<!-- 菜品描述 -->
<text class="dish-description">
{{ dishDetailData?.dishDescription }}
</text>
<!-- 价格信息 -->
<view class="price-section">
<view class="flex items-center gap-12rpx">
<text class="current-price">US${{ dishDetailData?.discountPrice }}</text>
<text class="original-price">US${{ dishDetailData?.originalPrice }}</text>
<view class="member-price-tag center">
<text class="member-price-text">{{ t('pages-store.store.members') }}: ${{ dishDetailData?.memberPrice }}</text>
</view>
</view>
<view class="text-#333 text-30rpx font-500">
<template v-if="dishDetailData?.stock">
{{ t('common.stock') }}: {{ Number(dishDetailData?.stock).toFixed(0) }}
</template>
</view>
</view>
</view>
<!-- 分隔线 -->
<view class="divider mb-36rpx"></view>
<!-- 选择调料区域 -->
<view class="pb-188rpx">
<template v-for="item in dishDetailData?.merchantSideDishVoList">
<view class="px-30rpx flex-center-sb mb-22rpx">
<view class="sauce-info">
<text class="sauce-title">{{ item.sideDishName }}</text>
<text class="sauce-subtitle mt-12rpx">{{ t('pages-store.store.choose') }}</text>
</view>
<view
v-if="+item.isRequired === 1"
class="required-tag h-64rpx center shrink-0"
:class="[
item.selectedIds && item.selectedIds.length !== 0
? 'bg-#00A76D/10 text-#00A76D font-500 !px-16rpx'
: 'bg-#f2f2f2 text-#333',
]"
>
<image
v-if="item.selectedIds && item.selectedIds.length !== 0"
src="@img-store/1338.png"
mode="aspectFill"
class="w-28rpx h-28rpx shrink-0 mr-4rpx"
/>
<text class="text-26rpx lh-26rpx">{{ t('pages-store.store.required') }}</text>
</view>
</view>
<!-- 调料选项列表 -->
<view class="mb-30rpx">
<view
v-for="(sauce, index) in item.merchantSideDishItemVoList"
:key="index"
class="sauce-option p-30rpx"
@click="selectSauce(item, sauce.id)"
>
<text class="sauce-name">{{ sauce.name }}</text>
<view class="flex items-center">
<text class="text-#00A76D mr-10rpx">+${{ sauce.price }}</text>
<!-- 单选按钮 -->
<image
:src="
item.selectedIds && item.selectedIds.includes(sauce.id)
? '/static/images/chef/133.png'
: '/static/images/chef/134.png'
"
class="w-48rpx h-48rpx shrink-0"
mode="aspectFit"
/>
</view>
</view>
</view>
</template>
</view>
<view class="!fixed bottom-0 left-0 right-0 px-30rpx">
<wd-button
custom-class="!h-98rpx !text-30rpx !text-#fff !lh-98rpx !rounded-16rpx"
block
@click="addCart"
>{{ t('pages-store.store.addToCart') }} US${{ totalPrice }}
</wd-button>
<view :style="[configStore.iosSafeBottomPlaceholder]"></view>
</view>
</view>
<view v-if="cartDataList.length > 0" @click="navigateToCart" class="fixed z-9 bottom-138rpx left-50% translate-x--50% px-26rpx h-88rpx bg-#14181B rounded-44rpx center text-28rpx text-#fff font-500">
<image src="@img/chef/125.png" class="w-28rpx h-28rpx shrink-0"></image>
<view class="ml-10rpx whitespace-nowrap line-clamp-1">{{ storeDetail.merchantName }}</view>
<view class="w-8rpx h-8rpx rounded-50% bg-white mx-8rpx"></view>
<text>{{ cartDataList.length }}</text>
</view>
</view>
</template>
<style lang="scss" scoped>
:deep(.wd-swiper__track) {
border-radius: 0 !important;
}
.dish-detail {
width: 100%;
min-height: 100vh;
background-color: #ffffff;
}
.top-image-container {
position: relative;
width: 750rpx;
height: 562rpx;
}
.dish-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.status-bar {
position: absolute;
top: 0;
left: 0;
width: 750rpx;
height: 88rpx;
background: rgba(0, 0, 0, 0.3);
}
.status-content {
width: 656rpx;
height: 24rpx;
margin: 32rpx 47rpx;
background: #d8d8d8;
border-radius: 4rpx;
}
.back-button {
position: absolute;
top: 104rpx;
left: 30rpx;
width: 48rpx;
height: 48rpx;
background: rgba(0, 0, 0, 0.64);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.back-icon {
width: 28rpx;
height: 28rpx;
background: #ffffff;
clip-path: polygon(60% 0%, 100% 50%, 60% 100%, 40% 75%, 65% 50%, 40% 25%);
}
.share-button {
position: absolute;
top: 94rpx;
right: 30rpx;
width: 68rpx;
height: 68rpx;
background: rgba(0, 0, 0, 0.64);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.share-icon {
width: 34rpx;
height: 36rpx;
background: #ffffff;
border-radius: 4rpx;
}
.dish-info {
padding: 40rpx 30rpx 32rpx;
}
.dish-title {
font-size: 40rpx;
font-weight: bold;
color: #333333;
line-height: 40rpx;
margin-bottom: 16rpx;
display: block;
}
.dish-description {
font-size: 24rpx;
color: #7d7d7d;
line-height: 28rpx;
margin-bottom: 12rpx;
display: block;
}
.price-section {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
}
.current-price {
font-size: 32rpx;
line-height: 32rpx;
color: #333333;
font-weight: 500;
}
.member-price-tag {
height: 42rpx;
padding-right: 16rpx;
padding-left: 20rpx;
background-image: url("/static/images/chef/1282.png");
background-size: 100% 100%;
background-repeat: no-repeat;
}
.member-price-text {
font-size: 28rpx;
font-weight: 500;
color: #fbe3c3;
}
.original-price {
font-size: 28rpx;
color: #999999;
text-decoration: line-through;
}
.divider {
height: 10rpx;
background: #f6f6f6;
}
.sauce-info {
flex: 1;
}
.sauce-title {
font-size: 40rpx;
color: #333333;
font-weight: bold;
line-height: 40rpx;
margin-bottom: 8rpx;
display: block;
}
.sauce-subtitle {
font-size: 24rpx;
color: #7d7d7d;
line-height: 24rpx;
display: block;
}
.required-tag {
border-radius: 8rpx;
min-width: 150rpx;
text-align: center;
}
.sauce-option {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1rpx solid #dfdfdf;
}
.sauce-name {
font-size: 32rpx;
color: #333333;
font-weight: 500;
line-height: 32rpx;
}
.radio-button {
width: 48rpx;
height: 48rpx;
border: 2rpx solid #999999;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.radio-button.selected {
border-color: #333333;
background: #ffffff;
}
.radio-dot {
width: 24rpx;
height: 24rpx;
background: #333333;
border-radius: 50%;
}
</style>
<style>
page {
background-color: #fff;
}
</style>
+703
View File
@@ -0,0 +1,703 @@
<script setup lang="ts">
import { debounce } from 'throttle-debounce'
import dayjs from 'dayjs'
import Config from '@/config/index'
const { t } = useI18n();
import StoreSkeleton from './components/store-skeleton.vue';
import { useScrollThreshold } from '@/hooks/useScrollThreshold'
import {
appCollectCollectPost, appMerchantCartCalculateSavingsPost, appMerchantCartListByMerchantIdPost,
appMerchantDetailMerchantIdGet,
appMerchantMenuMenuListByUserPost, type MerchantCartVo,
type MerchantVo
} from "@/service";
import {CollectionType} from "@/constant/enums";
import {useUserStore} from "@/store";
import { parseBusinessHoursUtils, getDistanceInMiles } from "@/utils/utils";
import CouponPopup from './components/coupon-popup.vue'
import {getMerchantCouponReceiveListApi} from "@/pages-user/service";
// import type { MerchantVo } from '@/service/types'
// 页面加载状态
const loading = ref(true);
const userStore = useUserStore();
const storeID = ref('')
onLoad((options: any)=> {
console.log(options)
if(options.id) {
storeID.value = options.id
getStoreDetail()
// getMenuList()
}
})
// 定时器用于更新闭店提示
let closingTimer: NodeJS.Timeout | null = null
// 启动闭店提示定时器
function startClosingTimer() {
if (closingTimer) {
clearInterval(closingTimer)
}
closingTimer = setInterval(() => {
if (storeDetail.value.businessHours) {
closingInfo.value = parseBusinessHours(storeDetail.value.businessHours)
// 如果不再显示提示,清除定时器
if (!closingInfo.value.show) {
clearInterval(closingTimer!)
closingTimer = null
}
}
}, 60000) // 每分钟更新一次
}
// 停止闭店提示定时器
function stopClosingTimer() {
if (closingTimer) {
clearInterval(closingTimer)
closingTimer = null
}
}
onShow(()=> {
nextTick(()=> {
if(storeID.value) {
// 查询当前店铺购物车信息
getCartInfo()
}
})
})
// 页面卸载时清除定时器
onUnmounted(() => {
stopClosingTimer()
})
// 获取商家详情信息
const storeDetail = ref<MerchantVo>({})
// 闭店提示信息
const closingInfo = ref({ show: false, minutes: 0 })
// 解析营业时间并判断是否在闭店前30分钟
function parseBusinessHours(businessHours: string) {
if (!businessHours) return { show: false, minutes: 0 }
const now = dayjs()
const currentDay = now.day() // 0=Sunday, 1=Monday, ..., 6=Saturday
const dayNames = [t('pages-store.store.weekdays.sunday'), t('pages-store.store.weekdays.monday'), t('pages-store.store.weekdays.tuesday'), t('pages-store.store.weekdays.wednesday'), t('pages-store.store.weekdays.thursday'), t('pages-store.store.weekdays.friday'), t('pages-store.store.weekdays.saturday')]
const todayName = dayNames[currentDay]
// 检查今天是否营业
if (!businessHours.includes(todayName)) {
return { show: false, minutes: 0 }
}
// 提取时间范围,格式如:05:00-16:00
const timeMatch = businessHours.match(/(\d{2}):(\d{2})-(\d{2}):(\d{2})/)
if (!timeMatch) {
return { show: false, minutes: 0 }
}
const [, startHour, startMin, endHour, endMin] = timeMatch
// 使用dayjs创建今天的开店和闭店时间
const openTime = now.hour(parseInt(startHour)).minute(parseInt(startMin)).second(0)
const closeTime = now.hour(parseInt(endHour)).minute(parseInt(endMin)).second(0)
// 如果闭店时间小于开店时间,说明跨天营业,闭店时间应该是明天
const actualCloseTime = closeTime.isBefore(openTime) ? closeTime.add(1, 'day') : closeTime
// 检查是否在营业时间内
const isOpen = now.isAfter(openTime) && now.isBefore(actualCloseTime)
if (!isOpen) {
return { show: false, minutes: 0 }
}
// 计算闭店前30分钟的时间点
const thirtyMinsBefore = actualCloseTime.subtract(30, 'minute')
// 判断是否在闭店前30分钟内
if (now.isAfter(thirtyMinsBefore) && now.isBefore(actualCloseTime)) {
const minutesLeft = actualCloseTime.diff(now, 'minute')
return { show: true, minutes: minutesLeft }
}
return { show: false, minutes: 0 }
}
const storeDistance = ref(null)
function getStoreDetail() {
loading.value = true
appMerchantDetailMerchantIdGet({
params: {
merchantId: storeID.value,
}
}).then((res: any) => {
console.log('商家详情', res)
storeDetail.value = res.data as MerchantVo
getMerchantCouponReceiveList()
// 解析营业时间并判断闭店提示
if (res.data.businessHours) {
closingInfo.value = parseBusinessHours(res.data.businessHours)
// 如果需要显示闭店提示,启动定时器
if (closingInfo.value.show) {
startClosingTimer()
}
}
if(res.data.merchantMenuVoList && res.data.merchantMenuVoList.length > 0) {
tabs.value = res.data.merchantMenuVoList.map((item) => {
return {
title: item.menuName,
key: item.id
}
})
}
// 商户的经纬度存在,并且用户的经纬度也存在
if(res.data.latitude && res.data.longitude && userStore.userLocation.latitude && userStore.userLocation.longitude) {
let distance = getDistanceInMiles(res.data.latitude, res.data.longitude, userStore.userLocation.latitude, userStore.userLocation.longitude)
console.log('距离商家距离', distance)
storeDistance.value = distance
}
// 判断配送和自取的开通状态
const hasDelivery = +storeDetail.value.deliveryService === 1
const hasPickup = +storeDetail.value.selfPickup === 1
if (!hasDelivery && !hasPickup) {
// 两个都没开通,不显示切换组件
showDeliverySwitch.value = false
} else if (!hasDelivery && hasPickup) {
// 只开通自取,默认选中自取
deliveryMethod.value = 1
showDeliverySwitch.value = true
} else if (hasDelivery && !hasPickup) {
// 只开通配送,默认选中配送
deliveryMethod.value = 0
showDeliverySwitch.value = true
} else {
// 两个都开通,默认选中配送
deliveryMethod.value = 0
showDeliverySwitch.value = true
}
}).catch((error) => {
console.error('获取商家详情失败:', error);
}).finally(()=> {
loading.value = false
})
}
const storeCouponList = ref([])
function getMerchantCouponReceiveList() {
getMerchantCouponReceiveListApi(storeID.value).then((res: any)=> {
console.log('商家优惠券列表', res)
storeCouponList.value = res.data
})
}
const cartDataList = ref<MerchantCartVo[]>([])
function getCartInfo() {
if(!userStore.isLogin) return
appMerchantCartListByMerchantIdPost({
params: {
merchantId: storeID.value,
}
}).then((res: any)=> {
console.log('购物车列表', res)
cartDataList.value = res.data
// 购物车有菜品,查询菜品会员折扣价
if(cartDataList.value.length > 0) {
appMerchantCartCalculateSavings()
}
})
}
// 查询菜品会员折扣价
const cartSavingsData = ref({})
function appMerchantCartCalculateSavings() {
appMerchantCartCalculateSavingsPost({
body: cartDataList.value.map(item => item.id)
}).then(res=> {
console.log('菜品会员折扣价', res)
cartSavingsData.value = res.data
})
}
// 优惠券列表(静态数据)
const couponList = ref([
{ id: 1, text: '20% Off' },
{ id: 2, text: '30% Off' },
{ id: 3, text: '$5 Off' },
{ id: 4, text: '40% Off' },
])
// 跳转到优惠券页面
const couponPopupRef = ref()
function handleClaimNow() {
couponPopupRef.value.init()
}
// 用户查询菜单列表
function getMenuList() {
appMerchantMenuMenuListByUserPost({
params: {
merchantId: storeID.value,
}
}).then(res=> {
console.log('商家菜单列表', res)
// 这里可以处理商家菜单列表数据
}).catch(error => {
console.error('获取商家菜单列表失败:', error);
})
}
// 配送方式
const deliveryMethod = ref(0);
const deliveryMethodOptions = [t('pages-store.store.delivery'), t('pages-store.store.pickup')];
const showDeliverySwitch = ref(true); // 是否显示配送方式切换组件
function handleClickSegmented(index: number) {
console.log("切换配送方式:", index);
if(+storeDetail.value.deliveryService !== 1 && index === 0) {
uni.showToast({
title: t('pages-store.store.toast.deliveryService'),
icon: 'none'
})
return
}
if(+storeDetail.value.selfPickup !== 1 && index === 1) {
uni.showToast({
title: t('pages-store.store.toast.selfPickup'),
icon: 'none'
})
return
}
deliveryMethod.value = index
}
const activeTab = ref(0);
const tabs = ref([]);
const showStatusBar = useScrollThreshold()
onPageScroll((e) => {
uni.$emit('page-scroll', e)
})
function navigateToDishes(item: any) {
uni.navigateTo({
url: '/pages-store/pages/store/dishes?id=' + item.id + '&storeId=' + storeID.value,
})
}
function navigateBack() {
uni.navigateBack({
delta: 1,
})
}
function navigateToCart() {
uni.navigateTo({
url:
'/pages-user/pages/cart/store-cart'
+ '?storeId=' + storeID.value
+ '&storeName=' + encodeURIComponent(storeDetail.value.merchantName),
})
}
// 收藏店铺
function handleCollectionClick() {
debouncedEmit(storeDetail.value.isCollect, storeDetail.value.id, CollectionType.STORE, ()=> {
storeDetail.value.isCollect = !storeDetail.value.isCollect
})
}
// 收藏菜品
function handleDishCollectionClick(item: any) {
debouncedEmit(item.isCollect, item.id, CollectionType.DISH, ()=> {
item.isCollect = !item.isCollect
})
}
// 防抖处理函数
const debouncedEmit = debounce(1300, (isCollected: boolean, id: string, type: CollectionType, callback: ()=> void) => {
// 收藏接口
appCollectCollectPost({
body: {
targetId: id,
targetType: type
}
}).then(res=> {
callback()
})
}, {
atBegin: true, // 立即触发
})
function navigateTo(url: string) {
uni.navigateTo({ url })
}
// 分享商家
function handleShare() {
uni.shareWithSystem({
summary: '',
href: `${Config.shareLink}pages-store/pages/store/index?id=${storeID.value}`,
success(){
// 分享完成,请注意此时不一定是成功分享
},
fail(){
// 分享失败
}
})
}
</script>
<template>
<view
class="animate-in fade-in animate-ease-out animate-duration-300"
v-show="loading"
>
<!-- 骨架屏 -->
<StoreSkeleton/>
</view>
<view
class="animate-in fade-in animate-ease-in animate-duration-300"
v-if="!loading"
>
<!-- 页面内容 -->
<view class="store-box">
<view class="relative">
<!-- 状态栏 -->
<view class="fixed top-0 left-0 z-9 w-full transition-all pt-6rpx" :class="[showStatusBar ? 'bg-#fff' : '']">
<status-bar />
<view class="flex-center-sb px-30rpx">
<image
@click="navigateBack"
src="@img/chef/1327.png"
mode="aspectFill"
class="w-48rpx h-48rpx relative z-1"
/>
<view class="flex items-center">
<image
@click="navigateTo('/pages-store/pages/store/search/index?id=' + storeID)"
src="@img-store/1333.png"
mode="aspectFill"
class="w-68rpx h-68rpx relative z-1 mr-20rpx"
/>
<view @click="handleCollectionClick" class="w-68rpx h-68rpx relative z-1 mr-20rpx">
<image
v-if="!storeDetail?.isCollect"
src="@img-store/1334.png"
mode="aspectFill"
class="w-68rpx h-68rpx"
/>
<image
v-else
src="@img-store/1337.png"
mode="aspectFill"
class="w-68rpx h-68rpx"
/>
</view>
<image
@click="handleShare"
src="@img-store/1335.png"
mode="aspectFill"
class="w-68rpx h-68rpx relative z-1"
/>
</view>
</view>
</view>
<view class="anchors"></view>
<!-- 主图 -->
<!-- <image-->
<!-- :src="storeDetail?.shopImages?.split(',')[0]"-->
<!-- mode="aspectFill"-->
<!-- class="w-750rpx h-562rpx absolute top-0 left-0"-->
<!-- />-->
<status-bar />
<wd-swiper class="bg-common" v-if="storeDetail && storeDetail?.shopImages?.split(',').length > 0" :list="storeDetail?.shopImages?.split(',')" height="562rpx" autoplay></wd-swiper>
</view>
<view class="px-30rpx pt-40rpx pb-42rpx">
<!-- 店铺信息 -->
<view class="text-center">
<view class="text-center text-40rpx lh-40rpx text-#333 font-bold">
{{ storeDetail?.merchantName }}
</view>
<view class="center text-24rpx lh-24rpx my-16rpx">
<view class="flex items-center">
<text class="text-#333 font-500">{{ storeDetail?.rating }}</text>
<image
src="@img/chef/124.png"
class="w-24rpx h-24rpx mx-4rpx"
></image>
<text class="text-#7D7D7D">({{ storeDetail?.commentCount }})</text>
</view>
<view class="flex items-center text-#CE7138 px-10rpx">
<image
src="@img-store/1339.png"
class="w-24rpx h-24rpx mr-4rpx"
></image>
CHEFLINK
</view>
<text v-if="+storeDetail?.deliveryService === 1" class="text-#7D7D7D"> {{ storeDetail?.deliveryTime }} {{ t('common.minutes') }}</text>
</view>
<view class="text-24rpx lh-24rpx text-#7D7D7D text-center mt-16rpx">
{{ t('pages-store.store.title') }} US${{ storeDetail?.minOrderPrice }}
</view>
<!--根据商家营业时间计算处理-->
<view class="text-24rpx lh-24rpx text-#7D7D7D text-center mt-16rpx">
<template v-if="closingInfo.show">
{{ t('pages-store.store.tips') }} {{ closingInfo.minutes }} {{ t('pages-store.store.tips1') }}
</template>
<template v-else>
{{ parseBusinessHoursUtils(storeDetail?.businessHours) }}
</template>
<text v-if="storeDistance" class="ml-10rpx">
{{ t('common.distance') }} {{ storeDistance }} {{ t('common.mile') }}
</text>
</view>
<text v-if="storeDetail?.totalOrderCount > 0" class="mt-16rpx h-48rpx bg-#00A76D/18 rounded-8rpx px-22rpx mx-auto text-24rpx lh-24rpx text-#00A76D mr-20rpx">
{{ t('common.sales') }} {{ storeDetail?.totalOrderCount }}
</text>
<text v-if="storeDetail?.reorderedCount > 0" class="mt-16rpx h-48rpx bg-#00A76D/18 rounded-8rpx px-22rpx mx-auto text-24rpx lh-24rpx text-#00A76D">
{{ storeDetail?.reorderedCount }} {{ t('pages-store.store.tips2') }}
</text>
</view>
<!-- 新功能插入区域 start -->
<!-- 商家优惠券 -->
<view class="py-24rpx mt-40rpx border-top border-bottom" v-if="storeCouponList.length">
<view class="flex items-center justify-between mb-24rpx">
<text class="text-28rpx lh-28rpx font-500 text-#333">{{ t('pages-store.store.merchantDiscounts') }}</text>
<view @click="handleClaimNow" class="flex items-center">
<text class="text-28rpx lh-28rpx font-500 text-#CE7138 mr-8rpx">{{ t('pages-store.store.claimNow') }}</text>
<i class="i-carbon:chevron-right text-24rpx text-#CE7138"></i>
</view>
</view>
<scroll-view
scroll-x
class="coupon-scroll"
:show-scrollbar="false"
enable-flex
>
<view class="coupon-container">
<view
v-for="(coupon, index) in storeCouponList"
:key="coupon.id || index"
class="coupon-item"
>
<view class="coupon-tag">
<text class="coupon-text">{{ coupon.nameZh }}</text>
</view>
</view>
</view>
</scroll-view>
</view>
<!-- 新功能插入区域 end -->
<!-- 自取 配送切换 -->
<!-- <view v-if="showDeliverySwitch" class="w-318rpx mt-40rpx">
<l-segmented :value="deliveryMethod" :options="deliveryMethodOptions" @click="handleClickSegmented" shape="round" bg-color="#F2F2F2" active-color="#333" />
</view> -->
<view v-if="showDeliverySwitch" class="border-#D8D8D8 border-solid border-1px rounded-20rpx min-h-164rpx mt-36rpx px-45rpx py-18rpx text-24rpx lh-24rpx flex-center-sb">
<template v-if="+storeDetail?.deliveryService === 1 && deliveryMethod === 0">
<view class="w-full h-full flex-1 text-center text-#CE7138 pr-44rpx">
<view>{{ t('pages-store.store.tips4') }} ${{ storeDetail?.minOrderPrice }}</view>
<view>{{ t('pages-store.store.tips5') }} ${{ storeDetail?.deliveryFee }}</view>
</view>
<view class="h-128rpx w-1rpx rotate-0 bg-#D8D8D8"></view>
<view class="w-full flex-1 center flex-col pl-44rpx">
<view class="text-#333 mb-8rpx">{{ storeDetail?.deliveryTime }} {{ t('common.minutes') }}</view>
<view class="text-#7D7D7D">{{ t('pages-store.store.earTime') }}</view>
</view>
</template>
<template v-if="+storeDetail?.selfPickup === 1 && deliveryMethod === 1">
<view class="w-full h-full flex-1 text-center text-#CE7138 pr-44rpx">
<view v-if="cartDataList.length > 0 && cartSavingsData && cartSavingsData.savings > 0">
{{ t('pages-store.store.use') }} {{ Config.appName }} {{ t('pages-store.store.tips3') }} ${{ cartSavingsData.savings }} {{ t('pages-store.store.discount') }}
</view>
<view v-else class="">--</view>
</view>
<view class="h-128rpx w-1rpx rotate-0 bg-#D8D8D8"></view>
<view class="w-full flex-1 center flex-col pl-44rpx">
<view class="text-#333 mb-8rpx">{{ storeDetail?.pickupTime }}{{ t('common.minutes') }}</view>
<view class="text-#7D7D7D">{{ t('pages-store.store.earTime') }}</view>
</view>
</template>
</view>
</view>
<wd-tabs v-model="activeTab" slidable="always" autoLineWidth>
<block v-for="item in tabs" :key="item">
<wd-tab :title="item.title">
</wd-tab>
</block>
</wd-tabs>
<view class="box mt--6px"></view>
<view v-if="tabs.length > 0" class="px-30rpx pb-120rpx">
<view v-if="storeDetail?.merchantMenuVoList[activeTab]?.dishList.length > 0" class="text-40rpx lh-40rpx font-500 my-36rpx">{{ t('pages-store.store.recommend') }}</view>
<view v-if="storeDetail?.merchantMenuVoList[activeTab]?.dishList.length > 0" class="grid grid-cols-2 gap-30rpx">
<template v-for="item in storeDetail?.merchantMenuVoList[activeTab]?.dishList">
<view @click="navigateToDishes(item)" class="w-100% mb-10rpx">
<view class="relative h-248rpx rounded-24rpx mb-28rpx">
<view @click.stop="handleDishCollectionClick(item)" class="w-68rpx h-68rpx absolute z-2 top-0 right-0">
<image
v-if="!item.isCollect"
src="@img-store/1334.png"
mode="aspectFill"
class="w-full h-full"
/>
<image
v-else
src="@img-store/1337.png"
mode="aspectFill"
class="w-full h-full"
/>
</view>
<image
:src="item?.dishImage?.split(',')[0]"
mode="aspectFill"
class="w-full h-full rounded-24rpx bg-common"
/>
</view>
<view class="line-clamp-1 text-30rpx text-#333 font-500">
{{ item.dishName }}
</view>
<view class="flex-center-sb mt-12rpx">
<text class="text-26rpx lh-30rpx text-#333 font-500">US${{ item.discountPrice }}</text>
<view class="member-price-tag text-[#FBE3C3] font-500 text-28rpx lh-28rpx center pl-6rpx break-all">
<text class="!text-24rpx">{{ t('pages-store.store.members') }}: </text>
${{ item.memberPrice }}
</view>
</view>
<view class="flex-center-sb mt-12rpx">
<view class="text-28rpx text-#999">
<view class="line-through">US${{ item.originalPrice }}</view>
<view>{{ t('pages-store.store.sales') }}:{{ item.salesCount }}</view>
</view>
<view class="center w-64rpx h-64rpx rounded-50% bg-white shadow-lg">
<image
src="@img/chef/1285.png"
class="w-30rpx h-30rpx shrink-0"
></image>
</view>
</view>
</view>
</template>
</view>
<template v-else>
<view class="py-100rpx center">
<image class="w-250rpx h-250rpx" src="@img/chef/100.png"></image>
</view>
</template>
</view>
<view @click="navigateTo('/pages-user/pages/member/index')" v-if="cartDataList.length > 0 && cartSavingsData && cartSavingsData.savings > 0" class="h-96rpx bg-#CE7138 pl-56rpx flex items-center fixed bottom-0 left-0 w-full z-9">
<image
src="@img/chef/1289.png"
class="mr-10rpx w-32rpx h-32rpx shrink-0"
></image>
<text class="text-[#fff] text-24rpx lh-24rpx">
{{ t('pages-store.store.use') }} {{ Config.appName }} {{ t('pages-store.store.tips3') }} ${{ cartSavingsData.savings }} {{ t('pages-store.store.discount') }}
</text>
</view>
</view>
<view v-if="cartDataList.length > 0" @click="navigateToCart" class="fixed z-9 bottom-138rpx left-50% translate-x--50% px-26rpx h-88rpx bg-#14181B rounded-44rpx center text-28rpx text-#fff font-500">
<image src="@img/chef/125.png" class="w-28rpx h-28rpx shrink-0"></image>
<view class="ml-10rpx whitespace-nowrap line-clamp-1">{{ storeDetail.merchantName }}</view>
<view class="w-8rpx h-8rpx rounded-50% bg-white mx-8rpx"></view>
<text>{{ cartDataList.length }}</text>
</view>
</view>
<coupon-popup
ref="couponPopupRef"
:coupon-list="storeCouponList"
@confirm="getMerchantCouponReceiveList"
/>
</template>
<style>
page {
background-color: #fff;
}
</style>
<style scoped lang="scss">
:deep(.wd-swiper__track) {
border-radius: 0;
}
:deep(.wd-tabs__nav-container) {
.is-active {
color: #333 !important;
font-weight: 500 !important;
}
}
:deep(.wd-tabs__nav-item-text) {
font-size: 30rpx !important;
//color: #7D7D7D;
padding-bottom: 32rpx !important;
}
:deep(.wd-tabs__line) {
border-radius: 0 !important;
height: 10rpx !important;
background-color: #333333 !important;
}
.box {
border-bottom: 10rpx solid #F6F6F6 !important;
}
.member-price-tag {
min-width: 190rpx;
height: 42rpx;
background-image: url("/static/images/chef/1282.png");
background-size: 100% 100%;
background-repeat: no-repeat;
}
.coupon-scroll {
width: 100%;
}
.coupon-container {
display: flex;
align-items: center;
white-space: nowrap;
}
.coupon-item {
margin-right: 20rpx;
flex-shrink: 0;
&:last-child {
margin-right: 0;
}
}
.coupon-tag {
min-width: 120rpx;
height: 52rpx;
display: flex;
align-items: center;
justify-content: center;
padding: 0 28rpx;
position: relative;
background-image: url("/static/images/5008.png");
background-size: 100% 100%;
background-repeat: no-repeat;
}
.coupon-text {
font-size: 24rpx;
line-height: 24rpx;
color: #CE7138;
font-weight: 400;
white-space: nowrap;
}
</style>
@@ -0,0 +1,81 @@
<script setup lang="ts">
import {appSearchListPost} from '@/service'
import SearchSkeleton from "@/pages/search/components/search-skeleton.vue";
const {t} = useI18n()
const props = defineProps<{
id?: string;
}>();
const keyword = ref('')
function handleSearch() {
nextTick(() => {
if (!keyword.value) {
return uni.showToast({title: t('common.prompt.please-enter-keyword-search'), icon: 'none'})
}
uni.navigateTo({
url: `/pages-store/pages/store/search/result?keyword=${keyword.value}&id=${props.id}`,
})
keyword.value = ''
})
}
const loading = ref(true)
onMounted(() => {
loading.value = true
// 查询热门搜索词列表
getHotSearchList()
})
const hotSearchList = ref([])
function getHotSearchList() {
appSearchListPost({}).then(res=> {
console.log('热门搜索词列表', res)
hotSearchList.value = res.data
}).finally(() => {
loading.value = false
})
}
function handleHotSearch(item: any) {
nextTick(() => {
uni.navigateTo({
url: `/pages-store/pages/store/search/result?keyword=${item.name}&id=${props.id}`,
})
})
}
</script>
<template>
<view class="">
<view
class="animate-in fade-in animate-ease-out animate-duration-300"
v-show="loading"
>
<search-skeleton />
</view>
<view
class="animate-in fade-in animate-ease-in animate-duration-300"
v-show="!loading"
>
<header-search focus class="" v-model="keyword" :placeholder="t('components.search.placeholder')" @search="handleSearch"/>
<view class="pl-30rpx mt-52rpx">
<view class="text-40rpx lh-40rpx text-#333 font-bold mb-24rpx tracking-[.04em]">
{{ t('pages.search.hot-title') }}
</view>
<template v-for="item in hotSearchList">
<view @click="handleHotSearch(item)" class="w-full h-144rpx flex items-center">
<image :src="item.logoUrl" class="w-64rpx h-64rpx mr-28rpx rounded-50%" mode="aspectFill"></image>
<view class="flex-1 border-b-solid border-b-1rpx border-b-#DFDFDF h-full flex items-center text-30rpx text-primary font-500 tracking-[.06em]">
{{ item.name }}
</view>
</view>
</template>
</view>
</view>
</view>
</template>
<style>
page {
background-color: #fff;
}
</style>
@@ -0,0 +1,64 @@
<script setup lang="ts">
import {appSearchDishInMerchantPost} from "@/service";
const props = defineProps<{
keyword?: string;
id?: string;
}>();
const { t } = useI18n();
const keyword = ref(props.keyword || "");
function handleSearch() {
nextTick(() => {
if (keyword.value) {
paging.value?.refresh()
}
});
}
const {paging, dataList, queryList, loading} = usePage<any>((pageNum: number, pageSize: number) =>
appSearchDishInMerchantPost({
params: {
pageNum,
pageSize,
},
body: {
keyword: keyword.value,
merchantId: props.id,
}
}),
)
function navigateToDishes(item: any) {
uni.navigateTo({
url: '/pages-store/pages/store/dishes?id=' + item.id + '&storeId=' + props.id,
})
}
</script>
<template>
<z-paging ref="paging" v-model="dataList" @query="queryList" bg-color="#fff">
<template #top>
<header-search
class="pb-42rpx"
v-model="keyword"
@search="handleSearch"
/>
</template>
<view class="grid grid-cols-2 gap-x-30rpx gap-y-46rpx pb-40rpx px-30rpx">
<template v-for="item in dataList">
<view @click="navigateToDishes(item)" class="w-330rpx overflow-hidden">
<image
:src="item?.dishImage?.split(',')[0]"
class="w-full h-186rpx rounded-24rpx mb-16rpx"
mode="aspectFill"
></image>
<text class="text-30rpx lh-30rpx text-#333 line-clamp-1 tracking-[.04em] font-500"
>{{ item.dishName }}</text
>
</view>
</template>
</view>
</z-paging>
</template>
<style scoped lang="scss">
</style>
View File
+12
View File
@@ -0,0 +1,12 @@
import {http} from '@/utils/http'
// 获取字典列表信息
export const getDictFineList = (data: Record<string, any>) =>
http.post<Dict[]>('/app/dict/findList', data);
// 查询营销活动指定的菜品列表 /app/merchantDish/marketingList/{marketActivityId}
export const getMarketingDishList = (marketActivityId: string) =>
http.post<any[]>('/app/merchantDish/marketingList/' + marketActivityId);
// 查询精选菜品列表
export const getFeaturedDishList = (data: Record<string, any>) =>
http.post<any[]>('/app/merchantDish/featuredDishList', data, data);
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Some files were not shown because too many files have changed in this diff Show More