feat(.): Achieve the sms code send

This commit is contained in:
漩葵 2024-06-10 23:57:29 +08:00
parent d834378c1c
commit 8a04c4e72e
14 changed files with 690 additions and 152 deletions

View File

@ -1,24 +1,20 @@
<template>
<client-only>
<client-only>
<el-menu
:default-active="activeIndex"
mode="horizontal"
:ellipsis="false"
@select="handleSelect"
:default-active="activeIndex"
mode="horizontal"
:ellipsis="false"
@select="handleSelect"
>
<el-menu-item index="/">
<img
style="width: 50px;"
src="/logo.svg"
alt="Element logo"
/>
</el-menu-item>
<el-menu-item index="/apply">立即申请</el-menu-item>
<el-menu-item index="/about">关于我们</el-menu-item>
<el-menu-item index="/status" disabled>设备监控</el-menu-item>
<el-sub-menu index="/area" disabled>
<el-menu-item index="/">
<img style="width: 50px" src="/logo.svg" alt="Element logo" />
</el-menu-item>
<el-menu-item index="/apply">立即申请</el-menu-item>
<el-menu-item index="/about" disabled>关于我们</el-menu-item>
<el-menu-item index="/status" disabled>设备监控</el-menu-item>
<el-sub-menu index="/area" disabled>
<template #title>区域服务</template>
<!-- <el-menu-item index="/area/ln1">辽宁一区</el-menu-item>
<!-- <el-menu-item index="/area/ln1">辽宁一区</el-menu-item>
<el-menu-item index="/area/ln2">辽宁二区</el-menu-item>
<el-menu-item index="/area/jx1">江西一区</el-menu-item>
<el-sub-menu index="/area/jx2">
@ -27,24 +23,22 @@
<el-menu-item index="/area/jx2/web">网页服务器</el-menu-item>
<el-menu-item index="/area/jx2/game">游戏服务器</el-menu-item>
</el-sub-menu> -->
</el-sub-menu>
<el-menu-item index="/forum" disabled>论坛</el-menu-item>
<el-menu-item index="/host" disabled>托管</el-menu-item>
<div class="flex-grow" />
<el-menu-item index="/login">登录</el-menu-item>
<el-menu-item index="register">注册</el-menu-item>
</el-sub-menu>
<el-menu-item index="/forum" disabled>论坛</el-menu-item>
<el-menu-item index="/host" disabled>托管</el-menu-item>
<div class="flex-grow" />
<el-menu-item index="/login">登录</el-menu-item>
<el-menu-item index="register">注册</el-menu-item>
</el-menu>
</client-only>
</client-only>
</template>
<script lang="ts" setup>
const activeIndex = ref('1')
const activeIndex = ref("1");
const handleSelect = (key: string, keyPath: string[]) => {
console.log(key, keyPath)
navigateTo(key)
}
console.log(key, keyPath);
navigateTo(key);
};
</script>
<style>
.flex-grow {

View File

@ -1,8 +1,11 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
devtools: { enabled: true },
modules: [
"@element-plus/nuxt",
'@vueform/nuxt'
],
})
runtimeConfig: {
apiSecret: "",
apiOpenid: "",
apiApikey: "",
apiSmsId: 0,
},
modules: ["@element-plus/nuxt", "@vueform/nuxt"],
});

View File

@ -17,6 +17,7 @@
"element-plus": "^2.7.4",
"nuxt": "^3.11.2",
"prisma": "^5.15.0",
"quanmsms": "^1.0.2",
"typescript": "^5.4.5",
"vue": "^3.4.27",
"vue-router": "^4.3.2"

78
pages/admin/index.vue Normal file
View File

