first commit
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
<script setup>
|
||||
import * as R from "ramda";
|
||||
const { t } = useI18n();
|
||||
// @ts-ignore
|
||||
import {debounce} from "throttle-debounce";
|
||||
// @ts-ignore
|
||||
import Config from "@/config";
|
||||
|
||||
const isSubmit = ref(false)
|
||||
|
||||
function submit() {
|
||||
isSubmit.value = true
|
||||
setTimeout(() => {
|
||||
isSubmit.value = false
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
|
||||
// 提交
|
||||
const handleSubmit = debounce(Config.debounceLongTime, submit, {
|
||||
atBegin: true
|
||||
})
|
||||
|
||||
</script>
|
||||
<script>
|
||||
// @ts-ignore
|
||||
import {debounce} from "throttle-debounce";
|
||||
// @ts-ignore
|
||||
import Config from "@/config";
|
||||
import {appUserCardSavePost} from "@/service";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
isSubmit: false
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
console.log('onLoad')
|
||||
// @ts-ignore
|
||||
this.handleSuccess = debounce(Config.debounceLongTime, this.handleSuccess, {
|
||||
atBegin: true
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
handleError() {
|
||||
uni.showToast({
|
||||
icon: 'none',
|
||||
title: 'Add credit card failed'
|
||||
})
|
||||
},
|
||||
async handleSuccess(data) {
|
||||
try {
|
||||
const params = {
|
||||
cardNumber: '************' + data.card.last4,
|
||||
cardId: data.id,
|
||||
}
|
||||
const res = await appUserCardSavePost({
|
||||
body: params
|
||||
})
|
||||
console.log('handleSuccess', res)
|
||||
|
||||
await uni.showToast({
|
||||
icon: 'none',
|
||||
title: 'The credit card was added successfully.'
|
||||
})
|
||||
// const eventChannel = this.getOpenerEventChannel();
|
||||
// const id = res.data
|
||||
// eventChannel.emit('acceptDataFromOpenedPage', {...params, id});
|
||||
setTimeout(uni.navigateBack, 1000)
|
||||
} catch (e) {
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script module="addRenderjs" lang="renderjs">
|
||||
import {loadStripe} from '@stripe/stripe-js/pure';
|
||||
import Config from '@/config/index'
|
||||
|
||||
// @ts-ignore
|
||||
export default {
|
||||
data(){
|
||||
return {
|
||||
isCompleted: false,
|
||||
stripe:null,
|
||||
elements:null,
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
console.log('mounted')
|
||||
this.init()
|
||||
},
|
||||
methods: {
|
||||
async init(){
|
||||
console.log('本次初始化使用的key', Config.stripeKey)
|
||||
loadStripe.setLoadParameters({advancedFraudSignals: false});
|
||||
const stripe = await loadStripe(Config.stripeKey);
|
||||
this.stripe=stripe
|
||||
console.log(stripe)
|
||||
const options = {
|
||||
appearance: {/*...*/},
|
||||
};
|
||||
this.elements = stripe.elements(options);
|
||||
const paymentElement = this.elements.create('card',{disableLink:true});
|
||||
paymentElement.mount('#payment-form');
|
||||
this.elements.get
|
||||
paymentElement.on('change', (event)=> {
|
||||
console.log(event)
|
||||
if (event.complete) {
|
||||
this.isCompleted = true
|
||||
// enable payment button
|
||||
}
|
||||
});
|
||||
},
|
||||
async handleSubmit(isSubmit){
|
||||
console.log('handleSubmit',isSubmit,this.isCompleted)
|
||||
console.log('handleSubmit',this.stripe)
|
||||
|
||||
if(!isSubmit||!this.isCompleted||!this.stripe){
|
||||
return
|
||||
}
|
||||
const {error, paymentMethod} = await this.stripe.createPaymentMethod({
|
||||
elements:this.elements,
|
||||
});
|
||||
console.log(error)
|
||||
console.log(paymentMethod)
|
||||
if (error) {
|
||||
// Handle error
|
||||
this.$ownerInstance.callMethod('handleError')
|
||||
} else {
|
||||
// Send paymentMethod.id to your server
|
||||
this.$ownerInstance.callMethod('handleSuccess',paymentMethod)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<view class="">
|
||||
<navbar customClass="!bg-transparent" />
|
||||
<view class="text-center px-30rpx mt-98rpx">
|
||||
<view class="text-46rpx lh-46rpx text-#333 font-bold">{{ Config.appName }}</view>
|
||||
<view class="text-32rpx text-#333 font-500 mt-70rpx mb-12rpx">{{ t('pages-user.card.title') }}</view>
|
||||
<view class="text-28rpx lh-28rpx text-#999">{{ t('pages-user.card.desc') }}</view>
|
||||
</view>
|
||||
<view class="mt-188rpx px-30rpx py-40rpx bg-#fff" id="payment-form"
|
||||
:prop="isSubmit"
|
||||
:change:prop="addRenderjs.handleSubmit"
|
||||
></view>
|
||||
|
||||
<!-- 底部确认按钮 -->
|
||||
<fixed-bottom-large-btn
|
||||
class="z-100"
|
||||
fixed
|
||||
:text="t('common.save')"
|
||||
@click="handleSubmit"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
page {
|
||||
background-color: #F5F5F5;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,81 @@
|
||||
<script setup lang="ts">
|
||||
import {getAllBalanceDetailTypeApi} from "@/pages-user/service";
|
||||
|
||||
const { t } = useI18n();
|
||||
const emit = defineEmits(['confirm'])
|
||||
|
||||
const show = ref(false);
|
||||
const value = ref()
|
||||
|
||||
function onOpen() {
|
||||
// 获取余额类型
|
||||
getAllBalanceDetailType()
|
||||
show.value = true;
|
||||
}
|
||||
|
||||
function getAllBalanceDetailType() {
|
||||
getAllBalanceDetailTypeApi(1, {}).then(res => {
|
||||
console.log('余额类型', res)
|
||||
columns.value = res.data
|
||||
if(res.data && res.data.length > 0) {
|
||||
value.value = res.data[0].code
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
show.value = false;
|
||||
}
|
||||
function handleSubmit() {
|
||||
let data = columns.value.find(item => item.code === value.value)
|
||||
emit('confirm', data)
|
||||
handleClose()
|
||||
}
|
||||
|
||||
const columns = ref([])
|
||||
|
||||
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('pages-user.balance.type') }}</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="value" label-key="name" value-key="code" />
|
||||
</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>
|
||||
@@ -0,0 +1,105 @@
|
||||
<script setup lang="ts">
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const { t } = useI18n();
|
||||
const emit = defineEmits(['confirm'])
|
||||
const show = ref(false);
|
||||
const code = ref("");
|
||||
const value = ref<number>(Date.now())
|
||||
|
||||
function onOpen() {
|
||||
show.value = true;
|
||||
}
|
||||
function handleClose() {
|
||||
show.value = false;
|
||||
}
|
||||
function handleSubmit() {
|
||||
console.log(value.value);
|
||||
|
||||
// 根据选择的时间戳计算月份的开始和结束时间戳
|
||||
const selectedDate = dayjs(value.value);
|
||||
const startOfMonth = selectedDate.startOf('month');
|
||||
const endOfMonth = selectedDate.endOf('month');
|
||||
|
||||
const createBeginTime = startOfMonth.valueOf().toString();
|
||||
const createEndTime = endOfMonth.valueOf().toString();
|
||||
|
||||
console.log('月份开始时间戳:', createBeginTime);
|
||||
console.log('月份结束时间戳:', createEndTime);
|
||||
|
||||
// 通过emit传递给父组件
|
||||
emit('confirm', {
|
||||
createBeginTime,
|
||||
createEndTime
|
||||
});
|
||||
|
||||
show.value = false;
|
||||
}
|
||||
const formatter = (type, value) => {
|
||||
switch (type) {
|
||||
case 'year':
|
||||
return value + ' ' + t('pages-user.balance.year')
|
||||
case 'month':
|
||||
return value + ' ' + t('pages-user.balance.month')
|
||||
default:
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
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('pages-user.balance.year-month-select') }}</view>
|
||||
<view @click="handleSubmit" class="text-30rpx text-#FF6106">{{ t('common.confirm') }}</view>
|
||||
</view>
|
||||
<view class="bg-#fff px-54rpx py-56rpx">
|
||||
<wd-datetime-picker-view :formatter="formatter" type="year-month" v-model="value" />
|
||||
</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;
|
||||
}
|
||||
}
|
||||
}
|
||||
& > uni-picker-view-column:nth-of-type(2) .uni-picker-view-group {
|
||||
.uni-picker-view-indicator {
|
||||
border-radius: 0 20rpx 20rpx 0 !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,151 @@
|
||||
<script setup lang="ts">
|
||||
import yearMoth from './components/year-moth.vue';
|
||||
import chooseType from './components/choose-type.vue';
|
||||
import {appUserUserBalanceDetailBalanceDetailListPost} from "@/service";
|
||||
import {useUserStore} from "@/store";
|
||||
import {formatTimestampWithMonthName} from "@/utils/utils";
|
||||
const { t } = useI18n();
|
||||
import {Agreement} from "@/constant/enums";
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 时间筛选参数
|
||||
const createBeginTime = ref('');
|
||||
const createEndTime = ref('');
|
||||
|
||||
const { paging, dataList, queryList } = usePage((pageNum: number, pageSize: number) =>
|
||||
appUserUserBalanceDetailBalanceDetailListPost({
|
||||
params: {
|
||||
pageNum,
|
||||
pageSize,
|
||||
createBeginTime: createBeginTime.value ? createBeginTime.value : '',
|
||||
createEndTime: createEndTime.value ? createEndTime.value : '',
|
||||
balanceTypeList: balanceTypeList.value.length > 0 ? balanceTypeList.value : [],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const yearMothRef = ref();
|
||||
const chooseTypeRef = ref();
|
||||
|
||||
function handleClickLeft() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
function navigateTo(url: string) {
|
||||
uni.navigateTo({
|
||||
url,
|
||||
});
|
||||
}
|
||||
|
||||
// 打开日期选择弹出框
|
||||
function openDatetimePicker() {
|
||||
yearMothRef.value.onOpen();
|
||||
}
|
||||
|
||||
// 打开类型选择弹出框
|
||||
function openChooseType() {
|
||||
chooseTypeRef.value.onOpen();
|
||||
}
|
||||
|
||||
const balanceTypeList = ref([])
|
||||
const chooseTypeStr = ref(null)
|
||||
function handleChooseTypeConfirm(data: { code: string; name: string }) {
|
||||
console.log(data);
|
||||
balanceTypeList.value = [data.code];
|
||||
chooseTypeStr.value = data.name
|
||||
paging.value?.refresh();
|
||||
}
|
||||
|
||||
// 处理年月选择确认
|
||||
function handleDateConfirm(data: { createBeginTime: string; createEndTime: string }) {
|
||||
createBeginTime.value = data.createBeginTime;
|
||||
createEndTime.value = data.createEndTime;
|
||||
console.log('更新时间筛选参数:', data);
|
||||
// 重新加载数据
|
||||
paging.value?.refresh();
|
||||
}
|
||||
|
||||
onShow(()=> {
|
||||
userStore.getUserInfo()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="bg-#F6F6F6">
|
||||
<image src="@img/chef/107.png" class="w-full h-472rpx"></image>
|
||||
<z-paging ref="paging" v-model="dataList" @query="queryList">
|
||||
<template #top>
|
||||
<wd-navbar
|
||||
safeAreaInsetTop
|
||||
:fixed="true"
|
||||
:placeholder="true"
|
||||
:bordered="false"
|
||||
custom-class="!bg-transparent"
|
||||
@click-left="handleClickLeft"
|
||||
>
|
||||
<template #title>
|
||||
<text class="text-34rpx text-#fff !font-400">{{ t('pages-user.balance.title') }}</text>
|
||||
</template>
|
||||
<template #left>
|
||||
<view class="shrink-0">
|
||||
<view class="i-carbon:chevron-left text-50rpx text-#fff ml-[-10rpx]"></view>
|
||||
</view>
|
||||
</template>
|
||||
</wd-navbar>
|
||||
<view class="h-254rpx box z-9 mx-18rpx mt-18rpx px-40rpx py-52rpx">
|
||||
<view @click="navigateTo('/pages/agreement/index?code=' + Agreement.BALANCE_EXPLANATION)" class="flex items-center">
|
||||
<text class="text-28rpx text-#333">{{ t('pages-user.balance.my-balance') }}:</text>
|
||||
<image src="@img/chef/108.png" class="w-28rpx h-28rpx shrink-0"></image>
|
||||
</view>
|
||||
<view class="flex-center-sb">
|
||||
<view class="text-58rpx lh-58rpx text-#333 font-bold mt-46rpx">{{ userStore.userInfo?.balance }}</view>
|
||||
<wd-button @click="navigateTo('/pages-user/pages/recharge/index')" custom-class="!min-w-160rpx !h-64rpx !rounded-20rpx !bg-#00A76D">{{ t('common.recharge') }}</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<view class="px-18rpx">
|
||||
<view class="bg-white rounded-36rpx overflow-hidden mt-18rpx py-30rpx">
|
||||
<view class="flex-center-sb pr-20rpx mb-8rpx">
|
||||
<view class="flex items-center">
|
||||
<view class="w-8rpx h-30rpx bg-#00A76D"></view>
|
||||
<text class="ml-10rpx text-34rpx text-#333">{{ t('pages-user.balance.detail-list') }}</text>
|
||||
</view>
|
||||
<view class="flex items-center">
|
||||
<view @click="openDatetimePicker" class="flex items-center mr-18rpx text-26rpx text-#000 bg-#F6F6F6 rounded-full px-14rpx py-10rpx">
|
||||
{{ t('pages-user.balance.month-filter') }}
|
||||
<image src="@img/chef/101.png" class="w-20rpx h-20rpx shrink-0 ml-8rpx"></image>
|
||||
</view>
|
||||
<view @click="openChooseType" class="flex items-center text-26rpx text-#000 bg-#F6F6F6 rounded-full px-14rpx py-10rpx">
|
||||
{{ chooseTypeStr ? chooseTypeStr : t('pages-user.balance.all') }}
|
||||
<image src="@img/chef/101.png" class="w-20rpx h-20rpx shrink-0 ml-8rpx"></image></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="px-20rpx">
|
||||
<template v-for="item in dataList">
|
||||
<view class="w-full border-bottom py-28rpx box-border">
|
||||
<view class="flex-center-sb text-#140005 text-28rpx leading-28rpx mb-14rpx">
|
||||
<view>{{ item.balanceTypeSpec }}</view>
|
||||
<view>{{ item.changeType === 1 ? '+' : '-' }}{{ item.changeAmount }}</view>
|
||||
</view>
|
||||
<view class="flex-center-sb text-#999 text-24rpx leading-24rpx">
|
||||
<view>{{ formatTimestampWithMonthName(item.createTime) }}</view>
|
||||
<view>{{ t('common.balance') }}:{{ item.afterChangeAmount }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</z-paging>
|
||||
<year-moth ref="yearMothRef" @confirm="handleDateConfirm" />
|
||||
<choose-type ref="chooseTypeRef" @confirm="handleChooseTypeConfirm" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.box {
|
||||
border-radius: 16rpx;
|
||||
background: linear-gradient(198deg, #D0F4E8 -8%, #FFFFFF 37%, #FFFFFF 56%, #FFFFFF 76%);
|
||||
box-sizing: border-box;
|
||||
border: 2rpx solid #FFFFFF;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,187 @@
|
||||
<template>
|
||||
<view class="cart-skeleton">
|
||||
<!-- 购物车列表 -->
|
||||
<view class="cart-list-section">
|
||||
<template v-for="i in 3" :key="i">
|
||||
<view class="cart-store-skeleton">
|
||||
<!-- 店铺信息 -->
|
||||
<view class="store-info">
|
||||
<!-- 店铺图片 -->
|
||||
<view class="store-image-skeleton skeleton-item"></view>
|
||||
|
||||
<!-- 店铺详情 -->
|
||||
<view class="store-details">
|
||||
<view class="store-name-skeleton skeleton-item"></view>
|
||||
<view class="store-items-skeleton skeleton-item"></view>
|
||||
<view class="store-address-skeleton skeleton-item"></view>
|
||||
</view>
|
||||
|
||||
<!-- 删除按钮 -->
|
||||
<view class="delete-btn-skeleton skeleton-item"></view>
|
||||
</view>
|
||||
|
||||
<!-- 查看购物车按钮 -->
|
||||
<view class="view-cart-btn-skeleton skeleton-item"></view>
|
||||
|
||||
<!-- 查看店铺按钮 -->
|
||||
<view class="view-store-btn-skeleton skeleton-item"></view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
|
||||
<!-- 空购物车状态 -->
|
||||
<view class="empty-cart-section" style="display: none;">
|
||||
<view class="empty-image-skeleton skeleton-item"></view>
|
||||
<view class="empty-text-skeleton skeleton-item"></view>
|
||||
<view class="explore-btn-skeleton skeleton-item"></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;
|
||||
}
|
||||
}
|
||||
|
||||
// 导航栏区域
|
||||
.navbar-skeleton {
|
||||
height: 88rpx;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// 标题区域
|
||||
.title-section {
|
||||
padding: 20rpx 30rpx 0;
|
||||
|
||||
.title-skeleton {
|
||||
width: 120rpx;
|
||||
height: 46rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// 购物车列表
|
||||
.cart-list-section {
|
||||
padding: 54rpx 30rpx 0;
|
||||
|
||||
.cart-store-skeleton {
|
||||
margin-bottom: 30rpx;
|
||||
border-radius: 24rpx;
|
||||
border: 2rpx solid #E8E8E8;
|
||||
overflow: hidden;
|
||||
|
||||
// 店铺信息
|
||||
.store-info {
|
||||
padding: 30rpx;
|
||||
display: flex;
|
||||
|
||||
// 店铺图片
|
||||
.store-image-skeleton {
|
||||
width: 118rpx;
|
||||
height: 118rpx;
|
||||
border-radius: 59rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
// 店铺详情
|
||||
.store-details {
|
||||
margin-left: 30rpx;
|
||||
flex: 1;
|
||||
|
||||
.store-name-skeleton {
|
||||
width: 200rpx;
|
||||
height: 30rpx;
|
||||
border-radius: 6rpx;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.store-items-skeleton {
|
||||
width: 150rpx;
|
||||
height: 28rpx;
|
||||
border-radius: 6rpx;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.store-address-skeleton {
|
||||
width: 250rpx;
|
||||
height: 28rpx;
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// 删除按钮
|
||||
.delete-btn-skeleton {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 40rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// 查看购物车按钮
|
||||
.view-cart-btn-skeleton {
|
||||
height: 88rpx;
|
||||
margin: 0 30rpx 20rpx;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
// 查看店铺按钮
|
||||
.view-store-btn-skeleton {
|
||||
height: 88rpx;
|
||||
margin: 0 30rpx 30rpx;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 空购物车状态
|
||||
.empty-cart-section {
|
||||
padding: 100rpx 30rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.empty-image-skeleton {
|
||||
width: 318rpx;
|
||||
height: 318rpx;
|
||||
border-radius: 159rpx;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.empty-text-skeleton {
|
||||
width: 200rpx;
|
||||
height: 32rpx;
|
||||
border-radius: 6rpx;
|
||||
margin-bottom: 60rpx;
|
||||
}
|
||||
|
||||
.explore-btn-skeleton {
|
||||
width: 400rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 750rpx) {
|
||||
.cart-list-section {
|
||||
padding-bottom: 20rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
const { t } = useI18n();
|
||||
const emit = defineEmits(['confirm'])
|
||||
|
||||
const show = ref(false);
|
||||
function onOpen() {
|
||||
show.value = true;
|
||||
}
|
||||
function handleClose() {
|
||||
show.value = false;
|
||||
}
|
||||
function handleDelete() {
|
||||
emit('confirm')
|
||||
handleClose()
|
||||
}
|
||||
defineExpose({
|
||||
onOpen,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<wd-popup
|
||||
v-model="show"
|
||||
position="bottom"
|
||||
@close="handleClose"
|
||||
>
|
||||
<view class="px-30rpx text-#333 text-center pt-48rpx pb-60rpx">
|
||||
<view class="text-40rpx lh-40rpx font-500">Delete shopping cart?</view>
|
||||
<view class="text-28rpx lh-28rpx mt-36rpx">
|
||||
Are you sure you want to delete the shopping cart?
|
||||
</view>
|
||||
<view class="mt-70rpx">
|
||||
<wd-button @click="handleDelete" custom-class="!h-108rpx !bg-14181B !text-36rpx !lh-108rpx !text-#fff !rounded-16rpx" block>
|
||||
{{ t('common.delete') }}
|
||||
</wd-button>
|
||||
<view @click="handleClose" class="text-center mt-52rpx text-36rpx lh-36rpx text-#333 font-500">{{ t('common.close') }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,50 @@
|
||||
<script setup lang="ts">
|
||||
const { t } = useI18n();
|
||||
const emit = defineEmits(['confirm','close'])
|
||||
|
||||
const show = ref(false);
|
||||
const goodsName = ref('');
|
||||
function onOpen(title: string) {
|
||||
if (title) {
|
||||
goodsName.value = title;
|
||||
}
|
||||
show.value = true;
|
||||
}
|
||||
function handleClose() {
|
||||
show.value = false;
|
||||
emit('close')
|
||||
}
|
||||
function confirmRemove(){
|
||||
emit('confirm')
|
||||
handleClose()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
onOpen,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<wd-popup
|
||||
v-model="show"
|
||||
position="bottom"
|
||||
@close="handleClose"
|
||||
>
|
||||
<view class="px-30rpx text-#333 text-center pt-48rpx pb-60rpx">
|
||||
<view class="text-40rpx lh-40rpx font-500">Remove the product?</view>
|
||||
<view class="text-28rpx lh-28rpx mt-36rpx">
|
||||
Are you sure you want to remove {{ goodsName }} from your shopping cart?
|
||||
</view>
|
||||
<view class="mt-70rpx">
|
||||
<wd-button @click="confirmRemove" custom-class="!h-108rpx !bg-14181B !text-36rpx !lh-108rpx !text-#fff !rounded-16rpx" block>
|
||||
{{ t('common.remove') }}
|
||||
</wd-button>
|
||||
<view @click="handleClose" class="text-center mt-52rpx text-36rpx lh-36rpx text-#333 font-500">{{ t('common.close') }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,321 @@
|
||||
<template>
|
||||
<view class="store-cart-skeleton">
|
||||
<!-- 标题区域 -->
|
||||
<view class="title-section">
|
||||
<view class="title-skeleton skeleton-item"></view>
|
||||
</view>
|
||||
|
||||
<!-- 商品列表 -->
|
||||
<view class="cart-items-section">
|
||||
<template v-for="i in 5" :key="i">
|
||||
<view class="cart-item-skeleton">
|
||||
<!-- 商品图片 -->
|
||||
<view class="item-image-skeleton skeleton-item"></view>
|
||||
|
||||
<!-- 商品信息 -->
|
||||
<view class="item-info">
|
||||
<view class="item-name-skeleton skeleton-item"></view>
|
||||
<view class="item-specs-skeleton skeleton-item"></view>
|
||||
<view class="price-row">
|
||||
<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="quantity-control-skeleton skeleton-item"></view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
|
||||
<!-- 添加商品按钮 -->
|
||||
<view class="add-items-section">
|
||||
<view class="add-items-btn-skeleton skeleton-item"></view>
|
||||
</view>
|
||||
|
||||
<!-- 分隔线 -->
|
||||
<view class="divider-skeleton skeleton-item"></view>
|
||||
|
||||
<!-- 选项备注工具 -->
|
||||
<view class="options-section">
|
||||
<!-- 餐具选项 -->
|
||||
<view class="utensils-option">
|
||||
<view class="option-icon-skeleton skeleton-item"></view>
|
||||
<view class="option-text-skeleton skeleton-item"></view>
|
||||
<view class="option-toggle-skeleton skeleton-item"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部结算栏 -->
|
||||
<view class="checkout-bar">
|
||||
<!-- 会员优惠提示 -->
|
||||
<view class="member-discount-skeleton skeleton-item"></view>
|
||||
|
||||
<!-- 结算信息 -->
|
||||
<view class="checkout-info">
|
||||
<view class="total-price-section">
|
||||
<view class="total-label-skeleton skeleton-item"></view>
|
||||
<view class="total-amount-skeleton skeleton-item"></view>
|
||||
</view>
|
||||
<view class="checkout-btn-skeleton skeleton-item"></view>
|
||||
</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;
|
||||
}
|
||||
}
|
||||
|
||||
.store-cart-skeleton {
|
||||
background-color: #fff;
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
// 导航栏区域
|
||||
.navbar-skeleton {
|
||||
height: 88rpx;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// 标题区域
|
||||
.title-section {
|
||||
padding: 20rpx 30rpx 22rpx;
|
||||
|
||||
.title-skeleton {
|
||||
width: 300rpx;
|
||||
height: 46rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// 商品列表
|
||||
.cart-items-section {
|
||||
.cart-item-skeleton {
|
||||
padding: 32rpx 30rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1rpx solid #F2F2F2;
|
||||
|
||||
// 商品图片
|
||||
.item-image-skeleton {
|
||||
width: 136rpx;
|
||||
height: 136rpx;
|
||||
border-radius: 16rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
// 商品信息
|
||||
.item-info {
|
||||
margin-left: 20rpx;
|
||||
flex: 1;
|
||||
|
||||
.item-name-skeleton {
|
||||
width: 200rpx;
|
||||
height: 30rpx;
|
||||
border-radius: 6rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.item-specs-skeleton {
|
||||
width: 250rpx;
|
||||
height: 24rpx;
|
||||
border-radius: 6rpx;
|
||||
margin-bottom: 18rpx;
|
||||
}
|
||||
|
||||
.price-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.current-price-skeleton {
|
||||
width: 80rpx;
|
||||
height: 30rpx;
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
|
||||
.original-price-skeleton {
|
||||
width: 70rpx;
|
||||
height: 24rpx;
|
||||
border-radius: 6rpx;
|
||||
margin-left: 10rpx;
|
||||
}
|
||||
|
||||
.member-price-skeleton {
|
||||
width: 170rpx;
|
||||
height: 28rpx;
|
||||
border-radius: 14rpx;
|
||||
margin-left: 10rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 数量控制
|
||||
.quantity-control-skeleton {
|
||||
width: 160rpx;
|
||||
height: 56rpx;
|
||||
border-radius: 32rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加商品按钮
|
||||
.add-items-section {
|
||||
height: 148rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding-right: 30rpx;
|
||||
|
||||
.add-items-btn-skeleton {
|
||||
width: 212rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 64rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// 分隔线
|
||||
.divider-skeleton {
|
||||
height: 10rpx;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// 选项备注工具
|
||||
.options-section {
|
||||
padding: 52rpx 30rpx 72rpx;
|
||||
|
||||
// 餐具选项
|
||||
.utensils-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 52rpx;
|
||||
|
||||
.option-icon-skeleton {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
border-radius: 22rpx;
|
||||
}
|
||||
|
||||
.option-text-skeleton {
|
||||
width: 250rpx;
|
||||
height: 32rpx;
|
||||
border-radius: 6rpx;
|
||||
margin-left: 28rpx;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.option-toggle-skeleton {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
border-radius: 22rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加备注
|
||||
.note-option {
|
||||
.option-icon-skeleton {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
border-radius: 22rpx;
|
||||
margin-right: 28rpx;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.option-text-skeleton {
|
||||
width: 150rpx;
|
||||
height: 32rpx;
|
||||
border-radius: 6rpx;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.note-textarea-skeleton {
|
||||
margin-left: 72rpx;
|
||||
height: 180rpx;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 底部结算栏
|
||||
.checkout-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 204rpx;
|
||||
|
||||
// 会员优惠提示
|
||||
.member-discount-skeleton {
|
||||
height: 76rpx;
|
||||
width: 100%;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
// 结算信息
|
||||
.checkout-info {
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 18rpx 30rpx 0;
|
||||
|
||||
.total-price-section {
|
||||
.total-label-skeleton {
|
||||
width: 120rpx;
|
||||
height: 30rpx;
|
||||
border-radius: 6rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.total-amount-skeleton {
|
||||
width: 100rpx;
|
||||
height: 36rpx;
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.checkout-btn-skeleton {
|
||||
width: 360rpx;
|
||||
height: 92rpx;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 750rpx) {
|
||||
.cart-items-section {
|
||||
.cart-item-skeleton {
|
||||
.item-info {
|
||||
.price-row {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,225 @@
|
||||
<script setup lang="ts">
|
||||
import {appMerchantCartDeleteByMerchantIdPost, appMerchantCartListMerchantPost} from "@/service";
|
||||
|
||||
const { t } = useI18n();
|
||||
import { useMessage } from "wot-design-uni";
|
||||
import RemoveCart from "./components/remove-cart.vue";
|
||||
import CartSkeleton from "./components/cart-skeleton.vue";
|
||||
const message = useMessage();
|
||||
|
||||
const removeCartRef = ref<InstanceType<typeof RemoveCart>>();
|
||||
|
||||
// 模拟数据
|
||||
interface CartStore {
|
||||
id: string;
|
||||
name: string;
|
||||
image: string;
|
||||
itemCount: number;
|
||||
totalPrice: number;
|
||||
deliveryAddress: string;
|
||||
}
|
||||
|
||||
const cartStores = ref<CartStore[]>([
|
||||
{
|
||||
id: "1",
|
||||
name: "Chuanwei Prefecture",
|
||||
image:
|
||||
"https://cdn.pixabay.com/photo/2020/05/29/04/16/chinese-5233488_640.jpg",
|
||||
itemCount: 3,
|
||||
totalPrice: 96.0,
|
||||
deliveryAddress: "New York",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "Valley Congee",
|
||||
image:
|
||||
"https://cdn.pixabay.com/photo/2020/05/29/04/16/chinese-5233488_640.jpg",
|
||||
itemCount: 2,
|
||||
totalPrice: 79.9,
|
||||
deliveryAddress: "New York",
|
||||
},
|
||||
]);
|
||||
|
||||
// 返回上一页
|
||||
const goBack = () => {
|
||||
uni.navigateBack();
|
||||
};
|
||||
|
||||
// 查看购物车
|
||||
const viewCart = (item: any) => {
|
||||
// uni.navigateTo({
|
||||
// url: '/pages-user/pages/cart/store-cart?storeId=' + item.id + '&storeName=' + item.merchantName + '&type=index',
|
||||
// });
|
||||
uni.navigateTo({
|
||||
url:
|
||||
'/pages-user/pages/cart/store-cart'
|
||||
+ '?storeId=' + item.id
|
||||
+ '&storeName=' + encodeURIComponent(item.merchantName)
|
||||
+ '&type=index',
|
||||
})
|
||||
};
|
||||
|
||||
// 查看店铺
|
||||
const viewStore = (storeId: string) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages-store/pages/store/index?id=${storeId}`,
|
||||
});
|
||||
};
|
||||
|
||||
// 删除店铺
|
||||
const removeStoreId = ref('')
|
||||
const deleteStore = (item: any) => {
|
||||
removeCartRef.value?.onOpen();
|
||||
removeStoreId.value = item.id
|
||||
};
|
||||
function confirmRemove() {
|
||||
appMerchantCartDeleteByMerchantIdPost({
|
||||
params: {
|
||||
merchantId: removeStoreId.value
|
||||
}
|
||||
}).then(res=> {
|
||||
getCartList()
|
||||
uni.showToast({
|
||||
icon: 'none',
|
||||
title: '删除成功'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 前往首页
|
||||
const goToHome = () => {
|
||||
uni.switchTab({
|
||||
url: "/pages/home/index",
|
||||
});
|
||||
};
|
||||
|
||||
const dataList = ref([]);
|
||||
|
||||
// 骨架屏加载状态
|
||||
const loading = ref(true);
|
||||
onMounted(() => {
|
||||
loading.value = true;
|
||||
getCartList()
|
||||
});
|
||||
function getCartList() {
|
||||
appMerchantCartListMerchantPost({}).then(res=> {
|
||||
console.log('购物车列表', res)
|
||||
dataList.value = res.data
|
||||
}).finally(()=> {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<z-paging
|
||||
bg-color="#fff"
|
||||
>
|
||||
<template #top>
|
||||
<navbar />
|
||||
<!-- 标题 -->
|
||||
<view class="px-30rpx mt-20rpx text-46rpx lh-46rpx font-bold text-#333">
|
||||
{{ t('pages-user.cart.title') }}
|
||||
</view>
|
||||
</template>
|
||||
<!-- 骨架屏 -->
|
||||
<view
|
||||
class="animate-in fade-in animate-ease-out animate-duration-300"
|
||||
v-show="loading"
|
||||
>
|
||||
<cart-skeleton />
|
||||
</view>
|
||||
<view
|
||||
class="animate-in fade-in animate-ease-in animate-duration-300"
|
||||
v-show="!loading"
|
||||
>
|
||||
<!-- 购物车列表 -->
|
||||
<view class="px-30rpx mt-54rpx">
|
||||
<template v-if="dataList.length > 0">
|
||||
<view
|
||||
v-for="(item, index) in dataList"
|
||||
:key="item.id"
|
||||
class="mb-30rpx rounded-24rpx border-2rpx border-solid border-[#E8E8E8] overflow-hidden"
|
||||
>
|
||||
<!-- 店铺信息 -->
|
||||
<view class="p-30rpx flex">
|
||||
<!-- 店铺图片 -->
|
||||
<view
|
||||
class="w-118rpx h-118rpx rounded-full overflow-hidden bg-[#D8D8D8]"
|
||||
>
|
||||
<image
|
||||
:src="item.logo"
|
||||
class="w-full h-full"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 店铺详情 -->
|
||||
<view class="ml-30rpx flex-1">
|
||||
<view class="text-30rpx text-[#333333] font-500">{{
|
||||
item.merchantName
|
||||
}}</view>
|
||||
<view class="text-28rpx text-[#7D7D7D]"
|
||||
>{{ item.merchantCartVoList.length }} {{ t('pages-user.cart.items') }} · ${{ item.cartTotalPrice || 0 }}</view>
|
||||
<view class="text-28rpx text-[#7D7D7D] line-clamp-1"
|
||||
>{{ t('pages-store.order.deliveryAddress') }}: {{ item.merchantAddress }}</view
|
||||
>
|
||||
</view>
|
||||
|
||||
<!-- 删除按钮 -->
|
||||
<view
|
||||
class="w-80rpx h-80rpx rounded-full bg-[#F2F2F2] flex items-center justify-center"
|
||||
@click="deleteStore(item)"
|
||||
>
|
||||
<image src="@img/chef/1278.png" class="w-80rpx h-80rpx"></image>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 查看购物车按钮 -->
|
||||
<view class="px-30rpx">
|
||||
<view
|
||||
class="h-88rpx rounded-16rpx bg-[#14181B] flex items-center justify-center mb-20rpx"
|
||||
@click="viewCart(item)"
|
||||
>
|
||||
<text class="text-30rpx text-white font-500"
|
||||
>{{ t('pages-user.cart.viewCart') }}</text
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 查看店铺按钮 -->
|
||||
<view class="px-30rpx pb-30rpx">
|
||||
<view
|
||||
class="h-88rpx rounded-16rpx bg-[#F2F2F2] flex items-center justify-center"
|
||||
@click="viewStore(item.id)"
|
||||
>
|
||||
<text class="text-30rpx text-[#333333] font-500"
|
||||
>{{ t('pages-user.cart.viewStore') }}</text
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- 空购物车状态 -->
|
||||
<template v-else>
|
||||
<view class="flex flex-col items-center justify-center py-100rpx">
|
||||
<image src="@img/chef/100.png" class="w-318rpx h-318rpx"></image>
|
||||
<text class="text-32rpx text-[#7D7D7D]">Your cart is empty</text>
|
||||
<view
|
||||
class="mt-60rpx w-400rpx h-88rpx rounded-16rpx bg-[#14181B] flex items-center justify-center"
|
||||
@click="goToHome"
|
||||
>
|
||||
<text class="text-30rpx text-white">Explore Restaurants</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
</z-paging>
|
||||
<remove-cart @confirm="confirmRemove" ref="removeCartRef" />
|
||||
</template>
|
||||
<style>
|
||||
page {
|
||||
background-color: #fff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,445 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
appMerchantCartAddCartByIdPost,
|
||||
appMerchantCartCalculateSavingsPost, appMerchantCartDeleteCartPost,
|
||||
appMerchantCartListByMerchantIdPost,
|
||||
type MerchantCartVo
|
||||
} from "@/service";
|
||||
const { t } = useI18n()
|
||||
import Config from '@/config/index'
|
||||
import RemoveStore from "./components/remove-store.vue";
|
||||
import StoreCartSkeleton from "./components/store-cart-skeleton.vue";
|
||||
import { onBeforeUnmount, ref } from "vue";
|
||||
import {useConfigStore} from "@/store";
|
||||
const configStore = useConfigStore();
|
||||
// 骨架屏加载状态
|
||||
const loading = ref(true);
|
||||
|
||||
// 是否需要餐具
|
||||
const needUtensils = ref(false);
|
||||
const removeStoreRef = ref<InstanceType<typeof RemoveStore>>()
|
||||
|
||||
function goBack() {
|
||||
if(type.value && type.value === 'index') {
|
||||
uni.redirectTo({
|
||||
url: `/pages-store/pages/store/index?id=${storeId.value}`,
|
||||
})
|
||||
} else {
|
||||
uni.navigateBack();
|
||||
}
|
||||
}
|
||||
|
||||
// 数量缓存与更新定时器
|
||||
const itemCountCache = new Map<string | number, number>()
|
||||
const pendingUpdateTargets = new Map<string | number, number>()
|
||||
const pendingUpdateTimers = new Map<string | number, ReturnType<typeof setTimeout>>()
|
||||
const updateDelay = 100
|
||||
|
||||
function syncItemCountCache(list: MerchantCartVo[] = []) {
|
||||
itemCountCache.clear()
|
||||
list.forEach(item => {
|
||||
if (item?.id != null) {
|
||||
itemCountCache.set(item.id, item.count ?? 0)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function clearPendingTimers() {
|
||||
pendingUpdateTimers.forEach(timer => clearTimeout(timer))
|
||||
pendingUpdateTimers.clear()
|
||||
pendingUpdateTargets.clear()
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearPendingTimers()
|
||||
})
|
||||
|
||||
function addCartCount(item: MerchantCartVo, count: number) {
|
||||
if (!item?.id || count <= 0) return
|
||||
appMerchantCartAddCartByIdPost({
|
||||
body: {
|
||||
id: item.id,
|
||||
dishId: item.dishId,
|
||||
merchantId: item.merchantId,
|
||||
count,
|
||||
}
|
||||
}).then(() => {
|
||||
getCartInfo()
|
||||
}).catch(() => {
|
||||
getCartInfo()
|
||||
})
|
||||
}
|
||||
|
||||
function deleteCartCount(item: MerchantCartVo, count: number) {
|
||||
if (!item?.id || count <= 0) return
|
||||
appMerchantCartDeleteCartPost({
|
||||
body: {
|
||||
id: item.id,
|
||||
count,
|
||||
}
|
||||
}).then(() => {
|
||||
getCartInfo()
|
||||
}).catch(() => {
|
||||
getCartInfo()
|
||||
})
|
||||
}
|
||||
|
||||
function scheduleCartUpdate(item: MerchantCartVo, targetValue: number) {
|
||||
if (!item?.id) return
|
||||
const key = item.id
|
||||
pendingUpdateTargets.set(key, targetValue)
|
||||
const existTimer = pendingUpdateTimers.get(key)
|
||||
if (existTimer) {
|
||||
clearTimeout(existTimer)
|
||||
}
|
||||
const timer = setTimeout(() => {
|
||||
pendingUpdateTimers.delete(key)
|
||||
const latestTarget = pendingUpdateTargets.get(key)
|
||||
pendingUpdateTargets.delete(key)
|
||||
if (typeof latestTarget !== 'number') {
|
||||
return
|
||||
}
|
||||
const prev = itemCountCache.get(key) ?? 0
|
||||
const diff = latestTarget - prev
|
||||
if (diff === 0) {
|
||||
return
|
||||
}
|
||||
const absDiff = Math.abs(diff)
|
||||
if (diff > 0) {
|
||||
addCartCount(item, absDiff)
|
||||
} else {
|
||||
deleteCartCount(item, absDiff)
|
||||
}
|
||||
itemCountCache.set(key, latestTarget)
|
||||
}, updateDelay)
|
||||
pendingUpdateTimers.set(key, timer)
|
||||
}
|
||||
|
||||
function normalizeInputNumberValue(payload: any): number {
|
||||
if (typeof payload === 'number') {
|
||||
return payload
|
||||
}
|
||||
if (payload && typeof payload === 'object') {
|
||||
if (typeof payload.detail?.value === 'number') {
|
||||
return payload.detail.value
|
||||
}
|
||||
if (typeof payload.value === 'number') {
|
||||
return payload.value
|
||||
}
|
||||
}
|
||||
const numeric = Number(payload)
|
||||
return Number.isNaN(numeric) ? 0 : numeric
|
||||
}
|
||||
|
||||
const delItemData = ref<MerchantCartVo | null>(null)
|
||||
|
||||
function handleQuantityChange(payload: any, item: MerchantCartVo) {
|
||||
if (!item) return
|
||||
const nextValue = normalizeInputNumberValue(payload)
|
||||
const key = item.id
|
||||
const prev = itemCountCache.get(key) ?? item.count ?? 0
|
||||
if (nextValue === prev) {
|
||||
return
|
||||
}
|
||||
if (nextValue < 0) {
|
||||
item.count = prev
|
||||
return
|
||||
}
|
||||
if (prev === 1 && nextValue === 0) {
|
||||
// item.count = prev
|
||||
delItemData.value = item
|
||||
removeStoreRef.value?.onOpen(item.merchantDishVo.dishName || '')
|
||||
return
|
||||
}
|
||||
scheduleCartUpdate(item, nextValue)
|
||||
}
|
||||
|
||||
function handleRemoveClose() {
|
||||
if (!delItemData.value) {
|
||||
return
|
||||
}
|
||||
delItemData.value.count = 1
|
||||
}
|
||||
|
||||
function handleRemove() {
|
||||
if (!delItemData.value) {
|
||||
return
|
||||
}
|
||||
deleteCartCount(delItemData.value, 1)
|
||||
delItemData.value = null
|
||||
}
|
||||
|
||||
function handleRemovePopupClose() {
|
||||
if (!delItemData.value) {
|
||||
return
|
||||
}
|
||||
const item = delItemData.value
|
||||
const key = item.id
|
||||
const fallback = itemCountCache.get(key) ?? 1
|
||||
item.count = fallback || 1
|
||||
delItemData.value = null
|
||||
}
|
||||
|
||||
const orderRemark = ref('')
|
||||
function goToCheckout() {
|
||||
// 检查库存是否充足
|
||||
const outOfStockItem = cartDataList.value.find(item => {
|
||||
const stock = Number(item.merchantDishVo?.stock)
|
||||
return !Number.isNaN(stock) && stock < item.count
|
||||
})
|
||||
if (outOfStockItem) {
|
||||
uni.showToast({
|
||||
title: `${outOfStockItem.merchantDishVo?.dishName || ''} ${t('common.prompt.stockInsufficient')}`,
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
uni.navigateTo({
|
||||
url: "/pages-store/pages/order/checkout?storeId=" + storeId.value + "&needTableware=" + needUtensils.value + "&orderRemark=" + orderRemark.value,
|
||||
});
|
||||
}
|
||||
|
||||
const storeId = ref('')
|
||||
const storeName = ref('')
|
||||
const type = ref(null)
|
||||
onLoad((options: any)=> {
|
||||
loading.value = true
|
||||
if(options.storeId) {
|
||||
storeId.value = options.storeId as string
|
||||
// storeName.value = options.storeName as string
|
||||
storeName.value = decodeURIComponent(options.storeName || '')
|
||||
if(options.type) {
|
||||
type.value = options.type
|
||||
}
|
||||
|
||||
// 查询当前店铺购物车详情
|
||||
getCartInfo()
|
||||
}
|
||||
})
|
||||
|
||||
const cartDataList = ref<MerchantCartVo[]>([])
|
||||
function getCartInfo() {
|
||||
appMerchantCartListByMerchantIdPost({
|
||||
params: {
|
||||
merchantId: storeId.value,
|
||||
}
|
||||
}).then((res: any)=> {
|
||||
console.log('购物车列表', res)
|
||||
cartDataList.value = res.data
|
||||
syncItemCountCache(res.data || [])
|
||||
|
||||
// 购物车有菜品,查询菜品会员折扣价
|
||||
if(cartDataList.value.length > 0) {
|
||||
appMerchantCartCalculateSavings()
|
||||
}
|
||||
}).finally(()=> {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
// 查询菜品会员折扣价
|
||||
const cartSavingsData = ref({})
|
||||
function appMerchantCartCalculateSavings() {
|
||||
appMerchantCartCalculateSavingsPost({
|
||||
body: cartDataList.value.map(item => item.id)
|
||||
}).then(res=> {
|
||||
console.log('菜品会员折扣价', res)
|
||||
cartSavingsData.value = res.data
|
||||
})
|
||||
}
|
||||
|
||||
function navigateTo(url: string) {
|
||||
uni.navigateTo({ url })
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<view class="">
|
||||
<navbar />
|
||||
<!-- 骨架屏 -->
|
||||
<view
|
||||
class="animate-in fade-in animate-ease-out animate-duration-300"
|
||||
v-show="loading"
|
||||
>
|
||||
<store-cart-skeleton/>
|
||||
</view>
|
||||
<view
|
||||
class="animate-in fade-in animate-ease-in animate-duration-300"
|
||||
v-show="!loading"
|
||||
>
|
||||
<!-- 标题 -->
|
||||
<view
|
||||
class="px-30rpx mt-20rpx mb-22rpx text-46rpx lh-46rpx font-bold text-#333"
|
||||
>
|
||||
{{ storeName }}
|
||||
</view>
|
||||
|
||||
<!-- 商品列表 -->
|
||||
<view
|
||||
v-for="(item, index) in cartDataList"
|
||||
:key="index"
|
||||
class="px-30rpx py-32rpx flex items-center border-bottom"
|
||||
>
|
||||
<!-- 商品图片 -->
|
||||
<image
|
||||
:src="item.merchantDishVo.dishImage?.split(',')[0]"
|
||||
mode="aspectFill"
|
||||
class="w-136rpx h-136rpx shrink-0 rounded-16rpx"
|
||||
></image>
|
||||
|
||||
<!-- 商品信息 -->
|
||||
<view class="item-info ml-20rpx flex-1">
|
||||
<view class="text-[#333333] text-30rpx lh-30rpx font-500">{{
|
||||
item.merchantDishVo.dishName
|
||||
}}</view>
|
||||
<view class="text-[#7D7D7D] text-24rpx lh-24rpx mt-20rpx" v-if="item.sideDishList?.length > 0">
|
||||
<template v-for="dish in item.sideDishList">
|
||||
<text class="mr-6rpx">
|
||||
{{ dish?.merchantSideDishItemVo?.name || '' }}
|
||||
</text>
|
||||
<text v-if="dish?.merchantSideDishItemVo?.price" class="text-#00A76D mr-10rpx">+${{ dish?.merchantSideDishItemVo?.price }}</text>
|
||||
</template>
|
||||
</view>
|
||||
|
||||
<view class="price-row flex items-center mt-18rpx">
|
||||
<text class="current-price text-[#333333] text-30rpx font-normal"
|
||||
>${{ item.merchantDishVo.discountPrice }}</text
|
||||
>
|
||||
<text
|
||||
v-if="item.merchantDishVo.originalPrice"
|
||||
class="original-price text-[#7D7D7D] text-24rpx font-normal line-through ml-10rpx"
|
||||
>${{ item.merchantDishVo.originalPrice }}</text
|
||||
>
|
||||
|
||||
<!-- 会员价标签 -->
|
||||
<view
|
||||
v-if="item.merchantDishVo.memberPrice"
|
||||
class="member-price-tag ml-10rpx center pl-16rpx pr-6rpx pb-2rpx"
|
||||
>
|
||||
<text class="text-[#FBE3C3] text-20rpx"
|
||||
>{{ t('pages-store.store.members') }}: ${{ item.merchantDishVo.memberPrice }}</text
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 数量控制 -->
|
||||
<wd-input-number
|
||||
v-model="item.count"
|
||||
long-press
|
||||
:min="0"
|
||||
:step="1"
|
||||
:input-width="80"
|
||||
button-size="56"
|
||||
custom-class="!bg-transparent"
|
||||
@change="(value) => handleQuantityChange(value, item)"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 添加商品 -->
|
||||
<view @click="goBack" class="h-148rpx flex items-center justify-end pr-30rpx">
|
||||
<view class="w-212rpx h-64rpx bg-#F2F2F2 rounded-64rpx center">
|
||||
<image
|
||||
src="@img/chef/1285.png"
|
||||
class="mr-16rpx w-24rpx h-24rpx shrink-0"
|
||||
></image>
|
||||
<text class="text-#333 text-28rpx lh-28rpx font-500">{{ t('pages-user.cart.addItems') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="h-10rpx bg-#F6F6F6"></view>
|
||||
|
||||
<!-- 选项备注工具 -->
|
||||
<view class="px-30rpx pt-52rpx pb-72rpx">
|
||||
<view @click="needUtensils = !needUtensils" class="pb-52rpx flex-center-sb">
|
||||
<view class="flex items-center">
|
||||
<image
|
||||
src="@img/chef/1283.png"
|
||||
class="mr-28rpx w-44rpx h-44rpx shrink-0"
|
||||
></image>
|
||||
<text class="text-[#333333] text-32rpx lh-32rpx font-500">
|
||||
{{ t('pages-user.cart.requestTableware') }}
|
||||
</text>
|
||||
</view>
|
||||
<image
|
||||
:src="
|
||||
needUtensils
|
||||
? '/static/images/chef/133.png'
|
||||
: '/static/images/chef/134.png'
|
||||
"
|
||||
class="w-44rpx h-44rpx shrink-0"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="">
|
||||
<view class="flex items-center mb-24rpx">
|
||||
<image
|
||||
src="@img/chef/1284.png"
|
||||
class="mr-28rpx w-44rpx h-44rpx shrink-0"
|
||||
></image>
|
||||
<text class="text-[#333333] text-32rpx lh-32rpx font-500">
|
||||
{{ t('pages-user.cart.addNote') }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="pl-72rpx">
|
||||
<view
|
||||
class="min-h-180rpx box-border bg-#F6F6F6 rounded-20rpx overflow-hidden px-20rpx"
|
||||
>
|
||||
<wd-textarea
|
||||
:maxlength="150"
|
||||
v-model="orderRemark"
|
||||
custom-class="!bg-#F6F6F6"
|
||||
custom-textarea-container-class="!bg-#F6F6F6"
|
||||
no-border
|
||||
auto-height
|
||||
:placeholder="t('pages-user.cart.addNote')"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-show="!loading && cartDataList.length > 0" class="h-204rpx"></view>
|
||||
</view>
|
||||
<!-- 底部结算栏 -->
|
||||
<view v-show="!loading && cartDataList.length > 0" class="fixed z-9 bottom-0 left-0 right-0 h-204rpx bg-white">
|
||||
<view @click="navigateTo('/pages-user/pages/member/index')" v-if="cartDataList.length > 0 && cartSavingsData && cartSavingsData.savings > 0" class="h-76rpx bg-#CE7138 pl-56rpx flex items-center">
|
||||
<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 class="bg-white flex-center-sb px-30rpx pt-18rpx">
|
||||
<view class="text-30rpx font-500">
|
||||
<view class="lh-30rpx mb-8rpx">{{ t('pages-user.cart.totalPrice') }}</view>
|
||||
<text class="text-[#EE2916] text-36rpx"
|
||||
>${{ cartSavingsData?.totalPayPrice }}</text
|
||||
>
|
||||
</view>
|
||||
<view
|
||||
class="w-360rpx h-92rpx bg-[#14181B] rounded-16rpx center"
|
||||
@click="goToCheckout"
|
||||
>
|
||||
<text class="text-white text-30rpx font-500">{{ t("pages-store.checkout.title") }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view :style="[configStore.iosSafeBottomPlaceholder]"></view>
|
||||
</view>
|
||||
<remove-store ref="removeStoreRef" @confirm="handleRemove" @close="handleRemoveClose"/>
|
||||
</view>
|
||||
</template>
|
||||
<style>
|
||||
page {
|
||||
background-color: #fff;
|
||||
}
|
||||
</style>
|
||||
<style lang="scss" scoped>
|
||||
.member-price-tag {
|
||||
height: 28rpx;
|
||||
background-image: url("/static/images/chef/1282.png");
|
||||
background-size: 100% 100%;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,140 @@
|
||||
<script setup lang="ts">
|
||||
import {useUserStore} from "@/store";
|
||||
import {EventEnum} from "@/constant/enums";
|
||||
import {appUserCardSelectDefaultPost} from "@/service";
|
||||
const { t } = useI18n();
|
||||
const userStore = useUserStore();
|
||||
|
||||
const props = defineProps({
|
||||
hideWallet: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
})
|
||||
|
||||
function changePayment(payment: 1 | 2) {
|
||||
payMethodOptions.value.payMethod = payment
|
||||
}
|
||||
|
||||
onLoad(()=> {
|
||||
// 查询用户默认信用卡
|
||||
appUserCardSelectDefault()
|
||||
})
|
||||
// 支付参数
|
||||
const payMethodOptions = ref({
|
||||
cardNumber: '',
|
||||
cardId: '',
|
||||
payMethod: 1, // 支付方式 1信用卡 2余额
|
||||
})
|
||||
function appUserCardSelectDefault() {
|
||||
appUserCardSelectDefaultPost({}).then(res=> {
|
||||
console.log('查询用户默认信用卡', res)
|
||||
payMethodOptions.value.cardId = res.data?.cardId || ''
|
||||
payMethodOptions.value.cardNumber = res.data?.cardNumber || ''
|
||||
})
|
||||
}
|
||||
|
||||
const confirmPayment = () => {
|
||||
if(payMethodOptions.value.payMethod === 1) {
|
||||
if(!payMethodOptions.value.cardId) {
|
||||
chooseCard()
|
||||
} else {
|
||||
uni.$emit(EventEnum.CHOOSE_PAYMENT_METHOD, payMethodOptions.value)
|
||||
uni.navigateBack()
|
||||
}
|
||||
} else {
|
||||
uni.$emit(EventEnum.CHOOSE_PAYMENT_METHOD, payMethodOptions.value)
|
||||
uni.navigateBack()
|
||||
}
|
||||
}
|
||||
|
||||
function chooseCard() {
|
||||
uni.navigateTo({
|
||||
url: '/pages-user/pages/select-credit-card/index',
|
||||
events: {
|
||||
// 为指定事件添加一个监听器,获取被打开页面传送到当前页面的数据
|
||||
selectedCard: function(data) {
|
||||
if(data) {
|
||||
payMethodOptions.value.cardId = data.cardId
|
||||
payMethodOptions.value.cardNumber = data.cardNumber
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function navigateTo(url: string) {
|
||||
uni.navigateTo({ url })
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<view class="">
|
||||
<navbar />
|
||||
<!-- 页面标题 -->
|
||||
<view class="px-30rpx pt-20rpx mb-88rpx">
|
||||
<text class="text-46rpx font-bold text-#333 leading-46rpx">{{ t('pages-user.choosePaymethod.title') }}</text>
|
||||
</view>
|
||||
|
||||
<view class="px-30rpx">
|
||||
<view v-if="!hideWallet" @click="changePayment(2)" class="flex-center-sb mb-66rpx">
|
||||
<view class="flex items-center">
|
||||
<image
|
||||
src="@img/chef/156.png"
|
||||
mode="aspectFill"
|
||||
class="w-44rpx h-44rpx shrink-0 mr-28rpx"
|
||||
/>
|
||||
<text class="text-32rpx lh-32rpx text-#333 font-500">{{ t('pages-user.choosePaymethod.wallet') }}(${{ userStore.userInfo.balance }})</text>
|
||||
</view>
|
||||
<!-- 单选按钮 -->
|
||||
<image
|
||||
:src="
|
||||
payMethodOptions.payMethod === 2
|
||||
? '/static/images/chef/133.png'
|
||||
: '/static/images/chef/134.png'
|
||||
"
|
||||
class="w-48rpx h-48rpx shrink-0"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view @click="changePayment(1)" class="flex-center-sb">
|
||||
<view class="flex items-center">
|
||||
<image
|
||||
src="@img/chef/138.png"
|
||||
mode="aspectFill"
|
||||
class="w-44rpx h-44rpx shrink-0 mr-28rpx"
|
||||
/>
|
||||
<text class="text-32rpx lh-32rpx text-#333 font-500">{{ t('pages-user.member.creditCard') }}</text>
|
||||
</view>
|
||||
<!-- 单选按钮 -->
|
||||
<image
|
||||
:src="
|
||||
payMethodOptions.payMethod === 1
|
||||
? '/static/images/chef/133.png'
|
||||
: '/static/images/chef/134.png'
|
||||
"
|
||||
class="w-48rpx h-48rpx shrink-0"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view v-if="payMethodOptions.cardId" class="h-98rpx rounded-16rpx bg-#F2F2F2 ml-72rpx mt-30rpx flex-center-sb px-28rpx">
|
||||
<text class="text-30rpx lh-30rpx text-#9E9E9E">{{ payMethodOptions.cardNumber }}</text>
|
||||
<view @click="chooseCard" class="h-50rpx rounded-25rpx bg-white center px-20rpx text-24rpx lh-24rpx text-#333">{{ t('pages-user.choosePaymethod.replace') }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部确认按钮 -->
|
||||
<fixed-bottom-large-btn
|
||||
class="z-100"
|
||||
fixed
|
||||
:text="t('common.confirm')"
|
||||
@click="confirmPayment"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
<style>
|
||||
page {
|
||||
background-color: #fff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,191 @@
|
||||
<script setup lang="ts">
|
||||
import { debounce } from 'throttle-debounce'
|
||||
import {appCollectListPost, appCollectCollectPost} from "@/service";
|
||||
import FoodBox from "@/pages/home/components/tabbar-home/components/food-box/index.vue";
|
||||
import Collection from "@/components/collection/index.vue";
|
||||
import {CollectionType} from "@/constant/enums";
|
||||
|
||||
const {t} = useI18n()
|
||||
const props = defineProps({
|
||||
currentIndex: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
index: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
})
|
||||
|
||||
// (1, "菜谱")(2, "菜品")(3, "配菜")(4, "商家")
|
||||
const typeList = [4,2,1]
|
||||
|
||||
watch(()=>props.currentIndex, (newVal, oldVal) => {
|
||||
console.log('currentIndex', newVal)
|
||||
console.log('props.index', props.index)
|
||||
if(newVal === props.index) {
|
||||
paging.value?.reload()
|
||||
}
|
||||
})
|
||||
|
||||
const {paging, dataList, queryList, loading} = usePage<any>((pageNum: number, pageSize: number) =>
|
||||
appCollectListPost({
|
||||
params: {
|
||||
pageNum,
|
||||
pageSize,
|
||||
},
|
||||
body: {
|
||||
targetType: typeList[props.currentIndex]
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
function navigateToRecipeDetail(id: string | number) {
|
||||
navigateTo(`/pages-user/pages/recipe/index?id=${id}`)
|
||||
}
|
||||
|
||||
function navigateToDishes(item: any) {
|
||||
uni.navigateTo({
|
||||
url: '/pages-store/pages/store/dishes?id=' + item.id + '&storeId=' + item.merchantId,
|
||||
})
|
||||
}
|
||||
|
||||
// 收藏菜谱
|
||||
function handleSubmitCollectRecipe(item:any) {
|
||||
debouncedEmit(item.merchantRecipeVo?.isCollect, item.merchantRecipeVo?.id, CollectionType.RECIPE, ()=> {
|
||||
item.merchantRecipeVo.isCollect = !item.merchantRecipeVo?.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 reload() {
|
||||
paging.value?.reload()
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
paging.value?.refresh()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
reload,
|
||||
refresh,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="h-full">
|
||||
<z-paging ref="paging" v-model="dataList" @query="queryList" :fixed="false" :auto="false">
|
||||
<view class="p-30rpx">
|
||||
<template v-if="currentIndex == 0">
|
||||
<!--商家-->
|
||||
<view v-for="item in dataList" :key="item.id">
|
||||
<food-box :item="item.merchantVo" />
|
||||
</view>
|
||||
</template>
|
||||
<template v-if="currentIndex == 1">
|
||||
<!--菜品-->
|
||||
<view class="grid grid-cols-2 gap-30rpx">
|
||||
<template v-for="item in dataList">
|
||||
<view @click="navigateToDishes(item.merchantDishVo)" class="w-100% mb-10rpx rounded-16rpx">
|
||||
<view class="relative h-248rpx rounded-24rpx mb-28rpx">
|
||||
<view @click.stop="handleDishCollectionClick(item.merchantDishVo)" class="w-68rpx h-68rpx absolute z-2 top-0 right-0">
|
||||
<image
|
||||
v-if="!item.merchantDishVo.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.merchantDishVo?.dishImage?.split(',')[0]"
|
||||
mode="aspectFill"
|
||||
class="w-full h-full rounded-24rpx"
|
||||
/>
|
||||
</view>
|
||||
<view class="line-clamp-1 text-30rpx lh-30rpx text-#333 font-500 mb-12rpx">
|
||||
{{ item.merchantDishVo.dishName }}
|
||||
</view>
|
||||
<view class="flex-center-sb">
|
||||
<text class="text-30rpx lh-30rpx text-#333 font-500">US${{ item.merchantDishVo.discountPrice }}</text>
|
||||
<view class="member-price-tag text-[#FBE3C3] text-18rpx center pl-6rpx">{{ t('pages-store.store.members') }}: ${{ item.merchantDishVo.memberPrice }}</view>
|
||||
</view>
|
||||
<view class="flex-center-sb mt-12rpx">
|
||||
<view class="text-28rpx text-#999">
|
||||
<view class="line-through">US${{ item.merchantDishVo.originalPrice }}</view>
|
||||
<view>{{ t('pages-store.store.sales') }}:{{ item.merchantDishVo.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>
|
||||
<template v-if="currentIndex == 2">
|
||||
<view class="grid grid-cols-2 gap-30rpx">
|
||||
<view v-for="item in dataList">
|
||||
<view @click="navigateToRecipeDetail(item.id)" class="w-312rpx">
|
||||
<image
|
||||
:src="item.merchantRecipeVo?.recipeImage?.split(',')[0]"
|
||||
class="w-310rpx h-296rpx rounded-24rpx mb-26rpx"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
<view class="flex-center-sb">
|
||||
<text class="text-30rpx lh-30rpx text-#333 line-clamp-1 tracking-[.04em] font-500"
|
||||
>{{ item.merchantRecipeVo?.recipeName }}</text
|
||||
>
|
||||
<view class="w-40rpx h-40rpx ml-14rpx shrink-0">
|
||||
<collection
|
||||
:is-collected="item.merchantRecipeVo?.isCollect"
|
||||
@collectionChange="handleSubmitCollectRecipe(item)"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</z-paging>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,49 @@
|
||||
<script setup lang="ts">
|
||||
import OrderSwiperList from "./components/order-swiper-list/order-swiper-list.vue";
|
||||
const {t} = useI18n()
|
||||
const segmentedValue = ref(0);
|
||||
const segmentedList = [
|
||||
'Store',
|
||||
'Dish',
|
||||
'Recipe',
|
||||
]
|
||||
|
||||
function handleSwiperChange(e) {
|
||||
segmentedValue.value = e.detail.current;
|
||||
}
|
||||
|
||||
const orderSwiperListRef = ref()
|
||||
onMounted(()=> {
|
||||
nextTick(()=> {
|
||||
orderSwiperListRef.value[segmentedValue.value].reload()
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view>
|
||||
<z-paging-swiper>
|
||||
<template #top>
|
||||
<navbar/>
|
||||
<view class="px-30rpx">
|
||||
<view class="text-46rpx lh-46rpx text-#333 font-bold mb-52rpx">{{ t('pages.mine.collection') }}</view>
|
||||
<l-segmented v-model="segmentedValue" :options="segmentedList" shape="round" bg-color="#F2F2F2" active-color="#333" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<swiper class="h-full"
|
||||
:current="segmentedValue"
|
||||
@change="handleSwiperChange">
|
||||
<swiper-item class="swiper-item" v-for="(item, index) in segmentedList" :key="index">
|
||||
<order-swiper-list ref="orderSwiperListRef" :currentIndex="segmentedValue" :index="index"></order-swiper-list>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
</z-paging-swiper>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
page {
|
||||
background-color: white;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,166 @@
|
||||
<script setup lang="ts">
|
||||
import ChooseImage from "@/components/choose-image/choose-image.vue";
|
||||
import {appFeedbackAddPost} from "@/service";
|
||||
import { z } from 'zod';
|
||||
const {t} = useI18n()
|
||||
|
||||
const form = ref({
|
||||
content: '',
|
||||
contactPhone: '',
|
||||
images: '',
|
||||
})
|
||||
|
||||
// 创建zod校验schema
|
||||
const createValidationSchema = () => {
|
||||
return z.object({
|
||||
content: z.string()
|
||||
.min(1, t('pages-user.complaints.validation.content-required'))
|
||||
.min(10, t('pages-user.complaints.validation.content-min-length'))
|
||||
.max(500, t('pages-user.complaints.validation.content-max-length')),
|
||||
contactPhone: z.string()
|
||||
.min(1, t('pages-user.complaints.validation.contact-phone-required'))
|
||||
.regex(/^[\d\s\-\+\(\)]+$/, t('pages-user.complaints.validation.contact-phone-invalid'))
|
||||
})
|
||||
}
|
||||
|
||||
const chooseImageRef = ref()
|
||||
|
||||
// 校验单个字段
|
||||
const validateField = (field: keyof typeof form.value) => {
|
||||
try {
|
||||
const schema = createValidationSchema()
|
||||
const fieldSchema = schema.shape[field]
|
||||
fieldSchema.parse(form.value[field])
|
||||
return true
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
uni.showToast({
|
||||
title: error.errors[0].message,
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 校验整个表单
|
||||
const validateForm = () => {
|
||||
try {
|
||||
const schema = createValidationSchema()
|
||||
schema.parse(form.value)
|
||||
return true
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
uni.showToast({
|
||||
title: error.errors[0].message,
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!validateForm()) {
|
||||
return
|
||||
}
|
||||
|
||||
appFeedbackAddPost({
|
||||
body: {
|
||||
...form.value
|
||||
}
|
||||
}).then(res=> {
|
||||
uni.showToast({
|
||||
title: t('toast.submitSuccess'),
|
||||
icon: 'none'
|
||||
})
|
||||
form.value = {
|
||||
content: '',
|
||||
contactPhone: '',
|
||||
images: '',
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleChooseImage = () => {
|
||||
chooseImageRef.value.init()
|
||||
}
|
||||
function onImageChange(files: string[]) {
|
||||
form.value.images = files[0]
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<navbar :title="t('pages-user.complaints.title')" />
|
||||
<view class="px-18rpx">
|
||||
<view class="mb-27rpx mt-32rpx text-28rpx text-#333">
|
||||
{{ t('pages-user.complaints.description') }}
|
||||
</view>
|
||||
<view class="bg-white rounded-14rpx px-18rpx pt-30rpx pb-24rpx">
|
||||
<view class="text-28rpx text-#333 mb-28rpx">{{ t('pages-user.complaints.feedback-content') }}:</view>
|
||||
<view
|
||||
class="min-h-234rpx box-border bg-#F6F6F6 rounded-14rpx overflow-hidden px-18rpx py-10rpx"
|
||||
>
|
||||
<wd-textarea
|
||||
v-model="form.content"
|
||||
:maxlength="500"
|
||||
custom-class="!bg-#F6F6F6"
|
||||
custom-textarea-container-class="!bg-#F6F6F6"
|
||||
no-border
|
||||
auto-height
|
||||
:placeholder="t('pages-user.complaints.feedback-content-placeholder')"
|
||||
@blur="validateField('content')"
|
||||
/>
|
||||
</view>
|
||||
|
||||
|
||||
<view>
|
||||
<view class="text-28rpx text-#333 mt-32rpx mb-24rpx">{{ t('pages-user.complaints.image') }}:</view>
|
||||
<view @click="handleChooseImage" class="relative w-210rpx h-210rpx">
|
||||
<image
|
||||
v-if="form.images"
|
||||
src="@img/chef/113.png"
|
||||
class="absolute top--10rpx right--10rpx z-10 w-36rpx h-36rpx"
|
||||
></image>
|
||||
<image
|
||||
v-if="!form.images"
|
||||
src="@img/chef/112.png"
|
||||
class="w-210rpx h-210rpx"
|
||||
></image>
|
||||
<image
|
||||
v-else
|
||||
:src="form.images"
|
||||
class="w-210rpx h-210rpx rounded-16rpx"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="mt-14rpx flex-center-sb bg-white h-86rpx rounded-14rpx px-18rpx">
|
||||
<view class="text-28rpx text-#333 ">{{ t('pages-user.complaints.contact-information') }}:</view>
|
||||
<wd-input
|
||||
no-border
|
||||
custom-class="!p-[12rpx+20rpx] rounded-10rpx"
|
||||
v-model.trim="form.contactPhone"
|
||||
placeholderStyle="font-size: 28rpx;line-height: 40rpx;color: #B1B1B1; text-align: right;"
|
||||
:placeholder="t('common.enter')"
|
||||
:maxlength="50"
|
||||
custom-input-class="text-right"
|
||||
@blur="validateField('contactPhone')"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="mt-38rpx text-28rpx text-#999">
|
||||
{{ t('pages-user.complaints.contact-information-tip') }}
|
||||
</view>
|
||||
<fixed-bottom-large-btn
|
||||
class="z-100"
|
||||
fixed
|
||||
:text="`${t('common.submit')}`"
|
||||
@click="handleSubmit"
|
||||
/>
|
||||
<ChooseImage ref="chooseImageRef" @change="onImageChange" />
|
||||
</view>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,166 @@
|
||||
<script setup lang="ts">
|
||||
import {appCouponExchangeCouponPost, appCouponUserCouponListPost} from "@/service";
|
||||
|
||||
const { t } = useI18n();
|
||||
import { throttle } from "throttle-debounce";
|
||||
import Config from "@/config";
|
||||
import {dayjs} from "@/plugin";
|
||||
|
||||
function getList(pageNum: number, pageSize: number) {
|
||||
return new Promise(resolve => {
|
||||
appCouponUserCouponListPost({
|
||||
params: {
|
||||
pageNum,
|
||||
pageSize,
|
||||
}
|
||||
}).then(res => {
|
||||
resolve(res)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const {paging, loading, firstLoaded, dataList, queryList} = usePage(getList)
|
||||
|
||||
|
||||
const keyword = ref<string>("");
|
||||
// 输入框是否聚焦
|
||||
const isSearch = ref<boolean>(false);
|
||||
// 是否禁用兑换按钮
|
||||
const isDisabled = computed(() => {
|
||||
return !keyword.value;
|
||||
});
|
||||
|
||||
function handleClearSearch() {
|
||||
isSearch.value = false;
|
||||
}
|
||||
function search(event: any) {
|
||||
if (!event.value) {
|
||||
isSearch.value = false;
|
||||
} else {
|
||||
}
|
||||
}
|
||||
const handleSearch = throttle(Config.throttleShortTime, search, {
|
||||
noLeading: true,
|
||||
noTrailing: false,
|
||||
});
|
||||
|
||||
// 兑换
|
||||
function handleSubmit() {
|
||||
console.log("兑换");
|
||||
appCouponExchangeCouponPost({
|
||||
body: {
|
||||
couponCode: keyword.value,
|
||||
}
|
||||
}).then(res=> {
|
||||
paging.value?.refresh()
|
||||
uni.showToast({
|
||||
title: t('toast.redemptionSuccessful'),
|
||||
icon: 'none'
|
||||
})
|
||||
})
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<z-paging
|
||||
ref="paging"
|
||||
:default-page-size="10"
|
||||
v-model="dataList"
|
||||
@query="queryList"
|
||||
bg-color="#fff"
|
||||
>
|
||||
<template #top>
|
||||
<navbar />
|
||||
<view class="px-30rpx pb-42rpx">
|
||||
<view class="text-46rpx text-#333 font-bold lh-46rpx mb-36rpx mt-10rpx">
|
||||
{{ t("pages-user.coupon.title") }}</view
|
||||
>
|
||||
<view
|
||||
class="flex items-center h-88rpx px-30rpx bg-#F2F3F6 rounded-88rpx box-border"
|
||||
>
|
||||
<image
|
||||
class="w-28rpx h-28rpx shrink-0"
|
||||
src="@img/chef/105.png"
|
||||
></image>
|
||||
<wd-input
|
||||
no-border
|
||||
focus-when-clear
|
||||
type="text"
|
||||
v-model="keyword"
|
||||
:maxlength="120"
|
||||
:placeholder="t('pages-user.coupon.search-placeholder')"
|
||||
placeholderStyle="font-size: 30rpx;line-height: 42rpx;color: #4A4A4A;"
|
||||
custom-class="flex-1 ml-16rpx !bg-transparent"
|
||||
@clear="handleClearSearch"
|
||||
@input="handleSearch"
|
||||
@focus="isSearch = true"
|
||||
@blur="isSearch = false"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<view class="px-30rpx my-38rpx" v-if="isSearch">
|
||||
<wd-button
|
||||
block
|
||||
custom-class="!h-108rpx !text-36rpx !rounded-16rpx"
|
||||
:disabled="isDisabled"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ t("pages-user.coupon.redeem-now") }}
|
||||
</wd-button>
|
||||
</view>
|
||||
|
||||
<template v-for="item in dataList" :key="item.id">
|
||||
<view class="coupon-item h-328rpx mx-36rpx 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">
|
||||
{{ item.snapshotNameZh }}
|
||||
</view>
|
||||
<view class="text-24rpx lh-32rpx text-#999 my-18rpx">
|
||||
{{ item.snapshotMerchantId ? t('pages-user.coupon.merchant-specific') : t('pages-user.coupon.all-merchants') }}
|
||||
</view>
|
||||
<view class="text-24rpx lh-32rpx text-#999 my-18rpx">
|
||||
{{ t('pages-user.coupon.expiry-date') }}{{ dayjs(Number(item.snapshotValidEnd))
|
||||
.format('MM:DD HH:mm:ss') }}
|
||||
</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>
|
||||
<template v-if="item.snapshotType === 2">
|
||||
<!-- 满减-->
|
||||
{{ item.snapshotDiscount }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ Number(item.snapshotDiscount * 100).toFixed(0) }}%
|
||||
</template>
|
||||
|
||||
{{ t('pages-store.store.discount') }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="h-84rpx lh-84rpx text-center text-28rpx text-#fff">
|
||||
{{ t('common.confirm') }}
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- <view-->
|
||||
<!-- v-if="dataList.length === 0 && !isSearch"-->
|
||||
<!-- class="flex flex-col items-center mt-88rpx"-->
|
||||
<!-- >-->
|
||||
<!-- <image class="w-552rpx h-552rpx" src="@img/chef/104.png"></image>-->
|
||||
<!-- <text class="text-28rpx text-#9E9E9E mt--100rpx"-->
|
||||
<!-- >{{ t('pages-user.coupon.no-coupons') }}</text-->
|
||||
<!-- >-->
|
||||
<!-- </view>-->
|
||||
</z-paging>
|
||||
</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,98 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
appCouponExchangeCouponPost,
|
||||
appCouponUserCouponListPost,
|
||||
appMerchantOrderCanUseCouponListMerchantIdGet
|
||||
} from "@/service";
|
||||
|
||||
const { t } = useI18n();
|
||||
import { throttle } from "throttle-debounce";
|
||||
import Config from "@/config";
|
||||
import {dayjs} from "@/plugin";
|
||||
import {useConfigStore} from "@/store";
|
||||
|
||||
const {proxy} = getCurrentInstance() as any
|
||||
|
||||
const eventChannel = proxy.getOpenerEventChannel();
|
||||
|
||||
const props = defineProps<{
|
||||
id?: string
|
||||
}>()
|
||||
|
||||
function getList(pageNum: number, pageSize: number) {
|
||||
return new Promise(resolve => {
|
||||
appMerchantOrderCanUseCouponListMerchantIdGet({
|
||||
params: {
|
||||
merchantId: props.id || ''
|
||||
}
|
||||
}).then(res => {
|
||||
resolve({ rows: res.data })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const {paging, loading, firstLoaded, dataList, queryList} = usePage(getList)
|
||||
|
||||
function confirmCoupon(item: any) {
|
||||
eventChannel.emit('selectedCoupon', item);
|
||||
uni.navigateBack()
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<z-paging
|
||||
ref="paging"
|
||||
:default-page-size="10"
|
||||
v-model="dataList"
|
||||
@query="queryList"
|
||||
bg-color="#fff"
|
||||
>
|
||||
<template #top>
|
||||
<navbar />
|
||||
<view class="px-30rpx pb-42rpx">
|
||||
<view class="text-46rpx text-#333 font-bold lh-46rpx mb-36rpx mt-10rpx">
|
||||
{{ t("pages-user.coupon.title") }}</view
|
||||
>
|
||||
</view>
|
||||
</template>
|
||||
<template v-for="item in dataList" :key="item.id">
|
||||
<view class="coupon-item h-328rpx mx-36rpx 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">
|
||||
{{ item.snapshotNameZh }}
|
||||
</view>
|
||||
<view class="text-24rpx lh-32rpx text-#999 my-18rpx">
|
||||
{{ item.snapshotMerchantId ? t('pages-user.coupon.merchant-specific') : t('pages-user.coupon.all-merchants') }}
|
||||
</view>
|
||||
<view class="text-24rpx lh-32rpx text-#999 my-18rpx">
|
||||
{{ t('pages-user.coupon.expiry-date') }}{{ dayjs(Number(item.snapshotValidEnd))
|
||||
.format('MM:DD HH:mm:ss') }}
|
||||
</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>
|
||||
<template v-if="item.snapshotType === 2">
|
||||
<!-- 满减-->
|
||||
{{ item.snapshotDiscount }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ Number(item.snapshotDiscount * 100).toFixed(0) }}%
|
||||
</template>{{ t('pages-store.store.discount') }}
|
||||
</view>
|
||||
</view>
|
||||
<view @click="confirmCoupon(item)" class="h-84rpx lh-84rpx text-center text-28rpx text-#fff">
|
||||
{{ t('common.confirm') }}
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</z-paging>
|
||||
</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,107 @@
|
||||
<script lang="ts" setup>
|
||||
import Config from '@/config'
|
||||
import {useUserStore} from '@/store'
|
||||
import { appUserEditUserInfoPost } from '@/service'
|
||||
import {debounce} from 'throttle-debounce';
|
||||
import {z} from "zod";
|
||||
import * as R from "ramda";
|
||||
|
||||
const {t} = useI18n()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const form = ref({
|
||||
firstName: userStore.userInfo?.firstName,
|
||||
surname: userStore.userInfo?.surname,
|
||||
})
|
||||
|
||||
const FormSchema = z.object({
|
||||
firstName: z.string().min(1, {message: t('pages-login.prompt.first-name')}),
|
||||
surname: z.string().min(1, {message: t('pages-login.prompt.last-name')}),
|
||||
})
|
||||
|
||||
|
||||
function checkForm(): boolean {
|
||||
const validateFormField = FormSchema.safeParse(form.value)
|
||||
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',
|
||||
})
|
||||
}
|
||||
return validateFormField.success
|
||||
}
|
||||
|
||||
|
||||
async function submit() {
|
||||
try {
|
||||
await appUserEditUserInfoPost({
|
||||
body: {
|
||||
...form.value,
|
||||
}
|
||||
})
|
||||
await uni.showToast({title: t('common.prompt.save-successfully'), icon: 'none'})
|
||||
await userStore.getUserInfo()
|
||||
setTimeout(uni.navigateBack, 1000)
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
// 提交
|
||||
const handleSubmit = R.when(checkForm, debounce(Config.debounceLongTime, submit, {
|
||||
atBegin: true
|
||||
}))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view>
|
||||
<navbar :title="t('navbar-nickname')"/>
|
||||
<view class="py-36rpx px-30rpx bg-#fff">
|
||||
<view class="">
|
||||
<view class="text-28-bold">{{ t("pages-login.sign-up.first-name") }}</view>
|
||||
<view
|
||||
class="mt-20rpx flex px-30rpx items-center h-88rpx border-2rpx border-solid border-#D4D4D4 rounded-20rpx">
|
||||
<wd-input
|
||||
no-border
|
||||
clearable
|
||||
:focus-when-clear="false"
|
||||
:cursorSpacing="20"
|
||||
:maxlength="40"
|
||||
v-model.trim="form.firstName"
|
||||
custom-class="flex-1"
|
||||
placeholder=""
|
||||
>
|
||||
</wd-input>
|
||||
</view>
|
||||
</view>
|
||||
<view class="mt-36rpx">
|
||||
<view class="text-28-bold">{{ t("pages-login.sign-up.last-name") }}</view>
|
||||
<view
|
||||
class="mt-20rpx flex px-30rpx items-center h-88rpx border-2rpx border-solid border-#D4D4D4 rounded-20rpx">
|
||||
<wd-input
|
||||
no-border
|
||||
clearable
|
||||
:focus-when-clear="false"
|
||||
use-prefix-slot
|
||||
:maxlength="40"
|
||||
:cursorSpacing="20"
|
||||
v-model.trim="form.surname"
|
||||
custom-class="flex-1"
|
||||
placeholder=""
|
||||
>
|
||||
</wd-input>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mt-318rpx px-30rpx">
|
||||
<wd-button custom-class="!h-108rpx !text-36rpx font-bold !rounded-16rpx" block @click="handleSubmit">
|
||||
{{ t('common.save') }}
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss"></style>
|
||||
@@ -0,0 +1,220 @@
|
||||
<script setup lang="ts">
|
||||
import { appHelpCenterListGet } from "@/service";
|
||||
import { throttle } from "throttle-debounce";
|
||||
import Config from "@/config";
|
||||
import Fuse from "fuse.js";
|
||||
|
||||
const { t } = useI18n();
|
||||
const selected = ref<string[]>([]);
|
||||
const keyword = ref<string>("");
|
||||
const isSearch = ref(false);
|
||||
const searchResult = ref([]);
|
||||
|
||||
const dataList = ref<any[]>([]);
|
||||
|
||||
function handleClearSearch() {
|
||||
isSearch.value = false;
|
||||
}
|
||||
|
||||
function search(event: any) {
|
||||
if (!event.value) {
|
||||
isSearch.value = false;
|
||||
} else {
|
||||
isSearch.value = true;
|
||||
const fuse = new Fuse(dataList.value, {
|
||||
threshold: 0.2,
|
||||
keys: ["title", "content"],
|
||||
});
|
||||
searchResult.value = fuse.search(event.value);
|
||||
selected.value = [];
|
||||
console.log("searchResult.value", searchResult.value);
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = throttle(Config.throttleShortTime, search, {
|
||||
noLeading: true,
|
||||
noTrailing: false,
|
||||
});
|
||||
|
||||
|
||||
onLoad(()=> {
|
||||
appHelpCenterListGet({}).then(res=> {
|
||||
console.log(res)
|
||||
if(res.data && res.data.length> 0) {
|
||||
dataList.value = res.data.map(item=> {
|
||||
return {
|
||||
id: item.id,
|
||||
title: item.question,
|
||||
content: item.answer
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view>
|
||||
<z-paging
|
||||
:auto="false"
|
||||
>
|
||||
<template #top>
|
||||
<navbar />
|
||||
<view class="px-30rpx pb-42rpx">
|
||||
<view class="text-46rpx text-#333 font-bold lh-46rpx mb-36rpx mt-10rpx">{{ t("pages.mine.help") }}</view>
|
||||
<view
|
||||
class="flex items-center h-88rpx px-30rpx bg-#F2F3F6 rounded-88rpx box-border"
|
||||
>
|
||||
<image class="w-28rpx h-28rpx shrink-0" src="@img/chef/100222.png"></image>
|
||||
<wd-input
|
||||
no-border
|
||||
focus-when-clear
|
||||
type="text"
|
||||
v-model="keyword"
|
||||
:maxlength="120"
|
||||
:placeholder="t('common.search')"
|
||||
placeholderStyle="font-size: 30rpx;line-height: 42rpx;color: #4A4A4A;"
|
||||
custom-class="flex-1 ml-16rpx !bg-transparent"
|
||||
@clear="handleClearSearch"
|
||||
@input="handleSearch"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<template v-if="isSearch">
|
||||
<view class="py-4rpx bg-#fff px-30rpx" v-if="searchResult.length">
|
||||
<wd-collapse v-model="selected">
|
||||
<wd-collapse-item
|
||||
:name="item.id"
|
||||
custom-class="!px-0"
|
||||
v-for="item in searchResult"
|
||||
:key="item.id"
|
||||
>
|
||||
<template #title="{ expanded, disabled, isFirst }">
|
||||
<view class="flex items-center justify-between">
|
||||
<view class="flex items-center">
|
||||
<image
|
||||
class="w-32rpx h-32rpx shrink-0 mr-14rpx"
|
||||
src="@img/chef/102.png"
|
||||
></image>
|
||||
<text
|
||||
class="text-32rpx text-#000 lh-32rpx transition-all"
|
||||
:class="[expanded ? '' : 'line-clamp-1']"
|
||||
>{{ item.item.title }}
|
||||
</text>
|
||||
</view>
|
||||
<image
|
||||
class="shrink-0 ml-12rpx w-30rpx h-30rpx transition-all ease-out"
|
||||
:class="[expanded && 'rotate-180']"
|
||||
src="@img/chef/101.png"
|
||||
></image>
|
||||
</view>
|
||||
</template>
|
||||
<view
|
||||
class="text-28rpx text-#666 lh-36rpx whitespace-pre-wrap"
|
||||
>
|
||||
<mp-html
|
||||
selectable
|
||||
:preview-img="false"
|
||||
:show-img-menu="false"
|
||||
:tag-style="{
|
||||
div: 'white-space: pre-wrap;',
|
||||
p: 'white-space: pre-wrap;',
|
||||
img: 'width:100%;max-width: 100%;height:auto;',
|
||||
}"
|
||||
:content="item.item.content"
|
||||
></mp-html>
|
||||
</view>
|
||||
</wd-collapse-item>
|
||||
</wd-collapse>
|
||||
</view>
|
||||
<view class="px-24rpx" v-show="!searchResult.length">
|
||||
<z-paging-empty-view
|
||||
:empty-view-fixed="false"
|
||||
empty-view-img="/static/images/16033@2x.png"
|
||||
:empty-view-img-style="{
|
||||
width: '450rpx',
|
||||
height: '450rpx',
|
||||
}"
|
||||
:empty-view-text="t('common.empty-view-text')"
|
||||
:empty-view-title-style="{
|
||||
marginTop: '24rpx',
|
||||
}"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
<view v-else-if="dataList.length" class="px-30rpx">
|
||||
<wd-collapse v-model="selected">
|
||||
<wd-collapse-item
|
||||
:name="item.id"
|
||||
custom-class="!px-0"
|
||||
v-for="item in dataList"
|
||||
:key="item.id"
|
||||
>
|
||||
<template #title="{ expanded, disabled, isFirst }">
|
||||
<view class="flex items-center justify-between">
|
||||
<view class="flex items-center">
|
||||
<image
|
||||
class="w-32rpx h-32rpx shrink-0 mr-14rpx"
|
||||
src="@img/chef/102.png"
|
||||
></image>
|
||||
<text
|
||||
class="text-32rpx text-#000 lh-32rpx transition-all"
|
||||
:class="[expanded ? '' : 'line-clamp-1']"
|
||||
>{{ item.title }}
|
||||
</text>
|
||||
</view>
|
||||
<image
|
||||
class="shrink-0 ml-12rpx w-30rpx h-30rpx transition-all ease-out"
|
||||
:class="[expanded && 'rotate-180']"
|
||||
src="@img/chef/101.png"
|
||||
></image>
|
||||
</view>
|
||||
</template>
|
||||
<view
|
||||
class="text-28rpx text-#666 lh-36rpx whitespace-pre-wrap"
|
||||
>
|
||||
<mp-html
|
||||
selectable
|
||||
:preview-img="false"
|
||||
:show-img-menu="false"
|
||||
:tag-style="{
|
||||
div: 'white-space: pre-wrap;',
|
||||
p: 'white-space: pre-wrap;',
|
||||
img: 'width:100%;max-width: 100%;height:auto;',
|
||||
}"
|
||||
:content="item.content"
|
||||
></mp-html>
|
||||
</view>
|
||||
</wd-collapse-item>
|
||||
</wd-collapse>
|
||||
</view>
|
||||
</z-paging>
|
||||
</view>
|
||||
</template>
|
||||
<style>
|
||||
page {
|
||||
background-color: #fff;
|
||||
}
|
||||
</style>
|
||||
<style scoped lang="scss">
|
||||
:deep(.wd-collapse) {
|
||||
.wd-collapse-item::after {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.wd-collapse-item__header {
|
||||
padding: 18rpx 0 !important;
|
||||
}
|
||||
.wd-collapse-item__body {
|
||||
padding: 0 !important;
|
||||
}
|
||||
.wd-collapse-item__header::after {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.wd-collapse-item__body {
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,114 @@
|
||||
<script lang="ts" setup>
|
||||
import { thumbnailImg } from "@/utils/utils";
|
||||
import { conversionMobile } from "@/utils";
|
||||
import {appUserUserInviteInviteListPost} from "@/service";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { paging, dataList, queryList, loading } = usePage<any>(
|
||||
(pageNum: number, pageSize: number) =>
|
||||
appUserUserInviteInviteListPost({
|
||||
params: {
|
||||
pageNum,
|
||||
pageSize,
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
function navigateTo(url: string) {
|
||||
uni.navigateTo({
|
||||
url,
|
||||
});
|
||||
}
|
||||
function handleClickLeft() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view>
|
||||
<z-paging ref="paging" v-model="dataList" @query="queryList">
|
||||
<template #top>
|
||||
<wd-navbar
|
||||
safeAreaInsetTop
|
||||
:fixed="true"
|
||||
:placeholder="true"
|
||||
:bordered="false"
|
||||
custom-class="!bg-transparent"
|
||||
@click-left="handleClickLeft"
|
||||
>
|
||||
<template #left>
|
||||
<view class="shrink-0">
|
||||
<view class="i-carbon:chevron-left text-50rpx text-#3D3D3D ml-[-10rpx]"></view>
|
||||
</view>
|
||||
</template>
|
||||
</wd-navbar>
|
||||
<view class="mb-46rpx mt-18rpx pl-30rpx text-#333 text-46rpx lh-46rpx font-bold">{{ t('pages.mine.the-person-invited') }} ({{ dataList.length }})</view>
|
||||
</template>
|
||||
<view class="px-18rpx">
|
||||
<view
|
||||
class="animate-in fade-in animate-ease-in animate-duration-300"
|
||||
v-show="!loading"
|
||||
>
|
||||
<view
|
||||
class="py-28rpx px-20rpx bg-#fff rounded-16rpx"
|
||||
:class="[index > 0 && 'mt-20rpx']"
|
||||
v-for="(item, index) in dataList"
|
||||
:key="item.id"
|
||||
>
|
||||
<view class="flex items-center justify-between">
|
||||
<view class="flex items-center shrink-0 mr-30rpx">
|
||||
<image
|
||||
class="w-106rpx h-106rpx rounded-full"
|
||||
:src="thumbnailImg(item.avatar)"
|
||||
></image>
|
||||
</view>
|
||||
<view class="flex-1">
|
||||
<view class="text-28rpx text-primary lh-42rpx font-bold"
|
||||
>
|
||||
{{ item?.firstName }} {{ item?.surname }}
|
||||
</view>
|
||||
<view class="mt-25rpx flex items-center">
|
||||
<!-- <image class="w-28rpx h-28rpx shrink-0 mr-2prx" src="@img-user/1479@2x.png"></image> -->
|
||||
<text class="text-24rpx text-#999 lh-34rpx font-bold">{{
|
||||
conversionMobile(item.phone)
|
||||
}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="animate-in fade-in animate-ease-in-out animate-duration-300"
|
||||
v-show="loading"
|
||||
>
|
||||
<view
|
||||
class="py-28rpx px-20rpx bg-#fff rounded-16rpx"
|
||||
:class="[i > 1 && 'mt-20rpx']"
|
||||
v-for="i in 10"
|
||||
:key="i"
|
||||
>
|
||||
<view class="flex items-center justify-between">
|
||||
<wd-skeleton
|
||||
animation="gradient"
|
||||
:row-col="[{ size: '106rpx', type: 'circle' }]"
|
||||
/>
|
||||
<view class="flex-1 ml-30rpx">
|
||||
<wd-skeleton
|
||||
animation="gradient"
|
||||
:row-col="[
|
||||
{ width: '94rpx', margin: '0px' },
|
||||
{ width: '354rpx' },
|
||||
]"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</z-paging>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,147 @@
|
||||
<script setup lang="ts">
|
||||
import Config from "@/config";
|
||||
import {appPreferenceChoosePost, appPreferenceListGet} from "@/service";
|
||||
const { t } = useI18n();
|
||||
|
||||
// 食物类别数据
|
||||
const foodCategories = ref([]);
|
||||
|
||||
// 计算选中的数量
|
||||
const selectedCount = computed(() => {
|
||||
return foodCategories.value.filter((item) => item.selected).length;
|
||||
});
|
||||
|
||||
// 计算是否可以继续
|
||||
const canContinue = computed(() => {
|
||||
return selectedCount.value > 0;
|
||||
});
|
||||
|
||||
// 切换选择状态
|
||||
const toggleSelection = (item: any) => {
|
||||
if (item.selected) {
|
||||
// 如果已选中,直接取消选中
|
||||
item.selected = false;
|
||||
} else {
|
||||
// 如果未选中,检查是否已达到最大选择数量
|
||||
if (selectedCount.value < 3) {
|
||||
item.selected = true;
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: t('toast.likeTips'),
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 继续按钮
|
||||
const handleContinue = () => {
|
||||
const selectedItems = foodCategories.value.filter((item) => item.selected);
|
||||
appPreferenceChoosePost({
|
||||
body: selectedItems.map((item) => item.id),
|
||||
}).then(res=> {
|
||||
uni.switchTab({
|
||||
url: Config.indexPath
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
// 跳过按钮
|
||||
const handleSkip = () => {
|
||||
uni.switchTab({
|
||||
url: Config.indexPath
|
||||
})
|
||||
};
|
||||
|
||||
onLoad(()=> {
|
||||
appPreferenceListGet({}).then(res=> {
|
||||
console.log(res, res)
|
||||
if(res.data && res.data.length > 0) {
|
||||
foodCategories.value = res.data.map((item) => {
|
||||
return {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
image: item.imageUrl,
|
||||
selected: false,
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 主要内容区域 -->
|
||||
<view class="px-30rpx pt-44rpx">
|
||||
<status-bar />
|
||||
<!-- 标题 -->
|
||||
<view class="text-56rpx lh-56rpx font-bold text-#333"
|
||||
>{{ t('pages-user.member-center.chooseLike') }}</view
|
||||
>
|
||||
|
||||
<!-- 描述文本 -->
|
||||
<view class="text-26rpx lh-26rpx text-#999 font-400 mt-16rpx mb-104rpx">
|
||||
{{ t('pages-user.member-center.likeDescription') }}
|
||||
</view>
|
||||
|
||||
<!-- 食物类别网格 -->
|
||||
<view class="px-36rpx">
|
||||
<view class="grid grid-cols-3 gap-40rpx">
|
||||
<view
|
||||
v-for="item in foodCategories"
|
||||
:key="item.id"
|
||||
class="flex flex-col items-center"
|
||||
@click="toggleSelection(item)"
|
||||
>
|
||||
<!-- 图片容器 -->
|
||||
<view
|
||||
class="w-148rpx h-148rpx box-border rounded-full overflow-hidden center"
|
||||
:class="
|
||||
item.selected ? 'border-4rpx border-#CE7138 border-solid' : ''
|
||||
"
|
||||
>
|
||||
<image
|
||||
:src="item.image"
|
||||
class="w-128rpx h-128rpx rounded-full"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 文字标签 -->
|
||||
<text
|
||||
class="text-28rpx text-center"
|
||||
:class="item.selected ? 'text-#CE7138' : 'text-#333'"
|
||||
>
|
||||
{{ item.name }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部按钮区域 -->
|
||||
<view class="mt-180rpx">
|
||||
<wd-button
|
||||
:disabled="!canContinue"
|
||||
custom-class="!h-98rpx !text-30rpx !text-white !bg-#14181B !rounded-16rpx mb-32rpx"
|
||||
block
|
||||
@click="handleContinue"
|
||||
>
|
||||
{{ t("common.continue") }}
|
||||
</wd-button>
|
||||
|
||||
<wd-button
|
||||
custom-class="!h-98rpx !text-#333 !text-30rpx !bg-#D8D8D8 !rounded-16rpx"
|
||||
block
|
||||
@click="handleSkip"
|
||||
>
|
||||
{{ t('common.skip') }}
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
page {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,82 @@
|
||||
<script setup lang="ts">
|
||||
import {appMembershipConfigGet} from "@/service";
|
||||
|
||||
const { t } = useI18n();
|
||||
function handleClickLeft() {
|
||||
uni.navigateBack();
|
||||
}
|
||||
|
||||
// 订阅
|
||||
function handleSubscribe() {
|
||||
uni.navigateTo({
|
||||
url: '/pages-user/pages/member/index'
|
||||
})
|
||||
}
|
||||
// 暂时跳过
|
||||
function handleSkip() {
|
||||
uni.navigateTo({
|
||||
url: "/pages-user/pages/member-center/choose-like",
|
||||
});
|
||||
}
|
||||
|
||||
const membershipConfig = ref({});
|
||||
onLoad(()=> {
|
||||
appMembershipConfigGet({}).then(res=> {
|
||||
console.log(res)
|
||||
membershipConfig.value = res.data;
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view>
|
||||
<wd-navbar
|
||||
safeAreaInsetTop
|
||||
:fixed="true"
|
||||
:placeholder="false"
|
||||
:bordered="false"
|
||||
custom-class="!bg-transparent"
|
||||
@click-left="handleClickLeft"
|
||||
>
|
||||
</wd-navbar>
|
||||
<image :src="membershipConfig?.centerTopImage" mode="aspectFill" class="w-full h-1762rpx" />
|
||||
<!-- <view class="px-30rpx py-37rpx">-->
|
||||
<!-- <image src="@img/ls/2.png" class="w-full h-1121rpx" />-->
|
||||
<!-- </view>-->
|
||||
<view class="px-30rpx pb-37rpx">
|
||||
<wd-button
|
||||
custom-class="!h-108rpx !text-30rpx !rounded-16rpx"
|
||||
block
|
||||
@click="handleSubscribe"
|
||||
>
|
||||
{{ t('pages-user.member-center.subscribe') }}
|
||||
</wd-button>
|
||||
</view>
|
||||
<view class="h-20rpx bg-#F6F6F6"></view>
|
||||
<view class="px-30rpx pb-82rpx pt-42rpx">
|
||||
<!-- <view class="text-48rpx lh-48rpx text-#333 font-500">-->
|
||||
<!-- {{ t('pages-user.member-center.notCurrentlyTrying') }}-->
|
||||
<!-- </view>-->
|
||||
<!-- <view class="mt-36rpx mb-42rpx flex items-center">-->
|
||||
<!-- <image src="@img/chef/100221.png" class="w-98rpx h-98rpx shrink-0 rounded-full" />-->
|
||||
<!-- <view class="text-26rpx lh-26rpx text-#333 font-400 ml-30rpx">-->
|
||||
<!-- {{ t('pages-user.member-center.youWillMissOut') }}-->
|
||||
<!-- </view>-->
|
||||
<!-- </view>-->
|
||||
<image :src="membershipConfig?.centerBottomImage" class="w-full h-200rpx" />
|
||||
<wd-button
|
||||
custom-class="!h-108rpx !text-#333 !text-30rpx !bg-#D8D8D8 !rounded-16rpx mt-46rpx"
|
||||
block
|
||||
@click="handleSkip"
|
||||
>
|
||||
{{ t('pages-user.member-center.temporarilySkip') }}
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
page {
|
||||
background-color: #fff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,148 @@
|
||||
<script setup lang="ts">
|
||||
import {appMembershipConfigGet, appMembershipRechargeItemGet} from "@/service";
|
||||
import {getDictFineList} from "@/pages-store/service";
|
||||
const { t, locale } = useI18n();
|
||||
import Config from '@/config/index'
|
||||
import {useConfigStore, useUserStore} from "@/store";
|
||||
const configStore = useConfigStore()
|
||||
const userStore = useUserStore();
|
||||
|
||||
function handleSubmit() {
|
||||
uni.navigateTo({
|
||||
url: '/pages-user/pages/member/join-member',
|
||||
success: function(res) {
|
||||
// 通过eventChannel向被打开页面传送数据
|
||||
res.eventChannel.emit('acceptDataFromOpenerPage', { data: membershipRechargeList.value })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const membershipConfig = ref({});
|
||||
const memberText = ref({
|
||||
title_delivery: '',
|
||||
desc_delivery: '',
|
||||
title_pickup: '',
|
||||
desc_pickup: ''
|
||||
})
|
||||
onLoad(()=> {
|
||||
appMembershipConfigGet({}).then(res=> {
|
||||
console.log(res)
|
||||
membershipConfig.value = res.data;
|
||||
})
|
||||
let isZh = locale.value === 'zh-Hans'
|
||||
getDictFineList({
|
||||
dictType: 'member_text'
|
||||
}).then(res=> {
|
||||
if(res.data) {
|
||||
res.data.forEach(item => {
|
||||
// 去除dictValue前后的空格,确保属性匹配正确
|
||||
const key = item.dictValue.trim();
|
||||
// 检查memberText中是否存在该属性
|
||||
if (memberText.value.hasOwnProperty(key)) {
|
||||
memberText.value[key] = isZh ? item.remark : item.dictLabel;
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
appMembershipRechargeItem()
|
||||
})
|
||||
|
||||
const membershipRechargeItem = ref({})
|
||||
const membershipRechargeList = ref([])
|
||||
function appMembershipRechargeItem() {
|
||||
appMembershipRechargeItemGet({}).then(res=> {
|
||||
console.log(res)
|
||||
membershipRechargeList.value = res.data || []
|
||||
membershipRechargeItem.value = res.data[0] || {}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="">
|
||||
<navbar />
|
||||
<view class="px-30rpx">
|
||||
<view class="mb-52rpx mt-20rpx">
|
||||
<view class="text-46rpx lh-46rpx text-#333 font-bold tracking-[.04em]">{{ Config.appName }}</view>
|
||||
<view class="text-32rpx lh-32rpx text-#999 font-500 tracking-[.04em] mt-24rpx">
|
||||
<text>(${{ membershipRechargeItem.rechargeAmount || 0 }}/{{ membershipRechargeItem.name || 0 }})</text>
|
||||
<template v-if="!userStore.userInfo.userMembershipVo">
|
||||
<text class="ml-20rpx text-#CE7138">{{ membershipRechargeItem.trialWeeksDisplay || 0 }} {{ t('pages-user.member.weeks') }}</text>
|
||||
<text class="ml-20rpx text-#CE7138">{{ t('pages-user.member.free') }}</text>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
<view class="h-160rpx rounded-16rpx bg-#FFEED8 border-#D8D8D8 border-solid border-1rpx flex-center-sb">
|
||||
<!-- <view class="">-->
|
||||
<!-- <view class="text-28rpx lh-32rpx text-#333 font-500 tracking-[.04em]">Chef OneOnly 2 orders are needed to recoup costs</view>-->
|
||||
<!-- <view class="text-22rpx lh-22rpx text-#999 mt-12rpx tracking-[.04em]">Monthly average level of members</view>-->
|
||||
<!-- </view>-->
|
||||
<image
|
||||
:src="membershipConfig?.purchasePageImage"
|
||||
class="w-full h-full shrink-0"
|
||||
></image>
|
||||
</view>
|
||||
<view class="flex-center-sb gap-30rpx my-52rpx">
|
||||
<view class="w-full min-h-340rpx rounded-20rpx border-#D8D8D8 border-solid border-2rpx pb-26rpx px-28rpx pt-20rpx">
|
||||
<image
|
||||
src="@img/chef/136.png"
|
||||
class="w-98rpx h-98rpx mb-18rpx"
|
||||
></image>
|
||||
<view class="text-26rpx lh-26rpx text-#333 font-500 tracking-[.06em] mb-38rpx">
|
||||
{{ memberText.title_delivery }}
|
||||
</view>
|
||||
<view class="text-22rpx lh-22rpx text-#7d7d7d tracking-[.06em]">
|
||||
{{ memberText.desc_delivery }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="w-full min-h-340rpx rounded-20rpx border-#D8D8D8 border-solid border-2rpx pb-26rpx px-28rpx pt-20rpx">
|
||||
<image
|
||||
src="@img/chef/137.png"
|
||||
class="w-98rpx h-98rpx mb-18rpx"
|
||||
></image>
|
||||
<view class="text-26rpx lh-26rpx text-#333 font-500 tracking-[.06em] mb-38rpx">
|
||||
{{ memberText.title_pickup }}
|
||||
</view>
|
||||
<view class="text-22rpx lh-22rpx text-#7d7d7d tracking-[.06em]">
|
||||
{{ memberText.desc_pickup }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- <image-->
|
||||
<!-- src="@img/ls/13.png"-->
|
||||
<!-- class="w-full h-444rpx"-->
|
||||
<!-- ></image>-->
|
||||
|
||||
<view class="pb-40rpx">
|
||||
<mp-html
|
||||
selectable
|
||||
:preview-img="false"
|
||||
:show-img-menu="false"
|
||||
:tag-style="{
|
||||
div: 'white-space: pre-wrap;',
|
||||
p: 'white-space: pre-wrap;',
|
||||
img: 'width:100%;max-width: 100%;height:auto;',
|
||||
}"
|
||||
:content="(membershipConfig?.content)"
|
||||
></mp-html>
|
||||
</view>
|
||||
|
||||
<view class="h-98rpx"></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="handleSubmit">{{ t('pages-user.member.btn-text') }} {{ Config.appName }}
|
||||
</wd-button>
|
||||
<view :style="[configStore.iosSafeBottomPlaceholder]"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
page {
|
||||
background-color: #fff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,334 @@
|
||||
<script setup lang="ts">
|
||||
import dayjs from 'dayjs';
|
||||
const { t } = useI18n();
|
||||
import {
|
||||
onMounted,
|
||||
getCurrentInstance
|
||||
} from 'vue';
|
||||
import Config from '@/config/index'
|
||||
import {useConfigStore, useUserStore} from "@/store";
|
||||
const configStore = useConfigStore()
|
||||
const userStore = useUserStore();
|
||||
import {EventEnum} from "@/constant/enums";
|
||||
import {appMembershipRechargePost, appUserCardSelectDefaultPost} from "@/service";
|
||||
import { tWithParams } from '@/utils/utils';
|
||||
|
||||
// 订阅选项
|
||||
const subscriptionPlans = ref([]);
|
||||
// 当前选中的会员下标
|
||||
const currentIndex = ref(0)
|
||||
|
||||
// 选择订阅计划
|
||||
const selectPlan = (index: string) => {
|
||||
currentIndex.value = index
|
||||
};
|
||||
|
||||
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
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
appUserCardSelectDefault()
|
||||
const instance = getCurrentInstance().proxy
|
||||
const eventChannel = instance.getOpenerEventChannel();
|
||||
eventChannel.on('acceptDataFromOpenerPage', function(data) {
|
||||
console.log('acceptDataFromOpenerPage', data)
|
||||
if(data.data) {
|
||||
subscriptionPlans.value = data.data
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 年度会员比月度会员优惠多少钱
|
||||
const annualDiscount = computed(() => {
|
||||
// 找到月度会员和年度会员
|
||||
const monthlyPlan = subscriptionPlans.value.find(plan => plan.membershipType === 1);
|
||||
const annualPlan = subscriptionPlans.value.find(plan => plan.membershipType === 2);
|
||||
|
||||
// 检查是否找到两种会员类型
|
||||
if (!monthlyPlan || !annualPlan) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 转换为数字进行计算
|
||||
const monthlyPrice = parseFloat(monthlyPlan.rechargeAmount);
|
||||
const annualPrice = parseFloat(annualPlan.rechargeAmount);
|
||||
|
||||
// 计算12个月的月度会员总价
|
||||
const yearlyMonthlyTotal = monthlyPrice * 12;
|
||||
|
||||
// 计算优惠金额
|
||||
const discount = yearlyMonthlyTotal - annualPrice;
|
||||
|
||||
// 如果有优惠(优惠金额大于0),返回优惠金额,否则返回null
|
||||
return discount > 0 ? parseFloat(discount.toFixed(2)) : null;
|
||||
});
|
||||
|
||||
// 计算处理后的描述信息
|
||||
const processedDescription = computed(() => {
|
||||
// 获取当前选中的会员信息
|
||||
const currentMembership = subscriptionPlans.value[currentIndex.value];
|
||||
if (!currentMembership) return '';
|
||||
|
||||
// 计算时间:当前日期加上对应时长
|
||||
let timeStr = '';
|
||||
const today = dayjs();
|
||||
|
||||
if (currentMembership.membershipType === 1) {
|
||||
// 月度会员:加一个月
|
||||
timeStr = today.add(1, 'month').format('YYYY-MM-DD');
|
||||
} else if (currentMembership.membershipType === 2) {
|
||||
// 年度会员:加一年
|
||||
timeStr = today.add(1, 'year').format('YYYY-MM-DD');
|
||||
}
|
||||
|
||||
// 确定类型显示文本
|
||||
const typeText = currentMembership.membershipType === 1 ? 'month' : 'year';
|
||||
|
||||
// 替换占位符
|
||||
return currentMembership.description
|
||||
.replace(/{time}/g, timeStr)
|
||||
.replace(/{price}/g, currentMembership.rechargeAmount)
|
||||
.replace(/{type}/g, typeText);
|
||||
});
|
||||
|
||||
|
||||
// 支付参数
|
||||
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 navigateTo(url: string) {
|
||||
uni.navigateTo({ url })
|
||||
}
|
||||
|
||||
const passwordInputRef = ref()
|
||||
function payPawSuccess(password: string) {
|
||||
payMethodOptions.value.payPassword = password
|
||||
appMembershipRecharge()
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
// 如果是余额支付,弹出支付密码弹窗
|
||||
if(payMethodOptions.value.payMethod === 2) {
|
||||
passwordInputRef.value?.showPasswordInput()
|
||||
} else {
|
||||
appMembershipRecharge()
|
||||
}
|
||||
}
|
||||
function appMembershipRecharge() {
|
||||
appMembershipRechargePost({
|
||||
body: {
|
||||
...payMethodOptions.value,
|
||||
autoSubscribe: autoSubscribe.value,
|
||||
rechargeItemId: subscriptionPlans.value[currentIndex.value].id
|
||||
}
|
||||
}).then(res=> {
|
||||
console.log('充值结果', res)
|
||||
uni.showToast({
|
||||
title: '充值成功',
|
||||
icon: 'none'
|
||||
})
|
||||
setTimeout(()=> {
|
||||
userStore.getUserInfo()
|
||||
uni.navigateBack({
|
||||
delta: 1,
|
||||
})
|
||||
}, 500)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// 是否订阅 autoSubscribe 1-是 2-否
|
||||
const autoSubscribe = ref(2)
|
||||
|
||||
function changeAutoSubscribe() {
|
||||
autoSubscribe.value = autoSubscribe.value === 1 ? 2 : 1
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view>
|
||||
<navbar />
|
||||
<view class="pl-30rpx mt-20rpx text-46rpx lh-46rpx text-#333 font-bold tracking-[.04em]"
|
||||
>{{ t('pages-user.member.join') }} {{ Config.appName }}</view
|
||||
>
|
||||
|
||||
<!-- 订阅选项 -->
|
||||
<view class="px-30rpx mt-66rpx">
|
||||
<view
|
||||
v-for="(item, index) in subscriptionPlans"
|
||||
:key="item.id"
|
||||
class="bg-white rounded-20rpx border border-#D8D8D8 mb-30rpx relative"
|
||||
:class="{ 'border-#CE7138': item.selected }"
|
||||
@click="selectPlan(index)"
|
||||
>
|
||||
<!-- 卡片内容 -->
|
||||
<view class="border-#D8D8D8 border-solid border-1px rounded-20rpx">
|
||||
<view class="px-28rpx py-40rpx flex items-center justify-between">
|
||||
<view class="flex-1">
|
||||
<view class="text-36rpx lh-36rpx text-#333 font-bold mb-28rpx">{{
|
||||
item.name
|
||||
}}</view>
|
||||
<!--userMembershipVo为null必为没开通过-->
|
||||
<!--hasUseFree用户是否已使用试用会员资格 1是 2 否-->
|
||||
<view v-if="!userStore.userInfo.userMembershipVo || +userStore.userInfo.userMembershipVo.hasUseFree === 2" class="text-28rpx lh-28rpx text-#CE7138 mb-32rpx"
|
||||
>{{ t('pages-user.member.freeTrial') }}:{{ item.trialWeeksDisplay }}
|
||||
</view>
|
||||
<view class="flex items-center text-28rpx lh-28rpx">
|
||||
<view class="text-#f00 font-500">
|
||||
$<template v-if="+item.membershipType === 2">{{ (item.rechargeAmount / 12).toFixed(2) }}</template>
|
||||
<template v-else>{{ item.rechargeAmount }}</template>
|
||||
<text class=" text-#333">
|
||||
/{{ t('pages-user.member.month') }}
|
||||
</text>
|
||||
</view>
|
||||
<text
|
||||
v-if="+item.membershipType === 2"
|
||||
class=" text-#999 ml-16rpx"
|
||||
>({{ tWithParams('pages-user.member.billedAnnually', { amount: item.rechargeAmount }) }})</text
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 单选按钮 -->
|
||||
<image
|
||||
:src="
|
||||
currentIndex === index
|
||||
? '/static/images/chef/133.png'
|
||||
: '/static/images/chef/134.png'
|
||||
"
|
||||
class="w-48rpx h-48rpx shrink-0"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</view>
|
||||
<!-- 年度计划的节省提示 -->
|
||||
<view
|
||||
v-if="+item.membershipType === 2 && annualDiscount"
|
||||
class="bg-[rgba(206,113,56,0.1)] rounded-b-20rpx p-16rpx"
|
||||
>
|
||||
<view class="flex items-center">
|
||||
<image
|
||||
src="@img/chef/139.png"
|
||||
class="w-32rpx h-32rpx mr-16rpx shrink-0"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text class="text-24rpx text-#CE7138">{{ tWithParams('pages-user.member.savingsPerYear', { amount: `$${annualDiscount}` }) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view @click="changeAutoSubscribe" class="border-#D8D8D8 border-solid border-1px p-24rpx flex-center-sb rounded-20rpx">
|
||||
<view class="">{{ t('pages-user.member.autoSubscribe') }}</view>
|
||||
<view class="center">
|
||||
<image
|
||||
:src="
|
||||
autoSubscribe === 1
|
||||
? '/static/images/chef/133.png'
|
||||
: '/static/images/chef/134.png'
|
||||
"
|
||||
class="w-44rpx h-44rpx shrink-0"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 支付方式 -->
|
||||
<view class="px-30rpx my-88rpx">
|
||||
<view
|
||||
@click="navigateTo('/pages-user/pages/choose-paymethod/index')"
|
||||
class="flex-center-sb text-32rpx lh-32rpx font-500 text-#333"
|
||||
>
|
||||
<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>
|
||||
|
||||
<!-- 账单信息 -->
|
||||
<view class="px-30rpx mt-40rpx text-28rpx text-#333 font-500 leading-32rpx mb-10rpx">
|
||||
{{ processedDescription }}
|
||||
<!-- <view class="text-28rpx text-#333 font-500 leading-32rpx mb-10rpx">-->
|
||||
<!-- Starting from June 25th, 2025, billing will be charged at a rate of US-->
|
||||
<!-- $96.00/year. No additional fees or penalties are required for-->
|
||||
<!-- cancellation.-->
|
||||
<!-- </view>-->
|
||||
<!-- <view class="text-28rpx text-#7D7D7D leading-28rpx">-->
|
||||
<!-- By joining Chef One, you authorize Uber to collect US $96.00 through any-->
|
||||
<!-- payment method in your account after the end of any applicable free-->
|
||||
<!-- trial period.-->
|
||||
<!-- </view>-->
|
||||
</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="handleSubmit">{{ t('pages-user.member.btn-text') }}
|
||||
</wd-button>
|
||||
<view :style="[configStore.iosSafeBottomPlaceholder]"></view>
|
||||
</view>
|
||||
|
||||
|
||||
<password-container @success="payPawSuccess" ref="passwordInputRef" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
page {
|
||||
background-color: #fff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,104 @@
|
||||
<script setup lang="ts">
|
||||
import dayjs from "dayjs";
|
||||
import usePage from "@/hooks/usePage";
|
||||
import {appMessageListPost, appMessageReadAllPost, appMessageReadPost} from "@/service";
|
||||
import {MessageTypeEnum} from "@/constant/enums";
|
||||
const {t} = useI18n()
|
||||
|
||||
const {paging, loading, firstLoaded, dataList, queryList} = usePage((pageNum: number, pageSize: number) => {
|
||||
return new Promise(resolve => {
|
||||
appMessageListPost({
|
||||
params: {
|
||||
pageNum: pageNum,
|
||||
pageSize: pageSize,
|
||||
}
|
||||
}).then(res => {
|
||||
resolve(res)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// 一键已读
|
||||
function readAll() {
|
||||
appMessageReadAllPost({}).then(() => {
|
||||
paging.value?.refresh()
|
||||
})
|
||||
}
|
||||
|
||||
// 消息设置为已读
|
||||
function readMessage(id: number) {
|
||||
appMessageReadPost({
|
||||
body: [id]
|
||||
}).then(() => {
|
||||
paging.value?.refresh()
|
||||
})
|
||||
}
|
||||
|
||||
function handleClickMsg(item: any) {
|
||||
readMessage(item.id)
|
||||
switch (item.messageType) {
|
||||
case MessageTypeEnum.COMMENTED: // 有人评论了你
|
||||
case MessageTypeEnum.REPLIED: // 有人回复了你
|
||||
case MessageTypeEnum.COMMENT_APPROVED: // 评论审核通过
|
||||
case MessageTypeEnum.COMMENT_REJECTED: // 评论审核未通过
|
||||
navigateTo('/pages-user/pages/recipe/index?id=' + item.objectId)
|
||||
break
|
||||
case MessageTypeEnum.MERCHANT_ACCEPTED: // 商家接单
|
||||
case MessageTypeEnum.REFUND_AGREED:
|
||||
case MessageTypeEnum.REFUND_REJECTED:
|
||||
case MessageTypeEnum.DELIVERY_STARTED:
|
||||
case MessageTypeEnum.DELIVERY_ARRIVED:
|
||||
case MessageTypeEnum.ORDER_WRITTEN_OFF:
|
||||
navigateTo('/pages-store/pages/order/index?id=' + item.objectId)
|
||||
break
|
||||
case MessageTypeEnum.SETTLEMENT_APPROVED: // 商家入驻审核通过
|
||||
case MessageTypeEnum.SETTLEMENT_REJECTED: //
|
||||
navigateTo('/pages-user/pages/store-settle-in/index')
|
||||
break
|
||||
}
|
||||
paging.value?.refresh()
|
||||
}
|
||||
|
||||
function navigateTo(url: string) {
|
||||
uni.navigateTo({url})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<z-paging ref="paging" v-model="dataList" @query="queryList">
|
||||
<template #top>
|
||||
<navbar :title="t('pages-user.message.title')">
|
||||
<template #right>
|
||||
<view @click="readAll" class="flex items-center text-#CE7138">
|
||||
<image src="@img/chef/140.png" class="w-40rpx h-40rpx shrink-0 mr-8rpx"></image>
|
||||
<text class="text-26rpx font-500">{{ t('pages-user.message.readAll') }}</text>
|
||||
</view>
|
||||
</template>
|
||||
</navbar>
|
||||
</template>
|
||||
<template v-for="item in dataList">
|
||||
<view class="w-full border-bottom px-40rpx py-42rpx flex items-center" @click="handleClickMsg(item)">
|
||||
<image class="w-120rpx h-120rpx shrink-0 mr-24rpx" src="@img/chef/141.png"></image>
|
||||
<view class="flex-1">
|
||||
<view class="w-full mb-22rpx flex items-start justify-between">
|
||||
<text class="text-34rpx lh-34rpx text-#000 font-500">{{ item.title }}</text>
|
||||
<text class="text-24rpx text-#666 shrink-0">{{
|
||||
dayjs(Number(item.createTime)).format('MM:DD HH:mm')
|
||||
}}
|
||||
</text>
|
||||
</view>
|
||||
<view class="text-26rpx text-#666 flex-center-sb">
|
||||
{{ item.content }}
|
||||
<view v-if="+item.readStatus === 2" class="w-14rpx h-14rpx bg-red-5 rounded-50%"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</z-paging>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
page {
|
||||
background-color: #fff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,163 @@
|
||||
<script lang="ts" setup>
|
||||
import {useUserStore} from '@/store'
|
||||
import {debounce} from 'throttle-debounce';
|
||||
import { appUserEditLoginPwdPost } from '@/service'
|
||||
import {z} from "zod";
|
||||
import * as R from "ramda";
|
||||
import Config from "@/config";
|
||||
|
||||
const userStore = useUserStore()
|
||||
const {t} = useI18n()
|
||||
|
||||
|
||||
const btnLoading = ref(false)
|
||||
|
||||
|
||||
const form = ref({
|
||||
oldLoginPwd: '',
|
||||
newLoginPwd: '',
|
||||
confirmPwd: '',
|
||||
})
|
||||
|
||||
|
||||
const FormSchema = z.object({
|
||||
oldLoginPwd: z.string()
|
||||
.min(1, {message: t('pages-user.pay-password.input-placeholder.enter-old-password')}),
|
||||
newLoginPwd: z.string()
|
||||
.min(1, {message: t('pages-user.pay-password.input-placeholder.enter-new-password')})
|
||||
.regex(/^\d{6}$/, {message: t('pages-user.pay-password.enter-6-digit-password')}),
|
||||
confirmPwd: z.string()
|
||||
.min(1, {message: t('pages-user.pay-password.input-placeholder.enter-new-password-again')})
|
||||
|
||||
}).refine((data) => data.newLoginPwd === data.confirmPwd, {
|
||||
path: ['confirmPwd'],
|
||||
message: t('pages-user.pay-password.two-passwords-inconsistent')
|
||||
})
|
||||
|
||||
function checkForm(): boolean {
|
||||
const validateFormFields = FormSchema.safeParse(form.value)
|
||||
if (!validateFormFields.success) {
|
||||
const errorMessage = validateFormFields.error.flatten().fieldErrors
|
||||
console.log(errorMessage)
|
||||
const fieldMessageArr = Object.values(errorMessage)
|
||||
console.log('fieldMessageArr', fieldMessageArr)
|
||||
if (fieldMessageArr[0].length) {
|
||||
uni.showToast({
|
||||
title: fieldMessageArr[0][0],
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return validateFormFields.success;
|
||||
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
try {
|
||||
btnLoading.value = true
|
||||
await appUserEditLoginPwdPost({
|
||||
body: {
|
||||
...form.value
|
||||
}
|
||||
})
|
||||
await uni.showToast({title: t('pages-user.password.change-password-successfully'), icon: 'none'})
|
||||
// await userStore.getUserInfo()
|
||||
setTimeout(() => {
|
||||
userStore.clear();
|
||||
uni.navigateTo({
|
||||
url: Config.loginPath
|
||||
})
|
||||
}, 1000)
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
btnLoading.value = true
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = R.when(checkForm, debounce(Config.debounceLongTime, submit, {
|
||||
atBegin: true
|
||||
}))
|
||||
|
||||
|
||||
function navigateTo(url: string) {
|
||||
uni.navigateTo({
|
||||
url,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<navbar :title="t('navbar-change-password')"/>
|
||||
<view class="page pt-20rpx">
|
||||
<view class="bg-#fff">
|
||||
|
||||
<view class="px-30rpx border-bottom">
|
||||
<wd-input
|
||||
no-border
|
||||
show-password
|
||||
clearable
|
||||
:focus-when-clear="false"
|
||||
:cursorSpacing="20"
|
||||
:maxlength="20"
|
||||
v-model.trim="form.oldLoginPwd"
|
||||
:placeholder="t('pages-user.pay-password.input-placeholder.enter-old-password')"
|
||||
placeholderStyle="font-size: 28rpx;line-height: 42rpx;color: #999999;"
|
||||
custom-input-class="!h-112rpx !text-28rpx !text-primary"
|
||||
></wd-input>
|
||||
</view>
|
||||
<view class="px-30rpx border-bottom">
|
||||
<wd-input
|
||||
no-border
|
||||
show-password
|
||||
clearable
|
||||
:focus-when-clear="false"
|
||||
:cursorSpacing="20"
|
||||
:maxlength="20"
|
||||
v-model.trim="form.newLoginPwd"
|
||||
:placeholder="t('pages-user.pay-password.input-placeholder.enter-new-password')"
|
||||
placeholderStyle="font-size: 28rpx;line-height: 42rpx;color: #999999;"
|
||||
custom-input-class="!h-112rpx !text-28rpx !text-primary"
|
||||
></wd-input>
|
||||
</view>
|
||||
<view class="px-30rpx border-bottom">
|
||||
<wd-input
|
||||
no-border
|
||||
show-password
|
||||
clearable
|
||||
:focus-when-clear="false"
|
||||
:cursorSpacing="20"
|
||||
:maxlength="20"
|
||||
v-model.trim="form.confirmPwd"
|
||||
:placeholder="t('pages-user.pay-password.input-placeholder.enter-new-password-again')"
|
||||
placeholderStyle="font-size: 28rpx;line-height: 42rpx;color: #999999;"
|
||||
custom-input-class="!h-112rpx !text-28rpx !text-primary"
|
||||
></wd-input>
|
||||
</view>
|
||||
</view>
|
||||
<view class="p-[62rpx+30rpx+2rpx] flex justify-end text-right">
|
||||
<text class="text-28rpx lh-42rpx text-#FF7002 underline"
|
||||
@click="navigateTo('/pages-user/pages/password/forget/index')">
|
||||
{{ t('navbar-forget-password') }}?
|
||||
</text>
|
||||
</view>
|
||||
<view class="mt-97rpx px-30rpx">
|
||||
<wd-button block custom-class="!h-108rpx !text-36rpx !rounded-16rpx"
|
||||
@click="handleSubmit">
|
||||
{{ t('common.confirm') }}
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.wd-input) {
|
||||
.wd-input__suffix {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,190 @@
|
||||
<script lang="ts" setup>
|
||||
import {useUserStore} from '@/store'
|
||||
import {debounce} from 'throttle-debounce';
|
||||
import {appUserForgetPwdPost} from '@/service'
|
||||
import {conversionMobile} from "@/utils";
|
||||
import {z} from "zod";
|
||||
import * as R from "ramda";
|
||||
import Config from "@/config";
|
||||
import {SmsType} from "@/constant/enums";
|
||||
|
||||
const userStore = useUserStore()
|
||||
const {t} = useI18n()
|
||||
const {isSend, getMsgCode} = useGetMsgCode()
|
||||
|
||||
const btnLoading = ref(false)
|
||||
|
||||
|
||||
const form = ref({
|
||||
areaCode: userStore.userInfo?.areaCode || '',
|
||||
phone: userStore.userInfo?.phone || '',
|
||||
captcha: '',
|
||||
confirmPwd: '',
|
||||
newLoginPwd: '',
|
||||
})
|
||||
|
||||
|
||||
const FormSchema = z.object({
|
||||
captcha: z.string()
|
||||
.min(1, {message: t('pages-user.pay-password.input-placeholder.enter-verification-code')}),
|
||||
confirmPwd: z.string()
|
||||
.min(1, {message: t('pages-user.pay-password.input-placeholder.enter-new-password')})
|
||||
.regex(/^\d{6}$/, {message: t('pages-user.pay-password.enter-6-digit-password')}),
|
||||
newLoginPwd: z.string()
|
||||
.min(1, {message: t('pages-user.pay-password.input-placeholder.enter-new-password-again')})
|
||||
|
||||
}).refine((data) => data.confirmPwd === data.newLoginPwd, {
|
||||
path: ['newLoginPwd'],
|
||||
message: t('pages-user.pay-password.two-passwords-inconsistent')
|
||||
})
|
||||
|
||||
function checkForm(): boolean {
|
||||
const validateFormFields = FormSchema.safeParse(form.value)
|
||||
if (!validateFormFields.success) {
|
||||
const errorMessage = validateFormFields.error.flatten().fieldErrors
|
||||
console.log(errorMessage)
|
||||
const fieldMessageArr = Object.values(errorMessage)
|
||||
console.log('fieldMessageArr', fieldMessageArr)
|
||||
if (fieldMessageArr[0].length) {
|
||||
uni.showToast({
|
||||
title: fieldMessageArr[0][0],
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return validateFormFields.success;
|
||||
|
||||
}
|
||||
|
||||
|
||||
function handleShowGetCode() {
|
||||
if (isSend.value > 0) {
|
||||
return
|
||||
}
|
||||
getMsgCode({
|
||||
type: SmsType.USER_FORGET_PASSWORD,
|
||||
phone: (form.value.phone),
|
||||
areaCode: form.value.areaCode
|
||||
})
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
try {
|
||||
btnLoading.value = true
|
||||
await appUserForgetPwdPost({
|
||||
body: {
|
||||
...form.value
|
||||
}
|
||||
})
|
||||
await uni.showToast({title: t('pages-user.password.forget-password-successfully'), icon: 'none'})
|
||||
await userStore.getUserInfo()
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1000)
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
btnLoading.value = true
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = R.when(checkForm, debounce(Config.debounceLongTime, submit, {
|
||||
atBegin: true
|
||||
}))
|
||||
|
||||
|
||||
function navigateTo(url: string) {
|
||||
uni.navigateTo({
|
||||
url,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<navbar :title="t('navbar-forget-password')"/>
|
||||
<view class="page pt-20rpx">
|
||||
<view class="bg-#fff">
|
||||
<view class="px-30rpx border-bottom">
|
||||
<wd-input
|
||||
no-border
|
||||
disabled
|
||||
disabledColor="transparent"
|
||||
:modelValue="conversionMobile(form.phone)"
|
||||
:placeholder="t('pages-user.pay-password.input-placeholder.enter-phone-number')"
|
||||
placeholderStyle="font-size: 28rpx;line-height: 42rpx;color: #999999;"
|
||||
custom-input-class="!h-112rpx !text-28rpx !text-primary"
|
||||
>
|
||||
</wd-input>
|
||||
</view>
|
||||
<view class="px-30rpx border-bottom">
|
||||
|
||||
<wd-input
|
||||
no-border
|
||||
clearable
|
||||
:focus-when-clear="false"
|
||||
:maxlength="6"
|
||||
v-model.trim="form.captcha"
|
||||
:placeholder="t('pages-user.pay-password.input-placeholder.enter-verification-code')"
|
||||
placeholderStyle="font-size: 28rpx;line-height: 42rpx;color: #999999;"
|
||||
custom-input-class="!h-112rpx !text-28rpx !text-primary"
|
||||
>
|
||||
<template #suffix>
|
||||
<view class="ml-20rpx">
|
||||
<wd-button :disabled="!!isSend"
|
||||
custom-class="!box-border !min-w-auto w-130rpx !h-50rpx !text-28rpx font-bold !bg-#1EB171 !rounded-8rpx"
|
||||
@click="handleShowGetCode">
|
||||
{{ isSend ? isSend + 'S' : t('common.obtain') }}
|
||||
</wd-button>
|
||||
</view>
|
||||
</template>
|
||||
</wd-input>
|
||||
|
||||
</view>
|
||||
<view class="px-30rpx border-bottom">
|
||||
<wd-input
|
||||
no-border
|
||||
show-password
|
||||
clearable
|
||||
:focus-when-clear="false"
|
||||
:maxlength="20"
|
||||
v-model.trim="form.newLoginPwd"
|
||||
:placeholder="t('pages-user.pay-password.input-placeholder.enter-new-password')"
|
||||
placeholderStyle="font-size: 28rpx;line-height: 42rpx;color: #999999;"
|
||||
custom-input-class="!h-112rpx !text-28rpx !text-primary"
|
||||
></wd-input>
|
||||
</view>
|
||||
<view class="px-30rpx">
|
||||
|
||||
<wd-input
|
||||
no-border
|
||||
show-password
|
||||
clearable
|
||||
:focus-when-clear="false"
|
||||
:maxlength="20"
|
||||
v-model.trim="form.confirmPwd"
|
||||
:placeholder="t('pages-user.pay-password.input-placeholder.enter-new-password-again')"
|
||||
placeholderStyle="font-size: 28rpx;line-height: 42rpx;color: #999999;"
|
||||
custom-input-class="!h-112rpx !text-28rpx !text-primary"
|
||||
></wd-input>
|
||||
</view>
|
||||
</view>
|
||||
<view class="mt-97rpx px-30rpx">
|
||||
<wd-button block custom-class="!h-108rpx !text-36rpx !rounded-16rpx"
|
||||
@click="handleSubmit">
|
||||
{{ t('common.confirm') }}
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.wd-input) {
|
||||
.wd-input__suffix {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,155 @@
|
||||
<script lang="ts" setup>
|
||||
import {useUserStore} from '@/store'
|
||||
import {debounce} from 'throttle-debounce';
|
||||
import {editPayPwd} from '@/pages-user/service'
|
||||
import {z} from "zod";
|
||||
import * as R from "ramda";
|
||||
import Config from "@/config";
|
||||
|
||||
const userStore = useUserStore()
|
||||
const {t} = useI18n()
|
||||
|
||||
|
||||
const btnLoading = ref(false)
|
||||
|
||||
|
||||
const form = ref({
|
||||
oldPayPwd: '',
|
||||
newPayPwd: '',
|
||||
confirmPwd: '',
|
||||
})
|
||||
|
||||
|
||||
const FormSchema = z.object({
|
||||
oldPayPwd: z.string()
|
||||
.min(1, {message: t('pages-user.pay-password.input-placeholder.enter-old-password')}),
|
||||
newPayPwd: z.string()
|
||||
.min(1, {message: t('pages-user.pay-password.input-placeholder.enter-new-password')})
|
||||
.regex(/^\d{6}$/, {message: t('pages-user.pay-password.enter-6-digit-password')}),
|
||||
confirmPwd: z.string()
|
||||
.min(1, {message: t('pages-user.pay-password.input-placeholder.enter-new-password-again')})
|
||||
|
||||
}).refine((data) => data.newPayPwd === data.confirmPwd, {
|
||||
path: ['confirmPwd'],
|
||||
message: t('pages-user.pay-password.two-passwords-inconsistent')
|
||||
})
|
||||
|
||||
function checkForm(): boolean {
|
||||
const validateFormFields = FormSchema.safeParse(form.value)
|
||||
if (!validateFormFields.success) {
|
||||
const errorMessage = validateFormFields.error.flatten().fieldErrors
|
||||
console.log(errorMessage)
|
||||
const fieldMessageArr = Object.values(errorMessage)
|
||||
console.log('fieldMessageArr', fieldMessageArr)
|
||||
if (fieldMessageArr[0].length) {
|
||||
uni.showToast({
|
||||
title: fieldMessageArr[0][0],
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return validateFormFields.success;
|
||||
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
try {
|
||||
btnLoading.value = true
|
||||
await editPayPwd(form.value)
|
||||
await uni.showToast({title: t('pages-user.pay-password.change-payment-password-successfully'), icon: 'none'})
|
||||
await userStore.getUserInfo()
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1000)
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
btnLoading.value = true
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = R.when(checkForm, debounce(Config.debounceLongTime, submit, {
|
||||
atBegin: true
|
||||
}))
|
||||
|
||||
|
||||
function navigateTo(url: string) {
|
||||
uni.navigateTo({
|
||||
url,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<navbar :title="t('navbar-change-payment-password')"/>
|
||||
<view class="page pt-20rpx">
|
||||
<view class="bg-#fff">
|
||||
|
||||
<view class="px-30rpx border-bottom">
|
||||
<wd-input
|
||||
no-border
|
||||
show-password
|
||||
clearable
|
||||
:focus-when-clear="false"
|
||||
:cursorSpacing="20"
|
||||
:maxlength="6"
|
||||
v-model.trim="form.oldPayPwd"
|
||||
:placeholder="t('pages-user.pay-password.input-placeholder.enter-old-password')"
|
||||
placeholderStyle="font-size: 28rpx;line-height: 42rpx;color: #999999;"
|
||||
custom-input-class="!h-112rpx !text-28rpx !text-primary"
|
||||
></wd-input>
|
||||
</view>
|
||||
<view class="px-30rpx border-bottom">
|
||||
<wd-input
|
||||
no-border
|
||||
show-password
|
||||
clearable
|
||||
:focus-when-clear="false"
|
||||
:cursorSpacing="20"
|
||||
:maxlength="6"
|
||||
v-model.trim="form.newPayPwd"
|
||||
:placeholder="t('pages-user.pay-password.input-placeholder.enter-new-password')"
|
||||
placeholderStyle="font-size: 28rpx;line-height: 42rpx;color: #999999;"
|
||||
custom-input-class="!h-112rpx !text-28rpx !text-primary"
|
||||
></wd-input>
|
||||
</view>
|
||||
<view class="px-30rpx border-bottom">
|
||||
<wd-input
|
||||
no-border
|
||||
show-password
|
||||
clearable
|
||||
:focus-when-clear="false"
|
||||
:cursorSpacing="20"
|
||||
:maxlength="6"
|
||||
v-model.trim="form.confirmPwd"
|
||||
:placeholder="t('pages-user.pay-password.input-placeholder.enter-new-password-again')"
|
||||
placeholderStyle="font-size: 28rpx;line-height: 42rpx;color: #999999;"
|
||||
custom-input-class="!h-112rpx !text-28rpx !text-primary"
|
||||
></wd-input>
|
||||
</view>
|
||||
</view>
|
||||
<view class="p-[62rpx+30rpx+2rpx] flex justify-end text-right">
|
||||
<text class="text-28rpx lh-42rpx text-#FF7002 underline"
|
||||
@click="navigateTo('/pages-user/pages/pay-password/forget/index')">{{ t('navbar-forget-password') }}?
|
||||
</text>
|
||||
</view>
|
||||
<view class="mt-97rpx px-30rpx">
|
||||
<wd-button block custom-class="!h-108rpx !text-36rpx !rounded-16rpx"
|
||||
@click="handleSubmit">
|
||||
{{ t('common.confirm') }}
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.wd-input) {
|
||||
.wd-input__suffix {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,186 @@
|
||||
<script lang="ts" setup>
|
||||
import {useUserStore} from '@/store'
|
||||
import {debounce} from 'throttle-debounce';
|
||||
import {forgetPayPwd} from '@/pages-user/service'
|
||||
import {conversionMobile} from "@/utils";
|
||||
import {z} from "zod";
|
||||
import * as R from "ramda";
|
||||
import Config from "@/config";
|
||||
import {SmsType} from "@/constant/enums";
|
||||
|
||||
const userStore = useUserStore()
|
||||
const {t} = useI18n()
|
||||
const {isSend, getMsgCode} = useGetMsgCode()
|
||||
|
||||
const btnLoading = ref(false)
|
||||
|
||||
|
||||
const form = ref({
|
||||
areaCode: userStore.userInfo?.areaCode || '',
|
||||
phone: userStore.userInfo?.phone || '',
|
||||
captcha: '',
|
||||
payPwd: '',
|
||||
confirmPwd: '',
|
||||
})
|
||||
|
||||
|
||||
const FormSchema = z.object({
|
||||
captcha: z.string()
|
||||
.min(1, {message: t('pages-user.pay-password.input-placeholder.enter-verification-code')}),
|
||||
payPwd: z.string()
|
||||
.min(1, {message: t('pages-user.pay-password.input-placeholder.enter-new-password')})
|
||||
.regex(/^\d{6}$/, {message: t('pages-user.pay-password.enter-6-digit-password')}),
|
||||
confirmPwd: z.string()
|
||||
.min(1, {message: t('pages-user.pay-password.input-placeholder.enter-new-password-again')})
|
||||
|
||||
}).refine((data) => data.payPwd === data.confirmPwd, {
|
||||
path: ['confirmPwd'],
|
||||
message: t('pages-user.pay-password.two-passwords-inconsistent')
|
||||
})
|
||||
|
||||
function checkForm(): boolean {
|
||||
const validateFormFields = FormSchema.safeParse(form.value)
|
||||
if (!validateFormFields.success) {
|
||||
const errorMessage = validateFormFields.error.flatten().fieldErrors
|
||||
console.log(errorMessage)
|
||||
const fieldMessageArr = Object.values(errorMessage)
|
||||
console.log('fieldMessageArr', fieldMessageArr)
|
||||
if (fieldMessageArr[0].length) {
|
||||
uni.showToast({
|
||||
title: fieldMessageArr[0][0],
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return validateFormFields.success;
|
||||
|
||||
}
|
||||
|
||||
|
||||
function handleShowGetCode() {
|
||||
if (isSend.value > 0) {
|
||||
return
|
||||
}
|
||||
getMsgCode({
|
||||
type: SmsType.USER_FORGET_PAYMENT_PASSWORD,
|
||||
phone: (form.value.phone),
|
||||
areaCode: form.value.areaCode
|
||||
})
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
try {
|
||||
btnLoading.value = true
|
||||
await forgetPayPwd(form.value)
|
||||
await uni.showToast({title: t('pages-user.pay-password.forget-payment-password-successfully'), icon: 'none'})
|
||||
await userStore.getUserInfo()
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1000)
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
btnLoading.value = true
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = R.when(checkForm, debounce(Config.debounceLongTime, submit, {
|
||||
atBegin: true
|
||||
}))
|
||||
|
||||
|
||||
function navigateTo(url: string) {
|
||||
uni.navigateTo({
|
||||
url,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<navbar :title="t('navbar-forget-payment-password')"/>
|
||||
<view class="page pt-20rpx">
|
||||
<view class="bg-#fff">
|
||||
<view class="px-30rpx border-bottom">
|
||||
<wd-input
|
||||
no-border
|
||||
disabled
|
||||
disabledColor="transparent"
|
||||
:modelValue="conversionMobile(form.phone)"
|
||||
:placeholder="t('pages-user.pay-password.input-placeholder.enter-phone-number')"
|
||||
placeholderStyle="font-size: 28rpx;line-height: 42rpx;color: #999999;"
|
||||
custom-input-class="!h-112rpx !text-28rpx !text-primary"
|
||||
>
|
||||
</wd-input>
|
||||
</view>
|
||||
<view class="px-30rpx border-bottom">
|
||||
|
||||
<wd-input
|
||||
no-border
|
||||
clearable
|
||||
:focus-when-clear="false"
|
||||
:maxlength="6"
|
||||
v-model.trim="form.captcha"
|
||||
:placeholder="t('pages-user.pay-password.input-placeholder.enter-verification-code')"
|
||||
placeholderStyle="font-size: 28rpx;line-height: 42rpx;color: #999999;"
|
||||
custom-input-class="!h-112rpx !text-28rpx !text-primary"
|
||||
>
|
||||
<template #suffix>
|
||||
<view class="ml-20rpx">
|
||||
<wd-button :disabled="!!isSend"
|
||||
custom-class="!box-border !min-w-auto w-130rpx !h-50rpx !text-28rpx font-bold !bg-#1EB171 !rounded-8rpx"
|
||||
@click="handleShowGetCode">
|
||||
{{ isSend ? isSend + 'S' : t('common.obtain') }}
|
||||
</wd-button>
|
||||
</view>
|
||||
</template>
|
||||
</wd-input>
|
||||
|
||||
</view>
|
||||
<view class="px-30rpx border-bottom">
|
||||
<wd-input
|
||||
no-border
|
||||
show-password
|
||||
clearable
|
||||
:focus-when-clear="false"
|
||||
:maxlength="6"
|
||||
v-model.trim="form.payPwd"
|
||||
:placeholder="t('pages-user.pay-password.input-placeholder.enter-new-password')"
|
||||
placeholderStyle="font-size: 28rpx;line-height: 42rpx;color: #999999;"
|
||||
custom-input-class="!h-112rpx !text-28rpx !text-primary"
|
||||
></wd-input>
|
||||
</view>
|
||||
<view class="px-30rpx">
|
||||
|
||||
<wd-input
|
||||
no-border
|
||||
show-password
|
||||
clearable
|
||||
:focus-when-clear="false"
|
||||
:maxlength="6"
|
||||
v-model.trim="form.confirmPwd"
|
||||
:placeholder="t('pages-user.pay-password.input-placeholder.enter-new-password-again')"
|
||||
placeholderStyle="font-size: 28rpx;line-height: 42rpx;color: #999999;"
|
||||
custom-input-class="!h-112rpx !text-28rpx !text-primary"
|
||||
></wd-input>
|
||||
</view>
|
||||
</view>
|
||||
<view class="mt-97rpx px-30rpx">
|
||||
<wd-button block custom-class="!h-108rpx !text-36rpx !rounded-16rpx"
|
||||
@click="handleSubmit">
|
||||
{{ t('common.confirm') }}
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.wd-input) {
|
||||
.wd-input__suffix {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,217 @@
|
||||
<script lang="ts" setup>
|
||||
import { useUserStore } from "@/store";
|
||||
import { debounce } from "throttle-debounce";
|
||||
import { conversionMobile } from "@/utils";
|
||||
import { z } from "zod";
|
||||
import { SmsType } from "@/constant/enums";
|
||||
import { setPayPwd } from "@/pages-user/service";
|
||||
import * as R from "ramda";
|
||||
import Config from "@/config";
|
||||
|
||||
const userStore = useUserStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const { isSend, getMsgCode } = useGetMsgCode();
|
||||
|
||||
const btnLoading = ref(false);
|
||||
|
||||
const form = ref({
|
||||
areaCode: userStore.userInfo?.areaCode || "",
|
||||
phone: userStore.userInfo?.phone || "",
|
||||
captcha: "",
|
||||
payPwd: "",
|
||||
confirmPwd: "",
|
||||
});
|
||||
|
||||
const FormSchema = z
|
||||
.object({
|
||||
captcha: z
|
||||
.string()
|
||||
.min(1, {
|
||||
message: t(
|
||||
"pages-user.pay-password.input-placeholder.enter-verification-code"
|
||||
),
|
||||
}),
|
||||
payPwd: z
|
||||
.string()
|
||||
.min(1, {
|
||||
message: t(
|
||||
"pages-user.pay-password.input-placeholder.enter-new-password"
|
||||
),
|
||||
})
|
||||
.regex(/^\d{6}$/, {
|
||||
message: t("pages-user.pay-password.enter-6-digit-password"),
|
||||
}),
|
||||
confirmPwd: z
|
||||
.string()
|
||||
.min(1, {
|
||||
message: t(
|
||||
"pages-user.pay-password.input-placeholder.enter-new-password-again"
|
||||
),
|
||||
}),
|
||||
})
|
||||
.refine((data) => data.payPwd === data.confirmPwd, {
|
||||
path: ["confirmPwd"],
|
||||
message: t("pages-user.pay-password.two-passwords-inconsistent"),
|
||||
});
|
||||
|
||||
function checkForm(): boolean {
|
||||
const validateFormFields = FormSchema.safeParse(form.value);
|
||||
if (!validateFormFields.success) {
|
||||
const errorMessage = validateFormFields.error.flatten().fieldErrors;
|
||||
console.log(errorMessage);
|
||||
const fieldMessageArr = Object.values(errorMessage);
|
||||
console.log("fieldMessageArr", fieldMessageArr);
|
||||
if (fieldMessageArr[0].length) {
|
||||
uni.showToast({
|
||||
title: fieldMessageArr[0][0],
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return validateFormFields.success;
|
||||
}
|
||||
|
||||
function handleShowGetCode() {
|
||||
if (isSend.value > 0) {
|
||||
return;
|
||||
}
|
||||
getMsgCode({
|
||||
type: SmsType.USER_SET_PAYMENT_PASSWORD,
|
||||
phone: form.value.phone,
|
||||
areaCode: form.value.areaCode,
|
||||
});
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
try {
|
||||
btnLoading.value = true;
|
||||
await setPayPwd(form.value);
|
||||
await uni.showToast({
|
||||
title: t("pages-user.pay-password.set-payment-password-successfully"),
|
||||
icon: "none",
|
||||
});
|
||||
await userStore.getUserInfo();
|
||||
setTimeout(() => {
|
||||
uni.navigateBack();
|
||||
}, 1000);
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
btnLoading.value = true;
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = R.when(
|
||||
checkForm,
|
||||
debounce(Config.debounceLongTime, submit, {
|
||||
atBegin: true,
|
||||
})
|
||||
);
|
||||
|
||||
function navigateTo(url: string) {
|
||||
uni.navigateTo({
|
||||
url,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<navbar :title="t('navbar-set-payment-password')" />
|
||||
<view class="page pt-20rpx">
|
||||
<view class="bg-#fff">
|
||||
<view class="px-30rpx border-bottom">
|
||||
<wd-input
|
||||
no-border
|
||||
disabled
|
||||
disabledColor="transparent"
|
||||
:modelValue="form.phone"
|
||||
custom-input-class="!h-112rpx !text-28rpx !text-primary"
|
||||
>
|
||||
</wd-input>
|
||||
</view>
|
||||
<view class="px-30rpx border-bottom">
|
||||
<wd-input
|
||||
no-border
|
||||
clearable
|
||||
:focus-when-clear="false"
|
||||
:cursorSpacing="20"
|
||||
:maxlength="6"
|
||||
v-model.trim="form.captcha"
|
||||
:placeholder="
|
||||
t(
|
||||
'pages-user.pay-password.input-placeholder.enter-verification-code'
|
||||
)
|
||||
"
|
||||
placeholderStyle="font-size: 28rpx;line-height: 42rpx;color: #999999;"
|
||||
custom-input-class="!h-112rpx !text-28rpx !text-primary"
|
||||
>
|
||||
<template #suffix>
|
||||
<view class="ml-20rpx">
|
||||
<wd-button
|
||||
:disabled="!!isSend"
|
||||
custom-class="!box-border !min-w-auto w-130rpx !h-50rpx !text-28rpx font-bold !bg-#1EB171 !rounded-8rpx"
|
||||
@click="handleShowGetCode"
|
||||
>
|
||||
{{ isSend ? isSend + "S" : t("common.obtain") }}
|
||||
</wd-button>
|
||||
</view>
|
||||
</template>
|
||||
</wd-input>
|
||||
</view>
|
||||
<view class="px-30rpx border-bottom">
|
||||
<wd-input
|
||||
no-border
|
||||
show-password
|
||||
clearable
|
||||
:focus-when-clear="false"
|
||||
:cursorSpacing="20"
|
||||
:maxlength="6"
|
||||
v-model.trim="form.payPwd"
|
||||
:placeholder="
|
||||
t('pages-user.pay-password.input-placeholder.enter-new-password')
|
||||
"
|
||||
placeholderStyle="font-size: 28rpx;line-height: 42rpx;color: #999999;"
|
||||
custom-input-class="!h-112rpx !text-28rpx !text-primary"
|
||||
></wd-input>
|
||||
</view>
|
||||
<view class="px-30rpx">
|
||||
<wd-input
|
||||
no-border
|
||||
show-password
|
||||
clearable
|
||||
:focus-when-clear="false"
|
||||
:cursorSpacing="20"
|
||||
:maxlength="6"
|
||||
v-model.trim="form.confirmPwd"
|
||||
:placeholder="
|
||||
t(
|
||||
'pages-user.pay-password.input-placeholder.enter-new-password-again'
|
||||
)
|
||||
"
|
||||
placeholderStyle="font-size: 28rpx;line-height: 42rpx;color: #999999;"
|
||||
custom-input-class="!h-112rpx !text-28rpx !text-primary"
|
||||
></wd-input>
|
||||
</view>
|
||||
</view>
|
||||
<view class="m-[128rpx+30rpx+30rpx]">
|
||||
<wd-button
|
||||
block
|
||||
custom-class="!h-108rpx !text-36rpx !rounded-16rpx"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ t("common.confirm") }}
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.wd-input) {
|
||||
.wd-input__suffix {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,156 @@
|
||||
<script setup lang="ts">
|
||||
import {appUserCardSelectDefaultPost} from "@/service";
|
||||
import useEventEmit from "@/hooks/useEventEmit";
|
||||
import {EventEnum} from "@/constant/enums";
|
||||
import { rechargeBalanceApi } from "@/pages-user/service";
|
||||
const { t } = useI18n();
|
||||
|
||||
const amount = ref()
|
||||
|
||||
const payMethodOptions = ref({
|
||||
payMethod: 1,
|
||||
cardId: '',
|
||||
cardNumber: '',
|
||||
})
|
||||
|
||||
function handleSubmit() {
|
||||
if(!amount.value) {
|
||||
uni.showToast({
|
||||
title: t('pages-user.recharge.description'),
|
||||
icon: 'none',
|
||||
})
|
||||
return
|
||||
}
|
||||
if(amount.value <= 0) {
|
||||
uni.showToast({
|
||||
title: t('pages-user.recharge.amount-invalid'),
|
||||
icon: 'none',
|
||||
})
|
||||
return
|
||||
}
|
||||
if(!payMethodOptions.value.cardId) {
|
||||
uni.showToast({
|
||||
title: t('pages-user.recharge.pay-method'),
|
||||
icon: 'none',
|
||||
})
|
||||
return
|
||||
}
|
||||
rechargeBalanceApi({
|
||||
payAmount: Number(amount.value),
|
||||
cardId: payMethodOptions.value.cardId,
|
||||
cardType: 1
|
||||
}).then(res => {
|
||||
console.log('充值余额', res)
|
||||
uni.showToast({
|
||||
title: t('pages-user.recharge.success'),
|
||||
icon: 'none',
|
||||
})
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1000)
|
||||
})
|
||||
}
|
||||
|
||||
function navigateTo(url: string) {
|
||||
uni.navigateTo({ url })
|
||||
}
|
||||
|
||||
onLoad(()=> {
|
||||
// 查询用户默认信用卡
|
||||
appUserCardSelectDefault()
|
||||
})
|
||||
|
||||
function appUserCardSelectDefault() {
|
||||
appUserCardSelectDefaultPost({}).then((res: any)=> {
|
||||
console.log('查询用户默认信用卡', res)
|
||||
payMethodOptions.value.cardId = res.data?.cardId || ''
|
||||
payMethodOptions.value.cardNumber = res.data?.cardNumber || ''
|
||||
})
|
||||
}
|
||||
useEventEmit(EventEnum.CHOOSE_PAYMENT_METHOD, (data) => {
|
||||
if(data) {
|
||||
payMethodOptions.value.cardId = data.cardId
|
||||
payMethodOptions.value.cardNumber = data.cardNumber
|
||||
payMethodOptions.value.payMethod = 1
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="">
|
||||
<navbar :title="t('pages-user.recharge.title')"/>
|
||||
|
||||
<view class="px-20rpx py-22rpx">
|
||||
<view class="bg-white rounded-12rpx p-28rpx">
|
||||
<view class="text-30rpx lh-30rpx text-#333 font-500 mb-30rpx">
|
||||
{{ t('pages-user.recharge.amount') }}
|
||||
</view>
|
||||
<view class="flex items-center bg-#F7F7F7 h-108rpx rounded-12rpx px-26rpx">
|
||||
<wd-input
|
||||
v-model="amount"
|
||||
:focus-when-clear="false"
|
||||
:placeholder="t('pages-user.recharge.description')"
|
||||
clearable
|
||||
confirm-type="search"
|
||||
custom-class="!text-30rpx !bg-transparent flex-1"
|
||||
no-border
|
||||
placeholderStyle="font-size: 30rpx;color: #999;"
|
||||
type="number"
|
||||
use-prefix-slot
|
||||
>
|
||||
</wd-input>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view
|
||||
@click="navigateTo('/pages-user/pages/choose-paymethod/index?hideWallet=true')"
|
||||
class="bg-white rounded-12rpx p-28rpx mt-30rpx flex-center-sb text-32rpx lh-32rpx font-500 text-#333"
|
||||
>
|
||||
<view class="flex items-center">
|
||||
<image
|
||||
src="@img/chef/138.png"
|
||||
class="w-44rpx h-44rpx mr-28rpx shrink-0"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text class="">
|
||||
{{ t('pages-user.choosePaymethod.creditCard') }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="flex items-center">
|
||||
<view class="mr-12px">
|
||||
<!--判断用户是否有信用卡-->
|
||||
<text v-if="!payMethodOptions.cardId">{{ t("pages-user.member.creditCard") }}</text>
|
||||
<text v-else>{{ payMethodOptions.cardNumber }}</text>
|
||||
</view>
|
||||
<image
|
||||
src="@img/chef/142.png"
|
||||
class="w-32rpx h-32rpx shrink-0"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<fixed-bottom-large-btn
|
||||
:text="t('common.recharge')"
|
||||
class="z-100"
|
||||
:fixed="true"
|
||||
@click="handleSubmit"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
page {
|
||||
background-color: #F5F5F5;
|
||||
}
|
||||
</style>
|
||||
<style scoped lang="scss">
|
||||
:deep(.wd-input__clear) {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
:deep(.wd-input__icon) {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,248 @@
|
||||
<template>
|
||||
<view class="recipe-skeleton">
|
||||
<!-- 顶部图片区域 -->
|
||||
<view class="image-section skeleton-item"></view>
|
||||
|
||||
<!-- 返回按钮 -->
|
||||
<view class="back-button-container">
|
||||
<view class="back-button skeleton-item"></view>
|
||||
</view>
|
||||
|
||||
<!-- 状态栏 -->
|
||||
<view class="status-bar skeleton-item"></view>
|
||||
|
||||
<!-- 内容卡片 -->
|
||||
<view class="content-card">
|
||||
<!-- 标题区域 -->
|
||||
<view class="title-section">
|
||||
<view class="recipe-icon skeleton-item"></view>
|
||||
<view class="recipe-title skeleton-item"></view>
|
||||
</view>
|
||||
|
||||
<!-- 材料区域 -->
|
||||
<view class="ingredients-section">
|
||||
<view class="section-title skeleton-item"></view>
|
||||
<view class="ingredients-content">
|
||||
<view
|
||||
v-for="i in 4"
|
||||
:key="i"
|
||||
class="ingredient-line skeleton-item"
|
||||
></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 视频预览区域 -->
|
||||
<view class="video-preview skeleton-item"></view>
|
||||
|
||||
<!-- 做法区域 -->
|
||||
<view class="steps-section">
|
||||
<view class="section-title skeleton-item"></view>
|
||||
<view class="steps-content">
|
||||
<view
|
||||
v-for="i in 8"
|
||||
:key="i"
|
||||
class="step-line skeleton-item"
|
||||
></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 收藏按钮 -->
|
||||
<view class="favorite-button skeleton-item"></view>
|
||||
|
||||
<!-- 点赞提示 -->
|
||||
<view class="like-hint skeleton-item"></view>
|
||||
|
||||
<!-- 用户头像列表 -->
|
||||
<view class="user-avatars">
|
||||
<view
|
||||
v-for="i in 5"
|
||||
:key="i"
|
||||
class="user-avatar skeleton-item"
|
||||
></view>
|
||||
</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;
|
||||
}
|
||||
}
|
||||
|
||||
.recipe-skeleton {
|
||||
background-color: #fff;
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
// 顶部图片区域
|
||||
.image-section {
|
||||
width: 750rpx;
|
||||
height: 750rpx;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
// 返回按钮
|
||||
.back-button-container {
|
||||
position: absolute;
|
||||
top: 104rpx;
|
||||
left: 30rpx;
|
||||
z-index: 10;
|
||||
|
||||
.back-button {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// 状态栏
|
||||
.status-bar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 750rpx;
|
||||
height: 88rpx;
|
||||
}
|
||||
|
||||
// 内容卡片
|
||||
.content-card {
|
||||
position: relative;
|
||||
margin-top: -68rpx;
|
||||
border-radius: 30rpx 30rpx 0 0;
|
||||
background-color: #fff;
|
||||
padding: 0 30rpx;
|
||||
min-height: 1000rpx;
|
||||
}
|
||||
|
||||
// 标题区域
|
||||
.title-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-top: 40rpx;
|
||||
margin-bottom: 40rpx;
|
||||
|
||||
.recipe-icon {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
border-radius: 22rpx;
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
|
||||
.recipe-title {
|
||||
width: 237rpx;
|
||||
height: 36rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// 材料区域
|
||||
.ingredients-section {
|
||||
margin-bottom: 40rpx;
|
||||
|
||||
.section-title {
|
||||
width: 120rpx;
|
||||
height: 30rpx;
|
||||
border-radius: 6rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.ingredients-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
|
||||
.ingredient-line {
|
||||
width: 100%;
|
||||
height: 30rpx;
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 视频预览区域
|
||||
.video-preview {
|
||||
width: 690rpx;
|
||||
height: 360rpx;
|
||||
border-radius: 20rpx;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
// 做法区域
|
||||
.steps-section {
|
||||
margin-bottom: 40rpx;
|
||||
|
||||
.section-title {
|
||||
width: 120rpx;
|
||||
height: 30rpx;
|
||||
border-radius: 6rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.steps-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
|
||||
.step-line {
|
||||
width: 100%;
|
||||
height: 30rpx;
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 收藏按钮
|
||||
.favorite-button {
|
||||
width: 310rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 44rpx;
|
||||
margin: 40rpx auto;
|
||||
}
|
||||
|
||||
// 点赞提示
|
||||
.like-hint {
|
||||
width: 353rpx;
|
||||
height: 24rpx;
|
||||
border-radius: 6rpx;
|
||||
margin: 30rpx auto;
|
||||
}
|
||||
|
||||
// 用户头像列表
|
||||
.user-avatars {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20rpx;
|
||||
margin-bottom: 60rpx;
|
||||
|
||||
.user-avatar {
|
||||
width: 68rpx;
|
||||
height: 68rpx;
|
||||
border-radius: 34rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 750rpx) {
|
||||
.video-preview {
|
||||
height: 300rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,392 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
appMerchantRecipeRecipeDetailPost,
|
||||
appMerchantRecipeAddViewCountPost,
|
||||
appCollectCollectPost,
|
||||
appCommentCommentListPost, appCommentPublishCommentPost
|
||||
} from "@/service";
|
||||
import { debounce } from 'throttle-debounce'
|
||||
import {CollectionType} from "@/constant/enums";
|
||||
const { t } = useI18n()
|
||||
import RecipeSkeleton from "./components/recipe-skeleton.vue";
|
||||
import CComment from "@/components/cc-comment/cc-comment.vue";
|
||||
import { useScrollThreshold } from "@/hooks/useScrollThreshold";
|
||||
import {formatTimestamp, formatTimestampWithMonthName} from "@/utils/utils";
|
||||
import {useUserStore, useConfigStore} from "@/store";
|
||||
const userStore = useUserStore()
|
||||
const configStore = useConfigStore()
|
||||
// 加载状态
|
||||
const loading = ref(true);
|
||||
// 获取菜谱详情
|
||||
const recipeId = ref('') // 菜谱ID
|
||||
const recipeDetail = ref<any>({})
|
||||
onLoad((options: any)=> {
|
||||
if(options.id) {
|
||||
recipeId.value = options.id
|
||||
getRecipeDetail()
|
||||
|
||||
// 获取评论列表
|
||||
getCommentList()
|
||||
}
|
||||
})
|
||||
function getRecipeDetail() {
|
||||
loading.value = true
|
||||
appMerchantRecipeRecipeDetailPost({
|
||||
params: {
|
||||
recipeId: recipeId.value
|
||||
}
|
||||
}).then(res=> {
|
||||
console.log('菜谱详情', res)
|
||||
recipeDetail.value = res.data
|
||||
|
||||
appMerchantRecipeAddViewCountPost({
|
||||
params: {
|
||||
recipeId: recipeId.value,
|
||||
}
|
||||
})
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
// 返回上一页
|
||||
const goBack = () => {
|
||||
uni.navigateBack();
|
||||
};
|
||||
|
||||
// 收藏菜谱
|
||||
const collectRecipeDebounce = debounce(1000, () => {
|
||||
appCollectCollectPost({
|
||||
body: {
|
||||
targetId: recipeId.value,
|
||||
targetType: CollectionType.RECIPE
|
||||
}
|
||||
}).then(res=> {
|
||||
recipeDetail.value.isCollect = !recipeDetail.value.isCollect;
|
||||
recipeDetail.value.collectCount = recipeDetail.value.isCollect ? recipeDetail.value.collectCount + 1 : recipeDetail.value.collectCount - 1
|
||||
})
|
||||
}, {
|
||||
atBegin: true, // 立即触发
|
||||
});
|
||||
|
||||
|
||||
const commentList = ref<any>([])
|
||||
function getCommentList() {
|
||||
appCommentCommentListPost({
|
||||
params: {
|
||||
pageNum: 1,
|
||||
pageSize: 100,
|
||||
},
|
||||
body: {
|
||||
targetId: recipeId.value,
|
||||
targetType: 1,
|
||||
}
|
||||
}).then(res=> {
|
||||
console.log('评论列表', res)
|
||||
commentList.value = 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 {
|
||||
childList: item.childList,
|
||||
id: item.id,
|
||||
topId: item.topId,
|
||||
parent_id: null, // 评论父级的id
|
||||
reply_id: null, // 被回复评论的id
|
||||
reply_name: null, // 被回复人名称
|
||||
target_id: recipeId.value,
|
||||
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), // 创建时间
|
||||
}
|
||||
})
|
||||
tableTotal.value = res.total
|
||||
})
|
||||
}
|
||||
|
||||
// 唤起新评论弹框
|
||||
let ccRef = ref(null);
|
||||
let myInfo = ref({
|
||||
user_id: userStore.userInfo.id, // 用户id
|
||||
});
|
||||
let tableTotal = ref(0); // 评论总数
|
||||
|
||||
const showStatusBar = useScrollThreshold();
|
||||
onPageScroll((e) => {
|
||||
uni.$emit("page-scroll", e);
|
||||
});
|
||||
|
||||
|
||||
const sendValue = ref('');
|
||||
function handleSend() {
|
||||
if (sendValue.value.trim() === '') {
|
||||
return;
|
||||
}
|
||||
console.log(sendValue.value)
|
||||
appCommentPublishCommentPost({
|
||||
body: {
|
||||
userPort: 1, // 1-普通用户 2-商家用户
|
||||
targetId: recipeId.value, // 评论对象ID
|
||||
targetType: 1, // 评论对象类型(1-菜谱 2-菜品 3-配菜)
|
||||
topId: '', // 顶级评论ID(对主体的直接评论)
|
||||
parentId: '', // 上级评论ID
|
||||
content: sendValue.value, // 评论内容
|
||||
}
|
||||
}).then(res=> {
|
||||
console.log(res)
|
||||
uni.showToast({
|
||||
title: t('toast.commentSuccess'),
|
||||
icon: 'none'
|
||||
})
|
||||
sendValue.value = ''
|
||||
getCommentList()
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="recipe-page">
|
||||
<!-- 骨架屏 -->
|
||||
<recipe-skeleton v-if="loading" />
|
||||
|
||||
<!-- 实际内容 -->
|
||||
<view v-else class="recipe-content animate-in fade-in animate-duration-300">
|
||||
<status-bar />
|
||||
<!-- 顶部图片区域 -->
|
||||
<view class="relative w-750rpx h-750rpx px-30rpx">
|
||||
<view
|
||||
class="fixed top-0 left-0 z-9 w-full px-30rpx transition-all pt-16rpx"
|
||||
:class="[showStatusBar ? 'bg-#fff' : '']"
|
||||
>
|
||||
<!-- 状态栏 -->
|
||||
<status-bar />
|
||||
<!-- 返回按钮 -->
|
||||
<image
|
||||
@click="goBack"
|
||||
src="@img/chef/1327.png"
|
||||
mode="aspectFill"
|
||||
class="w-48rpx h-48rpx relative z-1"
|
||||
/>
|
||||
</view>
|
||||
<!-- 主图 -->
|
||||
<image
|
||||
:src="recipeDetail?.recipeImage?.split(',')[0]"
|
||||
mode="aspectFill"
|
||||
class="w-750rpx h-750rpx absolute top-0 left-0"
|
||||
/>
|
||||
<!-- 标题 -->
|
||||
<view class="absolute z-1 bottom-102rpx left-0 w-full px-30rpx">
|
||||
<view
|
||||
class="line-clamp-1 text-40rpx lh-40rpx text-#fff font-bold mb-28rpx"
|
||||
>{{ recipeDetail?.recipeName || '' }}</view
|
||||
>
|
||||
<view class="flex-center-sb text-28rpx text-#fff">
|
||||
<text>{{ formatTimestamp(recipeDetail?.createTime) }}</text>
|
||||
<view class="flex items-center">
|
||||
<image
|
||||
src="@img/chef/1326.png"
|
||||
mode="aspectFill"
|
||||
class="w-32rpx h-32rpx mr-8rpx"
|
||||
/>
|
||||
<text>{{ recipeDetail?.viewCount > 999 ? '999+' : recipeDetail?.viewCount }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 渐变遮罩 -->
|
||||
<view
|
||||
class="absolute bottom-0 left-0 w-full h-240rpx bg-gradient-to-b from-transparent via-[rgba(0,0,0,0.5)] to-black"
|
||||
></view>
|
||||
</view>
|
||||
|
||||
<!-- 内容卡片 -->
|
||||
<view
|
||||
class="content-card bg-white rounded-t-30rpx mt--68rpx relative px-30rpx"
|
||||
>
|
||||
<!-- 标题区域 -->
|
||||
<view class="flex items-center pt-40rpx mb-40rpx">
|
||||
<view class="w-44rpx h-44rpx mr-12rpx">
|
||||
<image
|
||||
src="@img/chef/1325.png"
|
||||
mode="aspectFill"
|
||||
class="w-44rpx h-44rpx"
|
||||
/>
|
||||
</view>
|
||||
<text class="text-36rpx font-medium text-#333">{{ t('pages-user.recipe.title') }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 材料区域 -->
|
||||
<view class="mb-40rpx">
|
||||
<text class="text-30rpx leading-44rpx text-#333">
|
||||
{{ recipeDetail?.ingredients }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 视频预览区域 -->
|
||||
<view
|
||||
class="w-690rpx rounded-20rpx overflow-hidden mb-40rpx"
|
||||
>
|
||||
<template v-for="item in recipeDetail?.recipeImage?.split(',')">
|
||||
<wd-img :enable-preview="true" :src="item" class="mb-20rpx last:mb-0" height="360rpx" mode="aspectFill"
|
||||
radius="20rpx"
|
||||
width="100%"/>
|
||||
</template>
|
||||
</view>
|
||||
|
||||
<!-- 收藏按钮 -->
|
||||
<view class="flex justify-center mb-40rpx relative">
|
||||
<view
|
||||
:class="[recipeDetail.isCollect ? 'bg-#fff border-#FF2806 border-solid border-1px text-#333' : 'bg-#FF2806 text-white']"
|
||||
class="w-310rpx h-88rpx rounded-44rpx center relative z-2"
|
||||
@click="collectRecipeDebounce"
|
||||
>
|
||||
<view class="flex items-center">
|
||||
<image
|
||||
v-if="recipeDetail.isCollect"
|
||||
src="@img/chef/118.png"
|
||||
mode="aspectFill"
|
||||
class="w-44rpx h-44rpx"
|
||||
/>
|
||||
<image
|
||||
v-else
|
||||
src="@img/chef/1332.png"
|
||||
mode="aspectFill"
|
||||
class="w-44rpx h-44rpx"
|
||||
/>
|
||||
<text class="text-30rpx ml-10rpx">{{ recipeDetail.collectCount }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view
|
||||
v-if="!recipeDetail.isCollect"
|
||||
class="absolute bottom--16rpx left-50% translate-x-[-50%] w-310rpx h-88rpx bg-#FF2806 opacity-[.28] rounded-44rpx blur-18rpx"
|
||||
></view>
|
||||
</view>
|
||||
|
||||
<!-- 收藏提示 -->
|
||||
<view class="text-center mb-40rpx">
|
||||
<text class="text-24rpx lh-24rpx text-#9E9E9E"
|
||||
>{{ t('pages-user.recipe.desc') }}</text
|
||||
>
|
||||
</view>
|
||||
|
||||
<!-- 用户头像列表 -->
|
||||
<view v-if="recipeDetail?.collectUserAvatarList?.length > 0" class="flex justify-center items-center mb-60rpx">
|
||||
<view class="avatar-list flex items-center">
|
||||
<!-- 用户头像 -->
|
||||
<view
|
||||
v-for="i in recipeDetail?.collectUserAvatarList"
|
||||
:key="i"
|
||||
:style="{ zIndex: 1 + i }"
|
||||
class="avatar-item"
|
||||
>
|
||||
<image
|
||||
:src="i"
|
||||
class="w-full h-full rounded-full"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 更多图标 -->
|
||||
<view class="avatar-item more-icon z-99">
|
||||
<image
|
||||
class="w-full h-full rounded-full"
|
||||
mode="aspectFill"
|
||||
src="@img/chef/1328.png"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
<view class="pb-48rpx">
|
||||
<CComment
|
||||
ref="ccRef"
|
||||
v-model:myInfo="myInfo"
|
||||
v-model:tableData="commentList"
|
||||
v-model:tableTotal="tableTotal"
|
||||
:deleteMode="deleteMode"
|
||||
@replyFun="replyFun"
|
||||
@deleteFun="getCommentList"
|
||||
@update="getCommentList"
|
||||
></CComment>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="shadow-lg w-full flex-center-sb gap-30rpx bg-white py-12rpx px-30rpx">
|
||||
<view class="w-full h-74rpx center bg-#F6F6F6 rounded-16rpx px-28rpx">
|
||||
<wd-input
|
||||
no-border
|
||||
clearable
|
||||
:cursorSpacing="10"
|
||||
: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;"
|
||||
:placeholder="t('common.placeholder.pleaseEnter')"
|
||||
v-model="sendValue"
|
||||
@confirm="handleSend"
|
||||
>
|
||||
</wd-input>
|
||||
</view>
|
||||
<wd-button @click="handleSend" class="!h-74rpx !w-120rpx !rounded-16rpx !bg-#333 !text-white !text-30rpx">{{ t('common.send') }}</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.recipe-page {
|
||||
background-color: #fff;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.recipe-content {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.image-container {
|
||||
width: 750rpx;
|
||||
height: 750rpx;
|
||||
}
|
||||
|
||||
.content-card {
|
||||
min-height: 1000rpx;
|
||||
}
|
||||
|
||||
// 头像列表样式
|
||||
.avatar-list {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.avatar-item {
|
||||
width: 68rpx;
|
||||
height: 68rpx;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
border: 2rpx solid #fff;
|
||||
margin-left: -20rpx;
|
||||
position: relative;
|
||||
background-color: #f6f6f6;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&.more-icon {
|
||||
margin-left: -20rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,186 @@
|
||||
<script setup lang="ts">
|
||||
import {appSearchSearchRecipePost, appRecipeCategoryListGet, appCollectCollectPost} from "@/service";
|
||||
import {formatTimestamp} from "@/utils/utils";
|
||||
import TabsType from "@/pages/home/components/tabbar-home/components/tabs-type.vue";
|
||||
import Collection from "@/components/collection/index.vue";
|
||||
import AnimatedButton from "@/pages/search/components/animated-button/animated-button.vue";
|
||||
import SortPopup from "@/pages/search/components/sort-popup.vue";
|
||||
import {CollectionType} from "@/constant/enums";
|
||||
import {debounce} from "throttle-debounce";
|
||||
const { t } = useI18n();
|
||||
const recipeCategoryId = ref('')
|
||||
const currentSort = ref(1)
|
||||
const recipeTotal = ref('')
|
||||
const { paging, dataList, loading, queryList, firstLoaded } = usePage((pageNum, pageSize) => {
|
||||
// 搜索菜谱
|
||||
return new Promise(resolve => {
|
||||
appSearchSearchRecipePost({
|
||||
body: {
|
||||
pageNum: pageNum,
|
||||
pageSize: pageSize,
|
||||
recipeCategoryId: recipeCategoryId.value, // 菜谱分类ID
|
||||
sortType: currentSort.value // 排序方式
|
||||
}
|
||||
}).then(res => {
|
||||
recipeTotal.value = res.total; // 更新菜谱数据总条数
|
||||
resolve({rows: res.rows})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const allRecipeCategoryList = ref([]) // 所有菜谱分类列表
|
||||
function getRecipeCategory() {
|
||||
appRecipeCategoryListGet({}).then(res => {
|
||||
console.log('菜谱分类数据', res)
|
||||
allRecipeCategoryList.value = res.data;
|
||||
if (res.data.length > 0) {
|
||||
recipeCategoryId.value = res.data[0].id; // 默认选中第一个分类
|
||||
refresh()
|
||||
} else {
|
||||
recipeCategoryId.value = ''; // 没有分类时清空
|
||||
}
|
||||
})
|
||||
}
|
||||
function tabsTypeChange(data){
|
||||
console.log('选中的菜谱分类ID', data)
|
||||
recipeCategoryId.value = data;
|
||||
refresh()
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
if(paging.value) {
|
||||
paging.value.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
const sortPopupRef = ref(null)
|
||||
function toggleSort() {
|
||||
if (sortPopupRef.value) {
|
||||
sortPopupRef.value.onOpen()
|
||||
}
|
||||
}
|
||||
|
||||
const rotationCount = ref(0)
|
||||
function handleReset() {
|
||||
currentSort.value = 1;
|
||||
rotationCount.value ++
|
||||
refresh()
|
||||
}
|
||||
|
||||
function handleApply(sort: number) {
|
||||
currentSort.value = sort;
|
||||
refresh()
|
||||
}
|
||||
|
||||
onLoad(()=> {
|
||||
// 获取菜谱分类数据
|
||||
getRecipeCategory()
|
||||
})
|
||||
|
||||
|
||||
// 收藏菜品
|
||||
function handleSubmitCollectRecipe(item: any) {
|
||||
collectRecipe(item)
|
||||
}
|
||||
// 防抖处理函数
|
||||
const collectRecipe = debounce(1000, (item: any) => {
|
||||
appCollectCollectPost({
|
||||
body: {
|
||||
targetId: item.id,
|
||||
targetType: CollectionType.RECIPE
|
||||
}
|
||||
}).then(res=> {
|
||||
item.isCollect = !item.isCollect;
|
||||
})
|
||||
}, {
|
||||
atBegin: true, // 立即触发
|
||||
});
|
||||
|
||||
function navigateToRecipeDetail(id: string | number) {
|
||||
navigateTo(`/pages-user/pages/recipe/index?id=${id}`)
|
||||
}
|
||||
|
||||
function navigateTo(url: string) {
|
||||
uni.navigateTo({ url })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<z-paging ref="paging" v-model="dataList" @query="queryList" :auto="false" bg-color="#fff">
|
||||
<template #top>
|
||||
<navbar/>
|
||||
<!-- 分类滚动区域 -->
|
||||
<tabs-type @changeType="tabsTypeChange" :currentId="recipeCategoryId" :list="allRecipeCategoryList" labelKey="categoryName" imgKey="categoryImage" class="my-36rpx" />
|
||||
<!-- 筛选结果 -->
|
||||
<view class="flex-center-sb px-30rpx mb-36rpx">
|
||||
<view class="text-36rpx lh-36rpx text-#333 font-500 tracking-[.04em]"
|
||||
>{{ recipeTotal }} {{ t('pages.search.result.result') }}</view
|
||||
>
|
||||
<view class="flex items-center gap-20rpx">
|
||||
<view
|
||||
@click="toggleSort"
|
||||
class="bg-#F2F2F2 h-64rpx rounded-64rpx px-24rpx center text-26rpx text-#333 lh-26rpx font-500"
|
||||
>
|
||||
<text v-if="+currentSort === 1">{{ t('components.searchSort.time') }}</text>
|
||||
<text v-if="+currentSort === 2">{{ t('components.searchSort.comment') }}</text>
|
||||
<text v-if="+currentSort === 3">{{ t('components.searchSort.thumbsUp') }}</text>
|
||||
<text v-if="+currentSort === 4">{{ t('components.searchSort.view') }}</text>
|
||||
<image
|
||||
src="@img/chef/101.png"
|
||||
class="w-24rpx h-24rpx ml-10rpx"
|
||||
></image
|
||||
>
|
||||
</view>
|
||||
|
||||
<animated-button
|
||||
button-class="bg-#F2F2F2 h-64rpx rounded-64rpx px-24rpx center text-26rpx text-#333 lh-26rpx font-500"
|
||||
@click="handleReset"
|
||||
>
|
||||
{{ t('common.reset') }}
|
||||
<image
|
||||
src="@img/chef/131.png"
|
||||
class="w-24rpx h-24rpx ml-10rpx transition-transform duration-600"
|
||||
:style="{ transform: `rotate(${rotationCount * 360}deg)` }"
|
||||
></image>
|
||||
</animated-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<template v-for="(item,index) in dataList">
|
||||
<view @click="navigateToRecipeDetail(item.id)" :class="[index === 0 ? '' : 'mt-52rpx']" class="flex-center-sb px-30rpx">
|
||||
<image
|
||||
:src="item?.recipeImage?.split(',')[0]"
|
||||
mode="aspectFill"
|
||||
class="w-210rpx h-200rpx mr-28rpx shrink-0 rounded-24rpx"
|
||||
></image>
|
||||
<view class="flex-1 h-200rpx">
|
||||
<view class="text-36rpx lh-40rpx text-#333 font-500 mb-40rpx line-clamp-1 tracking-[.04em]">{{ item.recipeName }}</view>
|
||||
<view class="text-30rpx lh-30rpx text-#333 tracking-[.04em]">
|
||||
{{ formatTimestamp(item?.createTime) }}
|
||||
</view>
|
||||
<view class="mt-42rpx flex items-center gap-30rpx text-#999 text-30rpx">
|
||||
<view class="flex items-center gap-10rpx">
|
||||
<collection :is-collected="item.isCollect"
|
||||
@collectionChange="handleSubmitCollectRecipe(item)" />
|
||||
{{ item.collectCount }}
|
||||
</view>
|
||||
<view class="flex items-center gap-10rpx">
|
||||
<image
|
||||
src="@img/chef/132.png"
|
||||
class="w-40rpx h-40rpx"
|
||||
></image>
|
||||
{{ item.commentCount }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</z-paging>
|
||||
|
||||
<!-- 菜谱筛选弹窗 -->
|
||||
<sort-popup ref="sortPopupRef" @apply="handleApply" />
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,455 @@
|
||||
<template>
|
||||
<z-paging>
|
||||
<template #top>
|
||||
<navbar/>
|
||||
<view class="bg-white px-30rpx py-26rpx">
|
||||
<view class="flex items-center h-86rpx px-28rpx bg-#F6F7F9 rounded-20rpx">
|
||||
<wd-input v-model="mapSearchKeyword" :no-border="true" :placeholder="t('common.placeholder.pleaseEnter')"
|
||||
custom-class="!w-full !bg-transparent" placeholder-class="text-#9B9CA0 text-28rpx">
|
||||
<template #prefix>
|
||||
<view class="w-12rpx h-12rpx rounded-50% bg-#F97C34"></view>
|
||||
</template>
|
||||
</wd-input>
|
||||
</view>
|
||||
|
||||
<view class="" @click="handleUseLocation">
|
||||
<view class="text-32rpx text-#333 font-500 my-30rpx">{{ t('common.useMyCurrentLocation') }}</view>
|
||||
<view class="text-28rpx text-#333 font-500 flex items-center pb-20rpx pl-24rpx">
|
||||
<view class="i-carbon:location-current text-32rpx mr-14rpx mt-4rpx"></view>
|
||||
{{ t('common.useCurrentLocation') }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<view :change:prop="mapRenderjs.searchPlace" :prop="querySearch" class="bg-#f5f5f5">
|
||||
<view class="px-22rpx py-20rpx">
|
||||
<template v-if="placesLength === 0">
|
||||
<view class="center py-100rpx">
|
||||
<image class="w-250rpx h-250rpx" src="@img/chef/100.png"></image>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<view class="rounded-36rpx bg-white">
|
||||
<template v-for="(item,index) in logicStore.placesList" :key="index">
|
||||
<view :class="[index === logicStore.placesList.length - 1 ? '' : 'border-bottom']"
|
||||
class="px-22rpx pb-30rpx pt-34rpx"
|
||||
@click="handleClickLocation(item)">
|
||||
<!-- <view >{{ item }}</view> -->
|
||||
<view class="text-#000 text-26rpx font-bold">{{ item.displayName }}</view>
|
||||
<view class="text-#9B9CA0 text-24rpx flex-center-sb">
|
||||
<view>{{ item.formattedAddress }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
<view id="map" :change:prop="mapRenderjs.initMap" :prop="mapDataComputed" :user-location="userLocation"
|
||||
class="absolute left-0 bottom-0 w-0 h-0"></view>
|
||||
</view>
|
||||
</z-paging>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import Config from "@/config";
|
||||
import {EventEnum} from "@/constant/enums";
|
||||
import {useLogicStore} from "@/pages-user/store/logic";
|
||||
import {useUserStore} from "@/store";
|
||||
|
||||
const {t, locale} = useI18n();
|
||||
const userStore = useUserStore()
|
||||
const logicStore = useLogicStore()
|
||||
|
||||
// 生命周期:清空地址列表
|
||||
onMounted(() => {
|
||||
logicStore.clearPlacesList()
|
||||
})
|
||||
|
||||
const placesLength = computed(() => {
|
||||
return logicStore.placesList.length;
|
||||
});
|
||||
|
||||
const userLocation = computed(() => ({
|
||||
longitude: userStore.location.longitude,
|
||||
latitude: userStore.location.latitude
|
||||
}));
|
||||
|
||||
watch(
|
||||
() => logicStore.searchLoading,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
uni.showLoading({
|
||||
title: 'Loading...',
|
||||
mask: true,
|
||||
});
|
||||
} else {
|
||||
uni.hideLoading();
|
||||
}
|
||||
},
|
||||
{immediate: true}
|
||||
);
|
||||
|
||||
// 地图搜索关键词
|
||||
const mapSearchKeyword = ref<string>('');
|
||||
const querySearch = computed(() => {
|
||||
return {
|
||||
keyword: mapSearchKeyword.value,
|
||||
}
|
||||
})
|
||||
|
||||
function handleClickLocation(item: any) {
|
||||
console.log('item', item)
|
||||
uni.$emit(EventEnum.CHOOSE_ADDRESS, item)
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
// 使用当前位置
|
||||
function handleUseLocation() {
|
||||
// 检查是否获取到了当前定位
|
||||
if (!userStore.location.longitude || !userStore.location.latitude) {
|
||||
// 尝试再次获取定位
|
||||
uni.getLocation({
|
||||
isHighAccuracy: true,
|
||||
type: 'gcj02',
|
||||
geocode: true,
|
||||
success: async (res) => {
|
||||
getCityName(res.latitude, res.longitude)
|
||||
},
|
||||
fail: (err) => {
|
||||
const settings = uni.getAppAuthorizeSetting()
|
||||
console.log(settings)
|
||||
if (settings.locationAuthorized === 'denied') {
|
||||
uni.showToast({
|
||||
title: t('common.prompt.please-authorize-location-information'),
|
||||
icon: 'none'
|
||||
})
|
||||
setTimeout(()=> {
|
||||
uni.openAppAuthorizeSetting()
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
} else {
|
||||
uni.$emit(EventEnum.CHOOSE_ADDRESS, {
|
||||
displayName: userStore.location.location,
|
||||
formattedAddress: userStore.location.formattedAddress || '',
|
||||
location: {
|
||||
lng: userStore.location.longitude,
|
||||
lat: userStore.location.latitude
|
||||
}
|
||||
})
|
||||
uni.navigateBack()
|
||||
}
|
||||
}
|
||||
|
||||
function getCityName(latitude: number, longitude: number) {
|
||||
const url = `https://maps.googleapis.com/maps/api/geocode/json?latlng=${latitude},${longitude}&key=${Config.googleMapKey}&language=${locale.value === 'zh-Hans' ? 'zh-CN' : locale.value}`;
|
||||
|
||||
uni.request({
|
||||
url,
|
||||
method: 'GET',
|
||||
timeout: 10000,
|
||||
success: (res: any) => {
|
||||
const results = res.data?.results || [];
|
||||
console.log('geocode results:', results);
|
||||
|
||||
if (!results.length) {
|
||||
return handleFail();
|
||||
}
|
||||
|
||||
const addr = results[0]; // 最高匹配度
|
||||
const components = addr.address_components || [];
|
||||
|
||||
// 提取城市名的工具函数
|
||||
const pickAddress = (types: string[]) => {
|
||||
return components.find((item) => item.types.some((t) => types.includes(t)));
|
||||
};
|
||||
|
||||
// 寻找城市名(多层兜底)
|
||||
let cityObj =
|
||||
pickAddress(["locality"]) ||
|
||||
pickAddress(["administrative_area_level_2"]) ||
|
||||
pickAddress(["administrative_area_level_1"]) ||
|
||||
pickAddress(["political"]);
|
||||
|
||||
const cityName = cityObj?.long_name || null;
|
||||
|
||||
// 从 Google 获取完整地址
|
||||
const formattedAddress = addr.formatted_address || cityName || "";
|
||||
|
||||
console.log("city:", cityObj);
|
||||
console.log("formattedAddress:", formattedAddress);
|
||||
|
||||
if (!cityName) {
|
||||
return handleFail();
|
||||
}
|
||||
|
||||
// 存入 store
|
||||
userStore.location = {
|
||||
location: cityName,
|
||||
formattedAddress,
|
||||
longitude,
|
||||
latitude
|
||||
};
|
||||
|
||||
// 发事件
|
||||
uni.$emit(EventEnum.CHOOSE_ADDRESS, {
|
||||
displayName: cityName,
|
||||
formattedAddress,
|
||||
location: { lng: longitude, lat: latitude }
|
||||
});
|
||||
|
||||
uni.navigateBack();
|
||||
},
|
||||
|
||||
fail: () => handleFail()
|
||||
});
|
||||
|
||||
function handleFail() {
|
||||
uni.showToast({
|
||||
title: t('common.prompt.getLocationFailed'),
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const mapDataComputed = computed(() => {
|
||||
return {
|
||||
language: locale.value,
|
||||
lng: userStore.location.longitude || 174.7633,
|
||||
lat: userStore.location.latitude || -36.8485,
|
||||
}
|
||||
})
|
||||
|
||||
// 监听来自 renderjs 的事件
|
||||
onMounted(() => {
|
||||
// 监听搜索结果
|
||||
uni.$on('MAP_SEARCH_RESULT', (places: any) => {
|
||||
console.log(places, '接收到的搜索结果')
|
||||
if (places && places.length > 0) {
|
||||
// 解析实际返回的数据结构
|
||||
const parsedPlaces = places.map((placeArray: any) => {
|
||||
const placeData = placeArray[0]?.[0];
|
||||
if (!placeData) return null;
|
||||
|
||||
const placeId = placeData[1];
|
||||
const formattedAddress = placeData[8] || '';
|
||||
const locationArray = placeData[11];
|
||||
const displayNameArray = placeData[27];
|
||||
|
||||
return {
|
||||
id: placeId,
|
||||
displayName: displayNameArray?.[0] || formattedAddress,
|
||||
formattedAddress: formattedAddress,
|
||||
location: locationArray ? {
|
||||
lat: locationArray[0],
|
||||
lng: locationArray[1]
|
||||
} : null
|
||||
};
|
||||
}).filter(Boolean);
|
||||
|
||||
console.log('解析后的地址列表', parsedPlaces);
|
||||
logicStore.setPlacesList(parsedPlaces);
|
||||
} else {
|
||||
logicStore.setPlacesList([]);
|
||||
}
|
||||
})
|
||||
|
||||
// 监听地图未加载提示
|
||||
uni.$on('MAP_NOT_LOADED', () => {
|
||||
uni.showToast({
|
||||
title: 'Map is not loaded yet, please wait',
|
||||
icon: 'none'
|
||||
});
|
||||
})
|
||||
|
||||
// 监听搜索超时提示
|
||||
uni.$on('MAP_SEARCH_TIMEOUT', () => {
|
||||
uni.showToast({
|
||||
title: 'Search timeout, please try again',
|
||||
icon: 'none'
|
||||
});
|
||||
})
|
||||
|
||||
// 监听搜索加载状态
|
||||
uni.$on('MAP_SEARCH_LOADING', (isLoading: boolean) => {
|
||||
// logicStore.searchLoading = isLoading;
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// 清理事件监听
|
||||
uni.$off('MAP_SEARCH_RESULT')
|
||||
uni.$off('MAP_NOT_LOADED')
|
||||
uni.$off('MAP_SEARCH_TIMEOUT')
|
||||
uni.$off('MAP_SEARCH_LOADING')
|
||||
})
|
||||
</script>
|
||||
<script lang="renderjs" module="mapRenderjs">
|
||||
import {Loader} from "@googlemaps/js-api-loader"
|
||||
import * as R from 'ramda'
|
||||
import Config from '@/config'
|
||||
import {debounce} from "throttle-debounce";
|
||||
|
||||
let map = null
|
||||
let mapLoaded = false; // 地图加载完成标志
|
||||
// @ts-ignore
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
region: "US",
|
||||
lan: 'en',
|
||||
google: null,
|
||||
canvas: null,
|
||||
center: null,
|
||||
dotLottie: null,
|
||||
marker: null,
|
||||
markerViewList: [],
|
||||
AdvancedMarkerElement: null,
|
||||
lng: -77.0365,
|
||||
lat: 38.8977,
|
||||
userLocation: {
|
||||
longitude: 0,
|
||||
latitude: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
props: {
|
||||
userLocation: Object
|
||||
},
|
||||
mounted() {
|
||||
console.log('mounted mapRenderjs')
|
||||
mapLoaded = false; // 重置地图加载完成标志
|
||||
// !map&&this.initMap()
|
||||
this.searchPlace = debounce(200, this.searchPlace)
|
||||
},
|
||||
methods: {
|
||||
initMap({lng, lat, language}) {
|
||||
console.log('initMap', lng, lat)
|
||||
console.log('当前系统语言', language)
|
||||
|
||||
this.lan = language === 'zh-Hans' ? 'zh-CN' : 'en'
|
||||
this.region = language === 'zh-Hans' ? 'CN' : 'US'
|
||||
this.lng = language === 'zh-Hans' ? 116.4074 : -77.0365
|
||||
this.lat = language === 'zh-Hans' ? 39.9042 : 38.8977
|
||||
console.log('当前系统语言', this.lan)
|
||||
console.log('当前系统区域', this.region)
|
||||
|
||||
this.$nextTick(() => {
|
||||
const loader = new Loader({
|
||||
apiKey: Config.googleMapKey,
|
||||
version: "weekly",
|
||||
region: this.region, // 设置为美国
|
||||
language: this.lan,
|
||||
});
|
||||
|
||||
|
||||
loader.load().then(async (google) => {
|
||||
const {Map} = await google.maps.importLibrary("maps")
|
||||
this.google = google
|
||||
console.log('google', google.maps)
|
||||
// 地图相关配置
|
||||
// 指定自定义地图样式的 ID。
|
||||
// center: 地图初始中心点的经纬度(lat, lng)。
|
||||
// zoom: 地图初始缩放级别。
|
||||
// fullscreenControl: 是否显示全屏按钮(false 表示不显示)。
|
||||
// cameraControl: 是否显示相机控制(false 表示不显示)。
|
||||
// disableDefaultUI: 是否禁用所有默认 UI 控件(true 表示全部禁用)。
|
||||
// gestureHandling: 设置手势操作方式("greedy" 表示允许所有手势操作)
|
||||
const mapOptions = {
|
||||
mapId: 'ff2268c265ce7a40',
|
||||
center: {
|
||||
lat: this.lat,
|
||||
lng: this.lng,
|
||||
},
|
||||
zoom: 12,
|
||||
fullscreenControl: false,
|
||||
cameraControl: false,
|
||||
disableDefaultUI: true,
|
||||
gestureHandling: "greedy",
|
||||
};
|
||||
|
||||
map = new Map(document.getElementById("map"), mapOptions);
|
||||
mapLoaded = true; // 地图加载完成
|
||||
});
|
||||
})
|
||||
},
|
||||
async searchPlace({keyword}) {
|
||||
console.log('搜索关键词', keyword);
|
||||
if (!keyword) {
|
||||
uni.$emit('MAP_SEARCH_RESULT', [])
|
||||
return;
|
||||
}
|
||||
if (!mapLoaded) {
|
||||
console.log('地图未加载完成,无法搜索');
|
||||
uni.$emit('MAP_NOT_LOADED');
|
||||
return;
|
||||
}
|
||||
if (!map || !this.google) {
|
||||
return
|
||||
}
|
||||
uni.$emit('MAP_SEARCH_LOADING', true);
|
||||
let timeoutId;
|
||||
try {
|
||||
// 设置搜索超时
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
timeoutId = setTimeout(() => {
|
||||
uni.$emit('MAP_SEARCH_LOADING', false);
|
||||
reject(new Error('Search timeout'));
|
||||
}, 10000); // 10 秒超时
|
||||
});
|
||||
|
||||
const {Place, SearchByTextRankPreference} = await this.google.maps.importLibrary("places");
|
||||
const request = {
|
||||
textQuery: keyword,
|
||||
fields: ['id', 'displayName', 'location', 'formattedAddress', 'addressComponents'],
|
||||
locationBias: {lat: this.lat, lng: this.lng},
|
||||
language: this.lan,
|
||||
maxResultCount: 20,
|
||||
region: this.region,
|
||||
rankPreference: SearchByTextRankPreference.RELEVANCE
|
||||
};
|
||||
|
||||
const searchPromise = Place.searchByText(request);
|
||||
const {places} = await Promise.race([searchPromise, timeoutPromise]);
|
||||
console.log('地图搜索结果原始数据', places);
|
||||
|
||||
// 将 Google Places API 返回的对象转换为可序列化的数组格式
|
||||
const serializedPlaces = places.map(place => {
|
||||
// 提取需要的字段并转换为普通对象
|
||||
return [[{
|
||||
1: place.id,
|
||||
8: place.formattedAddress || '',
|
||||
9: place.addressComponents || [],
|
||||
11: place.location ? [place.location.lat(), place.location.lng()] : null,
|
||||
27: place.displayName ? [
|
||||
typeof place.displayName === 'string' ? place.displayName : place.displayName.text,
|
||||
place.displayName.languageCode || this.lan
|
||||
] : null
|
||||
}]];
|
||||
});
|
||||
|
||||
console.log('序列化后的搜索结果', serializedPlaces);
|
||||
uni.$emit('MAP_SEARCH_RESULT', serializedPlaces);
|
||||
} catch (e) {
|
||||
console.log('搜索错误原因', e);
|
||||
if (e.message === 'Search timeout') {
|
||||
uni.$emit('MAP_SEARCH_TIMEOUT');
|
||||
}
|
||||
uni.$emit('MAP_SEARCH_RESULT', []);
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
uni.$emit('MAP_SEARCH_LOADING', false);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
page {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
</style>
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,171 @@
|
||||
<template>
|
||||
<z-paging ref="paging" v-model="dataList" @query="queryList" :auto="false">
|
||||
<template #top>
|
||||
<navbar />
|
||||
<view class="pl-38rpx text-54rpx text-#000 font-bold">{{ t('pages-user.card.listCard') }}</view>
|
||||
</template>
|
||||
<view class="px-38rpx pt-30rpx">
|
||||
<view class="animate-in fade-in animate-ease-in animate-duration-300" v-show="!loading">
|
||||
<wd-radio-group v-model="selectedCard" shape="dot" @change="cardChange" checked-color="#000">
|
||||
<template v-for="(item, index) in dataList">
|
||||
<wd-swipe-action>
|
||||
<view class="h-96rpx mb-28rpx px-32rpx py-28rpx rounded-48rpx border-#DBDBDB border-solid border-1px">
|
||||
<wd-radio :value="index">{{ item.cardNumber }}</wd-radio>
|
||||
</view>
|
||||
<template #right>
|
||||
<view class="center h-full pl-20rpx">
|
||||
<wd-icon @click="handleDelete(item)" name="delete1" color="#333" size="32rpx"></wd-icon>
|
||||
</view>
|
||||
</template>
|
||||
</wd-swipe-action>
|
||||
</template>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<view class="animate-in fade-in animate-ease-out animate-duration-300" v-if="loading">
|
||||
<view class="" :class="[i > 1 && 'mt-10rpx']" v-for="i in 10" :key="i">
|
||||
<view class="flex items-center flex items-center py-38rpx px-30rpx bg-#fff">
|
||||
<view class="flex-1">
|
||||
<wd-skeleton
|
||||
animation="gradient"
|
||||
:row-col="[{ width: '114rpx' }, { width: '284rpx' }]"
|
||||
/>
|
||||
</view>
|
||||
<view class="shrink-0 ml-20rpx flex">
|
||||
<wd-skeleton animation="gradient" :row-col="[{ size: '52rpx', type: 'circle' }]" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<template #bottom>
|
||||
<view class="px-30rpx bg-white py-18rpx">
|
||||
<view class="flex items-center gap-22rpx">
|
||||
<wd-button custom-class="w-full !h-92rpx !text-#14181B !rounded-16rpx !font-500 !text-30rpx !bg-white !border-#14181B !border-solid !border-1px" block
|
||||
@click="handleAdd">
|
||||
{{ t('pages-user.card.add') }}
|
||||
</wd-button>
|
||||
<wd-button custom-class="w-full !h-92rpx !text-#fff !rounded-16rpx !text-32rpx" block
|
||||
@click="handleSubmit">
|
||||
{{ t('common.confirm') }}
|
||||
</wd-button>
|
||||
</view>
|
||||
|
||||
<view :style="[configStore.iosSafeBottomPlaceholder]"></view>
|
||||
</view>
|
||||
</template>
|
||||
</z-paging>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import {useConfigStore} from "@/store";
|
||||
import {debounce} from "throttle-debounce";
|
||||
import Config from "@/config";
|
||||
const { t } = useI18n();
|
||||
import {appUserCardDeletePost, appUserCardListPost} from "@/service";
|
||||
|
||||
const {proxy} = getCurrentInstance() as any
|
||||
const configStore = useConfigStore()
|
||||
|
||||
const eventChannel = proxy.getOpenerEventChannel();
|
||||
|
||||
|
||||
const creditCard = ref(null)
|
||||
const selectedCard = ref(null)
|
||||
const {paging, dataList, queryList, loading} = usePage<any>((pageNum: number, pageSize: number) =>
|
||||
appUserCardListPost({
|
||||
params: {
|
||||
pageNum,
|
||||
pageSize,
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
function cardChange(value) {
|
||||
console.log(value)
|
||||
console.log('value', dataList.value[value.value])
|
||||
creditCard.value = dataList.value[value.value]
|
||||
}
|
||||
function submit() {
|
||||
console.log('submit', selectedCard.value)
|
||||
console.log('submit', creditCard.value)
|
||||
if (!creditCard.value) {
|
||||
uni.showToast({
|
||||
icon: 'none',
|
||||
title: 'Please choose a credit card',
|
||||
})
|
||||
return
|
||||
}
|
||||
eventChannel.emit('selectedCard', creditCard.value);
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
|
||||
// 提交
|
||||
const handleSubmit = debounce(Config.debounceLongTime, submit, {
|
||||
atBegin: true
|
||||
})
|
||||
|
||||
function handleAdd() {
|
||||
uni.navigateTo({
|
||||
url: '/pages-user/pages/add-card/index'
|
||||
})
|
||||
}
|
||||
function handleDelete(item: any) {
|
||||
appUserCardDeletePost({
|
||||
body: {
|
||||
id: item.id,
|
||||
cardNumber: item.cardNumber,
|
||||
cardId: item.cardId,
|
||||
}
|
||||
}).then(res=> {
|
||||
uni.showToast({
|
||||
title: t('toast.deleteSuccess'),
|
||||
icon: 'none',
|
||||
})
|
||||
setTimeout(() => {
|
||||
paging.value?.refresh()
|
||||
}, 1000)
|
||||
})
|
||||
// 删除信用卡
|
||||
// deleteCreditCardApi({
|
||||
// id: item.id,
|
||||
// cardNumber: item.cardNumber,
|
||||
// cardId: item.cardId,
|
||||
// }).then(() => {
|
||||
// uni.showToast({
|
||||
// title: 'Delete successfully',
|
||||
// icon: 'none',
|
||||
// })
|
||||
// setTimeout(() => {
|
||||
// paging.value?.refresh()
|
||||
// }, 1000)
|
||||
// }).catch(() => {
|
||||
// uni.showToast({
|
||||
// title: 'Delete failed',
|
||||
// icon: 'none',
|
||||
// })
|
||||
// })
|
||||
}
|
||||
|
||||
function navigateTo(url: string) {
|
||||
uni.navigateTo({
|
||||
url,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
onShow(() => {
|
||||
nextTick(() => {
|
||||
paging.value?.refresh()
|
||||
})
|
||||
})
|
||||
</script>
|
||||
<style>
|
||||
page {
|
||||
background-color: #fff;
|
||||
}
|
||||
</style>
|
||||
<style lang="scss" scoped>
|
||||
:deep(.wd-radio) {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,96 @@
|
||||
<script setup lang="ts">
|
||||
import {debounce} from "throttle-debounce";
|
||||
import Config from "@/config";
|
||||
import {useConfigStore} from "@/store";
|
||||
import {setDayjsLocale, setWotDesignLocale} from "@/plugin";
|
||||
|
||||
const {locale} = useI18n();
|
||||
const configStore = useConfigStore();
|
||||
|
||||
const show = ref(false);
|
||||
|
||||
const languages = reactive([
|
||||
{
|
||||
name: "中文",
|
||||
systemValue: "zh-Hans",
|
||||
},
|
||||
{
|
||||
name: "English",
|
||||
systemValue: "en",
|
||||
},
|
||||
]);
|
||||
|
||||
function init() {
|
||||
show.value = true;
|
||||
}
|
||||
|
||||
function close() {
|
||||
show.value = false;
|
||||
}
|
||||
|
||||
function submit(item: { name: string; systemValue: string; }) {
|
||||
close();
|
||||
const localeLanguages = uni.getLocale()
|
||||
console.log(localeLanguages)
|
||||
if (item.systemValue === uni.getLocale()) {
|
||||
return;
|
||||
}
|
||||
uni.setLocale(item.systemValue);
|
||||
locale.value = item.systemValue;
|
||||
setWotDesignLocale()
|
||||
setDayjsLocale()
|
||||
// #ifdef APP-PLUS
|
||||
if (configStore.isIos) {
|
||||
setTimeout(() => {
|
||||
plus.runtime.restart();
|
||||
}, 1000)
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
|
||||
const handleSubmit = debounce(Config.debounceShortTime, submit, {
|
||||
atBegin: true,
|
||||
});
|
||||
|
||||
defineOptions({
|
||||
name: "ChooseLanguage",
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
init,
|
||||
close,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<wd-popup
|
||||
custom-class="!bg-transparent"
|
||||
|
||||
position="bottom"
|
||||
safe-area-inset-bottom
|
||||
v-model="show"
|
||||
@close="close"
|
||||
>
|
||||
<view class="p-[28rpx+20rpx]">
|
||||
<view class="bg-#ffff rounded-16rpx shadow-[0rpx_3rpx_6rpx_rgba(0,0,0,0.16)]">
|
||||
<view
|
||||
class="relative"
|
||||
v-for="(item, index) in languages"
|
||||
:key="item.systemValue"
|
||||
@click="handleSubmit(item)"
|
||||
>
|
||||
<view class="h-108rpx center text-28rpx lh-42rpx text-primary font-bold">
|
||||
<text>{{ item.name }}</text>
|
||||
</view>
|
||||
<view
|
||||
class="absolute bottom-0 w-340rpx left-205rpx border-bottom"
|
||||
v-if="index === 0"
|
||||
></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -0,0 +1,73 @@
|
||||
<script setup lang="ts">
|
||||
import {appUserLogOffPost} from "@/service";
|
||||
import {useUserStore} from "@/store";
|
||||
import {debounce} from "throttle-debounce";
|
||||
import Config from "@/config";
|
||||
|
||||
const {t} = useI18n();
|
||||
const userStore = useUserStore();
|
||||
|
||||
const show = ref(false);
|
||||
|
||||
function init() {
|
||||
show.value = true;
|
||||
}
|
||||
|
||||
function close() {
|
||||
show.value = false;
|
||||
}
|
||||
|
||||
async function confirmLogout() {
|
||||
try {
|
||||
close();
|
||||
// await appUserLogOffPost({
|
||||
// body: {}
|
||||
// });
|
||||
await uni.showToast({title: t("pages.mine.log-out-successfully"), icon: "none"});
|
||||
userStore.clear();
|
||||
uni.switchTab({
|
||||
url: Config.indexPath
|
||||
})
|
||||
} catch (error) {
|
||||
}
|
||||
}
|
||||
|
||||
const handleConfirmLogout = debounce(Config.debounceLongTime, confirmLogout, {
|
||||
atBegin: true
|
||||
});
|
||||
|
||||
defineOptions({
|
||||
name: "LogOut",
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
init,
|
||||
close,
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<wd-popup
|
||||
custom-class="!bg-transparent"
|
||||
position="bottom"
|
||||
safe-area-inset-bottom
|
||||
v-model="show"
|
||||
@close="close"
|
||||
>
|
||||
<view class="p-[40rpx+20rpx]">
|
||||
<view
|
||||
class="mb-20rpx py-46rpx px-102rpx flex items-center justify-center text-center text-28rpx text-primary font-bold bg-white shadow-[0rpx_3rpx_6rpx_rgba(0,0,0,0.16)] rounded-12rpx"
|
||||
>
|
||||
{{ t("pages.mine.login-out-tip") }}
|
||||
</view>
|
||||
<view
|
||||
class="h-100rpx flex items-center justify-center text-28rpx text-primary font-bold shadow-[0rpx_3rpx_6rpx_rgba(0,0,0,0.16)] bg-white rounded-12rpx"
|
||||
@click="handleConfirmLogout"
|
||||
>
|
||||
{{ t("common.confirm") }}
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -0,0 +1,149 @@
|
||||
<script setup lang="ts">
|
||||
import {useMessage} from "wot-design-uni";
|
||||
import {useConfigStore, useUserStore} from "@/store";
|
||||
import {conversionMobile} from "@/utils";
|
||||
import {Agreement} from "@/constant/enums";
|
||||
import ChooseLanguage from "./components/choose-language/choose-language.vue";
|
||||
import Logout from "./components/log-out/log-out.vue";
|
||||
import Config from "@/config";
|
||||
import {appUserLogOffPost} from "@/service";
|
||||
|
||||
const {t} = useI18n();
|
||||
const {locale} = useI18n();
|
||||
const userStore = useUserStore();
|
||||
const configStore = useConfigStore()
|
||||
const message = useMessage();
|
||||
const currentVersion = ref(configStore.appVersion)
|
||||
const chooseLanguageRef = ref<InstanceType<typeof ChooseLanguage>>()
|
||||
const logoutRef = ref<InstanceType<typeof Logout>>()
|
||||
|
||||
function handleChooseLanguage() {
|
||||
if (chooseLanguageRef.value) {
|
||||
chooseLanguageRef.value.init()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function navigateTo(url: string) {
|
||||
uni.navigateTo({url});
|
||||
}
|
||||
|
||||
|
||||
function handleSetOrUpdatePassword() {
|
||||
if (userStore.userInfo?.payPwd) {
|
||||
return navigateTo(`/pages-user/pages/pay-password/change/index`)
|
||||
}
|
||||
navigateTo(`/pages-user/pages/pay-password/set/index`)
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
if (logoutRef.value) {
|
||||
logoutRef.value.init()
|
||||
}
|
||||
}
|
||||
|
||||
function handleLogoutAccount() {
|
||||
message
|
||||
.confirm({
|
||||
msg: t('pages-user.setting.cancelAccountConfirm'),
|
||||
title: t('common.prompt.system-prompt'),
|
||||
cancelButtonText: t('common.close'),
|
||||
confirmButtonText: t('common.confirm'),
|
||||
confirmButtonProps: {
|
||||
customClass: '!bg-#000'
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
appUserLogOffPost({
|
||||
body: {
|
||||
userPort: 1,
|
||||
}
|
||||
}).then(res=> {
|
||||
uni.showToast({
|
||||
title: t('pages-user.setting.cancelAccountSuccess'),
|
||||
icon: 'none',
|
||||
})
|
||||
userStore.clear()
|
||||
uni.switchTab({
|
||||
url: Config.indexPath,
|
||||
})
|
||||
})
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<navbar :title="t('navbar-settings')"/>
|
||||
<view class="pt-20rpx">
|
||||
<view class="text-30-bold bg-#fff">
|
||||
<view
|
||||
class="flex justify-between items-center border-bottom font-bold p-[40rpx+20rpx]"
|
||||
@click="navigateTo('/pages-user/pages/password/change/index')"
|
||||
>
|
||||
<view>{{ t("pages-user.setting.modification") }}</view>
|
||||
<image
|
||||
src="@img/chef/100202.png"
|
||||
class="shrink-0 ml-16rpx w-22rpx h-30rpx"
|
||||
></image>
|
||||
</view>
|
||||
|
||||
<view
|
||||
@click="
|
||||
handleSetOrUpdatePassword
|
||||
"
|
||||
class="flex justify-between items-center p-[40rpx+20rpx] border-bottom"
|
||||
>
|
||||
<view>{{ t("pages-user.setting.payPwd") }}</view>
|
||||
<view class="flex items-center">
|
||||
<text>{{ t('navbar-settings') }}</text>
|
||||
<image
|
||||
src="@img/chef/100202.png"
|
||||
class="shrink-0 ml-16rpx w-22rpx h-30rpx"
|
||||
></image>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="flex justify-between items-center p-[40rpx+20rpx] border-bottom before:!bg-common"
|
||||
@click="handleChooseLanguage"
|
||||
>
|
||||
<view>{{ t("pages-user.setting.language") }}</view>
|
||||
<view class="flex items-center">
|
||||
<text>{{ locale === 'en' ? 'English' : '中文'}}</text>
|
||||
<image
|
||||
src="@img/chef/100202.png"
|
||||
class="shrink-0 ml-16rpx w-22rpx h-30rpx"
|
||||
></image>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="flex justify-between items-center font-bold p-[40rpx+20rpx]"
|
||||
@click="handleLogoutAccount"
|
||||
>
|
||||
<view>{{ t('pages-user.setting.cancelAccount') }}</view>
|
||||
<image
|
||||
src="@img/chef/100202.png"
|
||||
class="shrink-0 ml-16rpx w-22rpx h-30rpx"
|
||||
></image>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="px-30rpx mt-180rpx">
|
||||
<wd-button
|
||||
custom-class="!h-108rpx !text-36rpx !text-white !bg-#14181B !rounded-16rpx"
|
||||
block
|
||||
@click="handleLogout"
|
||||
>
|
||||
{{ t('pages-user.setting.logout') }}
|
||||
</wd-button>
|
||||
|
||||
</view>
|
||||
|
||||
<choose-language ref="chooseLanguageRef"/>
|
||||
<logout ref="logoutRef"/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,604 @@
|
||||
<script lang="ts" setup>
|
||||
import {z} from "zod";
|
||||
import * as R from 'ramda'
|
||||
import {debounce} from "throttle-debounce";
|
||||
import Config from "@/config";
|
||||
import ChooseImage from "@/components/choose-image/choose-image.vue";
|
||||
import {
|
||||
appMerchantCategoryListGet,
|
||||
appMerchantGetFirstShopSettledGet,
|
||||
appShopSettlementApplyPost,
|
||||
appShopSettlementDetailGet
|
||||
} from "@/service";
|
||||
import useEventEmit from "@/hooks/useEventEmit";
|
||||
import {EventEnum} from "@/constant/enums";
|
||||
import {useUserStore} from "@/store";
|
||||
|
||||
const userStore = useUserStore()
|
||||
const {t} = useI18n();
|
||||
|
||||
// 审核状态 null 未提交 1审核中 2审核通过 3审核驳回
|
||||
const auditStatus = ref(null);
|
||||
|
||||
const type = ref('')
|
||||
const rejectReason = ref('')
|
||||
const id = ref('')
|
||||
const shopInfo = ref({})
|
||||
onLoad((options) => {
|
||||
appMerchantGetFirstShopSettledGet({}).then(res=> {
|
||||
console.log(res)
|
||||
if(res.data) {
|
||||
shopInfo.value = res.data
|
||||
auditStatus.value = Number(res.data.auditStatus)
|
||||
rejectReason.value = res.data.rejectReason
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 点击了重新编辑
|
||||
function clickInitEdit() {
|
||||
auditStatus.value = null
|
||||
// appShopSettlementDetailGet({
|
||||
// params: {
|
||||
// id: id.value
|
||||
// }
|
||||
// }).then(res => {
|
||||
//
|
||||
// })
|
||||
|
||||
formData.value = shopInfo.value
|
||||
formData.value.idCardImage = shopInfo.value.idCardImage || ''
|
||||
formData.value.passportImage = shopInfo.value.passportImage || ''
|
||||
formData.value.foodOperationCertificateImage = shopInfo.value.foodOperationCertificateImage || ''
|
||||
handleShowTypePicker(false)
|
||||
}
|
||||
|
||||
// 表单数据
|
||||
const formData = ref({
|
||||
shopName: "",
|
||||
shopTypeId: "", // 所属类型(关联店铺类型表)
|
||||
category: "",
|
||||
street: "", // 街道
|
||||
city: "", // 市
|
||||
state: "", // 州
|
||||
detailAddress: "",
|
||||
description: "",
|
||||
idCardImage: "",
|
||||
passportImage: "",
|
||||
foodOperationCertificateImage: "",
|
||||
});
|
||||
|
||||
// 表单验证schema
|
||||
const FormSchema = z.object({
|
||||
shopName: z.string().min(1, {message: t("pages-user.store-settle-in.schema.storeName")}),
|
||||
category: z.string().min(1, {message: t("pages-user.store-settle-in.schema.category")}),
|
||||
detailAddress: z.string().min(1, {message: t("pages-user.store-settle-in.schema.detailAddress")}),
|
||||
description: z.string().min(1, {message: t("pages-user.store-settle-in.schema.description")}),
|
||||
// idCardImage: z.string().min(1, {message: t("pages-user.store-settle-in.schema.idCardFront")}),
|
||||
// passportImage: z.string().min(1, {message: t("pages-user.store-settle-in.schema.idCardBack")}),
|
||||
// foodOperationCertificateImage: z.string().min(1, {message: t("pages-user.store-settle-in.schema.businessLicense")}),
|
||||
});
|
||||
|
||||
const currentImageType = ref("");
|
||||
const chooseImageRef = ref();
|
||||
|
||||
// 状态
|
||||
const isSubmitting = ref(false);
|
||||
|
||||
// 表单验证
|
||||
function validateForm(): boolean {
|
||||
const validateResult = FormSchema.safeParse(formData.value);
|
||||
|
||||
if (!validateResult.success) {
|
||||
const fieldErrors = validateResult.error.flatten().fieldErrors;
|
||||
const firstError = R.values(fieldErrors)[0]?.[0];
|
||||
|
||||
if (firstError) {
|
||||
uni.showToast({
|
||||
title: firstError,
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
async function submitForm() {
|
||||
if (!validateForm()) return;
|
||||
|
||||
isSubmitting.value = true;
|
||||
|
||||
try {
|
||||
console.log("提交数据:", formData.value);
|
||||
appShopSettlementApplyPost({
|
||||
body: {
|
||||
...formData.value,
|
||||
}
|
||||
}).then(res => {
|
||||
uni.showToast({
|
||||
title: t("toast.submitSuccess"),
|
||||
icon: "none",
|
||||
});
|
||||
auditStatus.value = 1
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("提交失败:", error);
|
||||
uni.showToast({
|
||||
title: "提交失败,请重试",
|
||||
icon: "none",
|
||||
});
|
||||
} finally {
|
||||
isSubmitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 防抖提交
|
||||
const handleSubmit = debounce(Config.debounceLongTime, submitForm, {
|
||||
atBegin: true,
|
||||
});
|
||||
|
||||
// 选择图片 - 添加安全检查
|
||||
function chooseImage(type: string) {
|
||||
currentImageType.value = type;
|
||||
if (chooseImageRef.value?.init) {
|
||||
chooseImageRef.value.init();
|
||||
}
|
||||
}
|
||||
|
||||
// 图片选择回调 - 修复类型问题
|
||||
function onImageChange(images: string[]) {
|
||||
if (images.length > 0 && currentImageType.value) {
|
||||
// 使用类型断言确保类型安全
|
||||
(formData.value as any)[currentImageType.value] = images[0];
|
||||
}
|
||||
}
|
||||
|
||||
const show = ref(false);
|
||||
|
||||
function handleClose() {
|
||||
show.value = false;
|
||||
}
|
||||
|
||||
const columnsType = ref([])
|
||||
const typePickerValue = ref('')
|
||||
|
||||
function handleShowTypePicker(type: boolean) {
|
||||
appMerchantCategoryListGet({
|
||||
pageNum: 1,
|
||||
pageSize: 100,
|
||||
}).then(res => {
|
||||
console.log(res)
|
||||
columnsType.value = res.data
|
||||
show.value = type;
|
||||
typePickerValue.value = columnsType.value[0].id
|
||||
|
||||
if (!type) {
|
||||
const data = res.data.find(item => item.id === formData.value.shopTypeId)
|
||||
formData.value.category = data.name
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleSubmitPickType() {
|
||||
console.log(typePickerValue.value)
|
||||
const data = columnsType.value.find(item => item.id === typePickerValue.value)
|
||||
if (data) {
|
||||
formData.value.shopTypeId = data.id
|
||||
formData.value.category = data.name
|
||||
}
|
||||
handleClose()
|
||||
}
|
||||
|
||||
|
||||
function handleAddressPicker() {
|
||||
uni.navigateTo({
|
||||
url: '/pages-user/pages/search-address/index',
|
||||
})
|
||||
}
|
||||
|
||||
useEventEmit(EventEnum.CHOOSE_ADDRESS, (data) => {
|
||||
console.log('接收到的地址信息', data)
|
||||
|
||||
// 解析地址信息并填充表单
|
||||
if (data && data.addressComponents) {
|
||||
// 重置地址相关字段
|
||||
formData.value.street = ""
|
||||
formData.value.city = ""
|
||||
formData.value.state = ""
|
||||
|
||||
// 遍历地址组件,根据类型填充对应字段
|
||||
data.addressComponents.forEach((component: any) => {
|
||||
const types = component.types || []
|
||||
|
||||
// 街道号码 (street_number)
|
||||
if (types.includes('street_number')) {
|
||||
formData.value.street = component.longText + ' ' + formData.value.street
|
||||
}
|
||||
|
||||
// 街道名称 (route)
|
||||
if (types.includes('route')) {
|
||||
formData.value.street = (formData.value.street + ' ' + component.longText).trim()
|
||||
}
|
||||
|
||||
// 子区域 (sublocality_level_1) - 可作为街道的一部分
|
||||
if (types.includes('sublocality_level_1') && !formData.value.street) {
|
||||
formData.value.street = component.longText
|
||||
}
|
||||
|
||||
// 城市 (locality)
|
||||
if (types.includes('locality')) {
|
||||
formData.value.city = component.longText
|
||||
}
|
||||
|
||||
// 如果没有locality,使用administrative_area_level_3作为城市
|
||||
if (types.includes('administrative_area_level_3') && !formData.value.city) {
|
||||
formData.value.city = component.longText
|
||||
}
|
||||
|
||||
// 州/省 (administrative_area_level_1)
|
||||
if (types.includes('administrative_area_level_1')) {
|
||||
formData.value.state = component.shortText || component.longText
|
||||
}
|
||||
})
|
||||
|
||||
// 如果没有解析到街道信息,使用格式化地址的第一部分
|
||||
if (!formData.value.street && data.formattedAddress) {
|
||||
const addressParts = data.formattedAddress.split(', ')
|
||||
if (addressParts.length > 0) {
|
||||
formData.value.street = addressParts[0]
|
||||
}
|
||||
}
|
||||
|
||||
// 设置详细地址为完整的格式化地址
|
||||
if (data.formattedAddress) {
|
||||
formData.value.detailAddress = data.formattedAddress
|
||||
}
|
||||
|
||||
console.log('解析后的地址信息:', {
|
||||
street: formData.value.street,
|
||||
city: formData.value.city,
|
||||
state: formData.value.state,
|
||||
detailAddress: formData.value.detailAddress
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 删除图片
|
||||
function removeImage(type: string) {
|
||||
if (type && formData.value.hasOwnProperty(type)) {
|
||||
(formData.value as any)[type] = "";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<view class="">
|
||||
<navbar customClass="!bg-transparent"/>
|
||||
<view class="px-30rpx mt-18rpx mb-44rpx">
|
||||
<view class="text-56rpx lh-56rpx text-#333 font-bold">{{ t("pages-user.store-settle-in.title") }}</view>
|
||||
<view class="text-28rpx text-#666 lh-28rpx mt-20rpx pr-240rpx"
|
||||
>{{ t("pages-user.store-settle-in.desc") }}
|
||||
</view
|
||||
>
|
||||
</view>
|
||||
<view class="px-30rpx relative mb-30rpx z-1">
|
||||
<image
|
||||
class="absolute top--200rpx right-20rpx z-9 w-256rpx h-213rpx"
|
||||
src="@img/chef/109.png"
|
||||
></image>
|
||||
<view class="bg-white rounded-16rpx px-30rpx pb-46rpx relative z-10">
|
||||
<template v-if="auditStatus === null">
|
||||
<view class="border-bottom py-32rpx flex-center-sb">
|
||||
<view class="text-28rpx text-#333 w-140rpx">{{ t("pages-user.store-settle-in.storeName") }}:</view>
|
||||
<wd-input
|
||||
v-model.trim="formData.shopName"
|
||||
: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;"
|
||||
>
|
||||
</wd-input>
|
||||
</view>
|
||||
|
||||
<view class="border-bottom py-32rpx flex-center-sb" @click="handleShowTypePicker(true)">
|
||||
<view class="flex text-28rpx">
|
||||
<view class="text-#333 w-140rpx">{{ t("pages-user.store-settle-in.category") }}:</view>
|
||||
<template v-if="formData.category">
|
||||
<text class="text-#333">
|
||||
{{ formData.category }}
|
||||
</text>
|
||||
</template>
|
||||
<template v-else>
|
||||
<text class="text-#999">
|
||||
{{ t("common.select") }}
|
||||
</text>
|
||||
</template>
|
||||
</view>
|
||||
<image class="w-22rpx h-30rpx" src="@img/chef/100202.png"></image>
|
||||
</view>
|
||||
|
||||
<view class="border-bottom py-32rpx flex-center-sb" @click="handleAddressPicker">
|
||||
<view class="flex text-28rpx">
|
||||
<view class="text-#333 w-140rpx">{{ t("pages-user.store-settle-in.address") }}:</view>
|
||||
<template v-if="formData.street || formData.city || formData.state">
|
||||
<text class="text-#333 flex-1">
|
||||
{{ [formData.street, formData.city, formData.state].filter(Boolean).join(', ') }}
|
||||
</text>
|
||||
</template>
|
||||
<template v-else>
|
||||
<text class="text-#999">{{ t("common.select") }}</text>
|
||||
</template>
|
||||
</view>
|
||||
<image class="w-22rpx h-30rpx" src="@img/chef/100202.png"></image>
|
||||
</view>
|
||||
|
||||
<view class="border-bottom py-32rpx flex-center-sb">
|
||||
<view class="text-28rpx text-#333 w-140rpx">{{ t("pages-user.store-settle-in.detailedAddress") }}:</view>
|
||||
<wd-input
|
||||
v-model.trim="formData.detailAddress"
|
||||
: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;"
|
||||
>
|
||||
</wd-input>
|
||||
</view>
|
||||
|
||||
<view class="border-bottom py-32rpx">
|
||||
<view class="text-28rpx text-#333 mb-24rpx">{{ t("pages-user.store-settle-in.introduction") }}:</view>
|
||||
<view
|
||||
class="min-h-226rpx box-border bg-#F6F6F6 rounded-36rpx overflow-hidden px-18rpx py-10rpx"
|
||||
>
|
||||
<wd-textarea
|
||||
v-model="formData.description"
|
||||
:maxlength="500"
|
||||
:placeholder="t('components.placeholder')"
|
||||
auto-height
|
||||
custom-class="!bg-#F6F6F6"
|
||||
custom-textarea-container-class="!bg-#F6F6F6"
|
||||
no-border
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="border-bottom py-30rpx">
|
||||
<view class="text-28rpx text-#333 mb-24rpx">{{ t("pages-user.store-settle-in.idCardFront") }}:</view>
|
||||
<view class="flex-center-sb">
|
||||
<view class="relative" @click="chooseImage('idCardImage')">
|
||||
<image
|
||||
v-if="formData.idCardImage"
|
||||
class="absolute top--10rpx right--10rpx z-10 w-36rpx h-36rpx"
|
||||
src="@img/chef/113.png"
|
||||
@click.stop="removeImage('idCardImage')"
|
||||
></image>
|
||||
<image
|
||||
v-if="!formData.idCardImage"
|
||||
class="w-276rpx h-188rpx"
|
||||
src="@img/chef/110.png"
|
||||
></image>
|
||||
<image
|
||||
v-else
|
||||
:src="formData.idCardImage"
|
||||
class="w-276rpx h-188rpx rounded-16rpx"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
</view>
|
||||
<view class="relative" @click="chooseImage('passportImage')">
|
||||
<image
|
||||
v-if="formData.passportImage"
|
||||
class="absolute top--10rpx right--10rpx z-10 w-36rpx h-36rpx"
|
||||
src="@img/chef/113.png"
|
||||
@click.stop="removeImage('passportImage')"
|
||||
></image>
|
||||
<image
|
||||
v-if="!formData.passportImage"
|
||||
class="w-276rpx h-188rpx"
|
||||
src="@img/chef/111.png"
|
||||
></image>
|
||||
<image
|
||||
v-else
|
||||
:src="formData.passportImage"
|
||||
class="w-276rpx h-188rpx rounded-16rpx"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view>
|
||||
<view class="text-28rpx text-#333 mt-32rpx mb-24rpx">{{
|
||||
t("pages-user.store-settle-in.businessLicense")
|
||||
}}:
|
||||
</view>
|
||||
<view class="relative w-160rpx h-160rpx" @click="chooseImage('foodOperationCertificateImage')">
|
||||
<image
|
||||
v-if="formData.foodOperationCertificateImage"
|
||||
class="absolute top--10rpx right--10rpx z-10 w-36rpx h-36rpx"
|
||||
src="@img/chef/113.png"
|
||||
@click.stop="removeImage('foodOperationCertificateImage')"
|
||||
></image>
|
||||
<image
|
||||
v-if="!formData.foodOperationCertificateImage"
|
||||
class="w-160rpx h-160rpx"
|
||||
src="@img/chef/112.png"
|
||||
></image>
|
||||
<image
|
||||
v-else
|
||||
:src="formData.foodOperationCertificateImage"
|
||||
class="w-160rpx h-160rpx rounded-16rpx"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<view v-else class="center flex-col h-798rpx">
|
||||
<template v-if="auditStatus === 1">
|
||||
<image
|
||||
class="w-98rpx h-98rpx"
|
||||
src="@img/chef/1109.png"
|
||||
></image>
|
||||
<view class="text-42rpx lh-42rpx text-#333 font-500 mt-26rpx mb-18rpx">
|
||||
{{ t("pages-user.store-settle-in.audit.submitted") }}
|
||||
</view>
|
||||
<view class="text-28rpx lh-28rpx text-#868686 text-center">
|
||||
{{ t("pages-user.store-settle-in.audit.submittedDesc") }}
|
||||
</view>
|
||||
</template>
|
||||
<template v-else-if="auditStatus === 2">
|
||||
<image
|
||||
class="w-98rpx h-98rpx"
|
||||
src="@img/chef/1108.png"
|
||||
></image>
|
||||
<view class="text-42rpx lh-42rpx text-#333 font-500 mt-26rpx mb-18rpx">
|
||||
{{ t("pages-user.store-settle-in.audit.pass") }}
|
||||
</view>
|
||||
<view class="text-28rpx lh-28rpx text-#868686 text-center">
|
||||
{{ t("pages-user.store-settle-in.audit.passDesc") }}
|
||||
<br>
|
||||
<text class="mt-22rpx block">{{ t("pages-user.store-settle-in.audit.passDesc1") }}~</text>
|
||||
</view>
|
||||
</template>
|
||||
<template v-else-if="auditStatus === 3">
|
||||
<image
|
||||
class="w-98rpx h-98rpx"
|
||||
src="@img/chef/1110.png"
|
||||
></image>
|
||||
<view class="text-42rpx lh-42rpx text-#333 font-500 mt-26rpx mb-18rpx">
|
||||
{{ t("pages-user.store-settle-in.audit.reject") }}
|
||||
</view>
|
||||
<view class="text-28rpx lh-28rpx text-#868686">{{
|
||||
t("pages-user.store-settle-in.audit.rejectDesc")
|
||||
}}:{{ rejectReason }}
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<template v-if="auditStatus === null">
|
||||
<fixed-bottom-large-btn
|
||||
:text="`${t('common.submit')}`"
|
||||
class="z-100"
|
||||
fixed
|
||||
@click="handleSubmit"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="auditStatus === 3">
|
||||
<!--如果是走新增进来的且Id不存在-->
|
||||
<fixed-bottom-large-btn
|
||||
:text="`${t('common.reEdit')}`"
|
||||
class="z-100"
|
||||
fixed
|
||||
@click="clickInitEdit"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<ChooseImage ref="chooseImageRef" @change="onImageChange"/>
|
||||
|
||||
|
||||
<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.placeholder.pleaseSelect') }}</view>
|
||||
<view class="text-30rpx text-#FF6106" @click="handleSubmitPickType">{{ t('common.confirm') }}</view>
|
||||
</view>
|
||||
<view class="bg-#fff px-54rpx py-56rpx">
|
||||
<wd-picker-view v-model="typePickerValue" :columns="columnsType" label-key="name" value-key="id"/>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</view>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.form-item {
|
||||
@apply flex items-start py-30rpx border-b border-#f0f0f0;
|
||||
|
||||
&:last-child {
|
||||
@apply border-b-0;
|
||||
}
|
||||
}
|
||||
|
||||
.form-label {
|
||||
@apply text-28rpx text-primary font-medium w-160rpx flex-shrink-0;
|
||||
}
|
||||
|
||||
.form-input-wrapper {
|
||||
@apply flex-1 flex items-center justify-between;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
@apply flex-1 text-28rpx text-primary;
|
||||
}
|
||||
|
||||
.form-textarea-wrapper {
|
||||
@apply flex-1;
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
@apply w-full min-h-200rpx text-28rpx text-primary leading-40rpx p-20rpx bg-#f8f8f8 rounded-20rpx;
|
||||
}
|
||||
|
||||
.form-upload-wrapper {
|
||||
@apply flex-1 flex gap-20rpx;
|
||||
}
|
||||
|
||||
.upload-item {
|
||||
@apply w-160rpx h-120rpx border-2rpx border-dashed border-#ddd rounded-20rpx flex items-center justify-center bg-#f8f8f8;
|
||||
}
|
||||
|
||||
.upload-image {
|
||||
@apply w-full h-full rounded-20rpx;
|
||||
}
|
||||
|
||||
.upload-placeholder {
|
||||
@apply flex items-center justify-center w-full h-full;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
@apply w-full h-88rpx bg-#333 text-white text-32rpx font-medium rounded-44rpx;
|
||||
|
||||
&:disabled {
|
||||
@apply bg-#ccc;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
: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,51 @@
|
||||
<script lang="ts" setup>
|
||||
import {EventEnum} from '@/constant/enums'
|
||||
|
||||
const {t} = useI18n();
|
||||
const avatarUrl = ref('')
|
||||
const show = ref(true)
|
||||
|
||||
onLoad((options) => {
|
||||
if (options.data) {
|
||||
avatarUrl.value = decodeURIComponent(options.data)
|
||||
}
|
||||
})
|
||||
|
||||
function handleConfirm(event: { tempFilePath: string }) {
|
||||
console.log('确认', event)
|
||||
const {tempFilePath} = event
|
||||
uni.navigateBack()
|
||||
setTimeout(() => {
|
||||
uni.$emit(EventEnum.CROPPER_AVATAR, tempFilePath)
|
||||
}, 10)
|
||||
}
|
||||
|
||||
function imgLoadError(res: any) {
|
||||
console.log('加载失败', res)
|
||||
uni.showToast({title: '图片加载失败'})
|
||||
}
|
||||
|
||||
function imgLoaded() {
|
||||
console.log('加载成功')
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view>
|
||||
<navbar :title="t('common.cuttingImage')"/>
|
||||
<wd-img-cropper
|
||||
v-model="show"
|
||||
:img-src="avatarUrl"
|
||||
@cancel="handleCancel"
|
||||
@confirm="handleConfirm"
|
||||
@imgloaded="imgLoaded"
|
||||
@imgloaderror="imgLoadError"
|
||||
></wd-img-cropper>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,151 @@
|
||||
<script setup lang="ts">
|
||||
import Config from '@/config'
|
||||
import {useUserStore} from '@/store'
|
||||
import type chooseImageVue from '@/components/choose-image/choose-image.vue'
|
||||
import { appUserEditUserInfoPost } from '@/service'
|
||||
import {debounce} from 'throttle-debounce';
|
||||
import {upload} from '@/utils/upload/alioss'
|
||||
import {EventEnum} from "@/constant/enums";
|
||||
|
||||
const userStore = useUserStore()
|
||||
const {t} = useI18n()
|
||||
|
||||
|
||||
const chooseImageRef = ref<InstanceType<typeof chooseImageVue> | null>(null)
|
||||
const form = ref({
|
||||
avatar: userStore.userInfo?.avatar,
|
||||
})
|
||||
|
||||
|
||||
watch(
|
||||
() => userStore.userInfo,
|
||||
(newValue) => {
|
||||
console.log('userInfo', newValue)
|
||||
form.value = {
|
||||
avatar: newValue?.avatar || '',
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
},
|
||||
)
|
||||
|
||||
async function handleCropperAvatarSuccess(avatarUrl: string) {
|
||||
try {
|
||||
await uni.showLoading({
|
||||
title: t('common.prompt.up-cross'),
|
||||
mask: true,
|
||||
})
|
||||
const res = await upload(avatarUrl, '.png', 'app/avatar/')
|
||||
console.log('111111', res)
|
||||
form.value.avatar = res as string
|
||||
}finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑用户信息
|
||||
async function submit() {
|
||||
try {
|
||||
await appUserEditUserInfoPost({
|
||||
body: {
|
||||
avatar: form.value.avatar,
|
||||
}
|
||||
})
|
||||
await uni.showToast({title: t('common.prompt.save-successfully'), icon: 'none'})
|
||||
await userStore.getUserInfo()
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = debounce(Config.debounceLongTime, submit, {atBegin: true})
|
||||
|
||||
function handleAddImg() {
|
||||
chooseImageRef.value?.init()
|
||||
}
|
||||
|
||||
function handleChooseImageChange(event: string[]) {
|
||||
if (!Array.isArray(event)) {
|
||||
return
|
||||
}
|
||||
|
||||
navigateTo(`/pages-user/pages/user-info/cropper-avatar?data=${encodeURIComponent(event[0])}`)
|
||||
}
|
||||
|
||||
function handlePreviewImage() {
|
||||
chooseImageRef.value?.close()
|
||||
uni.previewImage({
|
||||
url: form.value.avatar,
|
||||
urls: [form.value.avatar as string],
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
function navigateTo(url: string) {
|
||||
uni.navigateTo({url})
|
||||
}
|
||||
|
||||
onLoad(async () => {
|
||||
uni.$on(EventEnum.CROPPER_AVATAR, handleCropperAvatarSuccess)
|
||||
})
|
||||
|
||||
onUnload(() => {
|
||||
uni.$off(EventEnum.CROPPER_AVATAR, handleCropperAvatarSuccess)
|
||||
})
|
||||
|
||||
onBackPress(() => {
|
||||
if (chooseImageRef.value?.show) {
|
||||
chooseImageRef.value.close()
|
||||
return true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<choose-image :isUpload="false" ref="chooseImageRef" @change="handleChooseImageChange">
|
||||
<template #top>
|
||||
<view class="p-[32rpx+20rpx] text-34rpx text-primary text-center font-bold" @click="handlePreviewImage">
|
||||
<text>{{ t('pages-user.user-info.view-larger-image') }}</text>
|
||||
</view>
|
||||
</template>
|
||||
</choose-image>
|
||||
<view class="h-full flex flex-col">
|
||||
<navbar :title="t('navbar-personal-information')"/>
|
||||
<view class="bg-#fff">
|
||||
<view class="p-18rpx flex items-center justify-between border-bottom">
|
||||
<view class="shrink-0 mr-20rpx text-30rpx text-primary">{{ t('pages-user.user-info.head-portrait') }}</view>
|
||||
<view class="flex items-center" @click="handleAddImg">
|
||||
<image :src="form.avatar" class="shrink-0 w-80rpx h-80rpx rounded-full"></image>
|
||||
<image
|
||||
src="@img/chef/100202.png"
|
||||
class="shrink-0 ml-16rpx w-22rpx h-30rpx"
|
||||
></image>
|
||||
</view>
|
||||
</view>
|
||||
<view class="px-18rpx py-37rpx flex items-center justify-between"
|
||||
@click="navigateTo('/pages-user/pages/edit-nickname/index')">
|
||||
<view class="shrink-0 mr-20rpx text-30rpx text-primary">{{ t('pages-user.user-info.nickname') }}</view>
|
||||
<view class="flex-1 flex items-center justify-end text-30rpx text-primary">
|
||||
<text>{{ `${userStore.userInfo.firstName} ${userStore.userInfo.surname}` }}</text>
|
||||
<image
|
||||
src="@img/chef/100202.png"
|
||||
class="shrink-0 ml-16rpx w-22rpx h-30rpx"
|
||||
></image>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
<view class="mt-318rpx px-30rpx">
|
||||
<wd-button custom-class="!h-108rpx !text-36rpx font-bold !rounded-16rpx" block @click="handleSubmit">
|
||||
{{ t('common.save') }}
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
:deep(.wd-picker-view-column__item--active) {
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,358 @@
|
||||
export type RefereeUser = {
|
||||
id: string
|
||||
avatar?: string
|
||||
nickname?: string
|
||||
mobile?: string
|
||||
}
|
||||
|
||||
/** 会员套餐 */
|
||||
export interface MemberPackage{
|
||||
createBy: string;
|
||||
createTime: string;
|
||||
updateBy: string;
|
||||
updateTime: string;
|
||||
id: string;
|
||||
delFlag: string;
|
||||
rechargeAmount: string;
|
||||
memberLevel: string;
|
||||
memberDays: number;
|
||||
discountAmount: string;
|
||||
sort: string;
|
||||
payAmount: string;
|
||||
backgroundImage?: any;
|
||||
}
|
||||
|
||||
/** 充值详情 */
|
||||
export interface RechargeDetail{
|
||||
createBy: string;
|
||||
createTime: string;
|
||||
updateBy: string;
|
||||
updateTime: string;
|
||||
id: string;
|
||||
delFlag: number;
|
||||
userId: string;
|
||||
rechargeAmount: string;
|
||||
memberLevel: string;
|
||||
memberDays: number;
|
||||
discountAmount: string;
|
||||
sort?: any;
|
||||
redeemCode?: any;
|
||||
isRedeem: number;
|
||||
rechargeType: number;
|
||||
byUserId?: any;
|
||||
voucher: string;
|
||||
auditStatus: number;
|
||||
failReason?: any;
|
||||
payTaxAmount: string;
|
||||
payAmount: string;
|
||||
backgroundImage?: any;
|
||||
memberExpireTime?: any;
|
||||
paynowQrCode?: any;
|
||||
}
|
||||
|
||||
export interface GoodsVo {
|
||||
createBeginTime?: any;
|
||||
createEndTime?: any;
|
||||
filterDisable: boolean;
|
||||
createBy: string;
|
||||
createTime: string;
|
||||
updateBy: string;
|
||||
updateTime: string;
|
||||
id: string;
|
||||
delFlag: number;
|
||||
coverImage: string;
|
||||
nameZh: string;
|
||||
nameEn: string;
|
||||
name?: any;
|
||||
goodsCategoryId: string;
|
||||
price: string;
|
||||
detailZh: string;
|
||||
detailEn: string;
|
||||
detail?: any;
|
||||
sizeChartZh: string;
|
||||
sizeChartEn: string;
|
||||
sizeChart?: any;
|
||||
isLike: number;
|
||||
associationGoodsIds?: any;
|
||||
type: number;
|
||||
payType: number;
|
||||
point?: any;
|
||||
sales: number;
|
||||
carouselImage: string;
|
||||
inventoryQuantity: number;
|
||||
goodsSpecVoList?: any;
|
||||
goodsCategoryVo?: any;
|
||||
associationGoodsVoList?: any;
|
||||
code: string;
|
||||
isCollect?: any;
|
||||
}
|
||||
|
||||
// 收藏商品
|
||||
export interface CollectGoods {
|
||||
createBeginTime?: any;
|
||||
createEndTime?: any;
|
||||
filterDisable: boolean;
|
||||
createBy?: any;
|
||||
createTime: string;
|
||||
updateBy?: any;
|
||||
updateTime?: any;
|
||||
id: string;
|
||||
userId: string;
|
||||
objectId: string;
|
||||
objectType: number;
|
||||
goodsVo: GoodsVo;
|
||||
store?: any;
|
||||
}
|
||||
|
||||
export interface OrderGoodsVo {
|
||||
createBeginTime?: any;
|
||||
createEndTime?: any;
|
||||
filterDisable: boolean;
|
||||
createBy: string;
|
||||
createTime: string;
|
||||
updateBy?: any;
|
||||
updateTime: string;
|
||||
id: string;
|
||||
delFlag: number;
|
||||
coverImage: string;
|
||||
nameZh: string;
|
||||
nameEn: string;
|
||||
name: string;
|
||||
price: string;
|
||||
orderId: string;
|
||||
buyNumber: number;
|
||||
point?: any;
|
||||
payPrice: string;
|
||||
refundAmount: string;
|
||||
payType: number;
|
||||
goodsId: string;
|
||||
specZh: string;
|
||||
specEn: string;
|
||||
spec: string;
|
||||
goodsType: number;
|
||||
goodsSpecId: string;
|
||||
orderNo: string;
|
||||
refundNumber: number;
|
||||
type: number;
|
||||
isEvaluate: number;
|
||||
}
|
||||
|
||||
|
||||
export interface StoreBusinessTimeVoLis {
|
||||
createBeginTime?: any;
|
||||
createEndTime?: any;
|
||||
filterDisable: boolean;
|
||||
createBy: string;
|
||||
createTime: string;
|
||||
updateBy?: any;
|
||||
updateTime: string;
|
||||
id: string;
|
||||
weekNums: string;
|
||||
timePeriod: string;
|
||||
storeId: string;
|
||||
sort?: any;
|
||||
}
|
||||
|
||||
export interface StoreVo {
|
||||
createBeginTime?: any;
|
||||
createEndTime?: any;
|
||||
filterDisable: boolean;
|
||||
createBy: string;
|
||||
createTime: string;
|
||||
updateBy?: any;
|
||||
updateTime: string;
|
||||
id: string;
|
||||
delFlag: number;
|
||||
coverImage: string;
|
||||
name: string;
|
||||
location: string;
|
||||
address: string;
|
||||
longitude: string;
|
||||
latitude: string;
|
||||
type: number;
|
||||
score: string;
|
||||
evaluationNum: number;
|
||||
contactNumber: string;
|
||||
inventoryAlert: number;
|
||||
introduction: string;
|
||||
introductionImages: string;
|
||||
carouselImages: string;
|
||||
clockLocation: string;
|
||||
clockLongitude: string;
|
||||
clockLatitude: string;
|
||||
clockRange: string;
|
||||
areaCode: string;
|
||||
storeBusinessTimeVoList: StoreBusinessTimeVoLis[];
|
||||
distance?: any;
|
||||
isCollectByLoginUser?: any;
|
||||
}
|
||||
|
||||
// 收藏门店
|
||||
export interface CollectStore {
|
||||
createBeginTime?: any;
|
||||
createEndTime?: any;
|
||||
filterDisable: boolean;
|
||||
createBy?: any;
|
||||
createTime: string;
|
||||
updateBy?: any;
|
||||
updateTime?: any;
|
||||
id: string;
|
||||
userId: string;
|
||||
objectId: string;
|
||||
objectType: number;
|
||||
goodsVo?: any;
|
||||
storeVo: StoreVo;
|
||||
}
|
||||
|
||||
export interface RefundOrderGoodsVoList {
|
||||
createBeginTime?: any;
|
||||
createEndTime?: any;
|
||||
filterDisable: boolean;
|
||||
createBy: string;
|
||||
createTime: string;
|
||||
updateBy?: any;
|
||||
updateTime: string;
|
||||
id: string;
|
||||
delFlag: number;
|
||||
refundOrderId: string;
|
||||
orderGoodsId: string;
|
||||
refundNumber: number;
|
||||
refundAmount: string;
|
||||
goodsId: string;
|
||||
goodsSpecId: string;
|
||||
orderGoodsVo: OrderGoodsVo;
|
||||
}
|
||||
|
||||
|
||||
// 退款订单
|
||||
export interface RefundedOrder {
|
||||
createBeginTime?: any;
|
||||
createEndTime?: any;
|
||||
filterDisable: boolean;
|
||||
createBy: string;
|
||||
createTime: string;
|
||||
updateBy?: any;
|
||||
updateTime: string;
|
||||
id: string;
|
||||
delFlag: number;
|
||||
orderId: string;
|
||||
userId: string;
|
||||
refundReason: string;
|
||||
refundInstruction: string;
|
||||
refundVoucher: string;
|
||||
refundStatus?: any;
|
||||
rejectReason?: any;
|
||||
refundOrderNo: string;
|
||||
orderNo: string;
|
||||
refundTime?: any;
|
||||
refundAmount: string;
|
||||
applyTime: string;
|
||||
auditStatus: number;
|
||||
refundOrderGoodsVoList: RefundOrderGoodsVoList[];
|
||||
merchantContact?: any;
|
||||
userVo?: any;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// 退款订单详情
|
||||
export interface RefundOrderDetail {
|
||||
createBeginTime?: any;
|
||||
createEndTime?: any;
|
||||
filterDisable: boolean;
|
||||
createBy: string;
|
||||
createTime: string;
|
||||
updateBy?: any;
|
||||
updateTime: string;
|
||||
id: string;
|
||||
delFlag: number;
|
||||
orderId: string;
|
||||
userId: string;
|
||||
refundReason: string;
|
||||
refundInstruction: string;
|
||||
refundVoucher: string;
|
||||
refundStatus?: any;
|
||||
rejectReason?: any;
|
||||
refundOrderNo: string;
|
||||
orderNo: string;
|
||||
refundTime?: any;
|
||||
refundAmount: string;
|
||||
applyTime: string;
|
||||
auditStatus: number;
|
||||
refundOrderGoodsVoList: RefundOrderGoodsVoList[];
|
||||
merchantContact: string;
|
||||
userVo?: any;
|
||||
}
|
||||
|
||||
// 商品
|
||||
export interface OrderGoodsVoList {
|
||||
createBeginTime?: any;
|
||||
createEndTime?: any;
|
||||
filterDisable: boolean;
|
||||
selected: boolean;
|
||||
createBy: string;
|
||||
createTime: string;
|
||||
updateBy?: any;
|
||||
updateTime: string;
|
||||
id: string;
|
||||
delFlag: number;
|
||||
coverImage: string;
|
||||
nameZh: string;
|
||||
nameEn: string;
|
||||
name: string;
|
||||
price: string;
|
||||
orderId: string;
|
||||
buyNumber: number;
|
||||
quantity: number;
|
||||
point?: any;
|
||||
payPrice: string;
|
||||
refundAmount: string;
|
||||
payType: number;
|
||||
goodsId: string;
|
||||
specZh: string;
|
||||
specEn: string;
|
||||
spec: string;
|
||||
goodsType: number;
|
||||
goodsSpecId: string;
|
||||
orderNo: string;
|
||||
refundNumber: number;
|
||||
type: number;
|
||||
images: string[];
|
||||
score: number;
|
||||
content: string,
|
||||
refundOrderGoodsVo: {
|
||||
createBeginTime?: any;
|
||||
createEndTime?: any;
|
||||
filterDisable: boolean;
|
||||
createBy: string;
|
||||
createTime: string;
|
||||
updateBy?: any;
|
||||
updateTime: string;
|
||||
id: string;
|
||||
delFlag: number;
|
||||
refundOrderId: string;
|
||||
orderGoodsId: string;
|
||||
refundNumber: number;
|
||||
refundAmount: string;
|
||||
goodsId: string;
|
||||
goodsSpecId: string;
|
||||
orderGoodsVo?: any;
|
||||
}
|
||||
}
|
||||
|
||||
// 信用卡
|
||||
export interface CreditCard {
|
||||
createBeginTime?: any;
|
||||
createEndTime?: any;
|
||||
filterDisable: boolean;
|
||||
createBy: string;
|
||||
createTime: string;
|
||||
updateBy?: any;
|
||||
updateTime: string;
|
||||
id: string;
|
||||
cardNumber: string;
|
||||
cardId: string;
|
||||
userId: string;
|
||||
isDefault?: any;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import {http} from '@/utils/http'
|
||||
import type {
|
||||
CollectGoods, CollectStore, CreditCard, MemberPackage,
|
||||
RechargeDetail,
|
||||
RefundedOrder, RefundOrderDetail,
|
||||
} from '@/pages-user/service/index.data'
|
||||
|
||||
/**
|
||||
* 用户信息
|
||||
*/
|
||||
|
||||
// 修改用户信息
|
||||
export const editUserInfo = (data: Record<string, any>) => http.post('/app/user/editUserInfo', data)
|
||||
// 设置支付密码
|
||||
export const setPayPwd = (data: Record<string, any>) => http.post('/app/user/setPayPwd', data)
|
||||
|
||||
// 修改支付密码
|
||||
export const editPayPwd = (data: Record<string, any>) => http.post('/app/user/editPayPwd', data)
|
||||
|
||||
// 忘记支付密码
|
||||
export const forgetPayPwd = (data: Record<string, any>) => http.post('/app/user/forgetPayPwd', data)
|
||||
|
||||
// 用户邀请列表
|
||||
export const getUserInviteList = (data: Record<string, any>, query: PageQuery) => http.post('/app/user/my/userInviteList', data, query)
|
||||
|
||||
|
||||
// 常见问题解答列表
|
||||
export const getFaqsList = (data: Record<string, any>, query: PageQuery) => http.post('/app/user/my/faqsList', data, query)
|
||||
|
||||
|
||||
|
||||
// 余额明细类型
|
||||
export const getAllBalanceDetailTypeApi = (userPort: any, data: Record<string, any>) => http.post(
|
||||
`/app/user/userBalanceDetail/allBalanceDetailType?userPort=${userPort}`,
|
||||
{}
|
||||
)
|
||||
// 获取州列表
|
||||
export const getStateListApi = () => http.post<Record<string, any>>('/app/continent/list')
|
||||
|
||||
// 充值余额
|
||||
export const rechargeBalanceApi = (data: Record<string, any>) => http.post('/app/userRechargeOrder/recharge', data)
|
||||
|
||||
// 查询首页未领取的非兑换码类型的商家优惠券
|
||||
export const getCouponReceiveListApi = () => http.post('/app/coupon/couponReceiveList')
|
||||
// 一键领取所有优惠券
|
||||
export const receiveAllCouponApi = () => http.post('/app/coupon/receiveAll')
|
||||
// 查询指定商家已领取和未领取的非对话吗优惠券
|
||||
export const getMerchantCouponReceiveListApi = (merchantId: string) => http.post(`/app/coupon/merchantCouponReceiveList/${merchantId}`)
|
||||
// 领取优惠券
|
||||
export const receiveCouponApi = (id: string) => http.post(`/app/coupon/receiveCoupon/${id}`)
|
||||
@@ -0,0 +1,26 @@
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
export const useLogicStore = defineStore('store-list-logic', () => {
|
||||
const placesList = ref([])
|
||||
|
||||
const searchLoading = ref(false)
|
||||
|
||||
const setPlacesList = (list: any) => {
|
||||
if (Array.isArray(list)) {
|
||||
placesList.value = list
|
||||
} else {
|
||||
console.error('setPlacesList: Expected an array, but received:', list);
|
||||
}
|
||||
}
|
||||
|
||||
function clearPlacesList() {
|
||||
placesList.value = []
|
||||
}
|
||||
|
||||
return {
|
||||
placesList,
|
||||
searchLoading,
|
||||
setPlacesList,
|
||||
clearPlacesList,
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user