git多人合作

git实现多人合作

整体思路

  • 1、首先需要新建一个组织,组织中新建一个仓库,组织中新建一个team。
  • 2、队员们frok组织中的仓库,此时队员自身账号中的仓库是fork仓库,而组织中的仓库为原仓库
  • 3、队员们clone自己的fork仓库到本地目录
  • 4、在本地目录git remote add upstream <组织仓库名>
  • 5、队员修改文件后,通过git add git commit git push到自己的仓库
  • 6、队员申请pull request
  • 7、仓库合并队员的分支
  • 8、其他队员git pull upstream main,将仓库中的最新文件合并到本地目录

详细过程

https://www.cnblogs.com/thousfeet/p/7840932.html
https://www.cnblogs.com/eyunhua/p/13215936.html

原理

图 0

数据获取与删除前后端

数据获取与删除前后端

数据获取

下图所示页面是页面挂载时的场景,场景值为0;
所以当页面挂载前(onBeforeMount),和监听(watch)到场景切换为0的时候需要进行数据获取。

图 0

实现思路

  • 后端创建view用于获取数据,并设置urls
  • 在组件中定义一个数组类型的变量名为dataShowVar,用于存储需要展示的数据
  • 封装获取数据的api
  • 在组件中定义获取数据的函数getData
  • 在watch监听scene中和onBeforeMount中调用GetData函数
  • 在table中设置:data=”dataShowVar”,设置el-table-column的prop属性

    要注意前端通过api传给后端的数据要和后端设置好的数据格式一致,eg后端要求传入json格式,前端需要加工好需要的json,然后让如请求体传给后端。

后端代码

  • view定义视图
  • urls定义访问接口
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    # 获取数据
    @csrf_exempt
    def get_data(request):
    if request.method == 'GET':
    data = AnomalyData.objects.all()
    data_list = []
    for item in data:
    data_list.append({
    'id': item.id,
    'name': item.name,
    'type': item.type,
    'date': item.date,
    'intensity_file': item.intensity_file.url,
    'impact_file': item.impact_file.url,
    'attribute_file': item.attribute_file.url
    })
    return JsonResponse({'status': 'success', 'code': 200, 'data': data_list})
    return JsonResponse({'status': 'fail', 'code': 400, 'message': 'Invalid request method'})
    图 2

前端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//el-table展示数据,关键在于:data属性,prop属性
<el-table stripe :data="dataShowVar">
<el-table-column label="序号" type="index" align="center" width="90px"></el-table-column>
<el-table-column prop="name" label="异常名称" width="180" ></el-table-column>
<el-table-column prop="type" label="异常类型" width="180" ></el-table-column>
<el-table-column prop="date" label="发生时间" ></el-table-column>
</el-table>
//定义数组,必须是数组,因为el-table的:data属性接受的是Array类型
let dataShowVar = ref<any[]>([{
name:'',
type:'',
date:'',
intensity_file:'',
impact_file:'',
attribute_file:''
}])

// 获取数据的函数
// 其中reqGetData是封装好的api
const getData = async()=>{
const res = await reqGetData()
if (res.code === 200){
dataShowVar.value = res.data
}
else{
console.log('获取数据失败')
}
}
// 场景切换为0时获取数据,更新表格
watch(scene, (newVal)=>{
if (newVal === 0){
getData()
}
})
// 挂载之前获取数据
onBeforeMount(()=>{
getData()
})

删除某一条数据

table中的每一行都有一个删除按钮,点击该按钮删除对应的一条数据

  • 首先实现后端,删除操作。实质是根据id删除数据库中的一条记录
  • 前端封装删除请求的api
  • 前端要传给后端id,注意传入的格式要和后端要求一致

    由于使用的是el-table,还需要使用插槽,才能将每一条记录的id传入。以下是使用方法的介绍

1
2
3
4
5
6
7
8
9
10
11
使用了 Vue 的插槽(slot)特性。插槽是 Vue 组件中的一个重要特性,它允许你在组件模板中定义可替换的内容。

在这段代码中,<el-table-column> 组件使用了一个名为 # 的插槽。这个插槽接收一个参数 row,这个参数表示当前行的数据。然后,这个插槽的内容(即 <el-button> 和 <el-popconfirm> 组件)会被插入到 <el-table-column> 组件的模板中。

<el-button> 组件是一个按钮,它有一个 icon 属性,这个属性定义了按钮的图标。这个按钮没有 @click 事件处理器,所以点击这个按钮不会有任何效果。

<el-popconfirm> 组件是一个弹出确认框,它有一个 :title 属性,这个属性定义了确认框的标题。这个标题是一个模板字符串,它包含了 row.attrName 的值。这个组件还有一个 @confirm 事件处理器,这个处理器会在用户点击确认按钮时被调用。这个处理器调用了 deleteData 函数,并传递了 row.id 作为参数。

<el-popconfirm> 组件还使用了一个名为 reference 的插槽。这个插槽的内容(即 <el-button> 组件)会被插入到 <el-popconfirm> 组件的模板中。这个按钮有一个 icon 属性,这个属性定义了按钮的图标。点击这个按钮会弹出确认框。

总的来说,这段代码定义了一个表格,这个表格的每一行都有一个删除按钮。点击这个按钮会弹出一个确认框,确认框的标题包含了当前行的 attrName 属性的值。点击确认按钮会删除当前行的数据。

后端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# views.py
@csrf_exempt
def delete_data(request):
if request.method == 'POST':
data = json.loads(request.body)
id = data.get('id')
if not id:
return JsonResponse({'status': 'fail', 'code': 400, 'message': 'Missing required fields'})
AnomalyData.objects.filter(id=id).delete()
response_data = {
'status': 'success',
'code': 200,
'args': data, # 请求参数
'headers': dict(request.headers), # 请求头
'url': request.build_absolute_uri(), # 请求的完整 URL
}
return JsonResponse(response_data)
return JsonResponse({'status': 'fail', 'code': 400, 'message': 'Invalid request method'})

# urls.py
urlpatterns = [
re_path(r'^deletedata/?$', views.delete_data, name='anomaly'),
]

前端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 使用了插槽
<el-table stripe :data="dataShowVar">
<el-table-column label="序号" type="index" align="center" width="90px"></el-table-column>
<el-table-column prop="name" label="异常名称" width="180" ></el-table-column>
<el-table-column prop="type" label="异常类型" width="180" ></el-table-column>
<el-table-column prop="date" label="发生时间" ></el-table-column>
<el-table-column prop="operator" label="操作">
<!-- 定义一个名为#的插槽,插槽接受参数row,代表当前行的数据 -->
<template #="{ row }">
<el-button
size="small"
icon="Monitor"
@click=""
>综合</el-button>
<!--使用了泡泡,确定是否删除-->
<el-popconfirm
:title="`确定要删除${row.attrName}属性吗?`"
width="250px"
@confirm="deleteData(row.id)"
>
<template #reference>
<el-button
size="small"
icon="Delete"
>删除</el-button>
</template>
</el-popconfirm>
<el-button size="small" icon="View" @click="scene=2">展示</el-button>
</template>
</el-table-column>
</el-table>
// 删除函数
// reqDeleteData是封装好的删除请求
const deleteData = async(row : any) => {
console.log('row',row)
//删除输入的数据要符合后端要求的格式
const body_json = {'id': row}
const res = await reqDeleteData(body_json)
if (res.code === 200){
console.log('删除成功')
getData()
}
else{
console.log('删除失败')
}
}