@ -0,0 +1,78 @@
<template>
<Head>
<Title>管理界面</Title>
<Meta name="description" />
</Head>
<client-only>
<el-table :data="tableData" style="width: 100%">
<el-table-column prop="appid" label="AppID" width="180" />
<el-table-column prop="name" label="申请人" width="180" />
<el-table-column prop="phone" label="手机号" width="180" />
<el-table-column label="配置" width="300">
<template #default="scope">
<el-icon color="blue"><ElIcon-Cpu /></el-icon>
{{ scope.row.resource.cpu }} Core
<el-icon color="green"><ElIcon-Stopwatch /></el-icon
>{{ scope.row.resource.ram }} GB
<el-icon color="gray"><ElIcon-Memo /></el-icon>
{{ scope.row.resource.disk }} GB
</template>
</el-table-column>
<el-table-column prop="usage" label="用途" width="360" />
<el-table-column label="状态" width="300">
<template #default="scope">
<el-icon :color="scope.row.apply ? 'green' : 'orange'"
><ElIcon-Stamp
/></el-icon>
{{ scope.row.apply ? "通过" : "待审核" }}
</template>
</el-table-column>
<el-table-column label="操作">
<template #default="scope">
<el-button
size="small"
type="success"
@click="ElMessage({ message: '同意', type: 'success' })"
>
同意
</el-button>
<el-button
size="small"
type="danger"
@click="ElMessage({ message: '拒绝', type: 'warning' })"
>
拒绝
</el-button>
</template>
</el-table-column>
</el-table>
</client-only>
</template>
<script lang="ts" setup>
const tableData = [
{
appid: "1",
name: "漩葵",
phone: 1922949224,
usage: "用于开展服务",
resource: {
cpu: 1,
ram: 2,
disk: 10,
},
apply: false,
},
{
appid: "2",
name: "BrianLing",
phone: 1922949224,
usage: "用于开展服务",
resource: {
cpu: 1,
ram: 2,
disk: 10,
},
apply: true,
},
];
</script>

View File

@ -1,76 +1,123 @@
<template>
<!--立即申请页面-->
<Head>
<Title>立即申请</Title>
<Meta name="description"/>
</Head>
<div style="width:50%;margin:auto;" :style="{
boxShadow: `var(--el-box-shadow-light)`,
}">
<ClientOnly >
<Vueform endpoint="/api/test" method="POST" view="tabs" style="padding:30px" >
<StaticElement tag="h4" align="center" content="个人信息" name="static" />
<TextElement name="username" label="昵称" placeholder="昵称" :columns="{ container: 4, label: 3, wrapper: 12 }" rules="required|min:3"/>
<PhoneElement name="phone" allow-incomplete unmask default="+86" :include="['cn']" label="手机号" placeholder="+86" :columns="{ container: 12, label: 1, wrapper: 4 }" rules="required"/>
<TextElement name="code" label="验证码" placeholder="xxxxxx" :columns="{ container: 4, label: 3, wrapper: 12 }" rules="required"/>
<ButtonElement name="button" :columns="{ container: 8, label: 3, wrapper: 12 }" button-label="发送验证码"/>
<StaticElement tag="h4" align="center" content="项目详情" name="static" />
<TextElement name="name" label="项目名称" placeholder="项目名称" :columns="{ container: 4, label: 4, wrapper: 12 }" rules="required"/>
<SelectElement
name="select"
default="辽宁一区"
label="地区"
:native="false"
:items="[
'辽宁一区',
'辽宁二区',
'江西一区',
]"
:columns="{ container: 4, label: 3, wrapper: 12 }"
rules="required"
/>
<RadiogroupElement
default="2 Core"
label="CPU核心数"
name="cpu"
:items="['2 Core', '4 Core', '6 Core','More']"
view="tabs"
/>
<RadiogroupElement
<!--立即申请页面-->
<Head>
<Title>立即申请</Title>
<Meta name="description" />
</Head>
<div
style="width: 50%; margin: auto"
:style="{
boxShadow: `var(--el-box-shadow-light)`,
}"
>
<ClientOnly>
<Vueform
endpoint="/api/test"
method="POST"
view="tabs"
style="padding: 30px"
>
<StaticElement
tag="h4"
align="center"
content="个人信息"
name="static"
/>
<TextElement
name="username"
label="昵称"
placeholder="昵称"
:columns="{ container: 4, label: 3, wrapper: 12 }"
rules="required|min:3"
/>
<PhoneElement
name="phone"
allow-incomplete
unmask
default="+86"
:include="['cn']"
label="手机号"
placeholder="+86"
:columns="{ container: 12, label: 1, wrapper: 4 }"
rules="required"
/>
<TextElement
name="code"
label="验证码"
placeholder="xxxxxx"
:columns="{ container: 4, label: 4, wrapper: 12 }"
rules="required"
/>
<ButtonElement
name="button"
:columns="{ container: 8, label: 3, wrapper: 12 }"
button-label="发送验证码"
/>
<StaticElement
tag="h4"
align="center"
content="项目详情"
name="static"
/>
<TextElement
name="name"
label="项目名称"
placeholder="项目名称"
:columns="{ container: 4, label: 4, wrapper: 12 }"
rules="required"
/>
<SelectElement
name="select"
default="辽宁一区"
label="地区"
:native="false"
:items="['辽宁一区', '辽宁二区', '江西一区']"
:columns="{ container: 4, label: 3, wrapper: 12 }"
rules="required"
/>
<RadiogroupElement
default="2 Core"
label="CPU核心数"
name="cpu"
:items="['2 Core', '4 Core', '6 Core', 'More']"
view="tabs"
/>
<RadiogroupElement
default="2 GB"
label="RAM容量"
name="ram"
:items="['2 GB', '4 GB', '6 GB','More']"
:items="['2 GB', '4 GB', '6 GB', 'More']"
view="tabs"
/>
<SliderElement
name="disk"
label="磁盘容量"
:default="5"
:min="1"
:max="40"
:format="(v: number) => v > 1 ? `${Math.round(v)} GB` : '1 GB'"
:add-classes="{
ElementLayout: {
innerWrapper: 'mt-12'
}
}"
/>
<EditorElement
name="usage"
label="用途说明"
rules="required|max:500"
name="disk"
label="磁盘容量"
:default="5"
:min="1"
:max="40"
:format="(v: number) => v > 1 ? `${Math.round(v)} GB` : '1 GB'"
:add-classes="{
ElementLayout: {
innerWrapper: 'mt-12',
},
}"
/>
<ButtonElement name="submit" button-label="提交申请" align="center" size="lg" submits/>
</Vueform>
</ClientOnly>
<EditorElement name="usage" label="用途说明" rules="required|max:500" />
</div>
<ButtonElement
name="submit"
button-label="提交申请"
align="center"
size="lg"
submits
/>
</Vueform>
</ClientOnly>
</div>
</template>
<script setup lang="ts">
import PhoneElemen from '@vueform/vueform'
const form = ref()
import PhoneElemen from "@vueform/vueform";
const form = ref();
</script>

