init project

This commit is contained in:
chenweijia 2024-08-13 10:09:04 +08:00
commit 8979cfd613
20 changed files with 2940 additions and 0 deletions

7
.env.development Normal file
View File

@ -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

6
.env.production Normal file
View File

@ -0,0 +1,6 @@
ENV = 'production'
VITE_CLI_PORT = 3000
VITE_SERVER_PORT = 8888
VITE_BASE_API = /api
VITE_FILE_API = /api

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
node_modules
.DS_Store
dist
dist-ssr
*.local

5
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptLibraryMappings">
<includedPredefinedLibrary name="Node.js Core" />
</component>
</project>

12
.idea/mobile-webapp.iml Normal file
View File

@ -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>

8
.idea/modules.xml Normal file
View File

@ -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>

12
index.html Normal file
View File

@ -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>

30
package.json Normal file
View File

@ -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"
}
}

84
src/App.vue Normal file
View File

@ -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>

17
src/api/user.js Normal file
View File

@ -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
})
}

View File

@ -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填充文字
* 8restore出栈
* */
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>

12
src/main.js Normal file
View File

@ -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');

16
src/router/index.js Normal file
View File

@ -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

7
src/utils/common.js Normal file
View File

@ -0,0 +1,7 @@
export const getLocal = (name) => {
return localStorage.getItem(name)
}
export const setLocal = (name, value) => {
localStorage.setItem(name, value)
}

52
src/utils/request.js Normal file
View File

@ -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

11
src/views/home/index.vue Normal file
View File

@ -0,0 +1,11 @@
<script setup>
</script>
<template>
</template>
<style scoped lang="less">
</style>

196
src/views/login/index.vue Normal file
View File

@ -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>

45
vite.config.js Normal file
View File

@ -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))
}
}
}
})

2296
yarn.lock Normal file

File diff suppressed because it is too large Load Diff