参考文档

https://cn.vuejs.org/api/composition-api-lifecycle.html
https://element-plus.org/zh-CN/component/table.html

文件上传和表单提交的前后端构建

文件上传和表单提交的前后端构建

前端的结构

  • 定义一个upload组件
  • 在父组件中引入三个upload组件
  • 在父组件中用ref获取每一个upload组件
  • 每个upload组件中定义fileList 和uploadFlag属性,并暴露(defineExpose)
  • upload中使用:http-request=自定义上传函数(后端有对应view),书写上传逻辑
  • 上传成功后fileList等一些列属性收集到值
  • 在父组件中点击submit,收集表单数据,包括name、type、date和三个upload组件中的fileList值,并发起提交请求。
  • 当然在api文件夹中封装请求,在组件中直接调用请求即可

upload组件

使用Element-Plus的组件upload https://element-plus.org/zh-CN/component/upload.html

upload中的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<template>
<!-- upload组件 -->
<!-- action字段中的值是后端提供的 -->
<!-- :http-request="uploadData",其中uploadData是自定义的上传函数 -->
<el-upload
v-model:fileList="fileList"
accept=".zip"
class="divBoxofUpload"
action="http://127.0.0.1:8000/upload"
multiple
:show-file-list="true"
:on-preview="handlePreview"
:on-remove="handleRemove"
:before-remove="beforeRemove"
:limit="1"
:on-exceed="handleExceed"
:on-change="handleChange"
:on-success="handleSuccess"
:http-request="uploadData"
>
<el-button size="default" type="primary" style="margin-right:10px;">点击上传压缩包</el-button>
</el-upload>
</template>

<script setup lang="ts">
import { ref, toRaw } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import{ reqUploadFile } from '@/api/map/index'
import type { UploadProps } from 'element-plus'

let fileList = ref<any[]>([])
let uploadFlag = ref(false)
const handleRemove: UploadProps['onRemove'] = (file, uploadFiles) => {
console.log(file, uploadFiles)
}
// 自定义上传函数的处理逻辑,将请求后返回的文件url,和文件name赋值给fileList,再由组件传给其父组件。
const uploadData: UploadProps['httpRequest'] = async(uploadData) => {
const res = await reqUploadFile(uploadData)
if (res.code === 200) {
console.log(res.data.name,res.data.file_url)
fileList.value = [{name:res.data.name , url:res.data.file_url}]

console.log('fileList',toRaw(fileList.value))
uploadFlag.value = true
} else {
ElMessage.error('Upload failed')
}
}
//暴露出父组件要访问的两个字段。
defineExpose({
fileList,
uploadFlag
})
</script>

<style scoped>
.divBoxofUpload {
padding-left: 10px;
display: flex;
align-items: center;
}
</style>

upload的父组件中的逻辑

upload的父组件中有一个表单,也就是场景为1时的el-card

  • 用于收集信息的表单name、type、date、和三个upload组件
  • 用于提交数据的submit按钮,取消提交的取消按钮
  • submit点击后发起表单提交的请求

表单提交的逻辑:收集信息,将表单中的name、type、date、和三个upload组件的fileList收集起来,提交给后端。后端返回200表示提交成功后,清空表单数据,切换场景。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
<template>
<el-card v-if="scene === 0" class="dataShow">
<el-button type="primary" size="default" icon="Plus" style="margin-bottom:10px" @click="addData">添加数据</el-button>
<el-button type="primary" size="default" icon="Plus" style="margin-bottom:10px" @click="scene=2">抽屉</el-button>
<h3>数据列表</h3>
<el-table stripe>
<el-table-column prop="name" label="异常名称" width="180"></el-table-column>
<el-table-column prop="type" label="异常类型" width="180"></el-table-column>
<el-table-column prop="date" label="发生时间"></el-table-column>
<el-table-column prop="operator" label="操作">
<el-button icon="Monitor">综合</el-button>
<el-button icon="Delete">删除</el-button>
</el-table-column>
</el-table>
</el-card>
<el-card v-if="scene === 1" class="dataLoad">
<el-form label-width="100px">
<el-form-item label="异常名称">
<el-input v-model="dataUpLoadVar.name"></el-input>
</el-form-item>
<el-form-item label="异常类型" >
<el-select v-model="dataUpLoadVar.type">
<el-option label="火灾" value="fire"></el-option>
<el-option label="地震" value="earthquake"></el-option>
<el-option label="水华" value="algae_bloom"></el-option>
<el-option label="滑坡" value="landslide"></el-option>
<el-option label="其他" value="other"></el-option>
</el-select>
</el-form-item>
<el-form-item label="发生时间" >
<el-input type="date" v-model="dataUpLoadVar.date"></el-input>
</el-form-item>
<el-form-item label="异常强度数据" >
<div class="uploadDiv">
<upload ref="loadIntensity" ></upload>
</div>
</el-form-item>
<el-form-item label="异常影响数据" >
<div class="uploadDiv">
<upload ref="loadInfluence" ></upload>
</div>
</el-form-item>
<el-form-item label="异常属性数据" >
<div class="uploadDiv">
<upload ref="loadProperty" ></upload>
</div>
</el-form-item>
</el-form>
<div class="btnPosition">
<el-button type="primary" size="default" @click="cancel">取消</el-button>
<el-button type="primary" size="default" @click="submitData">提交</el-button>
</div>
</el-card>
<!-- ref="mainCRef" 控制子组件 -->
<mainC v-if="scene === 2" @changemainScene="changeSceneTo"></mainC>
</template>

<script setup lang="ts">
import {ref, provide} from 'vue'
import {reqSubmitData} from '@/api/map/index'
import upload from './upload/index.vue'
import mainC from '@/view/mainC/index.vue'
import { ElMessage } from 'element-plus';
let scene = ref(0)
// 父传给子的属性
// let intensityFileList:Array<any> =[];
// let impactFileList:Array<any> = [];
// let attributeFileList:Array<any> = [];
provide('pscene', scene)
//数据上传页面所需变量
let dataUpLoadVar = ref<any>({
name:'',
type:'',
date:'',
intensity:'',
influence:'',
property:''
})
// 获取子组件的实例
const loadIntensity = ref()
const loadInfluence = ref()
const loadProperty = ref()
let mainCRef = ref()
const addData = ()=>{
scene.value = 1
console.log('添加数据')
//导入本地数据

}
// 提交函数
const submitData = () => {
console.log('提交数据')
const formData = new FormData()
formData.append('name', dataUpLoadVar.value.name)
formData.append('type', dataUpLoadVar.value.type)
formData.append('date', dataUpLoadVar.value.date)
// console.log(loadIntensity.value.fileList[0].url)
// console.log(loadInfluence.value.fileList[0].url)
// console.log(loadProperty.value.fileList[0].url)
formData.append('intensity_file',loadIntensity.value.fileList[0].url)
formData.append('impact_file', loadInfluence.value.fileList[0].url)
formData.append('attribute_file', loadProperty.value.fileList[0].url)
// 不能直接console.log(FormData),需要遍历
// 打印 FormData 对象的内容
for (let [key, value] of formData.entries()) {
console.log(key, value)
}

// 提交数据
reqSubmitData(formData).then(res => {
console.log(res)
if (res.code === 200) {
console.log('提交成功')
} else {
console.log('提交失败')
ElMessage.error('提交失败')
}
})
// 清空数据
dataUpLoadVar = Object.assign(dataUpLoadVar.value, {
name: '',
type: '',
date: '',
// ...
})

//切换场景
scene.value = 0
}