View File

@ -1,15 +1,63 @@
<template>
<!--登录界面-->
<Head>
<Title>登录界面</Title>
<Meta name="description"/>
</Head>
<div>
<el-text class="mx-1" type="primary" >
Primary
</el-text>
</div>
<!--登录界面-->
<Head>
<Title>登录界面</Title>
<Meta name="description" />
</Head>
<div
style="width: 25%; margin: auto"
:style="{
boxShadow: `var(--el-box-shadow-light)`,
}"
>
<ClientOnly>
<Vueform
endpoint="/api/test"
method="POST"
view="tabs"
style="padding: 30px"
>
<StaticElement tag="h4" align="center" content="登录" name="static" />
<PhoneElement
input-type="number"
name="phone"
allow-incomplete
unmask
default="+86"
:include="['cn']"
label="手机号"
placeholder="+86"
:columns="{ container: 12, label: 3, wrapper: 10 }"
rules="required|min:14|max:14"
/>
<TextElement
input-type="password"
name="password"
label="密码"
rules="required|min:8|max:16|confirmed"
:columns="{ container: 12, label: 3, wrapper: 10 }"
/>
<CheckboxElement
name="policy"
label="用户协议"
rules="required"
message="您必须同意用户政策才能继续"
>
已阅读并同意<ElLink href="/" target="_blank">用户协议</ElLink>
</CheckboxElement>
<CheckboxElement name="news"> 希望获取最新资讯 </CheckboxElement>
<ButtonElement
name="submit"
button-label="登录"
align="center"
size="lg"
submits
/>
</Vueform>
</ClientOnly>
</div>
</template>
<script setup lang="ts">
import PhoneElemen from "@vueform/vueform";
const form = ref();
</script>

View File

