init project
This commit is contained in:
commit
8979cfd613
|
|
@ -0,0 +1,7 @@
|
|||
ENV = 'development'
|
||||
|
||||
VITE_CLI_PORT = 3000
|
||||
VITE_SERVER_PORT = 8888
|
||||
VITE_BASE_API = /api
|
||||
VITE_FILE_API = /api
|
||||
VITE_BASE_PATH = http://127.0.0.1
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
ENV = 'production'
|
||||
|
||||
VITE_CLI_PORT = 3000
|
||||
VITE_SERVER_PORT = 8888
|
||||
VITE_BASE_API = /api
|
||||
VITE_FILE_API = /api
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 基于编辑器的 HTTP 客户端请求
|
||||
/httpRequests/
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="JavaScriptLibraryMappings">
|
||||
<includedPredefinedLibrary name="Node.js Core" />
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/mobile-webapp.iml" filepath="$PROJECT_DIR$/.idea/mobile-webapp.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Vite App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"name": "mobile-webapp",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "vite --host --mode development",
|
||||
"build": "vite build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vitejs/plugin-legacy": "^5.4.1",
|
||||
"axios": "^1.7.3",
|
||||
"dotenv": "^16.4.5",
|
||||
"js-md5": "^0.8.3",
|
||||
"pinia": "^2.2.1",
|
||||
"vant": "^4.8.5",
|
||||
"vue": "^3.4.21",
|
||||
"vue-router": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vant/auto-import-resolver": "^1.1.0",
|
||||
"@vitejs/plugin-vue": "^4.3.4",
|
||||
"less": "^4.1.3",
|
||||
"unplugin-auto-import": "^0.17.5",
|
||||
"unplugin-vue-components": "^0.26.0",
|
||||
"vite": "^5.1.6"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
|
||||
<template>
|
||||
<div id="app">
|
||||
<RouterView />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, toRefs } from 'vue'
|
||||
import { useRouter, RouterView } from 'vue-router'
|
||||
const router = useRouter()
|
||||
const state = reactive({
|
||||
transitionName: 'slide-left'
|
||||
})
|
||||
router.beforeEach((to, from) => {
|
||||
if (to.meta.index > from.meta.index) {
|
||||
state.transitionName = 'slide-left' // 向左滑动
|
||||
} else if (to.meta.index < from.meta.index) {
|
||||
// 由次级到主级
|
||||
state.transitionName = 'slide-right'
|
||||
} else {
|
||||
state.transitionName = '' // 同级无过渡效果
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
html, body {
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
#app {
|
||||
height: 100%;
|
||||
font-family: 'Avenir', Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
// text-align: center;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.router-view{
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin: 0 auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.slide-right-enter-active,
|
||||
.slide-right-leave-active,
|
||||
.slide-left-enter-active,
|
||||
.slide-left-leave-active{
|
||||
height: 100%;
|
||||
will-change: transform;
|
||||
transition: all 500ms;
|
||||
position: absolute;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
.slide-right-enter{
|
||||
opacity: 0;
|
||||
transform: translate3d(-100%, 0, 0);
|
||||
}
|
||||
.slide-right-leave-active{
|
||||
opacity: 0;
|
||||
transform: translate3d(100%, 0, 0);
|
||||
}
|
||||
.slide-left-enter{
|
||||
opacity: 0;
|
||||
transform: translate3d(100%, 0, 0);
|
||||
}
|
||||
.slide-left-leave-active{
|
||||
opacity: 0;
|
||||
transform: translate3d(-100%, 0, 0);
|
||||
}
|
||||
|
||||
.van-badge--fixed {
|
||||
z-index: 1000;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import service from '@/utils/request'
|
||||
|
||||
export const login = (data) => {
|
||||
return service({
|
||||
url: '/base/login',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export const register = (data) => {
|
||||
return service({
|
||||
url: '/register',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
<template>
|
||||
<div class="img-verify">
|
||||
<canvas ref="verify" :width="state.width" :height="state.height" @click="handleDraw"></canvas>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, onMounted, ref } from 'vue'
|
||||
const verify = ref(null)
|
||||
const state = reactive({
|
||||
pool: 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789', // 字符串
|
||||
width: 120,
|
||||
height: 40,
|
||||
imgCode: ''
|
||||
})
|
||||
defineExpose({ state })
|
||||
onMounted(() => {
|
||||
// 初始化绘制图片验证码
|
||||
state.imgCode = draw()
|
||||
})
|
||||
|
||||
// 点击图片重新绘制
|
||||
const handleDraw = () => {
|
||||
state.imgCode = draw()
|
||||
}
|
||||
|
||||
// 随机数
|
||||
const randomNum = (min, max) => {
|
||||
return parseInt(Math.random() * (max - min) + min)
|
||||
}
|
||||
// 随机颜色
|
||||
const randomColor = (min, max) => {
|
||||
const r = randomNum(min, max)
|
||||
const g = randomNum(min, max)
|
||||
const b = randomNum(min, max)
|
||||
return `rgb(${r},${g},${b})`
|
||||
}
|
||||
|
||||
// 绘制图片
|
||||
const draw = () => {
|
||||
// 3.填充背景颜色,背景颜色要浅一点
|
||||
const ctx = verify.value.getContext('2d')
|
||||
// 填充颜色
|
||||
ctx.fillStyle = randomColor(180, 230)
|
||||
// 填充的位置
|
||||
ctx.fillRect(0, 0, state.width, state.height)
|
||||
// 定义paramText
|
||||
let imgCode = ''
|
||||
// 4.随机产生字符串,并且随机旋转
|
||||
for (let i = 0; i < 4; i++) {
|
||||
// 随机的四个字
|
||||
const text = state.pool[randomNum(0, state.pool.length)]
|
||||
imgCode += text
|
||||
// 随机的字体大小
|
||||
const fontSize = randomNum(18, 40)
|
||||
// 字体随机的旋转角度
|
||||
const deg = randomNum(-30, 30)
|
||||
/*
|
||||
* 绘制文字并让四个文字在不同的位置显示的思路 :
|
||||
* 1、定义字体
|
||||
* 2、定义对齐方式
|
||||
* 3、填充不同的颜色
|
||||
* 4、保存当前的状态(以防止以上的状态受影响)
|
||||
* 5、平移translate()
|
||||
* 6、旋转 rotate()
|
||||
* 7、填充文字
|
||||
* 8、restore出栈
|
||||
* */
|
||||
ctx.font = fontSize + 'px Simhei'
|
||||
ctx.textBaseline = 'top'
|
||||
ctx.fillStyle = randomColor(80, 150)
|
||||
/*
|
||||
* save() 方法把当前状态的一份拷贝压入到一个保存图像状态的栈中。
|
||||
* 这就允许您临时地改变图像状态,
|
||||
* 然后,通过调用 restore() 来恢复以前的值。
|
||||
* save是入栈,restore是出栈。
|
||||
* 用来保存Canvas的状态。save之后,可以调用Canvas的平移、放缩、旋转、错切、裁剪等操作。 restore:用来恢复Canvas之前保存的状态。防止save后对Canvas执行的操作对后续的绘制有影响。
|
||||
*
|
||||
* */
|
||||
ctx.save()
|
||||
ctx.translate(30 * i + 15, 15)
|
||||
ctx.rotate((deg * Math.PI) / 180)
|
||||
// fillText() 方法在画布上绘制填色的文本。文本的默认颜色是黑色。
|
||||
// 请使用 font 属性来定义字体和字号,并使用 fillStyle 属性以另一种颜色/渐变来渲染文本。
|
||||
// context.fillText(text,x,y,maxWidth);
|
||||
ctx.fillText(text, -15 + 5, -15)
|
||||
ctx.restore()
|
||||
}
|
||||
// 5.随机产生5条干扰线,干扰线的颜色要浅一点
|
||||
for (let i = 0; i < 5; i++) {
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(randomNum(0, state.width), randomNum(0, state.height))
|
||||
ctx.lineTo(randomNum(0, state.width), randomNum(0, state.height))
|
||||
ctx.strokeStyle = randomColor(180, 230)
|
||||
ctx.closePath()
|
||||
ctx.stroke()
|
||||
}
|
||||
// 6.随机产生40个干扰的小点
|
||||
for (let i = 0; i < 40; i++) {
|
||||
ctx.beginPath()
|
||||
ctx.arc(randomNum(0, state.width), randomNum(0, state.height), 1, 0, 2 * Math.PI)
|
||||
ctx.closePath()
|
||||
ctx.fillStyle = randomColor(150, 200)
|
||||
ctx.fill()
|
||||
}
|
||||
return imgCode
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.img-verify canvas {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import { createApp } from 'vue';
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from '@/router/index'
|
||||
|
||||
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
app.mount('#app');
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: [{
|
||||
path: '/',
|
||||
redirect: '/login'
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
component: () => import('@/views/login/index.vue')
|
||||
}]
|
||||
})
|
||||
|
||||
export default router
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
export const getLocal = (name) => {
|
||||
return localStorage.getItem(name)
|
||||
}
|
||||
|
||||
export const setLocal = (name, value) => {
|
||||
localStorage.setItem(name, value)
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
import axios from 'axios'
|
||||
import {showDialog, showFailToast} from 'vant'// 引入axios
|
||||
import router from '@/router/index'
|
||||
|
||||
const service = axios.create({
|
||||
baseURL: import.meta.env.VITE_BASE_API,
|
||||
// withCredentials: true, // 跨域请求时是否需要使用凭证
|
||||
timeout: 99999
|
||||
})
|
||||
|
||||
service.interceptors.request.use(
|
||||
config => { // 在发送请求之前做些什么
|
||||
config.headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Token': localStorage.getItem('token') || '',
|
||||
...config.headers
|
||||
}
|
||||
return config
|
||||
},
|
||||
error => { // 对请求错误做些什么
|
||||
showDialog({
|
||||
title: '错误提示',
|
||||
message: error.message
|
||||
}).then(r => console.log(r))
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
service.interceptors.response.use(
|
||||
response => { // 对响应数据做点什么
|
||||
if (typeof response.data !== 'object') {
|
||||
showFailToast('服务端异常!')
|
||||
return Promise.reject(response)
|
||||
}
|
||||
if (response.data.code !== 200) {
|
||||
if (response.data.message) showFailToast(response.data.message)
|
||||
if (response.data.code === 416) {
|
||||
router.push({ path: '/login' })
|
||||
}
|
||||
if (response.data.token && window.location.hash === '#/login') {
|
||||
localStorage.setItem('token', response.data.token)
|
||||
axios.defaults.headers['X-Token'] = response.data.token
|
||||
}
|
||||
return Promise.reject(response.data)
|
||||
}
|
||||
return response.data
|
||||
},
|
||||
error => { // 对响应错误做点什么
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
export default service
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<script setup>
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
||||
</style>
|
||||
|
|
@ -0,0 +1,196 @@
|
|||
<template>
|
||||
<div class="login">
|
||||
<img class="logo" src="https://s.yezgea02.com/1604045825972/newbee-mall-vue3-app-logo.png" alt="">
|
||||
<div v-if="state.type === 'login'" class="login-body login">
|
||||
<van-form @submit="onSubmit">
|
||||
<van-field
|
||||
v-model="state.username"
|
||||
name="username"
|
||||
label="用户名"
|
||||
placeholder="用户名"
|
||||
:rules="[{ required: true, message: '请填写用户名' }]"
|
||||
/>
|
||||
<van-field
|
||||
v-model="state.password"
|
||||
type="password"
|
||||
name="password"
|
||||
label="密码"
|
||||
placeholder="密码"
|
||||
:rules="[{ required: true, message: '请填写密码' }]"
|
||||
/>
|
||||
<van-field
|
||||
center
|
||||
clearable
|
||||
label="验证码"
|
||||
placeholder="输入验证码"
|
||||
v-model="state.verify"
|
||||
>
|
||||
<template #button>
|
||||
<vue-img-verify ref="verifyRef" />
|
||||
</template>
|
||||
</van-field>
|
||||
<div style="margin: 16px;">
|
||||
<div class="link-register" @click="toggle('register')">立即注册</div>
|
||||
<van-button round block color="#1baeae" native-type="submit">登录</van-button>
|
||||
</div>
|
||||
</van-form>
|
||||
</div>
|
||||
<div v-else class="login-body register">
|
||||
<van-form @submit="onSubmit">
|
||||
<van-field
|
||||
v-model="state.registerUser"
|
||||
name="username1"
|
||||
label="用户名"
|
||||
placeholder="用户名"
|
||||
:rules="[{ required: true, message: '请填写用户名' }]"
|
||||
/>
|
||||
<van-field
|
||||
v-model="state.registerPassword"
|
||||
type="password"
|
||||
name="password1"
|
||||
label="密码"
|
||||
placeholder="密码"
|
||||
:rules="[{ required: true, message: '请填写密码' }]"
|
||||
/>
|
||||
<van-field
|
||||
center
|
||||
clearable
|
||||
label="验证码"
|
||||
placeholder="输入验证码"
|
||||
v-model="state.verify"
|
||||
>
|
||||
<template #button>
|
||||
<vue-img-verify ref="verifyRef" />
|
||||
</template>
|
||||
</van-field>
|
||||
<div style="margin: 16px;">
|
||||
<div class="link-login" @click="toggle('login')">已有登录账号</div>
|
||||
<van-button round block color="#1baeae" native-type="submit">注册</van-button>
|
||||
</div>
|
||||
</van-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
|
||||
import { reactive, ref } from 'vue'
|
||||
import vueImgVerify from '@/components/VueImageVerify.vue'
|
||||
import { showFailToast, showSuccessToast } from 'vant'
|
||||
import md5 from 'js-md5'
|
||||
|
||||
import { login, register } from '@/api/user'
|
||||
import { setLocal } from '@/utils/common'
|
||||
|
||||
|
||||
const verifyRef = ref(null)
|
||||
const state = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
registerUser: '',
|
||||
registerPassword: '',
|
||||
type: 'login',
|
||||
imgCode: '',
|
||||
verify: ''
|
||||
})
|
||||
|
||||
// 切换登录和注册两种模式
|
||||
const toggle = (v) => {
|
||||
state.type = v
|
||||
state.verify = ''
|
||||
}
|
||||
|
||||
// 提交登录或注册表单
|
||||
const onSubmit = async (values) => {
|
||||
state.imgCode = verifyRef.value.state.imgCode || ''
|
||||
if (state.verify.toLowerCase() !== state.imgCode.toLowerCase()) {
|
||||
showFailToast('验证码有误')
|
||||
return
|
||||
}
|
||||
if (state.type === 'login') {
|
||||
const { data } = await login({
|
||||
username: values.username,
|
||||
passwordMd5: md5(values.password)
|
||||
})
|
||||
setLocal('token', data)
|
||||
// 需要刷新页面,否则 router/index.js 文件里的 token 不会被重置
|
||||
window.location.href = '/'
|
||||
} else {
|
||||
await register({
|
||||
"loginName": values.username1,
|
||||
"password": values.password1
|
||||
})
|
||||
showSuccessToast('注册成功')
|
||||
state.type = 'login'
|
||||
state.verify = ''
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.login {
|
||||
.logo {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
display: block;
|
||||
margin: 80px auto 20px;
|
||||
}
|
||||
.login-body {
|
||||
padding: 0 20px;
|
||||
}
|
||||
.login {
|
||||
.link-register {
|
||||
font-size: 14px;
|
||||
margin-bottom: 20px;
|
||||
color: #1989fa;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
.register {
|
||||
.link-login {
|
||||
font-size: 14px;
|
||||
margin-bottom: 20px;
|
||||
color: #1989fa;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
.verify-bar-area {
|
||||
margin-top: 24px;
|
||||
.verify-left-bar {
|
||||
border-color: #1baeae;
|
||||
}
|
||||
.verify-move-block {
|
||||
background-color: #1baeae;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
.verify {
|
||||
>div {
|
||||
width: 100%;
|
||||
}
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
.cerify-code-panel {
|
||||
margin-top: 16px;
|
||||
}
|
||||
.verify-code {
|
||||
width: 40%!important;
|
||||
float: left!important;
|
||||
}
|
||||
.verify-code-area {
|
||||
float: left!important;
|
||||
width: 54%!important;
|
||||
margin-left: 14px!important;
|
||||
.varify-input-code {
|
||||
width: 90px;
|
||||
height: 38px!important;
|
||||
border: 1px solid #e9e9e9;
|
||||
padding-left: 10px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.verify-change-area {
|
||||
line-height: 44px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import { fileURLToPath, URL } from 'node:url'
|
||||
import { defineConfig } from 'vite'
|
||||
import vuePlugin from '@vitejs/plugin-vue'
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import { VantResolver } from 'unplugin-vue-components/resolvers'
|
||||
import * as dotenv from 'dotenv'
|
||||
import * as fs from 'fs'
|
||||
|
||||
export default defineConfig(({ command, mode}) => {
|
||||
const NODE_ENV = mode || 'development'
|
||||
const envFiles = [
|
||||
`.env.${NODE_ENV}`
|
||||
]
|
||||
for (const file of envFiles) {
|
||||
const envConfig = dotenv.parse(fs.readFileSync(file))
|
||||
for (const k in envConfig) {
|
||||
process.env[k] = envConfig[k]
|
||||
}
|
||||
}
|
||||
return {
|
||||
server: {
|
||||
port: process.env.VITE_CLI_PORT,
|
||||
proxy: {
|
||||
// 把key的路径代理到target位置
|
||||
// detail: https://cli.vuejs.org/config/#devserver-proxy
|
||||
[process.env.VITE_BASE_API]: { // 需要代理的路径 例如 '/api'
|
||||
target: `${process.env.VITE_BASE_PATH}:${process.env.VITE_SERVER_PORT}/`, // 代理到 目标路径
|
||||
changeOrigin: true,
|
||||
rewrite: path => path.replace(new RegExp('^' + process.env.VITE_BASE_API), ''),
|
||||
}
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
vuePlugin(),
|
||||
AutoImport({resolvers: [VantResolver()]}),
|
||||
Components({resolvers: [VantResolver()]})
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Loading…
Reference in New Issue