const cancel = ()=>{
console.log('取消')
// 清空数据
dataUpLoadVar = Object.assign(dataUpLoadVar.value,{
name:'',
type:'',
date:'',
intensity:'',
influence:'',
property:''
})
//切换场景
scene.value = 0
}
const changeSceneTo = (num: number)=>{
scene.value = num
}
</script>

后端结构

Django

  • models.py:这个文件定义了你的数据模型。在 Django 中,一个模型代表了数据库中的一个表,模型的一个实例代表了表中的一行。模型中的每个字段都是数据库表中的一个列。
  • views.py:这个文件定义了你的视图。在 Django 中,一个视图是一个 Python 函数,它接收一个 Web 请求并返回一个 Web 响应。这个响应可以是 HTML 的内容、重定向或者 404 错误等。
  • forms.py:这个文件定义了你的表单。在 Django 中,表单是用来收集和处理用户输入的一种工具。表单可以生成 HTML 表单的代码,验证和清理用户提交的数据。
  • urls.py:这个文件定义了你的 URL 配置。在 Django 中,URL 配置是一个 URL 模式和一个视图函数之间的映射。当 Django 收到一个请求时,它会根据请求的 URL,查找匹配的 URL 模式,然后调用相应的视图函数。

models

定义两个models,也就是定义两个表,分别是uploadFile和AnomalyData

uploadFile用于保存上传的每个文件的url,AnomalyData用于保存每次提交后的表单数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from django.db import models
import uuid

def generate_unique_name():
return str(uuid.uuid4())

class upLoadFile(models.Model):
# name = models.CharField(max_length=255, default=generate_unique_name) # 文件名
file_path = models.FileField(upload_to='uploads/') # 文件路径
# size = models.IntegerField() # 文件大小
class AnomalyData(models.Model):
# upload_to 可以是一个字符串,也可以是一个函数。如果它是一个字符串,
# 那么这个字符串会被添加到你的 MEDIA_ROOT 设置的路径后面,来形成文件的完整路径。
# 例如,如果你的 MEDIA_ROOT 是 /var/www/mywebsite/media/,并且 upload_to 是 'uploads/intensity/',
# 那么上传的文件会被保存到 /var/www/mywebsite/media/uploads/intensity/ 目录。
intensity_file = models.FileField(upload_to='submit/intensity/')
impact_file = models.FileField(upload_to='submit/impact/')
attribute_file = models.FileField(upload_to='submit/attribute/')
name = models.CharField(max_length=200)
type = models.CharField(max_length=200)
date = models.DateField()

form

创建表单数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from django import forms
class SubmitForm(forms.Form):
name = forms.CharField(label='异常名称')
type = forms.ChoiceField(label='异常类型', choices=[
('fire', '火灾'),
('earthquake', '地震'),
('algae_bloom', '水华'),
('landslide', '滑坡'),
('other', '其他'),
])
date = forms.DateField(label='发生时间')
density_file = forms.FileField(label='异常强度数据')
impact_file = forms.FileField(label='异常影响数据')
attribute_file = forms.FileField(label='异常属性数据')


# 上传文件的表单
class UploadFileForm(forms.Form):
file = forms.FileField(label='文件')

views

  • 实现上传文件和提交表单的视图

    上传文件后,返回文件再文件系统中的url和名称

提交表单数据后,将数据保存到AnomalyData表中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
from django.core.files.storage import FileSystemStorage
from .models import AnomalyData, upLoadFile

# 上传文件
@csrf_exempt
def upload_file(request):
if request.method == 'POST':
uploaded_file = request.FILES['file']
# 指定了上传文件的保存路径
fs = FileSystemStorage('uploads/')
name = fs.save(uploaded_file.name, uploaded_file)
# 生成url
file_url = fs.url(name)

# 保存文件路径到数据库
upLoad_data = upLoadFile(file_path=file_url)
upLoad_data.save()

# 成功后返回必要的code,status等字段,还需返回文件的名称和file_url
return JsonResponse({
'status': 'success',
'code': 200,
'data': {
'file_url': file_url,
'name': name
}
})

return JsonResponse({
'code': 400,
'error': 'Invalid method',
'status': 'failed'
})

# 提交表单
@csrf_exempt
def submit_form(request):
# 前端提供一个FormData,从FormData中获取需要的字段值,使用request.POST.get('字段名')
if request.method == 'POST':
name = request.POST.get('name')
type = request.POST.get('type')
date = request.POST.get('date')
intensity_file = request.POST.get('intensity_file')
impact_file = request.POST.get('impact_file')
attribute_file = request.POST.get('attribute_file')
print(name, type, date, intensity_file, impact_file, attribute_file)
# 如果有一个是空的,返回提交失败
if not all([name, type, date, intensity_file, impact_file, attribute_file]):
return JsonResponse({'status': 'fail', 'code': 400, 'message': 'Missing required fields'})
# 如果表单中的所有元素都有值,保存到数据库中的AnomalyData表中
# anomaly_data是一个AnomalyData实例,每个实例代表表中的一行记录
anomaly_data = AnomalyData(name=name, type=type, date=date, intensity_file=intensity_file, impact_file=impact_file, attribute_file=attribute_file)
anomaly_data.save()
return JsonResponse({'status': 'success', 'code': 200})

urls

定义上传文件和提交表单信息的url

1
2
3
4
5
6
7
8
9
10
11
12
13
from django.contrib import admin
from django.urls import re_path
from django.urls import path
from . import views

urlpatterns = [
re_path(r'^admin/?$', admin.site.urls),
re_path(r'^register/?$', views.register , name='register'),
re_path(r'^login/?$', views.login_view, name='login'),
re_path(r'^logout/?$', views.logout_view, name='logout'),
re_path(r'^upload/?$', views.upload_file, name='upload'),
re_path(r'^submit/?$', views.submit_form, name='submit'),
]

登录注册Django后端搭建

登录注册Django后端搭建