@ -1,34 +1,211 @@
<template>
<!--注册页面-->
<Head>
<Title>注册页面</Title>
<Meta name="description"/>
</Head>
<div style="width:50%;margin:auto;" :style="{
boxShadow: `var(--el-box-shadow-light)`,
}">
<ClientOnly >
<Vueform endpoint="/api/test" method="POST" view="tabs" style="padding:30px" >
<StaticElement tag="h4" align="center" content="个人信息" name="static" />
<TextElement name="username" allow-incomplete label="昵称" placeholder="昵称" :columns="{ container: 4, label: 3, wrapper: 8 }" rules="required|min:3|max:64"/>
<PhoneElement input-type="number" name="phone" allow-incomplete unmask default="+86" :include="['cn']" label="手机号" placeholder="+86" :columns="{ container: 12, label: 1, wrapper: 4 }" rules="required|max:6|min:6"/>
<TextElement name="code" label="验证码" placeholder="xxxxxx" :columns="{ container: 4, label: 3, wrapper: 12 }" rules="required"/>
<ButtonElement name="button" :columns="{ container: 8, label: 3, wrapper: 12 }" button-label="发送验证码"/>
<TextElement input-type="password" name="password" label="密码" rules="required|min:8|max:16|confirmed"/>
<TextElement input-type="password" name="password_confirmation" label="确认密码" rules="required|min:8|max:16"/>
<CheckboxElement name="policy" label="用户协议" rules="required" message="您必须同意用户政策才能继续">
已阅读并同意<ElLink href="/" target="_blank">用户协议</ElLink>
</CheckboxElement>
<CheckboxElement name="news">
希望获取最新资讯
</CheckboxElement>
<ButtonElement name="submit" button-label="提交申请" align="center" size="lg" submits/>
</Vueform>
</ClientOnly>
</div>
</template>
<script setup lang="ts">
import PhoneElemen from '@vueform/vueform'
const form = ref()
</script>
<!--注册页面-->
<Head>
<Title>注册页面</Title>
<Meta name="description" />
</Head>
<div
style="width: 25%; margin: auto"
:style="{
boxShadow: `var(--el-box-shadow-light)`,
}"
>
<ClientOnly>
<Vueform
endpoint="/api/test"
method="POST"
view="tabs"
:model-value="form"
@update:model-value="form = $event"
validate-on="change"
style="padding: 30px"
>
<StaticElement tag="h4" align="center" content="注册" name="static" />
<TextElement
ref="username$"
name="username"
allow-incomplete
label="昵称"
placeholder="昵称"
:columns="{ container: 12, label: 3, wrapper: 8 }"
rules="required|min:3|max:64"
@blur="Check()"
/>
<PhoneElement
ref="phone$"
input-type="number"
name="phone"
allow-incomplete
unmask
default="+86"
:include="['cn']"
label="手机号"
placeholder="+86"
:columns="{ container: 12, label: 3, wrapper: 10 }"
rules="required|min:14|max:14"
@blur="Check()"
:disabled="phoneChecked"
/>
<TextElement
name="code"
label="验证码"
placeholder="xxxxxx"
rules="required|max:6|min:6"
:columns="{ container: 8, label: 5, wrapper: 12 }"
@blur="CheckCode()"
:disabled="!codeBtnRef.disabled || phoneChecked"
ref="code$"
/>
<ButtonElement
name="button"
:columns="{ container: 4, label: 3, wrapper: 12 }"
@click="Send()"
:button-label="codeBtnRef.text"
:disabled="codeBtnRef.disabled || phoneChecked"
/>
<TextElement
input-type="password"
name="password"
label="密码"
rules="required|min:8|max:16|confirmed"
:columns="{ container: 12, label: 3, wrapper: 10 }"
/>
<TextElement
input-type="password"
name="password_confirmation"
label="确认密码"
rules="required|min:8|max:16"
:columns="{ container: 12, label: 3, wrapper: 10 }"
/>
<CheckboxElement
name="policy"
label="用户协议"
rules="required"
message="您必须同意用户政策才能继续"
>
已阅读并同意<ElLink href="/" target="_blank">用户协议</ElLink>
</CheckboxElement>
<CheckboxElement name="news"> 希望获取最新资讯 </CheckboxElement>
<ButtonElement
:disabled="canSubmit"
name="submit"
button-label="注册"
align="center"
size="lg"
submits
/><ButtonElement
name="reset"
danger
button-label="重置"
resets
size="sm"
/>
</Vueform>
</ClientOnly>
</div>
</template>
<script setup lang="ts">
import PhoneElemen from "@vueform/vueform";
const form = ref({});
const phoneChecked = ref(false);
const codeBtnRef = ref({ text: "发送验证码", color: "", disabled: false });
const username$ = ref(null);
const phone$ = ref(null);
const code$ = ref(null);
const canSubmit = ref(false);
function Check() {
const query = {
username: form.value.username,
phone: form.value.phone.replace("+86", ""),
};
if (!/^(?!1(4|7)\d{9})1[3-9]\d{9}$/.test(query.phone)) {
phone$.value.messageBag.clear("errors");
phone$.value.messageBag.append("手机号格式错误" + query.phone);
canSubmit.value = true;
return;
} else {
phone$.value.messageBag = [];
}
$fetch("/api/test/reg", {
method: "POST",
body: query,
}).then((res) => {
if (!res.phone) {
phone$.value.messageBag.append("手机号已经注册");
canSubmit.value = true;
} else {
canSubmit.value = false;
}
if (!res.username) {
username$.value.messageBag.append("昵称已存在");
canSubmit.value = true;
} else {
canSubmit.value = false;
}
});
}
function Send() {
Check();
if (!canSubmit.value) {
$fetch("/api/test/sms", {
method: "POST",
body: {
phone: form.value.phone.replace("+86", ""),
},
}).then((res) => {
switch (res.code) {
case 0:
ElMessage({ message: res.msg + "\n" + res.error, type: "error" });
break;
case 1:
ElMessage({ message: res.msg, type: "success" });
codeBtnRef.value.disabled = true;
codeBtnRef.value.text = "请查收";
break;
case 2:
ElMessage({ message: res.msg, type: "warning" });
codeBtnRef.value.disabled = true;
codeBtnRef.value.text = "请查收";
break;
default:
break;
}
});
}
console.info(form.value.phone.replace("+86", ""));
}
function CheckCode() {
if (!code$.value.invalid) {
$fetch("/api/test/code", {
method: "POST",
body: {
phone: form.value.phone.replace("+86", ""),
code: form.value.code,
},
}).then((res) => {
switch (res.code) {
case 0:
ElMessage({
message: res.msg,
type: "error",
});
break;
case 1:
ElMessage({ message: res.msg, type: "success" });
codeBtnRef.value.disabled = true;
codeBtnRef.value.text = "已验证";
phoneChecked.value = ture;
break;
case 2:
ElMessage({ message: res.msg, type: "warning" });
codeBtnRef.value.disabled = false;
codeBtnRef.value.text = "发送验证码";
break;
default:
ElMessage({ message: res.msg, type: "warning" });
break;
}
});
}
}
</script>

