feat: add google sign-in, utils, refactor imports
- Restructure API import paths from @/request/api/* to cleaner @/api/[module] format - Add three new utility classes: PhoneUtils for phone validation, ThrottleUtils and DebounceUtils for rate limiting - Implement official Google One Tap login flow, add the GIS client script to index.html - Clean up long inline template attributes in multiple Vue components for better readability - Fix constant import path and default object return values in booking components
This commit is contained in:
@@ -8,6 +8,7 @@
|
|||||||
/>
|
/>
|
||||||
<meta name="theme-color" content="#2D91FF" />
|
<meta name="theme-color" content="#2D91FF" />
|
||||||
<title>nianxx</title>
|
<title>nianxx</title>
|
||||||
|
<script src="https://accounts.google.com/gsi/client" async defer></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
@@ -1,24 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div class="booking-footer border-box bg-white flex flex-items-center font-family-misans-vf">
|
||||||
class="booking-footer border-box bg-white flex flex-items-center font-family-misans-vf"
|
<span class="amt font-size-20 font-bold color-FF3D60 line-height-28 flex flex-items-center mr-8">
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="amt font-size-20 font-bold color-FF3D60 line-height-28 flex flex-items-center mr-8"
|
|
||||||
>
|
|
||||||
{{ totalAmt }}
|
{{ totalAmt }}
|
||||||
</span>
|
</span>
|
||||||
<!-- <div class="flex flex-items-center" @click="emit('detailClick')">
|
<!-- <div class="flex flex-items-center" @click="emit('detailClick')">
|
||||||
<span class="font-size-12 color-A3A3A3 mr-4">明细</span>
|
<span class="font-size-12 color-A3A3A3 mr-4">明细</span>
|
||||||
<uni-icons type="up" size="16" color="#A3A3A3" />
|
<uni-icons type="up" size="16" color="#A3A3A3" />
|
||||||
</div> -->
|
</div> -->
|
||||||
<div
|
<div class="btn border-box rounded-10 flex flex-items-center ml-auto pl-8" @click="handleBooking">
|
||||||
class="btn border-box rounded-10 flex flex-items-center ml-auto pl-8"
|
<img class="icon" src="https://oss.nianxx.cn/mp/static/version_101/common/btn.png" />
|
||||||
@click="handleBooking"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
class="icon"
|
|
||||||
src="https://oss.nianxx.cn/mp/static/version_101/common/btn.png"
|
|
||||||
/>
|
|
||||||
<span class="font-size-16 font-500 color-white">立即支付</span>
|
<span class="font-size-16 font-500 color-white">立即支付</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -26,8 +16,8 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, defineProps, defineEmits, ref, onMounted, watch } from "vue";
|
import { computed, defineProps, defineEmits, ref, onMounted, watch } from "vue";
|
||||||
import { DebounceUtils } from "@/utils";
|
import { DebounceUtils } from "@/utils/DebounceUtils";
|
||||||
import { preOrder } from "@/request/api/OrderApi";
|
import { preOrder } from "@/api/order";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
@@ -36,11 +26,11 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
orderData: {
|
orderData: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {},
|
default: () => { },
|
||||||
},
|
},
|
||||||
selectedDate: {
|
selectedDate: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {},
|
default: () => { },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -69,10 +69,11 @@ import UserSection from "./components/UserSection/index.vue";
|
|||||||
import RefundPopup from "@/components/RefundPopup/index.vue";
|
import RefundPopup from "@/components/RefundPopup/index.vue";
|
||||||
import DetailPopup from "@/components/DetailPopup/index.vue";
|
import DetailPopup from "@/components/DetailPopup/index.vue";
|
||||||
import FooterSection from "./components/FooterSection/index.vue";
|
import FooterSection from "./components/FooterSection/index.vue";
|
||||||
import { goodsDetail, orderPay } from "@/request/api/GoodsApi";
|
import { goodsDetail, orderPay } from "@/api/goods";
|
||||||
import { useSelectedDateStore } from "@/store";
|
import { useSelectedDateStore } from "@/store";
|
||||||
import { GOODS_TYPE } from "@/constant/type";
|
import { GOODS_TYPE } from "@/constants/type";
|
||||||
import { ThrottleUtils, PhoneUtils } from "@/utils";
|
import { ThrottleUtils } from "@/utils/ThrottleUtils";
|
||||||
|
import { PhoneUtils } from "@/utils/PhoneUtils";
|
||||||
|
|
||||||
const refundVisible = ref(false);
|
const refundVisible = ref(false);
|
||||||
const detailVisible = ref(false);
|
const detailVisible = ref(false);
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { goodsDetail, commodityDailyPriceList } from "@/request/api/GoodsApi";
|
import { goodsDetail, commodityDailyPriceList } from "@/api/goods";
|
||||||
import { DateUtils } from "@/utils";
|
import { DateUtils } from "@/utils";
|
||||||
import TopNavBar from "@/components/TopNavBar/index.vue";
|
import TopNavBar from "@/components/TopNavBar/index.vue";
|
||||||
import ImageSwiper from "@/components/ImageSwiper/index.vue";
|
import ImageSwiper from "@/components/ImageSwiper/index.vue";
|
||||||
|
|||||||
@@ -1,56 +1,118 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-screen flex flex-col items-center justify-center">
|
<div class="h-screen flex flex-col items-center justify-center">
|
||||||
<div class="w-[304px] mt-[20px]">
|
<div id="buttonDiv" class="w-[304px] mt-[20px]"></div>
|
||||||
<button class="w-full h-[44px] bg-[#0ccd58] text-white rounded-[10px] text-center" @click="loginWithGoogle">
|
|
||||||
Continue with Google
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from "vue";
|
import { onMounted } from "vue";
|
||||||
import { oauthToken } from '@/api/login'
|
import { oauthToken } from '@/api/login'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
const generateState = () => {
|
const router = useRouter()
|
||||||
const array = new Uint32Array(4);
|
|
||||||
|
|
||||||
window.crypto.getRandomValues(array);
|
type GoogleIdentityServices = {
|
||||||
|
accounts: {
|
||||||
return Array.from(array, dec => dec.toString(16).padStart(8, '0')).join('');
|
id: {
|
||||||
}
|
initialize: (params: Record<string, unknown>) => void;
|
||||||
|
renderButton: (el: Element, options: Record<string, unknown>) => void;
|
||||||
// 用Google登录
|
};
|
||||||
const loginWithGoogle = () => {
|
};
|
||||||
const client_id = '你的Google客户端ID';
|
|
||||||
const redirect_uri = encodeURIComponent('http://10.10.3.1:5174/login');
|
|
||||||
const scope = encodeURIComponent('openid email profile');
|
|
||||||
const response_type = 'code'; // 推荐 code 模式(授权码模式,更安全);如果不需要后端参与,可填 'token' (隐式模式)
|
|
||||||
const state = generateState();
|
|
||||||
sessionStorage.setItem('oauth_state', state);
|
|
||||||
|
|
||||||
const authUrl = `https://google.com{client_id}&redirect_uri=${redirect_uri}&response_type=${response_type}&scope=${scope}&state=${state}`;
|
|
||||||
|
|
||||||
window.location.href = authUrl;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
declare global {
|
||||||
// 1. 从 URL 中获取谷歌传回的 state
|
interface Window {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
google?: GoogleIdentityServices;
|
||||||
const returnedState = urlParams.get('state');
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 2. 从本地缓存取出之前存的 state
|
function loadGoogleIdentityServices(): Promise<void> {
|
||||||
const originalState = sessionStorage.getItem('oauth_state');
|
if (window.google?.accounts?.id) {
|
||||||
|
return Promise.resolve();
|
||||||
// 3. 严格比对
|
|
||||||
if (!returnedState || returnedState !== originalState) {
|
|
||||||
// 清除缓存并拒绝后续登录逻辑
|
|
||||||
sessionStorage.removeItem('oauth_state');
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 比对成功,清除使用过的 state,继续执行后续拿 code 换 token 的业务
|
return new Promise((resolve, reject) => {
|
||||||
sessionStorage.removeItem('oauth_state');
|
const src = "https://accounts.google.com/gsi/client";
|
||||||
const code = urlParams.get('code');
|
const existing = document.querySelector < HTMLScriptElement > (
|
||||||
})
|
`script[src="${src}"]`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
if (window.google?.accounts?.id) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
existing.addEventListener("load", () => resolve(), { once: true });
|
||||||
|
existing.addEventListener(
|
||||||
|
"error",
|
||||||
|
() => reject(new Error("GIS load error")),
|
||||||
|
{ once: true },
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const script = document.createElement("script");
|
||||||
|
script.src = src;
|
||||||
|
script.async = true;
|
||||||
|
script.defer = true;
|
||||||
|
script.onload = () => resolve();
|
||||||
|
script.onerror = () => reject(new Error("GIS load error"));
|
||||||
|
document.head.appendChild(script);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadGoogleIdentityServices()
|
||||||
|
.then(() => {
|
||||||
|
if (!window.google?.accounts?.id) {
|
||||||
|
throw new Error("GIS not available");
|
||||||
|
}
|
||||||
|
|
||||||
|
window.google.accounts.id.initialize({
|
||||||
|
client_id:
|
||||||
|
"237078935964-e2pioe5hlfbcd4gaam4m9sn859iihdb9.apps.googleusercontent.com",
|
||||||
|
ux_mode: "popup",
|
||||||
|
callback: (response: any) => {
|
||||||
|
// 登录成功后,谷歌会直接把凭证传进这个函数
|
||||||
|
console.log("谷歌登录成功,收到凭证:", response);
|
||||||
|
|
||||||
|
// 这个 credential 就是 JWT (ID Token)
|
||||||
|
const idToken = response.credential;
|
||||||
|
|
||||||
|
if (idToken) {
|
||||||
|
// 【后续业务】:你可以将这串 idToken 通过 axios 发送给你自己的后端接口
|
||||||
|
// 或者是直接利用第三方平台(如 Firebase / Supabase)完成纯前端鉴权
|
||||||
|
console.log("拿到的 ID Token 是:", idToken);
|
||||||
|
oauthToken({ clientId: '6', openIdCode: [idToken], grant_type: 'google' })
|
||||||
|
.then((res) => {
|
||||||
|
console.log("获取到的 oauth token:", res)
|
||||||
|
|
||||||
|
router.go(-1)
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
context: "signin",
|
||||||
|
});
|
||||||
|
|
||||||
|
const container = document.getElementById("buttonDiv");
|
||||||
|
if (!container) {
|
||||||
|
throw new Error("Missing #buttonDiv");
|
||||||
|
}
|
||||||
|
|
||||||
|
window.google.accounts.id.renderButton(container, {
|
||||||
|
theme: "outline",
|
||||||
|
size: "large",
|
||||||
|
width: 304,
|
||||||
|
shape: "rectangular",
|
||||||
|
logo_alignment: "center",
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,24 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div class="footer bg-white border-box flex flex-items-center flex-justify-between p-12">
|
||||||
class="footer bg-white border-box flex flex-items-center flex-justify-between p-12"
|
<button v-if="['1', '2'].includes(statusCode)"
|
||||||
>
|
|
||||||
<button
|
|
||||||
v-if="['1', '2'].includes(statusCode)"
|
|
||||||
class="left border-none border-box bg-white rounded-10 flex flex-items-center flex-justify-center font-size-14 font-500 color-525866 mr-12"
|
class="left border-none border-box bg-white rounded-10 flex flex-items-center flex-justify-center font-size-14 font-500 color-525866 mr-12"
|
||||||
@click="emit('refund', orderData)"
|
@click="emit('refund', orderData)">
|
||||||
>
|
|
||||||
申请退款
|
申请退款
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button :class="[
|
||||||
:class="[
|
'right border-none rounded-10 flex flex-full flex-items-center flex-justify-center font-size-14 font-500 bg-theme-color-500',
|
||||||
'right border-none rounded-10 flex flex-full flex-items-center flex-justify-center font-size-14 font-500 bg-theme-color-500',
|
{
|
||||||
{
|
'bg-FF3D60': statusCode === '0',
|
||||||
'bg-FF3D60': statusCode === '0',
|
'color-white': ['1', '2', '3', '4', '5', '6'].includes(statusCode),
|
||||||
'color-white': ['1', '2', '3', '4', '5', '6'].includes(statusCode),
|
},
|
||||||
},
|
]" @click="handleButtonClick(orderData)">
|
||||||
]"
|
|
||||||
@click="handleButtonClick(orderData)"
|
|
||||||
>
|
|
||||||
{{ buttonspan }}
|
{{ buttonspan }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -26,7 +19,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { defineProps, defineEmits, computed } from "vue";
|
import { defineProps, defineEmits, computed } from "vue";
|
||||||
import { orderPayNow } from "@/request/api/OrderApi";
|
import { orderPayNow } from "@/api/order";
|
||||||
import { DebounceUtils } from "@/utils";
|
import { DebounceUtils } from "@/utils";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, getCurrentInstance } from "vue";
|
import { ref, getCurrentInstance } from "vue";
|
||||||
import { userOrderDetail, orderRefund } from "@/request/api/OrderApi";
|
import { userOrderDetail, orderRefund } from "@/api/order";
|
||||||
import TopNavBar from "@/components/TopNavBar/index.vue";
|
import TopNavBar from "@/components/TopNavBar/index.vue";
|
||||||
import OrderQrcode from "./components/OrderQrcode/index.vue";
|
import OrderQrcode from "./components/OrderQrcode/index.vue";
|
||||||
import VoucherList from "./components/VoucherList/index.vue";
|
import VoucherList from "./components/VoucherList/index.vue";
|
||||||
|
|||||||
@@ -1,20 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<z-paging
|
<z-paging :bg-color="'linear-gradient(180deg, ' +
|
||||||
:bg-color="
|
$theme -
|
||||||
'linear-gradient(180deg, ' +
|
color -
|
||||||
$theme -
|
100 +
|
||||||
color -
|
' 0%, #F5F7FA 100%) 0 86px / 100% 100px no-repeat'
|
||||||
100 +
|
" ref="paging" v-model="dataList" use-virtual-list :force-close-inner-list="true" cell-height-mode="dynamic"
|
||||||
' 0%, #F5F7FA 100%) 0 86px / 100% 100px no-repeat'
|
safe-area-inset-bottom @query="queryList">
|
||||||
"
|
|
||||||
ref="paging"
|
|
||||||
v-model="dataList"
|
|
||||||
use-virtual-list
|
|
||||||
:force-close-inner-list="true"
|
|
||||||
cell-height-mode="dynamic"
|
|
||||||
safe-area-inset-bottom
|
|
||||||
@query="queryList"
|
|
||||||
>
|
|
||||||
<template #top>
|
<template #top>
|
||||||
<TopNavBar title="全部订单" />
|
<TopNavBar title="全部订单" />
|
||||||
</template>
|
</template>
|
||||||
@@ -23,12 +14,7 @@
|
|||||||
<CustomEmpty statusspan="您暂无订单" />
|
<CustomEmpty statusspan="您暂无订单" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<OrderCard
|
<OrderCard v-for="(item, index) in dataList" :key="item.id || index" :orderData="item" @click="handleOrderClick" />
|
||||||
v-for="(item, index) in dataList"
|
|
||||||
:key="item.id || index"
|
|
||||||
:orderData="item"
|
|
||||||
@click="handleOrderClick"
|
|
||||||
/>
|
|
||||||
</z-paging>
|
</z-paging>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -37,7 +23,7 @@ import { ref } from "vue";
|
|||||||
import TopNavBar from "@/components/TopNavBar/index.vue";
|
import TopNavBar from "@/components/TopNavBar/index.vue";
|
||||||
import CustomEmpty from "@/components/CustomEmpty/index.vue";
|
import CustomEmpty from "@/components/CustomEmpty/index.vue";
|
||||||
import OrderCard from "./components/OrderCard/index.vue";
|
import OrderCard from "./components/OrderCard/index.vue";
|
||||||
import { userOrderList } from "@/request/api/OrderApi";
|
import { userOrderList } from "@/api/order";
|
||||||
|
|
||||||
const dataList = ref([]);
|
const dataList = ref([]);
|
||||||
const paging = ref(null);
|
const paging = ref(null);
|
||||||
|
|||||||
@@ -1,40 +1,26 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="tab-container relative">
|
<div class="tab-container relative">
|
||||||
<div class="tab-wrapper flex flex-items-center">
|
<div class="tab-wrapper flex flex-items-center">
|
||||||
<div
|
<div v-for="(item, index) in tabList" :key="item.id" :class="[
|
||||||
v-for="(item, index) in tabList"
|
'tab-item flex flex-items-center flex-justify-center relative',
|
||||||
:key="item.id"
|
activeIndex === index && 'tab-item-active',
|
||||||
:class="[
|
]" @click="handleTabClick(index)">
|
||||||
'tab-item flex flex-items-center flex-justify-center relative',
|
|
||||||
activeIndex === index && 'tab-item-active',
|
|
||||||
]"
|
|
||||||
@click="handleTabClick(index)"
|
|
||||||
>
|
|
||||||
<div class="tab-item-inner flex flex-items-center">
|
<div class="tab-item-inner flex flex-items-center">
|
||||||
<uni-icons
|
<uni-icons :class="['icon mr-4', activeIndex === index && 'icon-active']" fontFamily="znicons" size="20"
|
||||||
:class="['icon mr-4', activeIndex === index && 'icon-active']"
|
color="opacity">
|
||||||
fontFamily="znicons"
|
|
||||||
size="20"
|
|
||||||
color="opacity"
|
|
||||||
>
|
|
||||||
{{ zniconsMap[item.iconCode] }}
|
{{ zniconsMap[item.iconCode] }}
|
||||||
</uni-icons>
|
</uni-icons>
|
||||||
|
|
||||||
<span
|
<span :class="[
|
||||||
:class="[
|
'font-size-16 font-500 color-525866 ',
|
||||||
'font-size-16 font-500 color-525866 ',
|
activeIndex === index && 'tab-span-active',
|
||||||
activeIndex === index && 'tab-span-active',
|
]">
|
||||||
]"
|
|
||||||
>
|
|
||||||
{{ item.iconTitle }}
|
{{ item.iconTitle }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 每项内的下划线指示器,通过类控制显示/隐藏 -->
|
<!-- 每项内的下划线指示器,通过类控制显示/隐藏 -->
|
||||||
<div
|
<div class="tab-item-indicator" :class="{ visible: activeIndex === index }"></div>
|
||||||
class="tab-item-indicator"
|
|
||||||
:class="{ visible: activeIndex === index }"
|
|
||||||
></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -43,7 +29,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, ref, watch } from "vue";
|
import { onMounted, ref, watch } from "vue";
|
||||||
import { zniconsMap } from "@/static/fonts/znicons";
|
import { zniconsMap } from "@/static/fonts/znicons";
|
||||||
import { commodityTypePageList } from "@/request/api/GoodsApi";
|
import { commodityTypePageList } from "@/api/goods";
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|||||||
23
src/utils/DebounceUtils.ts
Normal file
23
src/utils/DebounceUtils.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* 防抖工具类
|
||||||
|
* 提供防抖功能,防止函数在短时间内被重复调用
|
||||||
|
*/
|
||||||
|
export class DebounceUtils {
|
||||||
|
/**
|
||||||
|
* 创建防抖函数
|
||||||
|
* @param {Function} func - 要防抖的函数
|
||||||
|
* @param {number} delay - 防抖延迟时间
|
||||||
|
* @returns {Function} 防抖后的函数
|
||||||
|
*/
|
||||||
|
static createDebounce(func: Function, delay: number) {
|
||||||
|
let timerId: number | null = null;
|
||||||
|
|
||||||
|
return function (...args: any[]) {
|
||||||
|
if (timerId) {
|
||||||
|
clearTimeout(timerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
timerId = setTimeout(() => func.apply(this, args), delay);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/utils/PhoneUtils.ts
Normal file
11
src/utils/PhoneUtils.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* 验证手机号工具类
|
||||||
|
* @param {string} phone - 手机号字符串
|
||||||
|
* @returns {boolean} 是否为有效手机号
|
||||||
|
*/
|
||||||
|
export class PhoneUtils {
|
||||||
|
static validatePhone(phone: string) {
|
||||||
|
const phoneRegex = /^1[3-9]\d{9}$/;
|
||||||
|
return phoneRegex.test(phone);
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/utils/ThrottleUtils.ts
Normal file
24
src/utils/ThrottleUtils.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* 节流工具类
|
||||||
|
* 提供节流功能,防止函数在短时间内被重复调用
|
||||||
|
*/
|
||||||
|
export class ThrottleUtils {
|
||||||
|
/**
|
||||||
|
* 创建节流函数
|
||||||
|
* @param {Function} func - 要节流的函数
|
||||||
|
* @param {number} delay - 节流延迟时间
|
||||||
|
* @returns {Function} 节流后的函数
|
||||||
|
*/
|
||||||
|
static createThrottle(func: Function, delay: number) {
|
||||||
|
let prev = Date.now();
|
||||||
|
|
||||||
|
return function (...args: any[]) {
|
||||||
|
let now = Date.now();
|
||||||
|
|
||||||
|
if (now - prev >= delay) {
|
||||||
|
func.apply(this, args);
|
||||||
|
prev = now;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user