安装Django并初始化项目

  • 安装 pip install django==2.2.28 此处指定了的版本,可以兼容环境中的其他库

  • 项目目录组织如下:

    /myproject
    ├── frontend 前端代码
    └── backend 后端代码

  • 在项目根目录下使用以下命令创建一个新的 Django 项目
    django-admin startproject backend

  • 处理数据库迁移
    python manage.py makemigrations 和 python manage.py migrate 是 Django 的命令行工具中的两个命令,用于处理数据库迁移。

    1、python manage.py makemigrations:这个命令会检查你的 Django 项目中的所有模型,并创建迁移文件。这些迁移文件是 Django 用来跟踪你的模型和数据库模式之间的变化的方式。每当你修改了模型(例如,添加字段、删除字段、更改字段类型等),你都需要运行这个命令来创建一个新的迁移文件。

    2、python manage.py migrate:这个命令会应用(执行)所有还未应用的迁移。这意味着它会执行迁移文件中的操作,将这些变化应用到数据库模式中。例如,如果你添加了一个新的字段,migrate 命令会在相应的表中添加一个新的列。

增加注册和登录的后端

  • 1、新建views.py文件夹,在其中写登陆注册的相关逻辑
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    import json
    from django.http import JsonResponse
    from django.views.decorators.csrf import csrf_exempt
    from django.contrib.auth.models import User
    from django.contrib.auth import authenticate, login

    # 注册功能
    @csrf_exempt
    def register(request):
    if request.method == 'POST':
    data = json.loads(request.body)
    username = data.get('username')
    password = data.get('password')

    # 验证用户名长度
    if len(username) < 4:
    return JsonResponse({'status': 'error', 'message': 'Username must be at least 4 characters'}, status=400)

    # 验证密码长度和格式
    if len(password) < 6 or not any(char.isdigit() for char in password) or not any(char.isalpha() for char in password):
    return JsonResponse({'status': 'error', 'message': 'Password must be at least 6 characters and contain at least one digit and one letter'}, status=400)

    user = User.objects.create_user(username, password=password)
    return JsonResponse({'status': 'success'}, status=201)

    # 登录
    @csrf_exempt
    def login_view(request):
    if request.method == 'POST':
    data = json.loads(request.body)
    username = data.get('username')
    password = data.get('password')
    user = authenticate(request, username=username, password=password)
    if user is not None:
    login(request, user)
    return JsonResponse({'status': 'success', 'token': request.session.session_key}, status=200)
    else:
    return JsonResponse({'status': 'error', 'message': 'Invalid credentials'}, status=401)
    else:
    return JsonResponse({'status': 'error', 'message': 'Invalid request method'}, status=400)
  • 2、在urls.py文件中配置路由
    1
    2
    3
    4
    5
    6
    7
    8
    9
    from django.contrib import admin
    from django.urls import path
    from . import views

    urlpatterns = [
    path('admin/', admin.site.urls),
    path('register/', views.register , name='register'),
    path('login/', views.login_view, name='login'),
    ]

启动服务并在postman中测试

python manage.py runserver

图 0

在实现前端时出现的问题

数据组织形式

在前端中我是用了res.data.username但是在后端中我没有使用Json格式返回数据,返回数据中也没有data字段,所以导致前端访问不到数据。

后端中的接口以login为例,必须以...\login\访问,而我前端访问的地址是login导致访问失败。解决办法是修改后端路由。

1
2
3
4
5
6
7
8
9
10
from django.contrib import admin
from django.urls import re_path # 一定要引入此库
from django.urls import path
from . import views

urlpatterns = [
re_path(r'^admin/?$', admin.site.urls),
re_path(r'^register/?$', views.register , name='register'),
re_path(r'^login/?$', views.login_view, name='login'),
]

地图组件联动开发

地图组件联动开发

开发目标简介:

  • 1、点击按钮切换到场景2。
  • 2、在该场景的div中创建两个地图组件,分别用于展示原始地图和综合后地图数据。
  • 3、使两个地图组件互相影响,缩放或拖动其中一个子组件中的地图,另外一个也跟着变化

创建地图组件

开发目标:

  • 实现瓦片地图加载啊
  • 地图缩放控件
  • 地图图层切换控件

使用leaflet.chinatmsproviders新建多种图层

基本语法:L.tileLayer.chinaProvider('Geoq.Normal.PurplishBlue',{})

该方法的第一个参数定义了图层类型

首先使用该方法返回并接受图层

接着定义baseLayer对象,其中保存了图层名及其值也就是上述函数返回的图层

然后这是地图初始化需要的值mapValue,是一个对象,其中包括坐标系、中心点经纬度、缩放等级、图层类型、是否添加缩放空间等属性。

定义初始化函数,在挂载时调用。添加控件

1
2
3
4
5
6
7
8
9
10
11
12
13
// map是在初始化函数外定义的响应式数据
map.value = L.map('mapid', mapValue) //第一个参数是放置地图的容器的id名,第二个参数是上述的初始化值
L.control.layers(baseLayers, null).addTo(map.value)

// L.control.layers(baseLayers, null).addTo(map.value) 这行代码做了以下几件事:

// L.control.layers(baseLayers, null) 创建了一个新的图层控制器。baseLayers 是一个对象,
// 它的每个键值对定义了一个基础图层。在这个例子中,null 表示没有叠加层。

// .addTo(map.value) 将这个图层控制器添加到了地图上。map.value 应该是一个 L.Map 实例。

//返回L.Map构造的实例
return map.value

完整的代码,此时还未实现动图组件互动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<template>
<div id="mapid" style="height: 100%"></div>
</template>

<script setup lang="ts">
import {ref, inject, onUnmounted, watch, nextTick, onMounted} from 'vue'
import L from 'leaflet'
let pscene: any = inject('pscene')
import 'leaflet.chinatmsproviders'

let map = ref<any>(null);
const normalm3 = L.tileLayer.chinaProvider(
'Geoq.Normal.PurplishBlue',
{}
)
// Google map layers
const normalMap = L.tileLayer.chinaProvider('Google.Normal.Map', {})
const satelliteMap = L.tileLayer.chinaProvider(
'Google.Satellite.Map',
{}
)
// 高德地图
const Gaode = L.tileLayer.chinaProvider('GaoDe.Normal.Map', {})
const Gaodimage = L.tileLayer.chinaProvider('GaoDe.Satellite.Map', { })


// Create base layers with different tile layers
const baseLayers = {
智图午夜蓝: normalm3,
谷歌地图: normalMap,
谷歌影像: satelliteMap,
高德地图: Gaode,
高德影像: Gaodimage,
}
//初始化地图
const mapValue = {
crs: L.CRS.EPSG3857,
center: [32.0603, 118.7969],
zoom: 12,
maxZoom: 18,
minZoom: 5,
layers: [normalMap],
zoomControl: true
}
const initMap = () => {
map.value = L.map('mapid', mapValue)

// Add control layers and zoom control to the map

L.control.layers(baseLayers, null).addTo(map.value)
return map.value
}

