first commit

This commit is contained in:
2026-02-26 09:32:03 +08:00
commit 36a8e4c51b
845 changed files with 116474 additions and 0 deletions
+168
View File
@@ -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>
+151
View File
@@ -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>
+225
View File
@@ -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>
+445
View File
@@ -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>
+49
View File
@@ -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>
+166
View File
@@ -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>
+166
View File
@@ -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&#45;&#45;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>
+98
View File
@@ -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>
+220
View File
@@ -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>
+148
View File
@@ -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>
+334
View File
@@ -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>
+104
View File
@@ -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>
+156
View File
@@ -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>
+392
View File
@@ -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>
+186
View File
@@ -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>
+149
View File
@@ -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>
+151
View File
@@ -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>
+358
View File
@@ -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;
}
+50
View File
@@ -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}`)
+26
View File
@@ -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,
}
})