初始化项目模板
This commit is contained in:
1 parent
80fd6a1d4b
commit
b3a69a1f27
69 files changed
+11088
No files matched your search
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<!-- 区域号码选择 -->
|
||||
<up-popup :show="isShow" round="20" closeable @close="close">
|
||||
<view class="container flex flex-col px-[20px]">
|
||||
<view class="title flex items-center justify-center mx-[15px]">
|
||||
<text class="txt text-[16px] py-[15px] font-bold">{{ t('areaCode') }}</text>
|
||||
</view>
|
||||
<view class="search-box my-[10px]">
|
||||
<up-search v-model="keyword" :showAction="false" :placeholder="t('areaCodePlace')" @change="handleKeywordChange"></up-search>
|
||||
</view>
|
||||
<!-- 主列表 -->
|
||||
<view class="index-list relative overflow-hidden">
|
||||
<scroll-view scroll-y :scroll-into-view="scrollIntoId" class="main-list h-[70vh] overflow-hidden" @scroll="handleScroll">
|
||||
<template v-if="isEmpty(keyword)">
|
||||
<view v-for="(group, index) in list" :key="index" :id="group.groupKey" class="group-container relative">
|
||||
<!-- 吸顶标题 -->
|
||||
<view class="group-title text-[14px] text-main py-[5px] sticky top-0 bg-white z-[999] sticky-title">
|
||||
{{ group.name }}
|
||||
</view>
|
||||
<view v-for="(item, idx) in group.data" :key="idx" class="list-item text-main text-[14px] py-[10px]" @click="handleSelect(item)">
|
||||
{{ `${item.name} (+${item.code})` }}
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<view v-else v-for="(item, idx) in filterList" :key="idx" class="list-item text-main text-[14px] py-[10px]" @click="handleSelect(item)">
|
||||
{{ `${item.name} (+${item.code})` }}
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 右侧索引 -->
|
||||
<view class="index-bar absolute top-0 right-0 py-[10px] gap-[10px] z-[9999]">
|
||||
<view
|
||||
v-for="(group, index) in list"
|
||||
:key="index"
|
||||
class="index-item text-center text-[12px] "
|
||||
:class="currentIntoId === group.groupKey ? 'bg-[#e61e30] rounded-full text-white' : 'text-[#e61e30]'"
|
||||
@click="handleIndexclick(group.groupKey)"
|
||||
>
|
||||
{{ group.groupKey }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</up-popup>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { nextTick, ref, watch } from 'vue';
|
||||
import { onLoad, onReady } from '@dcloudio/uni-app';
|
||||
import { useSystemInfo } from '@/hooks/useSystemInfo.js';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { isEmpty } from '@/common/common.js';
|
||||
import { apiWholeAreaCode } from '@/api/common.js';
|
||||
const { t, locale } = useI18n();
|
||||
|
||||
const { computedDomHeight } = useSystemInfo();
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
// 获取区域码数据
|
||||
const getAreaCodeList = async () => {
|
||||
apiWholeAreaCode({ type: '' }).then((res) => {
|
||||
if (res.success) {
|
||||
list.value = res.data.codeGroups;
|
||||
nextTick(() => {
|
||||
calculateGroupPositions();
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const isShow = ref(false);
|
||||
watch(
|
||||
() => props.show,
|
||||
(newVal) => {
|
||||
isShow.value = newVal;
|
||||
if (newVal) getAreaCodeList();
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
const emits = defineEmits(['close', 'select']);
|
||||
const close = () => emits('close');
|
||||
|
||||
const keyword = ref('');
|
||||
const filterList = ref([]);
|
||||
const handleKeywordChange = (e) => {
|
||||
if (!e) return (filterList.value = []);
|
||||
const result = [];
|
||||
for (let group of list.value) {
|
||||
for (let item of group.data) {
|
||||
const target = item.search?.replace(/\s+/g, '');
|
||||
if (target.includes(e)) {
|
||||
result.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
filterList.value = uniqueByCode(result, 'code');
|
||||
};
|
||||
|
||||
// 数组对象去重
|
||||
const uniqueByCode = (arr, field) => {
|
||||
const map = new Map(); // 用于存储已经出现过的 field
|
||||
return arr.filter((item) => {
|
||||
if (!map.has(item[field])) {
|
||||
map.set(item[field], true); // 将 field 添加到 Map 中
|
||||
return true; // 保留当前对象
|
||||
}
|
||||
return false; // 过滤掉重复对象
|
||||
});
|
||||
};
|
||||
|
||||
const handleSelect = (e) => {
|
||||
emits('select', e);
|
||||
};
|
||||
|
||||
// 数据
|
||||
const list = ref([]);
|
||||
|
||||
const scrollIntoId = ref(''); // 当前滚动到的锚点ID
|
||||
const currentIntoId = ref(''); // 当前的索引
|
||||
const groupPositions = ref([]); // 存储每个组的顶部位置
|
||||
|
||||
// 点击索引跳转
|
||||
const handleIndexclick = (title) => {
|
||||
keyword.value = '';
|
||||
scrollIntoId.value = title;
|
||||
currentIntoId.value = title;
|
||||
};
|
||||
|
||||
// 处理滚动事件
|
||||
const handleScroll = (event) => {
|
||||
const scrollTop = event.detail.scrollTop;
|
||||
// 根据滚动位置判断当前吸顶的标题
|
||||
for (let i = groupPositions.value.length - 1; i >= 0; i--) {
|
||||
if (scrollTop > groupPositions.value[i].top) {
|
||||
currentIntoId.value = groupPositions.value[i].title;
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 计算每个组的顶部位置
|
||||
const calculateGroupPositions = async () => {
|
||||
let heightList = [];
|
||||
for (let i = 0; i < list.value.length; i++) {
|
||||
const group = list.value[i];
|
||||
const data = await computedDomHeight(`#${group.groupKey}`);
|
||||
if (data) {
|
||||
heightList[i] = data.height || 0;
|
||||
groupPositions.value.push({
|
||||
title: group.groupKey,
|
||||
top: i > 0 ? heightList.slice(0, i).reduce((pre, cur) => cur + pre, 0) : 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -0,0 +1,91 @@
|
||||
<script setup>
|
||||
import { ref, watch, onMounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
const { t, locale } = useI18n();
|
||||
const emit = defineEmits(['confirm', 'close']);
|
||||
const props = defineProps({
|
||||
title: {
|
||||
// 選擇器標題
|
||||
type: String,
|
||||
default: () => {
|
||||
return '';
|
||||
},
|
||||
},
|
||||
show: {
|
||||
// 展示狀態
|
||||
type: Boolean,
|
||||
default: () => {
|
||||
return false;
|
||||
},
|
||||
},
|
||||
columns: {
|
||||
// 數組列表
|
||||
type: Array,
|
||||
default: () => {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
keyName: {
|
||||
type: String,
|
||||
default: () => {
|
||||
return 'name';
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const showPicker = ref(false); // 是否显示弹窗
|
||||
const columnList = ref([]); // 列表数据
|
||||
const onConfirm = (e) => {
|
||||
emit('confirm', e.value);
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
emit('close');
|
||||
};
|
||||
watch(
|
||||
() => props.show,
|
||||
(newVal) => {
|
||||
showPicker.value = newVal;
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
watch(
|
||||
() => props.columns,
|
||||
(newVal) => {
|
||||
// console.log(newVal);
|
||||
columnList.value.push(
|
||||
newVal.map((item) => {
|
||||
return {
|
||||
...item,
|
||||
};
|
||||
})
|
||||
);
|
||||
// console.log(columnList.value);
|
||||
},
|
||||
{
|
||||
// immediate: true,
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<!-- <up-popup :show="showPicker" :round="10" mode="bottom" :closeOnClickOverlay="true" @close="onCancel"> -->
|
||||
<up-picker
|
||||
:show="showPicker"
|
||||
:title="props.title"
|
||||
:confirmText="t('confirm')"
|
||||
:cancelText="t('cancel')"
|
||||
confirmColor="#274C40"
|
||||
:columns="columnList"
|
||||
:keyName="keyName"
|
||||
:closeOnClickOverlay="true"
|
||||
@confirm="onConfirm"
|
||||
@cancel="onCancel"
|
||||
@close="onCancel"
|
||||
></up-picker>
|
||||
<!-- </up-popup> -->
|
||||
</template>
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<view class="relative">
|
||||
<!-- 输入框触发 -->
|
||||
<input
|
||||
type="text"
|
||||
readonly
|
||||
:value="selectedDate ? selectedDate.format('YYYY-MM-DD') : '选择日期'"
|
||||
@click="showCalendar = !showCalendar"
|
||||
class="w-full px-4 py-2 border-2 border-[#9C032D] rounded-lg cursor-pointer bg-white"
|
||||
/>
|
||||
|
||||
<!-- 日历容器 -->
|
||||
<view
|
||||
v-if="showCalendar"
|
||||
class="fixed bottom-0 left-1/2 transform -translate-x-1/2 mb-4 w-[320px] bg-[#f8d7da] border-2 border-[#9C032D] rounded-lg p-4 z-50 shadow-xl"
|
||||
>
|
||||
<!-- 头部 -->
|
||||
<view class="flex justify-between items-center mb-4">
|
||||
<button @click="changeMonth(-1)" class="text-[#9C032D] hover:text-[#80022a] transition-colors">◀</button>
|
||||
<view class="font-bold text-[#9C032D]">
|
||||
{{ currentDate.format('YYYY年MM月') }}
|
||||
</view>
|
||||
<button @click="changeMonth(1)" class="text-[#9C032D] hover:text-[#80022a] transition-colors">▶</button>
|
||||
</view>
|
||||
|
||||
<!-- 星期 -->
|
||||
<view class="grid grid-cols-7 gap-1 mb-2">
|
||||
<view v-for="day in ['日', '一', '二', '三', '四', '五', '六']" :key="day" class="text-center text-sm text-[#9C032D] font-medium">
|
||||
{{ day }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 日期格子 -->
|
||||
<view class="grid grid-cols-7 gap-1">
|
||||
<view
|
||||
v-for="(date, index) in calendarDays"
|
||||
:key="index"
|
||||
@click="selectDate(date)"
|
||||
:class="[
|
||||
'text-center p-1 rounded cursor-pointer transition-colors',
|
||||
date.month() !== currentDate.month() ? 'text-gray-400' : 'text-[#9C032D]',
|
||||
date.isSame(selectedDate, 'day') ? 'bg-[#9C032D] text-white' : 'hover:bg-[#f5b5ba]',
|
||||
]"
|
||||
>
|
||||
{{ date.date() }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 确认按钮 -->
|
||||
<button @click="confirmDate" class="w-full mt-4 py-2 bg-[#9C032D] text-white rounded-lg hover:bg-[#80022a] transition-colors">确认</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: String, // 接收外部v-model
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'confirm']);
|
||||
|
||||
const showCalendar = ref(false);
|
||||
const currentDate = ref(dayjs());
|
||||
const selectedDate = ref(props.modelValue ? dayjs(props.modelValue) : null);
|
||||
|
||||
// 生成日历日期数组
|
||||
const calendarDays = computed(() => {
|
||||
const start = currentDate.value.startOf('month').startOf('week');
|
||||
const end = currentDate.value.endOf('month').endOf('week');
|
||||
const days = [];
|
||||
let day = start;
|
||||
|
||||
while (day <= end) {
|
||||
days.push(day);
|
||||
day = day.add(1, 'day');
|
||||
}
|
||||
return days;
|
||||
});
|
||||
|
||||
// 切换月份
|
||||
const changeMonth = (delta) => {
|
||||
currentDate.value = currentDate.value.add(delta, 'month');
|
||||
};
|
||||
|
||||
// 选择日期
|
||||
const selectDate = (date) => {
|
||||
selectedDate.value = date;
|
||||
};
|
||||
|
||||
// 确认选择
|
||||
const confirmDate = () => {
|
||||
showCalendar.value = false;
|
||||
emit('update:modelValue', selectedDate.value.format('YYYY-MM-DD'));
|
||||
emit('confirm', selectedDate.value.format('YYYY-MM-DD'));
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,214 @@
|
||||
<script setup>
|
||||
/**
|
||||
* @desc 通用布局组件
|
||||
*/
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
// import LoginPopup from './login-popup.vue';
|
||||
import { getSystemSafeArea, getSafeTopHeight, getSystemInfo, isEmpty } from '@/common/common.js';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { useSystemInfo } from '@/hooks/useSystemInfo.js';
|
||||
const { navHeight } = useSystemInfo();
|
||||
const props = defineProps({
|
||||
showBgImage: {
|
||||
// 是否展示导航栏
|
||||
type: Boolean,
|
||||
default: () => {
|
||||
return false;
|
||||
},
|
||||
},
|
||||
showNav: {
|
||||
// 是否展示导航栏
|
||||
type: Boolean,
|
||||
default: () => {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
navTitle: {
|
||||
//标题
|
||||
type: String,
|
||||
default: () => {
|
||||
return '';
|
||||
},
|
||||
},
|
||||
// navPlaceholder: {
|
||||
// //是否生成一个等高元素,防止塌陷
|
||||
// type: Boolean,
|
||||
// default: () => {
|
||||
// return false;
|
||||
// },
|
||||
// },
|
||||
navShowReturn: {
|
||||
//是否显示返回图标
|
||||
type: Boolean,
|
||||
default: () => {
|
||||
return false;
|
||||
},
|
||||
},
|
||||
navShowNavBar: {
|
||||
//是否显示更多图标
|
||||
type: Boolean,
|
||||
default: () => {
|
||||
return false;
|
||||
},
|
||||
},
|
||||
navColor: {
|
||||
//标题背景颜色
|
||||
type: String,
|
||||
default: () => {
|
||||
return '#fff';
|
||||
},
|
||||
},
|
||||
bgColor: {
|
||||
//标题字体、icon颜色
|
||||
type: String,
|
||||
default: () => {
|
||||
return '#fbfbfd';
|
||||
},
|
||||
},
|
||||
titleColor: {
|
||||
//标题字体、icon颜色
|
||||
type: String,
|
||||
default: () => {
|
||||
return '#1D1D1F';
|
||||
},
|
||||
},
|
||||
titleText: {
|
||||
// 顶部标题文字
|
||||
type: String,
|
||||
default: () => {
|
||||
return '';
|
||||
},
|
||||
},
|
||||
// showLogin: {
|
||||
// // 唤起登录
|
||||
// type: Boolean,
|
||||
// default: () => {
|
||||
// return false;
|
||||
// },
|
||||
// },
|
||||
});
|
||||
const emit = defineEmits(['getContentHeight', 'loginStatus']);
|
||||
const safeAreaInsetTop = ref(false); //是否开启顶部安全区适配
|
||||
const viewHeight = ref(0); //可用高度
|
||||
const pageHeight = ref(0); //页面高度
|
||||
const pageWidth = ref(getSystemInfo().windowWidth);
|
||||
const hasTabbar = ref(false);
|
||||
const user = useUserStore();
|
||||
const navPlaceholder = ref(false);
|
||||
// #ifndef MP-WEIXIN
|
||||
navPlaceholder.value = false;
|
||||
// #endif
|
||||
// #ifdef MP-WEIXIN
|
||||
navPlaceholder.value = true;
|
||||
// #endif
|
||||
|
||||
/**
|
||||
* @desc 返回上一页
|
||||
*/
|
||||
const returnBack = () => {
|
||||
uni.navigateBack(1);
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc 返回内容部分的高度
|
||||
*/
|
||||
const contentHeight = () => {
|
||||
emit('getContentHeight', {
|
||||
viewHeight: viewHeight.value,
|
||||
pageHeight: pageHeight.value,
|
||||
});
|
||||
};
|
||||
|
||||
const showSideNav = ref(false);
|
||||
// 关闭/打开侧边弹出层
|
||||
const operaSideNav = () => {
|
||||
showSideNav.value = !showSideNav.value;
|
||||
};
|
||||
|
||||
// 关闭登录弹窗
|
||||
// const onCloseLogin = () => {
|
||||
// const status = isEmpty(user.userToken); // true 为空,false为已登录
|
||||
// emit('loginStatus', status);
|
||||
// };
|
||||
|
||||
onMounted(() => {
|
||||
// #ifndef MP-ALIPAY
|
||||
safeAreaInsetTop.value = true;
|
||||
// #endif
|
||||
uni.showTabBar({
|
||||
success() {
|
||||
hasTabbar.value = true;
|
||||
},
|
||||
fail() {
|
||||
hasTabbar.value = false;
|
||||
},
|
||||
complete() {
|
||||
pageHeight.value = getSystemSafeArea();
|
||||
if (props.showNav) {
|
||||
viewHeight.value = pageHeight.value - getSafeTopHeight();
|
||||
} else {
|
||||
viewHeight.value = pageHeight.value;
|
||||
}
|
||||
contentHeight();
|
||||
},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="absolute left-[50%] right-0 top-0 translate-x-[-50%]" :style="{ height: `${pageHeight}px`, width: `${pageWidth}px` }">
|
||||
<up-navbar
|
||||
:title="props?.navTitle"
|
||||
:safeAreaInsetTop="safeAreaInsetTop"
|
||||
:placeholder="navPlaceholder"
|
||||
:bgColor="props.navColor"
|
||||
:titleStyle="{ color: props.titleColor }"
|
||||
:height="navPlaceholder ? '44px' : `${getSafeTopHeight()}px`"
|
||||
class="!flex-1"
|
||||
v-if="props.showNav"
|
||||
>
|
||||
<template #left>
|
||||
<view class="flex flex-row items-center gap-[10px]">
|
||||
<up-icon name="list-dot" :color="props.titleColor" size="26" @click="operaSideNav" v-if="props.navShowNavBar"></up-icon>
|
||||
<!-- #ifndef MP-BAIDU -->
|
||||
<up-icon name="arrow-left" :color="props.titleColor" size="24" @click="returnBack" v-if="props.navShowReturn"></up-icon>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</template>
|
||||
</up-navbar>
|
||||
<view
|
||||
class="relative left-0 right-0 top-0"
|
||||
:style="{
|
||||
height: `${viewHeight}px`,
|
||||
background: props.bgColor,
|
||||
top: navPlaceholder ? 0 : `${getSafeTopHeight()}px`,
|
||||
}"
|
||||
>
|
||||
<view v-if="showBgImage" class="w-full relative">
|
||||
<view
|
||||
class="w-full h-[calc(200px+var(--status-bar-height))] bg-cover bg-center bg-no-repeat"
|
||||
style="background-image: url('/static/test/top2.png')"
|
||||
></view>
|
||||
<text class="w-full text-[#fff] font-bold text-[20px] absolute left-0 right-0 bottom-[40px] text-center px-[40px]">
|
||||
{{ props.titleText }}
|
||||
</text>
|
||||
</view>
|
||||
<view
|
||||
class="absolute left-0 right-0 w-full overflow-y-auto"
|
||||
:style="{
|
||||
top: showBgImage ? 'calc(200px + var(--status-bar-height))' : props.showNav ? 0 : `${navHeight}px`,
|
||||
height: showBgImage ? `calc(100% - 200px - var(--status-bar-height))` : '100%',
|
||||
}"
|
||||
>
|
||||
<slot name="content" :row="{ viewHeight, pageHeight }"> </slot>
|
||||
</view>
|
||||
|
||||
|
||||
<!-- 登录 -->
|
||||
<!-- <LoginPopup :show="props.showLogin" @close="onCloseLogin"></LoginPopup> -->
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss"></style>
|
||||
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<up-popup :show="showPicker" :closeable="true" round="10" mode="bottom" @close="onClose">
|
||||
<view class="login">
|
||||
<view class="logo mb-[10rpx] mt-[50rpx] flex justify-center text-center">
|
||||
<image class="h-[140rpx] rounded-full" src="/static/common/logo.png" mode="heightFix"></image>
|
||||
</view>
|
||||
<Login @close="onClose"></Login>
|
||||
</view>
|
||||
</up-popup>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import Login from './login.vue';
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
// 展示狀態
|
||||
type: Boolean,
|
||||
default: () => {
|
||||
return false;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
const onClose = () => {
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const showPicker = ref(false);
|
||||
watch(
|
||||
() => props.show,
|
||||
(newVal) => {
|
||||
showPicker.value = newVal;
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,855 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import { areaCodeOptions, checkTelPhone, checkEmail, createV, showLoadingToast, closeToast, showToast, isEmpty } from '@/common/common.js';
|
||||
import { getLoginCode } from '@/common/login.js';
|
||||
import { login, register, sendCode, policyServer, loginLRInit } from '@/api/user.js';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import CommonPicker from './common-picker.vue';
|
||||
const { t, locale } = useI18n();
|
||||
const userStore = useUserStore();
|
||||
const isLogin = ref(true); // 是否登录,默认登录,为false表示注册
|
||||
import { uniLogin } from '@/common/login';
|
||||
const isMobile = ref(true); //是否手机号登录
|
||||
const isPassword = ref(false); //是否密码登录
|
||||
const isForgetPassword = ref(false); //是否忘记密码
|
||||
// const isDisable = ref(true); //是否登录注册按钮是否禁用,即是否通过校验
|
||||
const mobile = ref(''); // 手机号
|
||||
const areaCode = ref(''); // 地区码
|
||||
const code = ref(''); // 验证码
|
||||
const password = ref(''); // 密码
|
||||
const passwordConfirm = ref(''); // 确认密码
|
||||
const email = ref(''); // 邮箱
|
||||
const emit = defineEmits(['close']);
|
||||
const wxJsCode = ref({}); //wx授权码code
|
||||
const codeType = ref(''); // 区分code信息类型:=mpLogin(小程序中手机号+验证码登录);=mpMobile(小程序中授权获取手机号登录)
|
||||
|
||||
//同意条款相关
|
||||
const checked = ref(false);
|
||||
const onChange = (e) => {
|
||||
checked.value = !checked.value;
|
||||
};
|
||||
|
||||
//地区码弹框相关
|
||||
const columns = ref([]);
|
||||
const showCommonPicker = ref(false);
|
||||
const onConfirmCommonPicker = (data) => {
|
||||
areaCode.value = data[0].value;
|
||||
showCommonPicker.value = false;
|
||||
};
|
||||
const onCloseCommonPicker = () => {
|
||||
showCommonPicker.value = false;
|
||||
};
|
||||
|
||||
//导航手机号点击功能
|
||||
const onMobile = () => {
|
||||
isMobile.value = true;
|
||||
password.value = '';
|
||||
passwordConfirm.value = '';
|
||||
// isDisable.value = true;
|
||||
// console.log(isMobile.value);
|
||||
};
|
||||
//导航邮箱点击功能
|
||||
const onEmail = () => {
|
||||
isMobile.value = false;
|
||||
isPassword.value = true;
|
||||
password.value = '';
|
||||
passwordConfirm.value = '';
|
||||
// isDisable.value = true;
|
||||
};
|
||||
//去登录
|
||||
const toLogin = () => {
|
||||
isLogin.value = true;
|
||||
isMobile.value = true;
|
||||
isPassword.value = true;
|
||||
mobile.value = '';
|
||||
areaCode.value = '852';
|
||||
code.value = '';
|
||||
password.value = '';
|
||||
passwordConfirm.value = '';
|
||||
email.value = '';
|
||||
clearInterval(timerId2.value);
|
||||
};
|
||||
//去注册
|
||||
const toRegister = () => {
|
||||
isLogin.value = false;
|
||||
isMobile.value = true;
|
||||
isPassword.value = true;
|
||||
mobile.value = '';
|
||||
areaCode.value = '852';
|
||||
code.value = '';
|
||||
password.value = '';
|
||||
passwordConfirm.value = '';
|
||||
email.value = '';
|
||||
clearInterval(timerId1.value);
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc 登录
|
||||
*/
|
||||
const onLogin = () => {
|
||||
if (!checked.value) {
|
||||
uni.showToast({
|
||||
title: t('policyTips'),
|
||||
icon: 'none',
|
||||
duration: 2000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
//登录参数
|
||||
let params = {};
|
||||
|
||||
//小程序免验证登录,codeType.value = mpMobile时,表示本次登录为免验证登录,noVerify值为1
|
||||
if (codeType.value == 'mpMobile') {
|
||||
params.codeValue = JSON.stringify(wxJsCode.value); //微信端需要传递登录授权code
|
||||
params.noVerify = 1; // 免验证登录必传, codeType.value = mpMobile 时,为1表示本次登录为免验证登录
|
||||
} else {
|
||||
//手机号+验证码登录
|
||||
params.codeType = userStore.platform == 'h5' ? '' : codeType.value; // 小程序有效,仅在短信验证码登录时有值 mpLogin
|
||||
params.loginType = userStore.platform == 'h5' ? 'mobile' : ''; // h5环境下短信验证码登录必须要
|
||||
params.areaCode = isMobile.value ? areaCode.value : ''; // 区号
|
||||
params.mobile = isMobile.value ? mobile.value : ''; // 手机号
|
||||
params.verifyCode = code.value; // 验证码
|
||||
// #ifdef MP-WEIXIN || MP-ALIPAY
|
||||
params.codeValue = JSON.stringify(wxJsCode.value); //微信端需要传递登录授权code
|
||||
// #endif
|
||||
|
||||
//登录前的验证
|
||||
if (!LoginCheck()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(params);
|
||||
|
||||
try {
|
||||
uni.showLoading({
|
||||
mask: true,
|
||||
});
|
||||
login(params).then((res) => {
|
||||
console.log(res);
|
||||
uni.hideLoading();
|
||||
if (res.success) {
|
||||
userStore.userToken = res.data.token;
|
||||
userStore.isLogin = res.data.isLogin;
|
||||
userStore.setProfile({
|
||||
...res.data.userData,
|
||||
});
|
||||
emit('close');
|
||||
mobile.value = '';
|
||||
password.value = '';
|
||||
code.value = '';
|
||||
email.value = '';
|
||||
} else {
|
||||
//重新获取code
|
||||
// #ifdef MP-WEIXIN || MP-ALIPAY
|
||||
wxGetLoginCode();
|
||||
// #endif
|
||||
uni.showToast({
|
||||
title: res.message,
|
||||
icon: 'none',
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
//重新获取code
|
||||
// #ifdef MP-WEIXIN || MP-ALIPAY
|
||||
wxGetLoginCode();
|
||||
// #endif
|
||||
uni.showToast({
|
||||
title: error.message,
|
||||
icon: 'none',
|
||||
});
|
||||
} finally {
|
||||
}
|
||||
};
|
||||
//登录校验
|
||||
const LoginCheck = (val) => {
|
||||
if (isMobile.value) {
|
||||
//手机号登录
|
||||
if (!checkTelPhone(areaCode.value, mobile.value)) {
|
||||
uni.showToast({
|
||||
title: t('personal.mobileTips'),
|
||||
icon: 'none',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
if (isPassword.value) {
|
||||
//密码登录
|
||||
if (password.value == '' || password.value == null) {
|
||||
uni.showToast({
|
||||
title: '请输入密码',
|
||||
icon: 'none',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
//验证码登录
|
||||
if (code.value == '' || code.value == null) {
|
||||
uni.showToast({
|
||||
title: t('personal.validatePlaceholder'),
|
||||
icon: 'none',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
//邮箱登录
|
||||
if (!checkEmail(email.value)) {
|
||||
uni.showToast({
|
||||
title: '请输入正确的电子邮箱',
|
||||
icon: 'none',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
if (password.value == '' || password.value == null) {
|
||||
uni.showToast({
|
||||
title: '请输入密码',
|
||||
icon: 'none',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
//注册
|
||||
const onRegister = () => {
|
||||
if (!checked.value) {
|
||||
uni.showToast({
|
||||
title: t('policyTips'),
|
||||
icon: 'none',
|
||||
duration: 2000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let params = {
|
||||
regType: isMobile.value ? '1' : '2', //1手機號 2郵箱
|
||||
areaCode: isMobile.value ? areaCode.value : '',
|
||||
mobile: isMobile.value ? mobile.value : '',
|
||||
password: isPassword.value ? password.value : '',
|
||||
passwordConfirm: isPassword.value ? passwordConfirm.value : '',
|
||||
code: isMobile.value ? code.value : '',
|
||||
email: !isMobile.value ? email.value : '',
|
||||
// #ifdef MP-WEIXIN || MP-ALIPAY
|
||||
wxJsCode: wxJsCode.value, //微信端需要传递登录授权code
|
||||
// #endif
|
||||
};
|
||||
console.log(params);
|
||||
if (!RegisterCheck()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
uni.showLoading({
|
||||
mask: true,
|
||||
});
|
||||
//注册api
|
||||
register(params).then((res) => {
|
||||
uni.hideLoading();
|
||||
if (res.success) {
|
||||
userStore.userToken = res.data.token;
|
||||
userStore.setProfile({
|
||||
...res.data.user,
|
||||
});
|
||||
emit('close');
|
||||
|
||||
mobile.value = '';
|
||||
password.value = '';
|
||||
passwordConfirm.value = '';
|
||||
code.value = '';
|
||||
email.value = '';
|
||||
} else {
|
||||
//重新获取code
|
||||
// #ifdef MP-WEIXIN || MP-ALIPAY
|
||||
wxGetLoginCode();
|
||||
// #endif
|
||||
uni.showToast({
|
||||
title: res.message,
|
||||
icon: 'none',
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
//重新获取code
|
||||
// #ifdef MP-WEIXIN || MP-ALIPAY
|
||||
wxGetLoginCode();
|
||||
// #endif
|
||||
uni.showToast({
|
||||
title: error.message,
|
||||
icon: 'none',
|
||||
});
|
||||
} finally {
|
||||
}
|
||||
};
|
||||
//注册校验
|
||||
const RegisterCheck = (val) => {
|
||||
if (isMobile.value) {
|
||||
//手机号注册
|
||||
if (!checkTelPhone(areaCode.value, mobile.value)) {
|
||||
uni.showToast({
|
||||
title: '请输入正确的地区号和手机号',
|
||||
icon: 'none',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
if (code.value == '' || code.value == null) {
|
||||
uni.showToast({
|
||||
title: '请输入验证码',
|
||||
icon: 'none',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
//邮箱注册
|
||||
if (!checkEmail(email.value)) {
|
||||
uni.showToast({
|
||||
title: '请输入正确的电子邮箱',
|
||||
icon: 'none',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (password.value == '' || password.value == null) {
|
||||
uni.showToast({
|
||||
title: '请输入密码',
|
||||
icon: 'none',
|
||||
});
|
||||
return false;
|
||||
} else {
|
||||
if (password.value?.length < 6 || password.value?.length > 20) {
|
||||
uni.showToast({
|
||||
title: '密碼長度應為6-20位',
|
||||
icon: 'none',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (passwordConfirm.value == '' || passwordConfirm.value == null) {
|
||||
uni.showToast({
|
||||
title: '請再次輸入密碼',
|
||||
icon: 'none',
|
||||
});
|
||||
return false;
|
||||
} else {
|
||||
if (password.value != passwordConfirm.value) {
|
||||
uni.showToast({
|
||||
title: '兩次密碼不一樣',
|
||||
icon: 'none',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const dataV = ref(''); //加密数据
|
||||
const isCanWxLogin = ref(false); // 允许国内公司快速获取手机号登录
|
||||
/**
|
||||
* 初始化,获取data.v
|
||||
*/
|
||||
const initMobile = async () => {
|
||||
try {
|
||||
await loginLRInit().then((res) => {
|
||||
if (res.success) {
|
||||
areaCode.value = res.data.areaCodeList[0].value;
|
||||
isCanWxLogin.value = res.data.isAllowMpMobile; // 允许国内公司快速获取手机号登录
|
||||
res.data.areaCodeList.forEach((item, index) => {
|
||||
columns.value[index] = {
|
||||
value: item.value,
|
||||
name: item.label,
|
||||
};
|
||||
});
|
||||
dataV.value = res.data.v;
|
||||
console.log(dataV.value);
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: res.message,
|
||||
icon: 'none',
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
uni.showToast({
|
||||
title: error.message,
|
||||
icon: 'none',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const timerId1 = ref(null); // 定时器1(登录)
|
||||
const timerId2 = ref(null); // 定时器2(注册)
|
||||
const countDown1 = ref(60); // 倒计时1(登录)
|
||||
const allowSendCode1 = ref(true); // 允许发送验证码1(登录)
|
||||
const countDown2 = ref(60); // 倒计时2(注册)
|
||||
const allowSendCode2 = ref(true); // 允许发送验证码2(注册)
|
||||
//获取验证码
|
||||
const getValifyCode = () => {
|
||||
//手机号注册
|
||||
if (!checkTelPhone(areaCode.value, mobile.value)) {
|
||||
uni.showToast({
|
||||
title: t('personal.mobileTips'),
|
||||
icon: 'none',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
let account = areaCode.value + ',' + mobile.value;
|
||||
//加密data.v
|
||||
let postV;
|
||||
if (isLogin.value) {
|
||||
postV = createV(dataV.value, account);
|
||||
} else {
|
||||
postV = createV(dataV.value, account);
|
||||
}
|
||||
|
||||
let params = {
|
||||
// account: isMobile.value ? account : '', //1手機號 2郵箱
|
||||
// type: isMobile.value ? '1' : '2', //類型:1 手機號 2 郵箱
|
||||
// purpose: !isLogin.value ? '1' : !isForgetPassword.value ? '2' : '3', //目的:1註冊 2登錄 3忘記密碼
|
||||
areaCode: areaCode.value,
|
||||
mobile: mobile.value,
|
||||
v: postV, //加密数据
|
||||
};
|
||||
try {
|
||||
// console.log(params);
|
||||
uni.showLoading({
|
||||
mask: true,
|
||||
});
|
||||
sendCode(params).then((res) => {
|
||||
uni.hideLoading();
|
||||
if (res.success) {
|
||||
uni.showToast({
|
||||
title: t('personal.resendSuccess'),
|
||||
icon: 'none',
|
||||
});
|
||||
|
||||
// #ifdef MP
|
||||
codeType.value = 'mpLogin';
|
||||
// #endif
|
||||
|
||||
if (isLogin.value) {
|
||||
allowSendCode1.value = false;
|
||||
timerId1.value = setInterval(() => {
|
||||
countDown1.value--;
|
||||
if (countDown1.value == 0) {
|
||||
countDown1.value = 60;
|
||||
allowSendCode1.value = true;
|
||||
clearInterval(timerId1.value);
|
||||
// console.log(timerId1.value);
|
||||
}
|
||||
}, 1000);
|
||||
} else {
|
||||
allowSendCode2.value = false;
|
||||
timerId2.value = setInterval(() => {
|
||||
countDown2.value--;
|
||||
if (countDown2.value == 0) {
|
||||
countDown2.value = 60;
|
||||
allowSendCode2.value = true;
|
||||
clearInterval(timerId2.value);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: res.message,
|
||||
icon: 'none',
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
uni.showToast({
|
||||
title: error.message,
|
||||
icon: 'none',
|
||||
});
|
||||
}
|
||||
};
|
||||
//忘记密码
|
||||
const toForgetPassword = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/user/forgetPassword',
|
||||
});
|
||||
};
|
||||
|
||||
// 协议弹框相关
|
||||
const noticeData = ref({}); //协议数据
|
||||
const showAgreementPicker = ref(false);
|
||||
const onShowAgreementPicker = () => {
|
||||
showAgreementPicker.value = true;
|
||||
};
|
||||
const onCancelAgreementPicker = () => {
|
||||
showAgreementPicker.value = false;
|
||||
};
|
||||
const getPolicyServer = async () => {
|
||||
try {
|
||||
await policyServer({
|
||||
type: '',
|
||||
}).then((res) => {
|
||||
console.log(res);
|
||||
if (res.success) {
|
||||
noticeData.value = res.data?.content;
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: res.message,
|
||||
icon: 'none',
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
uni.showToast({
|
||||
title: error.message,
|
||||
icon: 'none',
|
||||
});
|
||||
}
|
||||
};
|
||||
/**
|
||||
* @desc 获取微信登录授权码
|
||||
*/
|
||||
const wxGetLoginCode = async () => {
|
||||
await getLoginCode().then((res) => {
|
||||
wxJsCode.value = res;
|
||||
console.log(wxJsCode.value, '新获取的code');
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc 免验证登录
|
||||
*/
|
||||
const quickLogin = () => {
|
||||
getLoginCode().then((res) => {
|
||||
// #ifdef MP
|
||||
codeType.value = 'mpMobile';
|
||||
// #endif
|
||||
wxJsCode.value = res;
|
||||
onLogin();
|
||||
});
|
||||
};
|
||||
|
||||
// 获取用户手机号码
|
||||
const onGetphonenumber = (ev) => {
|
||||
uni.$u.debounce(async () => {
|
||||
if (ev.detail.errMsg.includes('fail')) return;
|
||||
if (!checked.value) {
|
||||
uni.showToast({
|
||||
title: t('policyTips'),
|
||||
icon: 'none',
|
||||
duration: 2000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
uni.showLoading({
|
||||
title: t('loading'),
|
||||
mask: true,
|
||||
});
|
||||
const flag = await uniLogin(ev);
|
||||
if (!flag) {
|
||||
uni.showToast({
|
||||
title: '登录失败',
|
||||
icon: 'none',
|
||||
});
|
||||
} else {
|
||||
emit('close');
|
||||
console.log({
|
||||
flag,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.log({
|
||||
err,
|
||||
});
|
||||
} finally {
|
||||
uni.hideLoading();
|
||||
}
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const handleChecked = () => {
|
||||
if (!checked.value) {
|
||||
uni.showToast({
|
||||
title: t('policyTips'),
|
||||
icon: 'none',
|
||||
duration: 2000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await initMobile();
|
||||
// await getPolicyServer();
|
||||
// #ifdef MP-WEIXIN || MP-ALIPAY
|
||||
await wxGetLoginCode();
|
||||
// #endif
|
||||
});
|
||||
onUnmounted(() => {
|
||||
clearInterval(timerId1.value);
|
||||
clearInterval(timerId2.value);
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<!-- <view class="login-type flex text-[34rpx] text-theme">
|
||||
<view class="mobile-type flex-1 font-medium border-bottom" @tap="onMobile" :class="{ border-bottom-theme: isMobile }">{{ isLogin ? '手機號碼登入' : '手機號碼註冊' }}</view>
|
||||
<view class="email-type flex-1 font-medium border-bottom" @tap="onEmail" :class="{ border-bottom-theme: !isMobile }">{{ isLogin ? '電郵登入' : '電郵註冊' }}</view>
|
||||
</view> -->
|
||||
<!-- 微信快捷登录 -->
|
||||
<view v-if="isLogin && isCanWxLogin">
|
||||
<view class="login-box">
|
||||
<button
|
||||
v-if="!checked"
|
||||
class="up-reset-button bg-theme rounded-full mx-[30rpx] my-[40rpx] flex h-[92rpx] items-center justify-center text-[34rpx] font-medium text-white"
|
||||
@click="handleChecked"
|
||||
>
|
||||
{{ t('phoneQuickLogin') }}
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
class="up-reset-button bg-theme rounded-full mx-[30rpx] my-[40rpx] flex h-[92rpx] items-center justify-center text-[34rpx] font-medium text-white"
|
||||
open-type="getPhoneNumber"
|
||||
@getphonenumber="onGetphonenumber"
|
||||
>
|
||||
{{ t('phoneQuickLogin') }}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 登录 -->
|
||||
<view v-else-if="isLogin">
|
||||
<view class="mobile" v-if="isMobile">
|
||||
<view class="label text-[26rpx]">{{ t('personal.mobile') }}</view>
|
||||
<view class="mobile-text flex items-center border-round-theme">
|
||||
<view class="areaCode flex items-center">
|
||||
<view class="text-[30rpx]" @tap="showCommonPicker = true">+{{ areaCode }}</view>
|
||||
<view class="down-icon">
|
||||
<up-icon name="arrow-down-fill" size="8" color></up-icon>
|
||||
</view>
|
||||
</view>
|
||||
<!-- #ifndef MP-ALIPAY -->
|
||||
<up-input :placeholder="t('personal.mobilePlaceholder')" v-model="mobile" type="number" border="none"></up-input>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef MP-ALIPAY -->
|
||||
<input class="flex-1" :placeholder="t('personal.mobilePlaceholder')" v-model="mobile" type="number" border="none" />
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="email" v-else>
|
||||
<view class="label text-[26rpx]">{{ t('personal.email') }}</view>
|
||||
<view class="email-text flex items-center border-round-theme">
|
||||
<!-- #ifndef MP-ALIPAY -->
|
||||
<up-input :placeholder="t('personal.emailPlaceholder')" v-model="email" border="none"></up-input>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef MP-ALIPAY -->
|
||||
<input class="flex-1" :placeholder="t('personal.emailPlaceholder')" v-model="email" border="none" />
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="password" v-if="isPassword">
|
||||
<view class="label text-[26rpx]">{{ t('personal.password') }}</view>
|
||||
<view class="password-text flex items-center border-round-theme">
|
||||
<!-- #ifndef MP-ALIPAY -->
|
||||
<up-input :placeholder="t('personal.passwordPlaceholder')" v-model="password" type="password" border="none"></up-input>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef MP-ALIPAY -->
|
||||
<input class="flex-1" :placeholder="t('personal.passwordPlaceholder')" v-model="password" type="password" border="none" />
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="code" v-else>
|
||||
<view class="label text-[26rpx]">{{ t('personal.validateCode') }}</view>
|
||||
<view class="code-text flex items-center border-round-theme">
|
||||
<!-- #ifndef MP-ALIPAY -->
|
||||
<up-input :placeholder="t('personal.validatePlaceholder')" v-model="code" type="number" border="none"></up-input>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef MP-ALIPAY -->
|
||||
<input class="flex-1" :placeholder="t('personal.validatePlaceholder')" v-model="code" type="number" border="none" />
|
||||
<!-- #endif -->
|
||||
<view v-if="allowSendCode1" class="whitespace-nowrap text-[28rpx] font-medium text-theme" @tap="getValifyCode">{{
|
||||
t('personal.getValidateCode')
|
||||
}}</view>
|
||||
<view v-else class="whitespace-nowrap text-[28rpx] font-medium text-[rgb(131,131,131)]"> {{ countDown1 }}s{{ t('personal.resend') }} </view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 免验证登录 -->
|
||||
<!-- #ifdef MP -->
|
||||
<view class="flex flex-row items-center justify-center flex-1" @click="quickLogin">
|
||||
<text class="text-[15px]">{{ t('quickLogin') }}</text>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
<!-- <view class="text-right text-[26rpx] mx-[30rpx] text-gray-600" @tap="toForgetPassword">忘記密碼?</view> -->
|
||||
<view class="bg-theme rounded-full mx-[30rpx] my-[40rpx] flex h-[92rpx] items-center justify-center text-[34rpx] font-medium text-white" @tap="onLogin">{{
|
||||
t('login')
|
||||
}}</view>
|
||||
|
||||
<!-- <view class="flex mx-[30rpx] items-center justify-between text-[28rpx]">
|
||||
<view>沒有賬號?點挈 <text class="text-theme" @tap="toRegister">註冊</text></view>
|
||||
<view v-if="isMobile" class="text-theme" @tap="isPassword = !isPassword">{{ !isPassword ? '密碼登入' : '验证码登入' }}</view>
|
||||
</view> -->
|
||||
</view>
|
||||
<!-- 注册 -->
|
||||
<view v-else>
|
||||
<view class="mobile" v-if="isMobile">
|
||||
<view class="label text-[26rpx]">{{ t('personal.mobile') }}</view>
|
||||
<view class="mobile-text flex items-center border-round-theme">
|
||||
<view class="areaCode flex items-center">
|
||||
<view class="text-[30rpx]" @tap="showCommonPicker = true">+{{ areaCode }}</view>
|
||||
<view class="down-icon">
|
||||
<up-icon name="arrow-down-fill" size="8" color></up-icon>
|
||||
</view>
|
||||
</view>
|
||||
<!-- #ifndef MP-ALIPAY -->
|
||||
<up-input :placeholder="t('personal.mobilePlaceholder')" v-model="mobile" type="number" border="none"></up-input>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef MP-ALIPAY -->
|
||||
<input class="flex-1" :placeholder="t('personal.mobilePlaceholder')" v-model="mobile" type="number" border="none" />
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="email" v-else>
|
||||
<view class="label text-[26rpx]">{{ t('personal.email') }}</view>
|
||||
<view class="email-text flex items-center border-round-theme">
|
||||
<!-- #ifndef MP-ALIPAY -->
|
||||
<up-input :placeholder="t('personal.emailPlaceholder')" v-model="email" border="none"></up-input>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef MP-ALIPAY -->
|
||||
<input class="flex-1" :placeholder="t('personal.emailPlaceholder')" v-model="email" border="none" />
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="code" v-if="isMobile">
|
||||
<view class="label text-[26rpx]">{{ t('personal.validateCode') }}</view>
|
||||
<view class="code-text flex items-center border-round-theme">
|
||||
<!-- #ifndef MP-ALIPAY -->
|
||||
<up-input placeholder="t('personal.validatePlaceholder')" v-model="code" type="number" border="none"></up-input>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef MP-ALIPAY -->
|
||||
<input class="flex-1" placeholder="t('personal.validatePlaceholder')" v-model="code" type="number" border="none" />
|
||||
<!-- #endif -->
|
||||
<view v-if="allowSendCode2" class="whitespace-nowrap text-[28rpx] font-medium text-theme" @tap="getValifyCode">{{
|
||||
t('personal.getValidateCode')
|
||||
}}</view>
|
||||
<view v-else class="whitespace-nowrap text-[28rpx] font-medium text-[rgb(131,131,131)]">
|
||||
{{ `${countDown2}s ${t('personal.resend')}` }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="password">
|
||||
<view class="label text-[26rpx]">{{ t('personal.password') }}</view>
|
||||
<view class="password-text flex items-center border-round-theme">
|
||||
<!-- #ifndef MP-ALIPAY -->
|
||||
<up-input :placeholder="t('personal.setPassword')" v-model="password" type="password" border="none"></up-input>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef MP-ALIPAY -->
|
||||
<input class="flex-1" :placeholder="t('personal.setPassword')" v-model="password" type="password" border="none" />
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</view>
|
||||
<view class="password">
|
||||
<view class="label text-[26rpx]">{{ t('personal.confirmPassword') }}</view>
|
||||
<view class="password-text flex items-center border-round-theme">
|
||||
<!-- #ifndef MP-ALIPAY -->
|
||||
<up-input :placeholder="t('personal.passwordAgain')" v-model="passwordConfirm" type="password" border="none"></up-input>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef MP-ALIPAY -->
|
||||
<input class="flex-1" :placeholder="t('personal.passwordAgain')" v-model="passwordConfirm" type="password" border="none" />
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</view>
|
||||
<!-- :class="{ disable: isDisable }" -->
|
||||
<view
|
||||
class="bg-theme rounded-full mx-[30rpx] mt-[15rpx] my-[40rpx] flex h-[92rpx] items-center justify-center text-[34rpx] font-medium text-white"
|
||||
@tap="onRegister"
|
||||
>{{ t('regLogin') }}</view
|
||||
>
|
||||
<view class="flex mx-[30rpx] mb-[40rpx] items-center justify-between text-[28rpx]">
|
||||
<view
|
||||
>{{ t('hasAccount') }} <text class="text-theme" @tap="toLogin">{{ t('login') }}</text></view
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
<view class="text-center my-[40rpx] text-[28rpx]">
|
||||
<checkbox-group @change="onChange">
|
||||
<checkbox class="checkbox" :checked="checked" color="#00A88E"> </checkbox>
|
||||
{{ t('policyNote') }} <text class="text-theme font-medium" @tap="onShowAgreementPicker">{{ t('policy') }}</text>
|
||||
</checkbox-group>
|
||||
</view>
|
||||
<!-- 地区号 -->
|
||||
<CommonPicker
|
||||
:title="t('personal.selectAreaCode')"
|
||||
:show="showCommonPicker"
|
||||
:columns="columns"
|
||||
@confirm="onConfirmCommonPicker"
|
||||
@close="onCloseCommonPicker"
|
||||
></CommonPicker>
|
||||
<!-- 用户协议弹框 -->
|
||||
<up-popup :show="showAgreementPicker" :closeable="true" round="10" mode="bottom" @close="onCancelAgreementPicker">
|
||||
<view class="h-[70vh] flex flex-col px-[30rpx] pb-[30rpx] text-[28rpx]">
|
||||
<view class="py-[30rpx] text-center text-[36rpx]">
|
||||
{{ t('policy') }}
|
||||
</view>
|
||||
<view class="flex-1 overflow-y-scroll">
|
||||
<up-parse :content="noticeData"></up-parse>
|
||||
</view>
|
||||
</view>
|
||||
</up-popup>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.border-bottom-theme .login-type {
|
||||
height: 88rpx;
|
||||
margin: 30rpx;
|
||||
|
||||
.mobile-type,
|
||||
.email-type {
|
||||
text-align: center;
|
||||
line-height: 88rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile,
|
||||
.email,
|
||||
.password,
|
||||
.code {
|
||||
margin: 30rpx;
|
||||
|
||||
.label {
|
||||
font-size: 26rpx;
|
||||
padding-left: 40rpx;
|
||||
padding-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.mobile-text,
|
||||
.email-text,
|
||||
.password-text,
|
||||
.code-text {
|
||||
// margin: 0 15px;
|
||||
height: 100rpx;
|
||||
padding: 0 40rpx;
|
||||
border-radius: 100rpx;
|
||||
|
||||
.van-field {
|
||||
background-color: #ffffff !important;
|
||||
padding: 0 10rpx !important;
|
||||
}
|
||||
|
||||
.areaCode {
|
||||
margin-right: 17rpx;
|
||||
|
||||
.down-icon {
|
||||
width: 40rpx;
|
||||
padding-left: 10rpx;
|
||||
}
|
||||
|
||||
border-right: 2rpx solid #eee;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.disable {
|
||||
opacity: 0.3;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,135 @@
|
||||
<script setup>
|
||||
import {
|
||||
getPredictTime
|
||||
} from '@/pages/booking/api.js';
|
||||
import {
|
||||
onMounted,
|
||||
ref,
|
||||
reactive,
|
||||
watch
|
||||
} from 'vue';
|
||||
import {
|
||||
useI18n
|
||||
} from 'vue-i18n';
|
||||
import {
|
||||
getNowDate
|
||||
} from '@/common/common';
|
||||
import {
|
||||
isEmpty,
|
||||
showToast
|
||||
} from '../common/common';
|
||||
const {
|
||||
t,
|
||||
locale
|
||||
} = useI18n();
|
||||
const props = defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
date: {
|
||||
type: String,
|
||||
default: getNowDate(),
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['close', 'confirm']);
|
||||
const close = () => {
|
||||
emit('close');
|
||||
};
|
||||
const pckList = ref([]); // 套餐总数组
|
||||
const title = ref('');
|
||||
|
||||
/**
|
||||
* @desc 選擇套餐
|
||||
* @param res 選中的套餐
|
||||
*/
|
||||
const choiceValue = ref('');
|
||||
const choiceData = ref({});
|
||||
const choice = (res) => {
|
||||
if (!res.time.status) {
|
||||
return showToast(res.time.tips);
|
||||
}
|
||||
choiceData.value = {
|
||||
pckId: res.item.pckId,
|
||||
pckName: res.item.pckName,
|
||||
...res.time,
|
||||
};
|
||||
choiceValue.value = res.time.value;
|
||||
};
|
||||
|
||||
const confirm = () => {
|
||||
if (isEmpty(choiceData.value)) {
|
||||
return showToast(t('booking.useTimePlaceholder'));
|
||||
}
|
||||
emit('confirm', choiceData.value);
|
||||
close();
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc 查询用餐时间
|
||||
*/
|
||||
const init = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
getPredictTime({
|
||||
date: props.date,
|
||||
}).then((res) => {
|
||||
if (res.success) {
|
||||
pckList.value = res.data?.pckList;
|
||||
title.value = res.data?.title;
|
||||
resolve()
|
||||
} else {
|
||||
showToast(res.message)
|
||||
reject()
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
const timeFun = reactive({
|
||||
init
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
timeFun,
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
init()
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<up-popup :show="props.show" mode="bottom" round="20" :closeable="true" :safeAreaInsetBottom="true" @close="close">
|
||||
<view class="flex flex-col gap-[20px] p-[20px] h-[70vh] relative">
|
||||
<view class="flex h-[44px] items-center justify-center text-[16px] text-[#1D1D1F]">{{ title }}</view>
|
||||
<view class="flex flex-col overflow-auto time-height gap-[20px]">
|
||||
<view class="flex flex-col gap-[20px]" v-for="item in pckList" :key="item.pckId">
|
||||
<view class="flex flex-col gap-[10px]">
|
||||
<view class="text-main text-[14px] font-bold">{{ item.pckName }}</view>
|
||||
<view class="gap-[20px] grid grid-cols-2">
|
||||
<view class="flex items-center justify-center h-[35px] rounded-[5px] text-[14px]"
|
||||
v-for="time in item.useTimeData" :key="time.value" :class="{
|
||||
'!bg-theme text-white': choiceValue === time.value,
|
||||
'bg-[#999]/[.5] text-white': !time.status,
|
||||
'bg-white text-main border-round-theme': time.status,
|
||||
}" @click="choice({ item, time })">
|
||||
{{ time.label }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="my-[20px] flex absolute bottom-0 left-[20px] right-[20px]">
|
||||
<up-button class="h-[40px] flex-1" :text="t('confirm')" shape="circle" color="#B5966E"
|
||||
@click="confirm" />
|
||||
</view>
|
||||
</view>
|
||||
</up-popup>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.time-height {
|
||||
height: calc(100% - 40px - 44px - 40px); // 按钮高度+title高度+内边距上下共40px
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user