onMounted(() => {
const mapInstance = initMap()
//等初始化完成后,强制地图重新计算大小和位置
nextTick(() => {
// 强制地图重新计算大小和位置
if (mapInstance){
mapInstance.invalidateSize();
}
});
mapInstance.on('zoomend dragend', () => {
emit('map-moved', { center: mapInstance.getCenter(), zoom: mapInstance.getZoom() })
})

})

onUnmounted(() => {
if (map.value) {
map.value.remove();
map.value = null;
}
})

</script>

<style scoped>
#mapid {
height: 100%;
}
</style>

在父组件中导入组件

1
2
3
4
5
6
7
8
9
10
11
12
<template>
<div class="mainBox" >
<el-button size="small" style="margin:0px; border-color: aliceblue;" icon="CloseBold" @click="changeScene"></el-button>
<div class="mapOuterBox" >
<!-- 左边的地图组件 -->
<mapBox ref="mapBox1" class="leftMap" @map-moved="handelMapMoved2"></mapBox>
<!-- 中间的地图组件 -->
<mapBoxMid ref="mapBox2" class="midMap" @map-moved="handelMapMoved1"></mapBoxMid>
<div class="rightMap">文字展示</div>
</div>
</div>
</template>

互动效果实现

使用自定义事件emit,当地图发生缩放或者拖拽时,在附件中监听到该自定义事件。在父组件通知另外一个地图组件进行中心点和缩放等级的变化。

因此步骤如下

  • 在左中两个地图组件中,监听地图实例的缩放和拖拽事件
  • 监听到拖拽事件触发自定义事件 map-moved
  • 在父组件中分别获取到两个地图组件的实例,使用ref
  • 在父组件中的监听到map-move的后,分别调用handelMapmove2和handelMapmove1
  • 在父组件中定义handelMapmove2和handelMapmove1函数,前者用于调整中间地图组件的中心和缩放等级,后者用于调整左边地图组件。
  • 分别在两个地图组件中定义setMapView函数,并暴露出来,这样父组件中才能使用$refs调用该方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//地图组件中代码