6
pages/user/index.vue Normal file
View File

@ -0,0 +1,6 @@
<template>
<Head>
<Title>用户信息</Title>
<Meta name="description" />
</Head>
</template>

View File

@ -18,6 +18,11 @@ model User {
applications Application[]
}
model Adminer {
id Int @id @default(autoincrement())
adminId Int
}
model Application {
id Int @id @default(autoincrement())
name String
@ -30,3 +35,9 @@ model Application {
applicant User @relation(fields: [applicantId], references: [id])
applicantId Int
}
model Register {
id Int @id @default(autoincrement())
phone String
deadline DateTime
code String
}

27
server/api/test/code.ts Normal file
View File

@ -0,0 +1,27 @@
import { PrismaClient } from "@prisma/client";
const db = new PrismaClient();
export default defineEventHandler(async (event) => {
const body = await readBody(event);
const register = (await db.register.findFirst({
where: {
phone: body.phone,
},
orderBy: {
deadline: "desc", // 'asc' 表示升序,'desc' 表示降序
},
})) || {
deadline: new Date(),
};
const deadlineDate = new Date(register.deadline);
const now = new Date();
if (now > deadlineDate) {
return { code: 2, msg: "验证码超时了,请重新发送" };
} else if (register.code != body.code) {
console.log(body.code + " phone " + register.phone + " " + register.code);
return { code: 0, msg: "验证码错误" };
} else if (register.code == body.code) {
return { code: 1, msg: "验证码正确" };
} else {
return { code: -1, msg: "未知错误" };
}
});

View File

@ -1,9 +1,38 @@
import { PrismaClient } from "@prisma/client"
import { PrismaClient } from "@prisma/client";
const db = new PrismaClient()
const db = new PrismaClient();
export default defineEventHandler(async(event) => {
const body = await readBody(event)
console.info(body)
return body
})
export default defineEventHandler(async (event) => {
const body = await readBody(event);
console.info(body);
let isSend =
(await db.register.findFirst({
where: {
phone: body.phone,
},
})) != null;
if (isSend) {
const register = (await db.register.findFirst({
where: {
phone: body.phone,
},
orderBy: {
deadline: "desc", // 'asc' 表示升序,'desc' 表示降序
},
})) || {
deadline: new Date(),
};
const deadlineDate = new Date(register.deadline);
const now = new Date();
if (now <= deadlineDate) {
return { code: 2, msg: "三分钟内请勿重发送" };
} else {
return {
code: -1,
msg:
"发送成功" + now + "<=" + deadlineDate + ":" + (now <= deadlineDate),
};
}
}
return isSend;
});

19
server/api/test/reg.ts Normal file
View File

@ -0,0 +1,19 @@
import { PrismaClient } from "@prisma/client";
const db = new PrismaClient();
export default defineEventHandler(async (event) => {
const body = await readBody(event);
return {
username:
(await db.user.findFirst({
where: {
username: body.username,
},
})) == null,
phone:
(await db.user.findFirst({
where: {
phone: body.phone,
},
})) == null,
};
});

93
server/api/test/sms.ts Normal file
View File

@ -0,0 +1,93 @@
import { PrismaClient } from "@prisma/client";
import qmAPI from "quanmsms";
const config = useRuntimeConfig();
const smsApi = new qmAPI(config.apiOpenid.toString(), {
sms: {
apiKey: config.apiApikey,
},
});
const db = new PrismaClient();
function generateRandomCode(length: number) {
let code = "";
//const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';复杂验证码
const possible = "0123456789";
for (let i = 0; i < length; i++) {
code += possible.charAt(Math.floor(Math.random() * possible.length));
}
return code;
}
function generateDeadline() {
const now = new Date(); // 获取当前时间
const threeMinutesLater = new Date(now.getTime() + 3.5 * 60 * 1000); // 加上3分钟
return threeMinutesLater.toISOString(); // 转换为ISO格式字符串
}
function send(telstr: string) {
let tel = parseInt(telstr);
const code = generateRandomCode(6);
let param = {
tel: tel,
model_id: config.apiSmsId.toString(),
model_args: {
//模板id对应的变量key值,object格式内部自动处理成平台要求的str
code: code,
// 如果你的模板没变量,该值可为空
},
};
return smsApi
.sendSMS(param)
.then((response: { code: number; state: number }) => {
if (response.state == 200) {
db.register
.create({
data: {
phone: telstr,
deadline: generateDeadline(),
code: code,
},
})
.then((reg) => {
if (reg.phone == telstr) {
console.info(telstr + "发送成功");
}
});
return { code: 1, msg: "发送成功" };
} else {
console.info(tel);
return { code: 0, msg: "发送失败", error: response };
}
// 你的业务代码
});
}
export default defineEventHandler(async (event) => {
const body = await readBody(event);
let isSend =
(await db.register.findFirst({
where: {
phone: body.phone,
},
})) != null;
if (isSend) {
const register = (await db.register.findFirst({
where: {
phone: body.phone,
},
orderBy: {
deadline: "desc", // 'asc' 表示升序,'desc' 表示降序
},
})) || {
deadline: new Date(),
};
const deadlineDate = new Date(register.deadline);
const now = new Date();
if (now <= deadlineDate) {
return { code: 2, msg: "三分钟内请勿重发送" };
} else {
return send(body.phone);
}
} else {
return send(body.phone);
}
});

View File

@ -4858,6 +4858,11 @@ proxy-from-env@^1.1.0:
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
quanmsms@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/quanmsms/-/quanmsms-1.0.2.tgz#8e7748329f87fd5f5550fe8eb94b6ab11a54ff90"
integrity sha512-wY4A2tz5ZANlgJw4t3fTgmcRpXO6FkyAxSfipdWa8DZxtfbItjQ4BErlugiEmL0sB2cTwJG9P3w5NT7i56T11Q==
queue-microtask@^1.2.2:
version "1.2.3"
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"