//自定义事件
const setMapView = (center: L.LatLngExpression, zoom: number) => {
if (map.value) {
map.value.setView(center, zoom)
}
}
//暴露给父组件的方法
defineExpose({ setMapView })
//注意vue3的写法
const emit = defineEmits(['map-moved'])
onMounted(() => {
const mapInstance = initMap()
//等初始化完成后,强制地图重新计算大小和位置
nextTick(() => {
// 强制地图重新计算大小和位置
if (mapInstance){
mapInstance.invalidateSize();
}
});
mapInstance.on('zoomend dragend', () => {
emit('map-moved', { center: mapInstance.getCenter(), zoom: mapInstance.getZoom() })
})
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//父组件中代码
const mapBox1 = ref(null)
const mapBox2 = ref(null)

//重置第二个子组件的center和zoom
const handelMapMoved2 = (e:any)=>{
if(mapBox2.value){
console.log(mapBox2.value.setMapView(e.center, e.zoom))
}
}
const handelMapMoved1 = (e)=>{
if(mapBox1.value){
console.log(mapBox1.value.setMapView(e.center, e.zoom))
}

}

踩到的坑们

setup

1、在子组件的setup中定义的方法,在父组件中通过子组件实例(在父组件中的子组件标签中使用ref=’ziname’)ziname.的方法无法调用,必须在setup中暴露出该方法才行;暴露的方法也踩了坑,由于使用ts,一直提示不能使用return暴露函数。应该使用defineExpose({ setMapView })

2、地图总是加载左上角,因为组件的大小不是固定的px值,所以会发生变化,因此,需要在初始化完成后强制地图进行大小和位置计算。且在组件卸载后要清空地图的配置值。

3、将地图配置对象mapValue设置成响应式对象,对导致第一个被初始化的地图能加载出地图,而另外一个不能。因此放弃了响应式配置值实现互动的方法。

4、vue3中不能使用this,自定义方法,组件实例调用方法,均和vue2有差异

1
2
3
4
5
6
7
const emit = defineEmits(['map-moved'])
mapInstance.on('zoomend dragend', () => {
emit('map-moved', { center: mapInstance.getCenter(), zoom: mapInstance.getZoom() })
})

const mapBox1 = ref(null)
mapBox1.value.setMapView(e.center, e.zoom)

总结

以为自己掌握了知识,实践中却有很多问题。学好底层逻辑有助于快速发现问题和解决问题

登录界面开发

配置

组件

分别注册了两个组件,一个是login页面(src/views/login/index.vue)、另外一个是登陆后展示的layout页面(src/views/layout/index.vue)

路由配置

配置路由(src/router/index.ts)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//引入createRouter和createWebHashHistory方法
import { createRouter, createWebHashHistory } from "vue-router";
//创建router实例
const router = createRouter({
history: createWebHashHistory(),
routes:[
{
path:'/login',
component:()=>import('@/view/login/index.vue'), //导入组件
name:'login',
meta:{
title:'登录',
hidden:true,
icon:'el-icon-s-home'
}
},
{
path:'/',
name:'layout',
component:()=>import('@/view/layout/index.vue'),
meta: {
title: '', //菜单需要的标题
hidden: false,
icon: '',
}
}
],
//滚动行为,只有在history模式下才有用
// scrollBehavior(){
// return{
// left:0,
// top:0
// }
// }
})
export default router

在 Vue Router 中,router 和 route 是两个不同的概念。

  • router 是 Vue Router 的实例,它包含了一些方法和属性,如 push、> replace、go 等,用于控制路由的跳转和行为。你可以通过 this.$router 在 Vue 组件中访问到 router 实例。
  • route 是当前激活的路由信息对象,它包含了当前路由的路径、参数、查询字符串等信息。你可以通过 this.$route 在 Vue 组件中访问到当前的 route 对象。
  • router 和 route 的主要区别在于,router 是用于控制路由的跳转和行为的,而 route 是表示当前路由状态的信息对象。
  • 在大多数情况下,你可能需要使用 this.$router 来进行路由跳转,如 this.$router.push('/path')。而当你需要获取当前路由的信息,如路径、参数等,你可以使用 this.$route,如 this.$route.params.id。

在app.vue中使用router-view

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<div class="outer">
<router-view></router-view>
</div>
</template>
<script setup lang="ts">

</script>
<style scoped lang="scss">
.outer{
width: 100vw;
height:100vh;
margin: 0px;
padding: 0px;
}
</style>

静态页面

el-row/el-col

官网介绍:https://element-plus.org/zh-CN/component/layout.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<template>
<div class="login_box">
<el-row>
{/* :span总和是24,各设置为12,那就是两列对半分。 */}
<el-col :span="12" :xs="24">
<el-form class="login_form">
<h2>地表异常预警平台</h2>
<el-form-item prop="username">
<el-input prefix-icon="User"></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input type="password" prefix-icon="Lock"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" class="login_but">登录</el-button>
</el-form-item>
</el-form>
</el-col>
<el-col :span="12" :xs="0"></el-col>
</el-row>
</div>
</template>

<script setup lang="ts"></script>

<style lang="scss" scoped>
.login_box {
width: 100%;
height: 100%;
background-image: url('../../assets/login/bg.gif');
background-size: cover;
.login_form {
position: relative;
padding: 40px;
width: 60%;
top: 30vh;
.login_but{
width: 100%;
}
}
}
</style>


css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<style lang="scss" scoped>
.login_box {
//能用100%是因为父盒子有定义width和height。这里的百分比是相对于父盒子的
//如果父盒子没有明确的width和height,那么百分比会失效
width: 100%;
height: 100%;
background-image: url('../../assets/login/bg.gif');
//cover和contain都会保持图片的宽高比,区别在于contain会留白,cover会完全覆盖,
//也就是cover会导致,图片某部分被裁剪
background-size: cover;
.login_form {
//position属性可能有static、realative、sbsolute、sticky和fixed
position: relative;
padding: 40px;
width: 60%;
top: 30vh;
.login_but{
width: 100%;
}
}
}
</style>

positon的值

  • static:这是元素的默认值。静态定位的元素不会受到 topbottomleftright 的影响。
  • relative:元素按照正常的文档流进行定位,然后相对于它的正常位置进行偏移。偏移不会影响其他元素的位置。
  • absolute:元素脱离正常的文档流,并相对于最近的已定位父元素(position 属性为 relativeabsolutefixedsticky 的元素)进行定位。如果没有已定位的父元素,那么它会相对于 body 元素进行定位。
  • fixed:元素脱离正常的文档流,并相对于浏览器窗口进行定位。即使页面滚动,它仍然会停留在同一的位置。
  • sticky:元素在滚动范围内表现为 relative,而在滚动范围外表现为 fixed。也就是说,元素会“粘”在特定位置,然后随着页面滚动。

动态绑定

mock模拟服务器

使用mock模拟后端

  • 安装mock
    pnpm i mockjs --save
  • 模拟数据和服务
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    //导入mock库
    import Mock from 'mockjs'
    //模拟数据
    const user = Mock.mock({
    fixedUser: [
    {
    username: 'admin',
    password: 'admin111',
    avatar:
    ' https://tse4-mm.cn.bing.net/th/id/OIP-C.NNF59vntIv7760rOlX1zMgAAAA?w=187&h=188&c=7&r=0&o=5&cb=10&dpr=1.2&pid=1.7'
    },
    {
    username: 'user',
    password: 'user111',
    avatar:
    'https://tse1-mm.cn.bing.net/th/id/OIP-C.D34PjxR7ud-vxeTDvs5Z8gAAAA?w=135&h=150&c=7&r=0&o=5&cb=10&dpr=1.2&pid=1.7'
    }
    ]
    })

    export default()=>{
    Mock.mock('/api/login', 'post', (req: any) => {
    const { username, password } = JSON.parse(req.body)
    //从模拟数据中找
    const userItem = user.fixedUser.find(
    (item: any) => item.username === username && item.password === password
    )
    //如果用户数据在模拟数据中,return200,和token
    if (userItem) {
    return {
    code: 200,
    data: {
    //返回用户名,用户头像和token
    username: userItem.username,
    avatar: userItem.avatar,
    token: 'admin-token'
    }
    }
    } else {
    return {
    code: 400,
    message: '用户名或密码错误'
    }
    }
    })
    }
  • 在main.ts中引入mock
    1
    2
    import configureMockServer from './mock/user.ts';
    configureMockServer()

新建仓库

新建仓库用来存放用户数据,发起登录请求等

  • 安装pinia
    pnpm i pinia

  • 新建一个大仓库(src/store/index.ts)

    1
    2
    3
    import {createPinia} from 'pinia'
    const pinia = createPinia()
    export default pinia
  • 封装本地存储、获取和移除token的功能(src/utils/token)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    //封装本地存储和读取数据的方法
    //本地存储数据
    export const SET_TOKEN = (token: string) => {
    localStorage.setItem('TOKEN', token)
    }
    //本地存储获取数据
    export const GET_TOKEN = () => {
    return localStorage.getItem('TOKEN')
    }
    export const REMOVE_TOKEN = () => {
    return localStorage.removeItem('TOKEN')
    }
  • 在main.ts中注册pinia

    1
    2
    import pinia from './store'
    app.use(pinia)
  • 创建用户仓库

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    //存放用户数据的仓库
    import { defineStore } from 'pinia'
    import { reqLogin } from '@/api/user'
    import { SET_TOKEN, GET_TOKEN, } from '@/utils/token'
    export const userStore = defineStore({
    id: 'user',
    state: () => {
    return {
    token: GET_TOKEN(),
    username: '',
    avatar: ''
    }
    },
    //异步操作
    actions: {
    //登录
    async userLogin(data: any) {
    const res = await reqLogin(data)
    if (res.code === 200) {
    //将用户名、头像存入仓库
    this.username = res.data.username
    this.avatar = res.data.avatar
    //将token存入localStorage
    SET_TOKEN(res.data.token)
    } else {
    //登录失败
    return Promise.reject(new Error(res.message))
    }
    }
    }
    })

创建并暴露用户登录api

在(src/api/user/index.ts)中书写相关代码

1
2
3
4
5
6
7
8
9
//导入二次封装的axios
import request from '@/utils/request'

// 枚举接口
enum API{
LOGIN_URL = '/login'
}
//暴露登录接口
export const reqLogin = (data:any)=> request.post<any, any>(API.LOGIN_URL,data)

v-model绑定

  • 双向绑定

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    <template>
    <div class="login_box">
    <el-row>
    <el-col :span="12" :xs="24">
    {/* 使用ref获取el-form组件 */}
    <el-form class="login_form" :model="LoginForm" :rules="rules" ref="loginFormRef">
    <h2>我的平台</h2>
    <el-form-item prop="username">
    {/* 使用v-model双向绑定 */}
    <el-input prefix-icon="User" v-model="LoginForm.username"></el-input>
    </el-form-item>
    <el-form-item prop="password">
    <el-input type="password" prefix-icon="Lock" v-model="LoginForm.password"></el-input>
    </el-form-item>
    <el-form-item>
    {/* 绑定点击事件发起登录请求 */}
    <el-button type="primary" class="login_but" @click="login">登录</el-button>
    </el-form-item>
    </el-form>
    </el-col>
    <el-col :span="12" :xs="0"></el-col>
    </el-row>
    </div>
    </template>
  • 点击事件触发login

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    <script setup lang="ts">
    import { ref } from 'vue'
    import { userStore } from '@/store/modules/user'
    import {useRouter, useRoute} from 'vue-router'
    import { ElNotification } from 'element-plus';
    //新建一个loginForm用于收集表单数据
    let LoginForm = ref({
    username: '',
    password: ''
    })
    //获取用户仓库
    let ustore = userStore()
    //获取路由器
    let $router = useRouter()
    let $route = useRoute()
    //获取表单组件
    let loginFormRef = ref()
    const login = async()=>{
    await loginFormRef.value.validate()
    //登录逻辑
    //首先,发起请求,将用户名和密码发送给后端
    //然后,后端返回一个token,将token存储在本地
    //最后,跳转到首页
    try{
    await ustore.userLogin(LoginForm.value)
    //RU: 如果有redirect参数,就跳转到redirect,否则跳转到首页
    let redirect: any = $route.query.redirect
    $router.push({ path: redirect || '/' })
    // 提示登录成功
    ElNotification({
    title: `欢迎回来${ustore.username}`,
    message: '欢迎回来',
    type: 'success'
    });
    } catch(error){

    // 提示登录失败
    ElNotification({
    type: 'error',
    message: (error as Error).message,
    });
    }
    }

    const validatorUsername = (rule:any , value:string, callback:any)=>{
    if(value== undefined){
    callback(new Error('用户名不能为空'))
    }
    if(value.length>=5 && value.length<=20){
    callback()
    }else{
    callback(new Error('用户名必须是5-20位的字符串'))
    }
    }

    const validatorPassword = (rule:any , value:string, callback:any)=>{
    if(value== undefined){
    callback(new Error('密码不能为空'))
    }
    if(value.length > 5){
    callback()
    }else{
    callback(new Error('密码长度必须大于5位'))
    }
    }
    //定义表达验证需要配置的对象
    const rules = {
    username: [
    //validatorUsername在上面定义
    {trigger:'blur', validator: validatorUsername}
    ],
    password: [
    {trigger:'blur', validator:validatorPassword}
    ]
    }
    </script>

规则校验

使用elementPlus规定好的关键字,实现进入规则校验

  • 在表单中绑定rules属性为rules对对象
    1
    <el-form class="login_form" :model="LoginForm" :rules="rules" ref="loginFormRef">
  • 在script中,定义表单验证的配置对象
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    //这点代码中定义的前两个对象用于rules对象中
    const validatorUsername = (rule:any , value:string, callback:any)=>{
    if(value== undefined){
    callback(new Error('用户名不能为空'))
    }
    if(value.length>=5 && value.length<=20){
    callback()
    }else{
    callback(new Error('用户名必须是5-20位的字符串'))
    }
    }

    const validatorPassword = (rule:any , value:string, callback:any)=>{
    if(value== undefined){
    callback(new Error('密码不能为空'))
    }
    if(value.length > 5){
    callback()
    }else{
    callback(new Error('密码长度必须大于5位'))
    }
    }
    //定义表达验证需要配置的对象
    const rules = {
    username: [
    //validatorUsername在上面定义
    //失去焦点后触发
    {trigger:'blur', validator: validatorUsername}
    ],
    password: [
    {trigger:'blur', validator:validatorPassword}
    ]
    }
  • 在登录点击事件中,发起请求之前先进行规则校验,校验通过后才能发起请求

路由跳转

登录成功后跳转到页面

1
2
3
4
5
6
7
8
let $router = useRouter()
let $route = useRoute()

登录函数中:
登录成功后:
//如果有redirect参数,就跳转到redirect,否则跳转到首页
let redirect: any = $route.query.redirect
$router.push({ path: redirect || '/' })

项目集成配置

项目集成配置

1、集成element-plus

官网地址:https://element-plus.gitee.io/zh-CN/

安装命令: pnpm install element-plus @element-plus/icons-vue

1
2
3
4
5
6
//在入口文件中main.ts中全局安装element-plus
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
////@ts-ignore忽略当前文件ts类型的检测否则有红色提示(打包会失败)
import zhCn from 'element-plus/es/locale/lang/zh-cn'
app.use(ElementPlus, { locale: zhCn })

同样别忘了把icon图标都注册成全局组件,注册方式和svg图标全局化的方式一样

1
import * as ElementPlusIconsVue from '@element-plus/icons-vue'

2、别名配置

在开发项目时文件与文件关系很复杂,因此需要给src文件配置一个别名
安装:pnpm i --save-dev @types/path

1
2
3
4
5
6
7
8
9
10
11
12
13
//在vite.config.ts中配置
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': path.resolve('./src') // 相对路径别名配置,使用 @ 代替 src
}
}
})
1
2
3
4
5
6
7
8
9
// tsconfig.json
{
"compilerOptions": {
"baseUrl": "./", // 解析非相对模块的基地址,默认是当前目录
"paths": { //路径映射,相对于baseUrl
"@/*": ["src/*"]
}
}
}

3、环境变量配置

项目开发过程中,至少会经历 开发环境、测试环境和生产环境(即正式环境) 三个阶段。不同阶段请求的状态(如接口地址等)不尽相同,若手动切换接口地址是相当繁琐且易出错的。于是环境变量配置的需求就应运而生,我们只需做简单的配置,把环境状态切换的工作交给代码。

项目根目录分别添加 开发、生产和测试环境的文件!

1
2
3
.env.development
.env.production
.env.test

文件内容

1
2
3
4
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = 'development'
VITE_APP_TITLE = '硅谷甄选运营平台'
VITE_APP_BASE_API = '/dev-api'
1
2
3
NODE_ENV = 'production'
VITE_APP_TITLE = '硅谷甄选运营平台'
VITE_APP_BASE_API = '/prod-api'
1
2
3
4
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = 'test'
VITE_APP_TITLE = '硅谷甄选运营平台'
VITE_APP_BASE_API = '/test-api'

配置运行命令:package.json

1
2
3
4
5
6
"scripts": {
"dev": "vite --open",
"build:test": "vue-tsc && vite build --mode test",
"build:pro": "vue-tsc && vite build --mode production",
"preview": "vite preview"
},

通过import.meta.env获取环境变量

4、SVG图标配置

在开发项目的时候经常会用到svg矢量图,而且我们使用SVG以后,页面上加载的不再是图片资源,

安装依赖 pnpm install vite-plugin-svg-icons -D

在vite.config.ts中配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import path from 'path'
export default () => {
return {
plugins: [
createSvgIconsPlugin({
// 指定要缓存的图标文件夹
iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
// 指定 symbolId 格式
symbolId: 'icon-[dir]-[name]',
}),
],
}
}

main.ts入口文件中引入 import 'virtual:svg-icons-register'

将svg封装为全局组件

步骤如下

  • 创建一个Svgicon组件 src/components/SvgIcon.vue
  • 在src/components文件夹下创建一个index.t文件,用于注册全部全局文件
  • 在入口文件引入src/index.ts文件,通过app.use方法安装自定义插件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// src/components/SvgIcon.vue
<template>
<div>
<svg :style="{ width: width, height: height }">
<use :xlink:href="prefix + name" :fill="color"></use>
</svg>
</div>
</template>

<script setup lang="ts">
defineProps({
//xlink:href属性值的前缀
prefix: {
type: String,
default: '#icon-'
},
//svg矢量图的名字
name: String,
//svg图标的颜色
color: {
type: String,
default: ""
},
//svg宽度
width: {
type: String,
default: '16px'
},
//svg高度
height: {
type: String,
default: '16px'
}

})
</script>
<style scoped></style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// src/componebts/index.ts
import SvgIcon from './SvgIcon/index.vue'
//分页器
import Pagination from './Pagination/index.vue'
//一个仓库
import Category from './Category/index.vue'
//引入element-plus提供全部图标组件
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
//全局对象
const allGloablComponent: any = { SvgIcon, Pagination, Category }
//对外暴露插件对象
export default {
//务必叫做install方法
install(app: any) {
//注册项目全部的全局组件
Object.keys(allGloablComponent).forEach((key) => {
//注册为全局组件
app.component(key, allGloablComponent[key])
})
//将element-plus提供图标注册为全局组件
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
}
}
1
2
3
// main.ts
import gloablComponent from './components/index';
app.use(gloablComponent);

5、集成sass

安装sass和sass-loader: pnpm install --save-dev sass sass-loader

接下来我们为项目添加一些全局的样式

在src/styles目录下创建一个index.scss文件,当然项目中需要用到清除默认样式,因此在index.scss引入reset.scss

1
@import reset.scss

但是你会发现在src/styles/index.scss全局样式文件中没有办法使用$变量.因此需要给项目中引入全局变量$.

在./src/style 中创建一个variable.scss文件! 在这里可以写入一些全局可用的样式变量。

在vite.config.ts文件配置如下:

1
2
3
4
5
6
7
8
9
10
export default defineConfig((config) => {
css: {
preprocessorOptions: {
scss: {
javascriptEnabled: true,
additionalData: '@import "./src/styles/variable.scss";', //;不要忘记
},
},
},
})

6、axios二次封装

安装:pnpm install axios

在开发项目的时候避免不了与后端进行交互,因此我们需要使用axios插件实现发送网络请求。在开发项目的时候我们经常会把axios进行二次封装。

目的:
1:使用请求拦截器,可以在请求拦截器中处理一些业务(开始进度条、请求头携带公共参数)
2:使用响应拦截器,可以在响应拦截器中处理一些业务(进度条结束、简化服务器返回的数据、处理http网络错误)

在根目录下创建utils/request.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import axios from "axios";
import { ElMessage } from "element-plus";
//创建axios实例
let request = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API,
timeout: 5000
})
//请求拦截器
request.interceptors.request.use(config => {
return config;
});
//响应拦截器
request.interceptors.response.use((response) => {
return response.data;
}, (error) => {
//处理网络错误
let msg = '';
let status = error.response.status;
switch (status) {
case 401:
msg = "token过期";
break;
case 403:
msg = '无权访问';
break;
case 404:
msg = "请求地址错误";
break;
case 500:
msg = "服务器出现问题";
break;
default:
msg = "无网络";

}
ElMessage({
type: 'error',
message: msg
})
return Promise.reject(error);
});
export default request;

7、API接口统一管理

在src目录下创建api文件夹

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//示例
//统一管理咱们项目用户相关的接口

import request from '@/utils/request'

import type {

loginFormData,
loginResponseData,
userInfoReponseData,

} from './type'

//项目用户相关的请求地址

enum API {
LOGIN_URL = '/admin/acl/index/login',
USERINFO_URL = '/admin/acl/index/info',
LOGOUT_URL = '/admin/acl/index/logout',

}
//登录接口
export const reqLogin = (data: loginFormData) =>
request.post<any, loginResponseData>(API.LOGIN_URL, data)
//获取用户信息

export const reqUserInfo = () =>

request.get<any, userInfoReponseData>(API.USERINFO_URL)

//退出登录
export const reqLogout = () => request.post<any, any>(API.LOGOUT_URL)

数组的方法

数组的方法

some

检测数组中所有元素是否满足制定条件,满足返回true,否则false

该方法依次检测数组中每个元素。如果有一个元素满足条件,则返回true,剩余的元素不会再执行检测。如果没有满足条件的元素,则返回false

一真即真

⚠️注意:some()不对空数组进行检测、some()不改变原数组。

every

检测数组中所有元素是否符合指定条件,符合返回true,否则false

该方法根据指定条件检测数组中所有元素。如果监测到有一个元素不满足,则返回false,且剩余元素不会再次检测。如果所有条件都满足则返回true。

一假即假 ,而且只要有一个元素是假,其后面的元素将不再遍历

⚠️注意:every()不对空数组进行检测、every()不改变原数组。

https://juejin.cn/post/6996892724100562974

forEach

forEach() 方法用于调用数组的每个元素,并将元素传递给回调函数。

forEach(): 没有返回值,本质上等同于 for 循环,对每一项执行 function 函数。map是返回一个新数组,原数组不变,forEach 是不改变原数组(尽管回调函数 callbackFn 在被调用时可能会改变原数组)。
不支持 continue,用 return false 或 return true 代替。
不支持 break,用 try catch/every/some 代替。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
arr = [1,2,3,4,5,6]
arr.forEach(item=>console.log(item)) //1 2 3 4 5 6
//实现 continue
arr.forEach((item)=>{
if (item == 3){
return;
}
console.log(item) //1 2 4 5 6
})
arr.some((item)=>{
if(item == 3){
return;
}
console.log(item)
})
//实现break
arr.every((item)=>{
if(item == 3){
return false;
}
else{
console.log(item)
return true
}
})

try {
arr.forEach((item) => {
if (item == 3) {
throw new Error('wrong')
}
console.log(item)
})
} catch (e) {
console.log(e)
}

map

map() 方法创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。
返回值是一个新数组,每个元素都是回调函数的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 语法:
var new_array = arr.map(function callback(currentValue[, index[, array]]) {
// Return element for new_array
}[, thisArg])
// 参数:
// callback 生成新数组元素的函数,使用三个参数:
// currentValue callback 数组中正在处理的当前元素。
// index可选 callback 数组中正在处理的当前元素的索引。
// array可选 callback map 方法被调用的数组。
// thisArg可选 执行 callback 函数时使用的this 值。

arr = ['1', '2',' 3', '4', '5', '6']
const arry = arr.map( str => parseInt(str))
console.log(arry)

https://blog.csdn.net/qq_43437571/article/details/95634526

filter

和map类似,Array的filter也接收一个函数。但是和map不同的是, filter把传入的函数依次作用于每个元素,然后根据返回值是 true 还是false决定保留还是丢弃该元素。

filter 接收的回调函数,其实可以有多个参数。通常我们仅使用第一个参数,表示Array的某个元素。回调函数还可以接收另外两个参数,表示元素的位置和数组本身:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var arr = ['A', 'B', 'C'];
var r = arr.filter(function (element, index, self) {
console.log(element); // 依次打印'A', 'B', 'C'
console.log(index); // 依次打印0, 1, 2
console.log(self); // self就是变量arr
return true;
});

//巧用filter进行数组去重
arr = [1, 2, 3, 3, 4, 5, 2, 6, 1, 7]
const r = arr.filter((item, index, self) => {
if (self.indexOf(item) === index) {
return true
}
})
console.log(r)

https://blog.csdn.net/qq_39207948/article/details/80357367

reduce

图 0

https://blog.csdn.net/qq_43441476/article/details/105559561