前端

界面设计与开发

改变背景颜色动画效果

重点关注的其实是如何做到颜色加深或者变浅,参考下面的例子,关键就是调rgba色的透明值部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.MainNavbarUserLogin {

background-color: var(--accent-100);

/* 指定转化时的效果 */
transition: background-color 0.2s ease 0s;
}

.MainNavbarUserLogin:hover {
color: var(--text-200);
/* 悬停时的文本颜色 */
background-color: rgba(214, 198, 225, 0.7);
}

点击切换CSS样式

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
<template>
<div class="TypeNavbar">
<div class="TypeNavbarItem" @click="selectType(0)" :class="{ 'selected': TypeIndex.index === 0 }">
<p>全部</p>
</div>
<div class="TypeNavbarItem" @click="selectType(1)" :class="{ 'selected': TypeIndex.index === 1 }">
<p>动画</p>
</div>
<div class="TypeNavbarItem" @click="selectType(2)" :class="{ 'selected': TypeIndex.index === 2 }">
<p>现实</p>
</div>
<div class="TypeNavbarItem" @click="selectType(3)" :class="{ 'selected': TypeIndex.index === 3 }">
<p>科技</p>
</div>
<div class="TypeNavbarItem" @click="selectType(4)" :class="{ 'selected': TypeIndex.index === 4 }">
<p>动物</p>
</div>
</div>
</template>

<script setup lang="ts">
import { } from "vue";

import { SelectedTypeIndexStore } from '../stores/SelectedIndexStore'

const TypeIndex = SelectedTypeIndexStore()


const selectType = (index: number) => {

TypeIndex.index = index;
};



</script>

<style lang="scss" scoped>
.TypeNavbar {
display: flex;
justify-content: start;
align-items: center;
width: 100%;

gap: 50px;
margin-top: 20px;
margin-bottom: 50px;
font-size: 16px;
font-weight: bold;
}

// .TypeNavbarItem {
// padding: 0px 20px;
// }

.TypeNavbarItem.selected:hover {
color: var(--text-200);
/* 悬停时的文本颜色 */
background-color: rgba(214, 198, 225, 0.7);
}

.TypeNavbarItem.selected {
display: flex;
justify-content: space-around;
align-items: center;

padding: 0px 20px;
height: 40px;
min-width: 40px;
border: 1px solid transparent;

background-color: var(--accent-100);

backdrop-filter: blur(20px);
border-radius: 16px;
// 指定转化时的效果
transition: background-color 0.2s cubic-bezier(0.05, 0, 0.2, 1) 0s;
}
</style>

手写radio

将input的type设置为radio即可,需要注意的是如何更改小圆点的颜色,网上查了很多,但是都复杂且无效,实际上只需要更改accent-color就可以更改小圆点颜色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div class="payMethod">
<p>支付方式</p>

<label>
<input type="radio" name="paymentMethod" value="alipay">
支付宝
</label>

<label>
<input type="radio" name="paymentMethod" value="wechat">
微信支付
</label>
</div>

根据index选中的值切换组件

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="IndexView" v-if="TypeIndex.index == 0">
<MainNavbar />

</div>
<div class="IndexView" v-if="TypeIndex.index == 2">
<MainNavbar />
<TypeNavbar />
<CollectionList msg="热门现实数字藏品" />
</div>
<div class="IndexView" v-if="TypeIndex.index == 3">
<MainNavbar />
<TypeNavbar />

<CollectionList msg="热门科技数字藏品" />

</div>
<div class="IndexView" v-if="TypeIndex.index == 4">
<MainNavbar />
<TypeNavbar />

<CollectionList msg="热门动物数字藏品" />
</div>
</template>

阴影设置

box-shadow

box-shadow: 0 10px 10px rgba(0, 0, 0, 0.1);

属性:水平偏移为0px,垂直偏移为10px,模糊半径为10px,阴影颜色为深度为0.1的黑色。

transition的解释看下面的向上移动5px的动画

1
2
3
4
5
6
7
8
9
10
.CollectionListItem {
box-shadow: 0 10px 10px rgba(0, 0, 0, 0.1); /* 调整阴影效果 */
transition: box-shadow 0.3s ease, transform 0.3s ease; /* 添加过渡效果 */

}
.CollectionListItem:hover {
box-shadow: 0 20px 20px rgba(0, 0, 0, 0.2); /* 鼠标悬停时的阴影效果 */
transform: translateY(-5px); /* 鼠标悬停时向上移动10px */
}

向上移动5px的动画

ransition: box-shadow 0.3s ease, transform 0.3s ease; /* 添加过渡效果 */

属性:box-shadowtransform 属性在0.3秒内以ease(平滑)的方式过渡。

1
2
3
4
5
6
7
8
9
10
.CollectionListItem {
box-shadow: 0 10px 10px rgba(0, 0, 0, 0.1); /* 调整阴影效果 */
transition: box-shadow 0.3s ease, transform 0.3s ease; /* 添加过渡效果 */

}
.CollectionListItem:hover {
box-shadow: 0 20px 20px rgba(0, 0, 0, 0.2); /* 鼠标悬停时的阴影效果 */
transform: translateY(-5px); /* 鼠标悬停时向上移动10px */
}

图片按照比例缩放

object-fit

object-fit 是 CSS 中用于控制替换元素(如 <img><video><object>)的尺寸和裁剪的属性。这个属性允许你定义替换元素在其容器内的尺寸和位置,以及如何调整替换元素的内容以适应这些尺寸。

object-fit 属性有以下几个可能的取值:

  1. fill: 默认值。替换元素被拉伸以填满容器,可能导致元素的宽高比发生变化。
1
2
3
img {
object-fit: fill;
}
  1. contain: 替换元素被缩放以适应容器,保持其宽高比可能在容器内留有空白
1
2
3
img {
object-fit: contain;
}
  1. cover: 替换元素被缩放以填满容器,保持其宽高比可能裁剪超出容器的部分。(最常用)
1
2
3
img {
object-fit: cover;
}
  1. none: 替换元素保持其原始尺寸,可能溢出容器。
1
2
3
img {
object-fit: none;
}
  1. scale-down: 替换元素的尺寸被缩小以适应容器,但不会超过其原始尺寸,可能留有空白。
1
2
3
img {
object-fit: scale-down;
}

使用 object-fit 属性,你可以更灵活地控制替换元素在容器内的布局和尺寸,以适应设计的需要。

示例:

1
2
3
<div class="CollectionListItemImage" style="height: 150px; width: 280px;">
<img style="height: 100%; width: 100%; border-radius: 20px 20px 0px 0px; object-fit: cover;" :src="item.imageUrl" alt="" />
</div>

手写上传文件框

ui部分:

截屏2024-02-21 20.55.43

Html:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div class="CreateViewBodyLeft">
<p style="font-size: 36px; font-weight: bold;">创建NFT</p>
<p style="font-size: 20px; margin-top: 10px;">铸造项目后,您将无法更改其任何信息。</p>
<div class="CreateViewBodyLeftUpdate">
<el-icon size="40">
<Upload />
</el-icon>
<p style="font-size: 20px; font-weight: bold; margin-top: 20px;">拖拽媒体</p>
<p style="font-size: 16px; color: var(--primary-100); font-weight: bold;">浏览文件</p>
<p style="font-size: 16px;">最大尺寸: 50MB</p>
<p style="font-size: 16px;">JPG、PNG、GIF、SVG、MP4</p>
</div>
</div>

Css:

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
.CreateViewBodyLeft {
min-width: 40%;

.CreateViewBodyLeftUpdate {
display: flex; /* 使用 Flex 布局 */
flex-direction: column; /* 主轴方向为垂直,交叉轴方向为水平 */
justify-content: center; /* 在主轴上居中对齐 */
align-items: center; /* 在交叉轴上居中对齐 */
gap: 5px; /* 设置子元素之间的间距 */

height: 80%;
width: 100%;
min-width: 80%;

max-height: 600px;
border: 1px dashed var(--text-200); /* 设置边框为虚线 */
border-radius: 20px;
margin-top: 30px;
background-color: var(--bg-200); /* 设置背景颜色,颜色使用 CSS 变量 */

transition: background-color 0.2s cubic-bezier(0.05, 0, 0.2, 1) 0s; /* 添加背景颜色过渡效果,持续0.2秒,使用贝塞尔曲线,无延迟 */
}


.CreateViewBodyLeftUpdate:hover {
border: 1px solid var(--text-200); /* 设置边框为实线*/
background-color: rgba(18, 18, 18, 0.04); /* 设置半透明背景颜色 */
}
}

功能实现部分:

Flex布局竖向排列元素

使用flex布局的flex-direction设置为column即可

1
2
3
4
5
6
7
8
.CreateViewBodyLeftUpdate {
display: flex; /* 使用 Flex 布局 */
flex-direction: column; /* 主轴方向为垂直,交叉轴方向为水平 */
justify-content: center; /* 在主轴上居中对齐 */
align-items: center; /* 在交叉轴上居中对齐 */
gap: 5px; /* 设置子元素之间的间距 */
}

鼠标滑动展示菜单

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
<div class="MainNavbarUserInfo" @mouseover="showUserMenu" @mouseleave="hideUserMenu">
<el-icon :size="20">
<User />
</el-icon>
</div>

<transition name="fade">
<div class="MainNavbarUserMenu" v-if="isUserMenuVisible" @mouseover="showUserMenu" @mouseleave="hideUserMenu">

</div>
</transition>


<style lang="scss" scoped>
/* 整个导航栏容器 */

.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease;
}

.fade-enter-from,
.fade-leave-to {
opacity: 0;
}


</style>

下拉菜单

主要是使用absolute定位,并使用z-index来使得菜单维持在界面最上方

但是目前存在缩放后位置有偏差的问题(‼️)

1
2
3
<div class="MainNavbarUserMenu" v-if="isUserMenuVisible" @mouseover="showUserMenu" @mouseleave="hideUserMenu">

</div>
1
2
3
4
5
6
7
.MainNavbarUserMenu {
position: absolute;
z-index: 9999;
top: 70px;
right: 210px;
}

卡片式轮播图

采用的是element-plus中的el-carousel来实现

type属性改为card就能实现卡片轮播,具体实现看下面的示例

示例:

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
<template>
<div class="IndexView" v-if="TypeIndex.index == 0">
<el-carousel :interval="4000" type="card" height="300px" >
<el-carousel-item v-for="(item, index) in recommendedCollections" :key="index" style="border-radius: 20px 20px 0px 0px;">
<img :src="item.imageUrl" alt="NFT Image" style="height: 100%; width: 100%; border-radius: 20px 20px 0px 0px; object-fit: cover;">
<h3 text="2xl" justify="center">{{ item.title }}</h3>
<p>{{ item.price }}</p>
<p>{{ item.tradingVolume }}</p>
</el-carousel-item>
</el-carousel>
<Rank/>
</div>
</template>

<style scoped>
.el-carousel__item h3 {
color: #475669;
opacity: 0.75;
line-height: 200px;
margin: 0;
text-align: center;
}

.el-carousel__item:nth-child(2n) {
background-color: #99a9bf;
}

.el-carousel__item:nth-child(2n + 1) {
background-color: #d3dce6;
}

</style>

动画效果

1、使用Vue提供的transition组件

前提是transition中存在v-if来控制组件的出现与否,而且注意v-if的组件必须就位于transition的下一个包裹代码汇总

这个transition中的过渡时间不生效一直不生效(‼️)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<transition name="fade">
<div class="MainNavbarUserMenu" v-if="isUserMenuVisible" @mouseover="showUserMenu" @mouseleave="hideUserMenu">

</div>
</transition>

<style lang="scss" scoped>
/* 整个导航栏容器 */

.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease;
}

.fade-enter-from,
.fade-leave-to {
opacity: 0;
}


</style>

2、使用Css提供的transition属性

transition 属性是 CSS 中用于设置过渡效果的属性。过渡效果可以让元素在状态改变时平滑地过渡到新状态,而不是突然地改变样式。transition 属性可以应用于元素的各种样式属性,如颜色、大小、位置等,以实现平滑的过渡效果。

transition 属性有以下语法:

1
transition: property duration timing-function delay;
  • property:指定要过渡的样式属性,可以是一个或多个属性,用逗号分隔。也可以使用关键字 all 表示所有属性。例如:width, height, color, all,background-color

  • duration:指定过渡的持续时间,以秒(s)或毫秒(ms)为单位。例如:0.5s, 1000ms

  • timing-function:指定过渡效果的时间函数,用于定义过渡过程中的速度变化。常见的有 ease(默认值,缓慢开始,然后加速)、linear(匀速)、ease-in(缓慢开始)、ease-out(缓慢结束)、ease-in-out(缓慢开始和结束)等。

  • delay(可选):指定在过渡开始之前的延迟时间,以秒(s)或毫秒(ms)为单位。例如:0.2s, 300ms

下面是一个例子,演示了如何使用 transition 属性:

1
2
3
4
5
6
7
8
9
/* 对于所有属性,持续时间为0.3秒,使用ease时间函数,延迟0.1秒 这个比较常用*/
.element {
transition: all 0.3s ease 0.1s;
}

/* 对于颜色属性,持续时间为1秒,使用linear时间函数,无延迟 */
.element2 {
transition: color 1s linear;
}

示例:

1
2
3
4
5
6
7
8
9
10
11
12
.MainNavbarUserCart {

/* 指定转化时的效果 */
transition: background-color 0.2s cubic-bezier(0.05, 0, 0.2, 1) 0s;
}

.MainNavbarUserCart:hover {
color: var(--text-200);
/* 悬停时的文本颜色 */
background-color: rgba(214, 198, 225, 0.7);
}

排行榜实现

重点关注如何实现序号正确排列的

还没好好看(‼️)

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
<template>
<div class="Rank">
<div v-for="(collectionGroup, index) in groupedCollections" :key="index" class="RankLeft">
<div class="RankTitle">
<p style="flex: 1;">#</p>
<p style="flex: 7;">系列</p>
<p style="flex: 4;text-align: end;">交易量</p>
</div>
<div class="RankBody">
<div v-for="(collection, innerIndex) in collectionGroup" :key="innerIndex" class="RankBodyItem">
<p style="flex: 1;">{{ collection.rank }}</p>
<div style="flex: 7;" class="RankBodyItemContent">
<div style="flex: 0.3;">
<img :src="collection.imageUrl" alt=""
style="height: 100%; width: 100%; border-radius: 20px; object-fit: cover; aspect-ratio: 1/1;">
</div>
<div style="padding-left: 20px;">
<p>{{ collection.title }}</p>
<p style="color: var(--text-200); padding-top: 10px;">地板价:{{ collection.price }}</p>
</div>
</div>
<p style="flex: 4; text-align: end;">{{ collection.tradingVolume }}</p>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';

const recommendedCollections = ref([
{
imageUrl: 'https://i.seadn.io/s/raw/files/6662e4fbea8ad15eb84990bc68351d57.png?auto=format&dpr=1&h=500&fr=1 1x, https://i.seadn.io/s/raw/files/6662e4fbea8ad15eb84990bc68351d57.png?auto=format&dpr=1&h=500&fr=1 2x',
title: 'Mint Genesis NFT',
price: '0.01 ETH',
tradingVolume: '68 ETH',
},
]);

const groupedCollections = computed(() => {
const grouped = [];
for (let i = 0; i < recommendedCollections.value.length; i += 5) {
grouped.push(recommendedCollections.value.slice(i, i + 5).map((collection, index) => ({ ...collection, rank: i + index + 1 })));
}
return grouped;
});
</script>

图片正方形实现

aspect-ratio: 1/1;

1
2
3
4
5
<div style="flex: 1;">
<img src="https://xxx.jpg" alt=""
style="height: 100%; width: 100%; border-radius: 20px; object-fit: cover; aspect-ratio: 1/1;">
</div>

圆角带图标的输入框效果

如何实现圆角带图标的输入框

主要是去掉输入框默认样式:border: 0px;

Html:

1
2
3
4
5
6
<div class="SearchInput">
<el-icon :size="16">
<Search />
</el-icon>
<input type="text" placeholder="搜索">
</div>

css:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* 输入框样式 */
.SearchInput {
display: flex;
justify-content: start;
align-items: center;

background-color: #FFFFFF;
border-radius: 12px;
max-width: 500px;

padding: 12px;

input {
outline: none;
padding-left: 10px;
font-size: 16px;
width: 200px; /* 调整输入框的宽度 */
border: 0px;
}
}

悬浮图片

主要是使用absolute定位,使图片悬浮在左下角

截屏2024-02-21 20.56.15

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.UserBackgroundAvatar {
display: flex;
justify-content: center;
align-items: center;
position: absolute;

/* 设置绝对定位,相对于包含它的 .UserBackground 定位 */
left: 5%;
bottom: -10%;
width: 20vh;
height: 20vh;

border-radius: 50%;
background-color: white;

box-shadow: 0 10px 10px rgba(0, 0, 0, 0.1);
}

手写悬浮二级菜单

这个比较死可以直接拿来用

截屏2024-02-21 20.54.50

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
<div class="Condition">
<p>最近收到</p>
<!-- 根据isConditionListVisible决定class是rotate-0还是rotate-180 -->
<el-icon :size="16" @click="toggleConditionList" :class="isConditionListVisible ? 'rotate-180' : 'rotate-0'">
<ArrowDownBold />
</el-icon>

<div class="ConditionList" v-if="isConditionListVisible">
<div class="ConditionListItem">
<p>最近收到</p>
</div>
<div class="ConditionListItem">
<p>价格从高到低</p>
</div>
<div class="ConditionListItem">
<p>价格从低到高</p>
</div>
<div class="ConditionListItem">
<p>最近创建的</p>
</div>
<div class="ConditionListItem">
<p>最高销售价格</p>
</div>
<div class="ConditionListItem">
<p>最早的</p>
</div>
</div>
</div>

TypeScript部分:

1
2
3
4
5
6
7
8
9
<script setup lang="ts">
import { ref } from "vue"
// 定义变量控制是否展示ConditionList
let isConditionListVisible = ref(false);
// 定义一个函数用于控制ConditionList的显示与隐藏
const toggleConditionList = () => {
isConditionListVisible.value = !isConditionListVisible.value;
};
</script>

CSS部分:

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
.Condition {
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
position: relative;
width: 250px;
height: 50px;
background-color: #FFFFFF;
border-radius: 12px;
border: 0.5px solid var(--text-200);
padding: 12px;

.rotate-180 {
transform: rotate(180deg);
transition: 0.25s ease-out;
}

.rotate-0 {
transition: 0.25s ease-out;
}

.ConditionList {
display: flex;
justify-content: center;
align-items: flex-start;
flex-direction: column;
position: absolute;
top: 60px;
right: 0px;
width: 250px;
background-color: #FFFFFF;
border-radius: 12px;
box-shadow: 0 10px 10px rgba(0, 0, 0, 0.1);
padding: 10px;

.ConditionListItem {
display: flex;
justify-content: flex-start;
align-items: center;
width: 100%;
height: 100%;
border-radius: 12px;
padding: 15px;
}

.ConditionListItem:hover {
cursor: pointer;
background-color: rgba(0, 0, 0, 0.1);
}
}
}

手写朝着点击方向移动的动画效果

截屏2024-02-15 22.38.14

方法一:
该方法是纯自己想,缺点在于开局就会转动,优点在于遇到开局需要运动的需求,可以用上这个

方法二:

该方法才是通用的方法,首先是大体的html结构部分,抛弃方法一中的,每个位置上都放置一个白色选择块,再通过点击显示点击位置的白色选择块的方法

实际上应该只放置一个白色选择块,点击后通过css中的translateX,将该唯一白色选择块移动至点击位置,最后用transition控制移动动画的长度

HTML部分:

1
2
3
4
5
6
7
8
9
<div class="FilterSectionType" style="flex: 2;">
<!-- 应用selectType方法 -->
<div :class="{ 'Selected0': TypeIndex.index === 0, 'Selected1': TypeIndex.index === 1 }">
<!-- Content of the div -->
</div>
<p @click="selectType(0)" style="position: absolute; left: 15%; z-index: 9999;">热门</p>

<p @click="selectType(1)" style="position: absolute; right: 15%; z-index: 9999;">最佳</p>
</div>

TypeScript部分:

1
2
3
4
5
6
7
8
9
10
11
<script setup lang="ts">
import { ref } from "vue"
// 定义TypeIndex

let TypeIndex = ref(0)
// 实现selectType方法
const selectType = (index: number) => {
TypeIndex.value = index

}
</script>

CSS部分:

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
.FilterSectionType {
display: flex;
justify-content: center;
align-items: center;
position: relative;
min-width: 150px;
height: 40px;
border-radius: 10px;
background-color: var(--accent-100);

/* 定义共同的样式 */
@mixin selected-style {
position: absolute;
width: 50%;
height: 80%;
border-radius: 10px;
background-color: var(--bg-100);
transition: 0.25s ease-out;
}

.Selected0 {
@include selected-style;
transform: translateX(-45%);
}

.Selected1 {
@include selected-style;
transform: translateX(45%);
}
}

absolute布局居中对齐

参考文章:绝对定位position:absolute;实现居中对齐_position: absolute; 居中-CSDN博客

实现原理为left以及top的布局是根据元素左上角进行定位的,但是我们的需求是元素的中心点居中,那么这个时候再使用translate(-50%, -50%)将从元素左上角变换至元素中心点,达成效果

1
2
3
4
5
6
7
8
9
.main {
position: absolute;
width: 700px;
height: 500px;
background: pink;
left: 50%; /* 起始是在body中,横向距左50%的位置 */
top: 50%; /* 起始是在body中,纵向距上50%的位置,这个点相当于body的中心点,div的左上角的定位 */
transform: translate(-50%, -50%); /* 水平、垂直都居中 */
}

taliwindcss版:

1
<div class="absolute left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2"></div>

悬停于v-for形成的元素时,仅其中一个元素,而不是全部都改变

创建一个对应cartList的数组来实现,首先是通过map创建了一个全是false的数组(序号与cartList对应),该数组内的值会在鼠标移入时触发mouseover事件,将该值改为true

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
<template>
<div v-for="(item, index) in cartList" :key="index" class="DetailBelow" @mouseover="showDelete(index)" @mouseleave="hideDelete()">
<div style="flex: 1;">
<p v-if="isDeleteVisible[index] && !isDeleteVisible[index].value">{{ item.price }}</p>
<div v-else @click="deleteCart(index)">
<el-icon>
<Delete />
</el-icon>
</div>
</div>
</div>
</template>
<script setup lang="ts">
// 定义一个变量isDeleteVisible
let isDeleteVisible = cartList.value.map(() => ref(false));
//当cartList改变时,isDeleteVisible也重新赋值
watch(cartList.value, (newValue, oldValue) => {
isDeleteVisible = newValue.map(() => ref(false));
console.log('watch 已触发', oldValue)
})
// 实现showDelete方法
const showDelete = (index: number) => {
isDeleteVisible.forEach((item, i) => (item.value = i === index));
};
// 实现hideDelete方法
const hideDelete = () => {
isDeleteVisible.forEach((item) => (item.value = false));
};
</script>

实现展开隐藏一段文字的效果

截屏2024-02-21 20.57.15

截屏2024-02-21 20.57.37

通过用不同的样式,实现该效果:

展开之前:

text-overflow: ellipsis: 用于在文本溢出时显示省略号(…)

white-space: nowrap:用于防止文本换行

width: 75vh;限定长度

展开之后:

不需要任何样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- 展开之前 -->
<div class="descriptionDetail" v-if="!isExpanded">
<p style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 75vh;">
一段很长的文字....
</p>
<!-- 展开放在div内部实现跟在文字后面的效果 -->
<p @click="toggleExpand"><a href="#" style="min-width: 10vh; font-weight: bold;">展开</a></p>
</div>


<!-- 展开之后 -->
<div class="descriptionDetail" v-else>
<p>
一段很长的文字....
</p>
</div>
<!-- 展开放在div内部实现位于文字下一行的效果 -->
<p @click="toggleExpand" v-if="isExpanded"><a href="#" style="min-width: 10vh; font-weight: bold;">收起</a></p>

箭头反转的动画效果

关键在于记忆rotate这个旋转度数的transform效果,以及作为transition的例子

HTML部分:

点击<el-icon>元素时,通过toggleTypeList方法来切换isTypeListVisible的值,从而改变<el-icon>元素的旋转效果。

1
2
3
<el-icon :size="16" @click="toggleTypeList" :class="isTypeListVisible ? 'rotate-180' : 'rotate-0'">
<ArrowDownBold />
</el-icon>

CSS部分:

.rotate-180类将元素旋转180度,并在0.25秒内以ease-out的过渡效果完成

1
2
3
4
5
6
7
8
.rotate-180 {
transform: rotate(180deg);
transition: 0.25s ease-out;
}

.rotate-0 {
transition: 0.25s ease-out;
}

手写checkBox

目前还不能实现改变check的颜色之类的样式,后续再来补‼️

HTML部分:

1
2
3
4
5
6
<div class="TypeListItem">
<label>
<input type="checkbox" name="type" value="all" checked>
<span style="margin-left: 20px;">现实</span>
</label>
</div>

CSS部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.TypeListItem {
display: flex;
justify-content: flex-start;
align-items: center;
width: 100%;
height: 100%;
border-radius: 12px;
padding: 15px;
}

.TypeListItem:hover {
cursor: pointer;
background-color: rgba(0, 0, 0, 0.1);
}

/* 勾选框变大,勾选背景颜色为var(--bg-100),勾选时颜色为var(--bg-200) */
input[type="checkbox"] {
transform: scale(1.5);
}

媒体查询

注意max-width为在窗口宽度小于等于 1300px 时,min-width为在窗口宽度大于等于 1300px 时,启用样式

1
2
3
4
5
6
7
/* 在窗口宽度小于等于 1300px 时,调整最小宽度 */
@media (max-width: 1300px) {
.CollectionListItem {
min-width: 155px;
max-width: 255px;
}
}

点击翻页功能

主要是通过更新数组来实现(还没好好看‼️)

这里本来是做一个滑动的动画效果,但是实在是没太多思路,有的思路也到处有问题,于是就换成更新数组来实现了

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
<div class="CollectionListAll">
<div class="PageBefore" @click="goToPreviousPage">
<el-icon>
<ArrowLeftBold />
</el-icon>
</div>
<!-- 使用translateX实现翻页效果 style="transform:translateX(-280px)" -->
<div class="CollectionListItems">
<div v-for="(item, index) in displayedItems" :key="index" class="CollectionListItem" @click="toNft">
<div class="CollectionListItemImage" style="height: 150px; width: 100%;">
<img style="height: 100%; width: 100%; border-radius: 20px 20px 0px 0px; object-fit: cover;"
:src="item.imageUrl" alt="" />
</div>

<p style="text-align: left; padding: 10px 20px;">{{ item.title }}</p>
<div class="CollectionListItemDetail">
<div>
<p class="text-base font-normal">交易价格</p>
<p>{{ item.price }}</p>
</div>
<div>
<p class="text-base font-normal">24小时交易量</p>
<p>{{ item.tradingVolume }}</p>
</div>
</div>
</div>
</div>
<!-- TODO:Page应该以fix或者absulute的布局放在外层,目前的布局会与overflow:hidden冲突 -->
<div class="PageNext" @click="goToNextPage">
<el-icon>
<ArrowRightBold />
</el-icon>
</div>
</div>

TypeScript部分:

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
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from "vue"

const currentPage = ref(0);
const displayedItems = ref<Collection[]>([]);

const updateDisplayedItems = () => {
const itemsPerPage = calculateItemsPerPage();
const startIndex = currentPage.value * itemsPerPage;
displayedItems.value = collectionItems.slice(startIndex, startIndex + itemsPerPage);
};

const calculateItemsPerPage = () => {
const screenWidth = window.innerWidth;

if (screenWidth < 912) {
return 2;
} else if (screenWidth < 1235) {
return 3;
} else if (screenWidth < 1520) {
return 4;
} else if (screenWidth < 1809) {
return 5;
} else {
return 6;
}
};

const goToPreviousPage = () => {
if (currentPage.value > 0) {
currentPage.value--;
updateDisplayedItems();
}
};

const goToNextPage = () => {
const totalPages = Math.ceil(collectionItems.length / calculateItemsPerPage());
if (currentPage.value < totalPages - 1) {
currentPage.value++;
updateDisplayedItems();
}
};

onMounted(() => {
updateDisplayedItems();
window.addEventListener('resize', updateDisplayedItems);
});

onBeforeUnmount(() => {
window.removeEventListener('resize', updateDisplayedItems);
});

</script>

手写侧栏(‼️)

通常用于侧边导航

通过ul以及li来展示菜单项

同时把菜单项放入了数组menu

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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
<template>
<div class="Sidebar">
<div class="sidebar-logo-container">
<img class="h-8" src="https://yiming_chang.gitee.io/vue-pure-admin/logo.svg">
<p>HyperStarAdmin</p>
</div>
<el-scrollbar height="90%">

<ul>
<!-- 遍历菜单项 -->
<li v-for="(menu, index) in menus" :key="index">
<div class="menu-item" @click="selectMenu(index, menu.children, menu.path!)"
:class="{ 'active-menu': selectedMenu === index }">
<el-icon color="#3E8CFF" v-if="selectedMenu === index">
<component :is="menu.icon"></component>
</el-icon>
<el-icon v-else>
<component :is="menu.icon"></component>
</el-icon>
<p>{{ menu.label }}</p>
<!-- 如果有子菜单,显示箭头 -->
<el-icon v-if="menu.children" class="ml-7">
<ArrowDownBold v-if="!ifShowSubMenu" />
<ArrowUpBold v-else />
</el-icon>

</div>
<!-- 如果有子菜单,渲染子菜单 -->
<ul v-if="menu.children && ifShowSubMenu">
<li v-for="(child, childIndex) in menu.children" :key="childIndex">
<div class="menu-item child-menu"
@click="selectSubMenu(index, childIndex, menu.children[childIndex].path!)"
:class="{ 'active-menu': selectedSubMenu === childIndex }">
<p class="ml-6">{{ child.label }}</p>
</div>
</li>
</ul>
</li>
</ul>


</el-scrollbar>

</div>
</template>

<script setup lang="ts">
// const handleOpen = (key: string, keyPath: string[]) => {
// console.log(key, keyPath)
// }
// const handleClose = (key: string, keyPath: string[]) => {
// console.log(key, keyPath)
// }

import { ref } from 'vue';
import { useRouter } from 'vue-router';
// 实例化router
const router = useRouter();

const selectedMenu = ref<number | null>(null);
const selectedSubMenu = ref<number | null>(null);
// ifShowSubMenu
const ifShowSubMenu = ref<boolean>(false);

const menus = [
{ label: '首页', icon: 'HomeFilled', path: '/' },
{ label: '数字藏品管理', icon: 'GoodsFilled', path: '/goods' },
{
label: '用户管理',
icon: 'Menu',
path: '/account',
},
{
label: '交易管理',
icon: 'List',
path: '/order',
children: [
{ label: '订单处理', path: '/audit' },
{ label: '营销与推广', path: '/marketing' },
],
},

{ label: '数据', icon: 'TrendCharts', path: '/data' },
{ label: '设置', icon: 'Setting', path: '/setting' },
];
// 保证第一个选择的是首页
selectedMenu.value = 0;


const toggleSubMenu = () => {
// 翻转子菜单的显示状态
ifShowSubMenu.value = !ifShowSubMenu.value;
if (ifShowSubMenu.value) {
selectedSubMenu.value = 0; // 清除子菜单的选中状态
}
};


const selectMenu = (index: number, ifChildren: any, path: string) => {
if (!ifChildren) {
selectedMenu.value = index;
selectedSubMenu.value = null; // 清除子菜单的选中状态
router.push(path)

} else {
selectedMenu.value = null;
selectedSubMenu.value = 0; // 清除子菜单的选中状态
router.push(menus[index].children![0].path!);
toggleSubMenu();

}

};

const selectSubMenu = (parentIndex: number, childIndex: number, path: string) => {
router.push(path)

selectedMenu.value = null;
selectedSubMenu.value = childIndex;
};



</script>

<style lang="scss" scoped>
.Sidebar {
height: 100%;
border-radius: 24px;
background: #fff;
.sidebar-logo-container {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;

padding: 20px 0;

p {
font-weight: 800;
color: var(--text-100);
font-size: 18px;

}
}

.menu-item {
display: flex;
align-items: center;
gap: 12px;


padding: 15px 20px;
margin: 10px;
cursor: pointer;
transition: all 0.3s ease; /* 添加过渡效果 */

&:hover {
border-radius: 20px;
background: var(--primary-100);
}


p {
font-size: 16px;
color: var(--text-100);
}

&.active-menu {
border-radius: 20px;
background: var(--primary-100);


}
&.child-menu {
// 定义子菜单的样式
&.active-menu {
background: var(--primary-100);

}
}
}
}
</style>

不使用flex的情况下位于一行中

使用inline-block布局

注意想要处于同一行的元素,都需要加上inline-block

1
2
3
4
5
6
7
8
9
<div class="">
<p class="text-2xl inline-block">76.345</p>
<div class="inline-block bg-[#5DB1FF] rounded-3xl p-2">
<div class="flex justify-center items-center gap-2">
<img class="size-4" src="../assets/images/trending-up-white@2x.png">
<p class="text-xs text-white">23.5% (+10)</p>
</div>
</div>
</div>

点击菜单(‼️)

重点记忆下v-for,经常容易忘记

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
<template>
<div class="StatusSelection">
<div v-for="(item, index) in items" :key="index" class="item" @click="handleItemClick(index)" :class="{ active: selectedIndex === index }">
<p>{{ item }}</p>
</div>
</div>
</template>

<script setup lang="ts">
import { ref } from 'vue';

const items = ref(['全部订单', '待付款', '代发货', '已发货', '退款中', '已完成']);
const selectedIndex = ref(-1);

const handleItemClick = (index: number) => {
selectedIndex.value = index;
};
</script>

<style scoped>
.StatusSelection {
display: flex;
justify-content: flex-start;
align-content: center;
gap: 20px;
min-height: 80px; /* 为item多留出boder的距离 */
padding: 16px;

.item {
cursor: pointer;
transition: all 0.1s ease-out;
padding: 8px 16px;

&:hover {
color: var(--accent-100);
border-bottom: 2px solid var(--accent-100);
}

&.active {
color: var(--accent-100);
border-bottom: 2px solid var(--accent-100);
}
}
}
</style>

手写文件上传框

HTML部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<div class="flex-[1_1_0] min-h-96 bg-white rounded-2xl m-10 p-10">
<p class="text-left font-medium">分类图片</p>
<p class="text-neutral-500 my-2 mb-10">选择产品照片或在此处一次拖放,最多1张照片</p>
<div v-if="!uploadedImage" @click="openFileInput" class="flex flex-col justify-center items-center gap-5 h-40 border border-dashed border-text-200 rounded-2xl mt-30 bg-bg-200 cursor-pointer transition-bg-20 hover:border-solid hover:border-text-200 hover:bg-rgba-18-18-18-0.04">
<el-icon size="40">
<Upload />
</el-icon>
<p class="text-16 text-accent-100 font-bold">
拖拽媒体或点击选择文件
</p>
<p> 最大尺寸:50MB</p>
<input id="fileInput" type="file" ref="fileInput" style="display: none;" @change="uploadFile">
</div>
<img v-else :src="uploadedImage" alt="上传的图片" />
</div>

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<script setup lang="ts">
import { ref } from "vue"


import { uploadImage } from "../api/collections"
import { addType } from "../api/type"
import router from "../router";

let name = ref("");

// 定义上传后的图片URL
const uploadedImage = ref<string | null>(null);

// 通过div点击input的方法
const openFileInput = () => {
const fileInput = document.getElementById('fileInput') as HTMLInputElement;
if (fileInput) {
fileInput.click();
}
};

// 上传图片
const uploadFile = async () => {
const fileInput = document.getElementById('fileInput') as HTMLInputElement;
// 确保存在文件
if (fileInput && fileInput.files && fileInput.files.length > 0) {
const formData = new FormData();
formData.append('file', fileInput.files[0]);

await uploadImage(formData).then((res) => {
uploadedImage.value = res as string;
}).catch((err) => {
console.log(err);
});
}
};

// 添加分类
const handleAddType = async() => {
const formData = new FormData();

// 判断是否有分类名称输入
if(!name.value){
ElMessage.error('分类名称不能为空');
return;
}

if (uploadedImage.value) {
formData.append('name', name.value);
formData.append('cover', uploadedImage.value);

await addType(formData).then((res) => {
console.log(res);
ElMessage.success('添加分类成功');
router.push('/type');
}).catch((err) => {
console.log(err);
ElMessage.error('添加分类失败,请重试');
})
}else{
ElMessage.error('请上传分类图片');
}
};

404界面使用

router部分:
通过 path: '/:catchAll(.*)'来匹配所有的路径,当用户访问错误的路径时就会展示NotFoundView组件

1
2
3
4
5
6
7
8
const routes: Array<RouteRecordRaw> = [
{
path: '/:catchAll(.*)', // 匹配所有路径
name: "NotFoundView",
component: () => import("../views/NotFoundView.vue"),
}

];

404界面部分:

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
<template>
<div class="NotFoundView dis">
<MainNavbar/>
<img class="w-[600px] m-auto py-10 rounded-sm" src="../assets/images/Page_not_found.png" alt="">
<p class="text-4xl font-bold">此页面已丢失</p>
<p class="text-2xl font-medium text-text-200">我们已进行深入和广泛的探索</p>
<p class="text-2xl font-medium text-text-200">但我们无法找到您要找的页面</p>
<div class="w-40 bg-accent-200 rounded-2xl cursor-pointer px-2 py-5 mx-auto my-10" @click="toIndex">
<p class="text-white font-bold text-lg">导航返回主页</p>
</div>
</div>
</template>

<script setup lang="ts">
import { } from "vue"
import router from "../router";


import MainNavbar from '../components/MainNavbar.vue'

const toIndex = () => {
router.push({
name: 'IndexView',
})
}
</script>

<style lang="scss" scoped></style>

历史导航栏(‼️)

关注下是如何监听路由的

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
<div class="Header">
<div class="nav-tabs">
<div v-for="(tab, index) in tabs" :key="index" :class="{ 'nav-tab-item': true, active: currentTab === index }"
@click="switchTab(index)">
<p>{{ tab.name }}</p>
<el-icon size="14" @click.stop="closeTab(index)">
<Close />
</el-icon>
</div>
</div>
<div class="user-tabs">
<el-icon size="20">
<Search />
</el-icon>
<el-icon size="20">
<Bell />
</el-icon>
<el-icon size="20" @click="toggleFullScreen" style="cursor: pointer;">
<FullScreen />
</el-icon>
<div class="user-info">
<img class="w-8 h-8 rounded-full object-cover aspect-square"
src="https://demo.buildadmin.com/static/images/avatar.png">
<p>Admin</p>
</div>
<el-icon size="20">
<Tools />
</el-icon>
</div>
</div>

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
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
import { ref, watch, onMounted } from 'vue';
import { useRouter, RouteLocationNormalizedLoaded } from 'vue-router';

const isFullScreen = ref(false);

const toggleFullScreen = () => {
if (isFullScreen.value) {
exitFullScreen();
} else {
enterFullScreen();
}
};

const enterFullScreen = () => {
const element = document.documentElement;

if (element.requestFullscreen) {
element.requestFullscreen();
}

isFullScreen.value = true;
};

const exitFullScreen = () => {
if (document.exitFullscreen) {
document.exitFullscreen();
}

isFullScreen.value = false;
};

watch(isFullScreen, (newValue) => {
// 在这里可以处理全屏状态变化后的逻辑
console.log('全屏状态变化:', newValue);
});



const tabs = ref([
{ name: '首页', route: '/' },
{ name: '订单管理', route: '/audit' },
{ name: '数宇藏品管理', route: '/goods' },
{ name: '用户管理', route: '/account' },
{ name: '设置', route: '/setting' },
{ name: '数据', route: '/data' },
]);

const currentTab = ref(0);
const router = useRouter();

// 切换导航
const switchTab = (index: number) => {
currentTab.value = index;
const route = tabs.value[index].route;
router.push(route);
console.log(route)
};

// 关闭标签
const closeTab = (index: number) => {
const isCurrentTab = index === currentTab.value;

// 移除标签
tabs.value.splice(index, 1);

// 如果关闭的是当前标签,则导航到前一个标签
if (isCurrentTab && tabs.value.length > 0) {
const prevTab = tabs.value[Math.max(index - 1, 0)];
switchTab(tabs.value.indexOf(prevTab));
}
};

// 监听路由变化,更新当前标签
onMounted(() => {
const updateCurrentTab = () => {
const currentRoute = router.currentRoute.value as RouteLocationNormalizedLoaded;

// 检查当前路由是否在tabs数组中,如果不在就添加
const index = tabs.value.findIndex(tab => tab.route === currentRoute.path);
if (index === -1) {
tabs.value.push({ name: String(currentRoute.name || '未命名'), route: currentRoute.path });
}

// 更新当前标签
const newIndex = tabs.value.findIndex(tab => tab.route === currentRoute.path);
currentTab.value = newIndex !== -1 ? newIndex : 0;
};

router.afterEach(updateCurrentTab);
updateCurrentTab();
});

CSS部分:

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
@mixin flex-center {
display: flex;
justify-content: center;
align-items: center;
}
@mixin flex-stat {
display: flex;
justify-content: flex-start;
align-items: center;
}

.Header {
display: flex;
justify-content: space-between;
align-items: center;


width: 100%;
height: 50px;

background-color: #fff;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
border-radius: 14px;


padding: 0 20px 0 0;
margin: 24px;


.nav-tabs {
@include flex-stat;

position: relative;
overflow-x: auto;
overflow-y: hidden;

scrollbar-width: none;


width: 846px;
height: 100%;


.nav-tab-item {
@include flex-center;

gap: 10px;

height: 100%;


cursor: pointer;
white-space: nowrap;
color: var(--text-100);
border-radius: 14px;

padding: 0 20px;

&.active {

background-color: var(--bg-200);

}
}
}

.user-tabs {
@include flex-center;
gap: 20px;

.user-info {
@include flex-center;
gap: 10px;
}
}
}

vue3+ts+vite初始化项目

1、使用vite构建项目

1
sudo npm create vite@latest 

可能会出现输入命令没反应的情况

1
2
3
4
npm config set registry=https://registry.npmmirror.com 

//执行以下命令查看是否配置成功
npm config get registry

2、依次命令0

最后出现初始页,代表成功

1
2
3
cd NFT-Admin(将文件切换到该文件夹)
npm install (安装项目依赖)
npm run dev (运行项目)

接着记得给权限,否则无法保存任何文件

1
sudo chmod -R 777 /Users/tec/NFT-Admin

3、将项目文件夹拖入VsCode

但是会出现两个报错

第一个是找不到模组vue

1
2
import { ref } from 'vue'
// Cannot find module 'vue'. Did you mean to set the 'moduleResolution' option to 'node', or to add aliases to the 'paths' option?Vetur(2792)

解决方法为修改tsconfig.json文件,然后关闭 VScode ,重新启动一下项目即可。

bundler改为node

1
2
// "moduleResolution": "bundler",
"moduleResolution": "node",

第二个是两个组件VolarVetur冲突

Vetur是针对vue2的,Volar是针对vue3的关一个就行

1
2
import HelloWorld from './components/HelloWorld.vue'
// "Module '\"/Users/tec/NFT-Platform/src/components/HelloWorld.vue\"' has no default export."

4、引入element plus

首先通过 npm 下载

1
npm install element-plus --save

其次安装两个插件实现自动导入

1
npm install -D unplugin-vue-components unplugin-auto-import

最后配置vite.config.ts文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
})

最后还有在tsconfig.json文件(用于解决找不到名称“ELMessage”,实际上已经能运行等bug)

1
2
3
4
5
6
7
8
{
"compilerOptions": {
// 省略
},
// 下面是重点
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "auto-imports.d.ts"],
}

还有在vite.config.ts文件中进行配置,不过好像上面element-plus中的时候就已经引入过了,但是我新建项目时,发现不重新输一遍,还是报错

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
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import tailwindcss from 'tailwindcss'
import autoprefixer from 'autoprefixer'

// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),

],

css: {
postcss: {
plugins: [tailwindcss, autoprefixer]
}
}

})

5、引入vue-router

1
npm install vue-router@4

新建 router 文件夹,在该文件夹下面新建 index.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";

// 2. 配置路由
const routes: Array<RouteRecordRaw> = [
{
path: "/",
component: () => import("../views/index.vue"),
},
{
path: "/hello",
component: () => import("../components/HelloWorld.vue"),
},
];
// 1.返回一个 router 实列,为函数,里面有配置项(对象) history
const router = createRouter({
history: createWebHistory(),
routes,
});

// 3导出路由 然后去 main.ts 注册 router.ts
export default router

在 main.ts 中挂载 router

1
2
3
4
5
6
7
8
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from "./router/index"

createApp(App)
.use(router)
.mount('#app')

将程序入口 App.vue 内部改为 router入口

1
2
3
4
5
6
7
<script setup lang="ts">

</script>

<template>
<router-view></router-view>
</template>

6、一键生成vue模版

在 VsCode 界面按住 command+shift+P ,然后在上方的输入栏中输入snippets,回车后,再次输入vue,进入 vue.json 的文件中,输入下面的模版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"Print to console": {
"prefix": "vue",
"body": [
"<template>",
" <div class=\"\"></div>",
"</template>\n",
"<script setup lang=\"ts\">",
"import {} from \"vue\"",
"</script>\n",
"<style lang=\"scss\" scoped></style>",
"$2"
],
"description": "Log output to console"
}
}

然后在 vue 文件中,输入 vue 则可得到下面的模版

1
2
3
4
5
6
7
8
9
10
11
<template>
<div class="">

</div>
</template>

<script setup lang="ts">
import { } from "vue"
</script>

<style lang="scss" scoped></style>

7、引入scss

在 vue3+vite 中已经内置了 scss 的相关启动器,只需要下载一个 sass 就行,比 webpack 所需要加入的依赖少得多

1
npm install -D sass

8、引入tailwindcss

见下面的tailwindcss学习部分

9、引入axios

见下面的axios学习学习部分

Pinia学习

Pinia使用

1、首先安装Pinia

1
2
# 需要 cd 到的项目目录下
sudo npm install pinia

2、更改main.ts文件

1
2
3
4
5
6
7
8
9
import { createApp } from 'vue'
import { createPinia } from 'pinia' // 导入 Pinia
import App from '@/App.vue'

const app = createApp(App)

app
.use(createPinia()) // 启用 Pinia
.mount('#app')

3、创建stores文件夹

在该文件夹下面就可以创建相关的Store文件

按照之前做过的来说,一般是一个store一个文件的,但是我觉得很多比较重复,所以就相似的放到一起了

SelectedIndexStore.ts:

1
2
3
4
5
6
7
8
9
10
11
12
import { defineStore } from 'pinia';

export const SelectedTypeIndexStore = defineStore('SelectedTypeIndexStore', {
state: () => ({
index: 0,
}),
});
export const SelectedUserIndexStore = defineStore('SelectedUserIndexStore', {
state: () => ({
index: 0,
}),
});

CollectionStore.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
// src/stores/index.ts
import { defineStore } from 'pinia';

export const RecommendedCollectionStore = defineStore('RecommendedCollectionStore', {
state: () => ({
collections: [
{
imageUrl: '',
title: '',
price: '',
tradingVolume: '',
}
],
}),
});
export const CollectionRankingStore = defineStore('CollectionRankingStore', {
state: () => ({
collections: [
{
imageUrl: '',
title: '',
price: '',
tradingVolume: '',
}
],
}),
});
export const PopularAnimationCollectionStore = defineStore('PopularAnimationCollectionStore', {
state: () => ({
collections: [
{
imageUrl: '',
title: '',
price: '',
tradingVolume: '',
}
],
}),
});

4、使用 store 实例

主要首先从store的ts文件中引入

然后定义示例

最后根据示例属性进行使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { RecommendedCollectionStore } from '../stores/CollectionStore'
import { SelectedTypeIndexStore } from '../stores/SelectedIndexStore'
import { Collection } from '../interfaces/Collection';


// 像 useRouter 那样定义一个变量拿到实例
const RecommendedCollection = RecommendedCollectionStore()

// 初始值
const recommendedCollections: Collection[] = [
{
imageUrl: 'https://i.seadn.io/s/raw/files/6662e4fbea8ad15eb84990bc68351d57.png?auto=format&dpr=1&h=500&fr=1 1x, https://i.seadn.io/s/raw/files/6662e4fbea8ad15eb84990bc68351d57.png?auto=format&dpr=1&h=500&fr=1 2x',
title: 'Mint Genesis NFT',
price: '0.01 ETH',
tradingVolume: '68 ETH',
},
]

// 使用 setState 方法赋值
RecommendedCollection.collections = recommendedCollections

pinia持久化

原文链接:Vue3.0使用Pinia + 持久化 - 掘金 (juejin.cn)

main.ts的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
import { createApp } from 'vue'
import App from './App.vue'

//如果没有使用 pinia-plugin-persist持久化插件
import {createPinia} from 'pinia'
createApp(APP).use(store).use(router).use(createPinia()).mount('#app')

//如果使用持久化
import piniaPersist from 'pinia-plugin-persist'
cosnt pinia = createPinia()
pinia.use(piniaPersist)
createApp(APP).use(store).use(router).use(pinia).mount('#app')

使用持久化选项persist:true

1
2
3
4
5
6
7
8
9
10
11
12
import { defineStore } from 'pinia';
// 引入Type接口
import { Type } from '../interfaces/Type';

export const TypeStore = defineStore('TypeStore', {
state: () => ({
typeInfo: [] as Type[], // Add an array to store the categories
}),
//数据持久化配置 这里是当前所有变量都持久化
persist:true
});

进阶配置,指定本地如何储存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { defineStore } from "pinia";
export const userStore = defineStore("user", {
state: () => {
return {
count: 1,
age: 11,
};
},
//整个仓库持久化存储
persist: {
enabled: true,
//指定字段存储,并且指定存储方式:
strategies: [
{ storage: sessionStorage, paths: ['count', 'age'] }, // age 和 count字段用sessionStorage存储
{ storage: localStorage, paths: ['accessToken'] }, // accessToken字段用 localstorage存储
],
},
});

上面的是错的(😅)

首先是下载sudo npm i pinia-plugin-persistedstate

然后在main.ts中引入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ① 引入createPinia方法从pinia
import { createPinia } from 'pinia'
// ② 拿到pinia实例
const pinia = createPinia()


// 1 引入数据持久化插件
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
// 2 pinia使用数据持久化插件
pinia.use(piniaPluginPersistedstate)

const app = createApp(App)
app
.use(pinia) // 启用 Pinia

pinia内部直接获取网络请求的数据(‼️)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { defineStore } from 'pinia';
import { getRecommendedCollections } from '@/api/collections';
import { Collection } from '@/interfaces/Collection';

export const useRecommendedCollectionStore = defineStore('recommendedCollection', {
state: () => ({
collections: [] as Collection[],
}),
actions: {
async fetchData() {
try {
const res = await getRecommendedCollections();
this.collections = res.data ?? [];
} catch (error) {
console.error(error);
}
},
},
});

tailwindcss学习

安装

官网完全缺少了vite.config的步骤,还多余了一些步骤‼️

原文链接:vite+vue3使用tailwindcss - 掘金 (juejin.cn)(原文的vite.config配置是有问题的)

1.利用npm安装tailwindcss

安装 Tailwind 以及其它依赖项:

1
npm install -D tailwindcss@latest postcss@latest autoprefixer@latest

2.创建tailwindcss的配置文件

1
npx tailwindcss init

这将会在您的项目根目录创建一个最小化的 tailwind.config.js 文件:

1
2
3
4
5
6
7
8
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [],
theme: {
extend: {}
},
plugins: []
}

3.在入口中引入tailwind

1
import "tailwindcss/tailwind.css"

4.配置tailwind.config.js文件

1
2
3
4
5
6
7
8
9
10
export default {
content: [
"./index.html",
"./src/**/*.{vue,js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}

tailwind.config.js 文件中,配置 content 选项指定所有的 pages 和 components ,使得 Tailwind 可以在生产构建中,对未使用的样式进行tree-shaking

5.配置vite.config选项

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
//注意引入
import tailwindcss from 'tailwindcss'
import autoprefixer from 'autoprefixer'

// https://vitejs.dev/config/
export default defineConfig({
/*plugins: [
vue(),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),

],*/

//下面是新加入的内容
css: {
postcss: {
plugins: [tailwindcss, autoprefixer]
}
}
})

使用postcsstailwindcssautoprefixer插件对,css进行处理

6.配置vscode的代码提示

这个步骤vscode配过一次就行,其实配置到这里我已经完成对tailwind的安装,但在模板中仍没有智能的提示,此时需要去settings.json中,在末尾添加以下代码段:

1
2
3
"editor.quickSuggestions": {
"strings": true
}

基本属性

  1. 布局

- w: width

- max-w: max-width

- h: height

- max-h: max-height

- m: margin

- mt: margin-top

- mb: margin-bottom

- ml: margin-left

- mr: margin-right

- p: padding

- pt: padding-top

- pb: padding-bottom

- pl: padding-left

- pr: padding-right

  1. 文本样式

- font: font-family

- text: text-color, text-alignment, text-transform, font-size

- leading:line-height

- tracking: letter-spacing

- uppercase: text-transform: uppercase

- lowercase: text-transform: lowercase

  1. 背景和边框

- bg: background-color

- border: border-style, border-width, border-color

- rounded: border-radius

- shadow: box-shadow

  1. 弹性盒子布局

- flex: display: flex

- justify: justify-content

- items: align-items

- self: align-self

- order: order

- flex-grow: flex-grow

- flex-shrink: flex-shrink

  1. 网格布局

- grid-cols: grid-template-columns

- grid-rows: grid-template-rows

- gap: grid-gap

  1. 响应式设计

- sm, md, lg, xl: 分别对应移动设备、平板、桌面、大屏幕

- hover: 鼠标悬停时的样式

- focus: 元素获取焦点时的样式

除了上面列举的 Tailwind CSS 缩写和对应含义之外,Tailwind CSS 还提供了很多其他的实用程序类,以下是一些常用的 Tailwind CSS 缩写和对应含义:

  1. 边框和分隔符

- divide: 分隔符 (border-color, border-style, border-width)

- divide-x: 水平分隔符 (border-color, border-style, border-width)

- divide-y: 垂直分隔符 (border-color, border-style, border-width)

- border-collapse: 设置边框是否合并

  1. Flexbox 尺寸和排列

- flex-wrap: 等同于 flex-flow 中的 wrap

- flex-row, flex-row-reverse, flex-col, flex-col-reverse: flex-direction 的简写

- flex-1…flex-12: 设置 flex-grow、flex-shrink 和 flex-basis 属性

- gap-x: 水平包裹在对象(如 flex 子元素)之间的间距。

- gap-y: 垂直包裹在对象(如 flex 子元素)之间的间距。

- space-x: 水平排列中对象(如 flex 子元素)之间的空间

- space-y: 垂直排列中对象(如 flex 子元素)之间的空间

  1. Z-index

- z-{n}: 设置 z-index 的值,其中 n 为正整数

  1. 动画

- animate-{name}: 向元素添加动画(使用 @keyframes 中定义的动画名称)

  1. 列表样式

- list-style-{type}: 设置列表项的类型 (disc, decimal, decimal-leading-zero)

  1. 转换和过渡

- transform: 让元素旋转、缩放、倾斜、平移等

- transition-{property}: 用于添加一个过度效果 {property} 的值是必需的。

  1. 颜色

- text-{color}: 设置文本颜色

- bg-{color}: 设置背景颜色

- border-{color}: 设置边框颜色

  1. 字体权重

- font-thin: 字体细

- font-light: 字体轻

- font-normal: 字体正常

- font-medium: 字体中等

- font-semibold: 字体半粗

- font-bold: 字体粗

- font-extrabold: 字体特粗

- font-black: 字体黑

  1. SVG

- fill-{color}: 设置 SVG 填充颜色

- stroke-{color}: 设置 SVG 描边颜色

  1. 显示和隐藏

- hidden: 隐藏元素(display: none)

- invisible: 隐藏元素,但仍保留该元素的布局和尺寸

- visible: 显示元素

  1. 清除浮动

- clear-{direction}: 清除某个方向的浮动效果

  1. 容器

- container: 将内容限制在最大宽度的容器内部

- mx-auto: 实现水平居中(margin-left 和 margin-right 设置为 auto)

以上是一些常用的 Tailwind CSS 缩写及其对应的意义,覆盖了基础的布局、文本、背景、边框、弹性盒子布局、网格布局和响应式设计,有助于更快速地开发出具有良好用户体验的 Web 应用程序。

常用属性

flex布局

1
2
3
<div class="flex justify-center items-center flex-col gap-2">

</div>

字体大小

依次加减2px

1
2
3
<p class="text-xs text-sm text-base text-lg text-xl">

</p>

字重

从font-normal开始依次加减100

1
2
3
<p class="font-light font-normal font-medium font-semibold font-bold font-extrabold">

</p>

字位置偏向

1
2
3
<p class="text-left">

</p>

字颜色:

常用灰色

1
2
3
<p class="text-neutral-500">

</p>

任意值

1
2
3
<p class="text-[#50d71e]">
<!-- ... -->
</p>

鼠标变为手型

1
2
3
<p class="cursor-pointer">
<!-- ... -->
</p>

圆角

下面的为只有top的左右都加上圆角

1
2
3
<div class="rounded-t-2xl">
<!-- ... -->
</div>

!important

在前面加上!

1
2
3
<p class="!text-base">
<!-- ... -->
</p>

常用例子

灰色的圆形div:

前面的flex布局时为了,内部的元素水平垂直居中

1
2
3
<div class="flex justify-center items-start p-3 bg-neutral-300 rounded-full">
<el-icon><ChatLineSquare /></el-icon>
</div>

圆形且缩放正常的图片(可以通过更改rounded的值获取圆润边框图片):

1
2
3
<img src="https://opensea.io/static/images/logos/opensea-logo.svg" alt="" 
style="height: 90%; width: 90%; border-radius: 50%; object-fit: cover; aspect-ratio: 1/1;">
<img class="w-9 h-9 rounded-full object-cover aspect-square" src="../assets/images/Avatar1@2x.png" alt="">

悬浮时的灰色背景

1
<div class="flex justify-start items-center hover:bg-zinc-100 rounded-xl cursor-pointer"></div>

自动省略的文字:

1
<p class="w-10 text-left font-normal text-xs text-ellipsis whitespace-nowrap overflow-hidden">东大dsadasdsad寺是日本最著名、最重要的寺庙之一,也是奈良的地标。</p>

创建了一个具有渐变背景色的 div 元素

1
background-image: linear-gradient(117.67deg, rgb(32, 129, 226), rgb(250, 250, 250));

bg-gradient-to-br: 这个类表示定义一个背景渐变色,并指定了渐变的方向为从左上角(top-left)到右下角(bottom-right)。

from-blue-500: 这个类指定了渐变色的起始颜色为名为 “blue-500” 的蓝色变体。这里的 “blue-500” 可能是 Tailwind CSS 中定义的某种蓝色色彩。

1
<div class="w-full h-80 bg-gradient-to-br from-blue-500 to-white rounded-t-2xl"></div>

taliwind的阴影

drop-shadow-[0_0px_10px_rgba(0,0,0,0.25)]: 这个类定义了一个投影效果,参数为 [0_0px_10px_rgba(0,0,0,0.25)],表示在 (0, 0) 坐标处产生一个 10 像素的阴影,颜色为 RGBA(0,0,0,0.25),即黑色带有透明度。

1
2
3
<div class="flex justify-between items-center drop-shadow-[0_0px_10px_rgba(0,0,0,0.25)] bg-white rounded-2xl mt-5 p-10 mx-5">
<p>通过搜索获取帮助</p>
</div>

taliwind的过渡

transition-bg duration-300: 这两个类一起使用,定义了背景色变化时的过渡效果,持续时间为 300 毫秒。这样在背景色发生变化时会有一个平滑的过渡效果。

1
2
3
4
5
<div class="w-full flex justify-between items-center hover:bg-accent-100 transition-bg duration-300 rounded-xl px-3 py-2">
<p class="font-bold">通过搜索获取帮助</p>
<el-icon size="20" color="#9A73B5"><ArrowRightBold /></el-icon>
</div>

absolute布局居中对齐

参考文章:绝对定位position:absolute;实现居中对齐_position: absolute; 居中-CSDN博客

taliwindcss版:

1
<div class="absolute left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2"></div>

优化写法

使用@layer引入自定义样式

1
2
3
4
5
6
7
8
9
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer utilities {
.content-auto {
content-visibility: auto;
}
}

在class中使用自定义样式

1
2
3
<div class="lg:dark:content-auto">
<!-- ... -->
</div>

使用 Tailwindcss 影响一些基础样式的解决方法

原文地址:使用 Tailwindcss 影响一些基础样式的解决方法 - 掘金 (juejin.cn)

禁用 preflight

首先,如果我们不需要使用 Preflight提供的基础样式,那么我们可以使用“一刀切”的形式,禁用 Preflight如下:

1
2
3
4
5
6
// tailwind.config.js
module.exports = {
corePlugins: {
preflight: false,
}
}

我们需要在项目中的 tailwind.config.jscorePlugins部分设置preflight设置为false.

重写基础样式

通过 @layer base 指令中添加自定义的css,例如,我们想重新定义标题类的样式:

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
@tailwind base;

@layer base {
h1 {
@apply text-2xl;
}
h2 {
@apply text-xl;
}
h3 {
@apply text-lg;
}
h4 {
@apply text-base;
}
h5 {
@apply text-sm;
}
h6 {
@apply text-xs;
}
}

@tailwind components;
@tailwind utilities;

通过使用 @layer指令,Tailwind 将自动将这些样式移到 @tailwind base的同一位置,以避免出现一些意外问题。

Sass学习

主要是学习了@mixin结合@include的用法,抽离出css进行封装,多次使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 定义共同的样式
@mixin selected-style {
position: absolute;
width: 50%;
height: 80%;
border-radius: 10px;
background-color: var(--bg-100);
transition: 0.25s ease-out;
}

.Selected0 {
@include selected-style;
transform: translateX(-45%);
}

.Selected1 {
@include selected-style;
transform: translateX(45%);
}

ECharts(‼️)

引入

首先下载包

1
npm install echarts --save

然后在想要使用的位置,引入包(这里属于局部引入,个人觉得比全部引入好些)

1
import * as echarts from 'echarts';

基础使用

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 ref="chartContainer" style="width: 600px; height: 400px;"></div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue';
import * as echarts from 'echarts';

const chartContainer = ref<HTMLElement | null>(null);
let myChart: echarts.ECharts | null = null;

onMounted(() => {
// 在组件挂载后初始化 ECharts 实例
myChart = echarts.init(chartContainer.value!);

// 调用渲染图表的方法
renderChart();
});

// 在这里编写渲染图表的方法
const renderChart = () => {
// ECharts 配置项
const options: echarts.EChartOption = {
// 在这里配置你的柱状图数据和其他选项
xAxis: {
type: 'category',
data: ['Category 1', 'Category 2', 'Category 3'],
},
yAxis: {
type: 'value',
},
series: [
{
name: 'Series 1',
type: 'bar',
data: [30, 40, 20],
},
],
};

// 使用 setOption 方法设置图表配置
myChart?.setOption(options);
};
</script>

基础样式

柱状图:

改变y轴位置(默认左侧)

1
2
3
4
5
6
7
8
// ECharts 配置项
const options = {
yAxis: {
type: 'value',
position: 'right', // 将 Y 轴移到右侧显示
},
};

桩状增加圆角

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ECharts 配置项
const options = {
series: [
{
itemStyle: {
normal: {
// 这里设置柱形图圆角 [左上角,右上角,右下角,左下角]
barBorderRadius: [8, 8, 0, 0]
}
}
},
],
};

桩状改变颜色

1
2
3
4
5
6
7
8
9
// ECharts 配置项
const options = {
series: [
{
color: ['#5DB1FF'],
},
],
};

颠倒柱状图,x轴变为y轴,y轴变为x轴

调换一下两个轴的type就行

1
2
3
4
5
6
7
8
9
// ECharts 配置项
const options = {
xAxis: {
type: 'value',
},
yAxis: {
type: 'category',
},
};

默认柱状图或折线图过小的问题

参考文章:echarts图表的大小调整的解决方案 - 简书 (jianshu.com)

参数具体含义如图所示:

1
2
3
4
5
6
7
8
9
10
11
// ECharts 配置项
const options = {
grid: {
x: 0,
y: 10,
x2: 0,
y2: 20,
borderWidth: 1,
},
};

折线图:

折线下渐变色,以及弧度大小,以及折线颜色

原文参考:echarts 折线图小圆点修改为实心,折线图下方半透明效果_echarts折线图实心点-CSDN博客

效果参考:

![截屏2024-02-23 22.01.35](/Users/tec/Library/Application Support/typora-user-images/截屏2024-02-23 22.01.35.png)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ECharts 配置项
const options = {
series: [
{
color: ['#5DB1FF'],
areaStyle: {
color: {
type: 'linear', //折线颜色
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: 'rgba(93, 177, 255, 0.2)' }, // 开始颜色
{ offset: 1, color: 'rgba(93, 177, 255, 0)' }, // 结束颜色
],
},
},
smooth: 0.5,
},
],
};

折线上小圆点大小,以及是否实现

1
2
3
4
5
6
7
8
9
10
11
// ECharts 配置项
const options = {
series: [
{
type: 'line',
symbol: 'circle', //将小圆点改成实心 不写symbol默认空心
symbolSize: 6, //小圆点的大小
},
],
};

饼图:

集大成的效果参考:

截屏2024-02-23 22.38.07控制图例的位置,摆放方向等各类属性

原文参考:echarts 饼图以及图例的位置及大小,环图中间字_echarts饼状图文字位置-CSDN博客

1
2
3
4
5
6
7
8
9
10
11
12
13
// ECharts 配置项
const options = {
// 配置选项
legend: {
data: ['男', '女', '未知'], // 添加图例名称
orient: 'vertical', // 控制图例是竖着放还是横着放
left: '70%', // 图例距离左的距离
y: 'center', // 图例上下居中
itemGap: 30, // 控制每一项的间距,也就是图例之间的距离
itemHeight: 15, // 控制图例图形的高度
},
};

控制饼图本身的位置大小等各类属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ECharts 配置项
const options = {
series: [
{
name: 'Access From',
type: 'pie',
radius: ['40%', '70%'], // 内圈以及外圈的大小
center: ['30%', '50%'], // 图的位置,距离左跟上的位置,50%时位于中央
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10, //圆角
borderColor: '#fff',
borderWidth: 2
},
},
],
};

外圈+内圈的样式

官网参考:Examples - Apache ECharts

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
// ECharts 配置项
const options = {
series: [
{
name: 'Access From',
type: 'pie',
radius: ['40%', '70%'], // 内圈以及外圈的大小
center: ['30%', '50%'], // 图的位置,距离左跟上的位置
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: 30, // 内圈在选中是的字体大小
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
},
],
};

axios学习(‼️)

下载

首先是通过npm下载

1
npm install axios

封装的axios文件解析(‼️)

需要注意的部分包括:

headers里面的内容根据后端需求改变,目前是Token

这里藏着一个我到项目结束才发现的坑

URL作为baseURL应该直接为空,否则当部署到不同的服务器时就会报错

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
import axios, { AxiosInstance, AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios'

// 引入ErrorResult接口
import {ErrorResult} from '../interfaces/ErrorResult';

// 注意为空,而非本地的地址
const URL: string = ''
// const URL: string = 'http://localhost:5173'
enum RequestEnums {
TIMEOUT = 20000,
OVERDUE = 600, // 登录失效
FAIL = 999, // 请求失败
SUCCESS = 200, // 请求成功
}
const config = {
// 默认地址
baseURL: URL as string,
// 设置超时时间
timeout: RequestEnums.TIMEOUT as number,
// 跨域时候允许携带凭证
withCredentials: true,
}

class RequestHttp {
// 定义成员变量并指定类型
service: AxiosInstance;
public constructor(config: AxiosRequestConfig) {
// 实例化axios
this.service = axios.create(config);

/**
* 请求拦截器
* 客户端发送请求 -> [请求拦截器] -> 服务器
* token校验(JWT) : 接受服务器返回的token,存储到vuex/pinia/本地储存当中
*/
this.service.interceptors.request.use(
(config: any) => {
const token = localStorage.getItem('token') || '';
return {
...config,
headers: {
'Token': token, // 请求头中携带token信息
}
}
},
(error: AxiosError) => {
// 请求报错
Promise.reject(error)
}
)

/**
* 响应拦截器
* 服务器换返回信息 -> [拦截统一处理] -> 客户端JS获取到信息
*/
this.service.interceptors.response.use(
(response: AxiosResponse) => {
const { data, config } = response; // 解构

if (data.code === RequestEnums.OVERDUE) {
// 登录信息失效,应跳转到登录页面,并清空本地的token
localStorage.setItem('token', '');
// router.replace({
// path: '/login'
// })
return Promise.reject(data);
}
// 全局错误信息拦截(防止下载文件得时候返回数据流,没有code,直接报错)
if (data.status && data.status !== RequestEnums.SUCCESS) {
console.log("data.error:" + data.error);
ElMessage.error(data); // 此处也可以使用组件提示报错信息
return Promise.reject(data)
}
return data;
},
(error: AxiosError) => {
const { response } = error;
if (response) {
console.log("response:" + (response.data as ErrorResult).status);
this.handleCode((response.data as ErrorResult).status)
}
if (!window.navigator.onLine) {
ElMessage.error('网络连接失败');

}
// 处理错误的响应
return Promise.reject(error);
}
)
}
// 全局错误处理
handleCode(code: number): void {
switch (code) {
case 200:
ElMessage.error('不存在该用户');
break;
case 201:
ElMessage.error('用户名已存在');
break;
case 204:
ElMessage.error('登录已失效,请重新登录');
break;
default:
ElMessage.error('请求失败');
break;
}
}

// 常用方法封装
get<T>(url: string, params?: object): Promise<T> {
return this.service.get(url, { params });
}
post<T>(url: string, params?: object): Promise<T> {
return this.service.post(url, params);
}
put<T>(url: string, params?: object): Promise<T> {
return this.service.put(url, params);
}
// 修改delete方法,允许传递FormData格式的数据
delete<T>(url: string, data?: FormData): Promise<T> {
return this.service.delete(url, { data });
}

}

// 导出一个实例对象
export default new RequestHttp(config);

配置

vite.config.ts中进行配置,注意server部分

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
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import tailwindcss from 'tailwindcss'
import autoprefixer from 'autoprefixer'

// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),

],
server: {
host: '0.0.0.0',
port: 5173,
proxy: {
'/api': {
target: 'http://qexo.moefish.net:5409', //实际请求地址
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ""),
// 显示请求代理后的真实地址
bypass(req, res, options) {
const proxyUrl = new URL(req.url || "", options.target)?.href || "";
res.setHeader("x-res-proxyUrl", proxyUrl);
},
},
}
},
css: {
postcss: {
plugins: [tailwindcss, autoprefixer]
}
}

})

API封装

注意参数如何使用,以及方法注意要正确

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import axios from './';
// 引入Collection接口
import { Type } from '../interfaces/Type';


// 添加分类
export const addType = (params:any) => {
return axios.post('api/categories/add', params);
}
// 获取所有藏品分类
export const getAllTypes = () => {
return axios.get<Type[]>('api/categories/all');
};

// 根据objectId获取分类
export const getTypeById = (objectId
: string) => {
return axios.get<Type>('api/categories/objects/' + objectId);
}

API使用

注意定义的时候需要加上Ref<Type[]>,防止出现类型报错

以及asyncawait的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script setup lang="ts">
import { Ref, onMounted, ref } from 'vue';


// 引入接口Type
import { Type } from '../interfaces/Type';


// 引入api
import { getAllTypes } from '../api/type';

const tableData:Ref<Type[]> = ref([]);


onMounted(async() => {
await getAllTypes().then((res) => {
tableData.value = res;
counts.value = tableData.value.length;
})
});
</script>

ElementPlus

element plus引入图标

首先通过npm下载图标包

1
sudo npm install @element-plus/icons-vue

接着在main.ts进行配置

1
2
3
4
5
6
7
8
9
10
11
12
13
import ElementPlus from 'element-plus'
import * as ElIcons from '@element-plus/icons-vue'


const app = createApp(App)
for (const name in ElIcons){
app.component(name,(ElIcons as any)[name])
}

app
.use(ElementPlus)

.mount('#app')

使用图标

1
<el-icon size="16" color="#000"><Search /></el-icon>

el-scrollbar

用于替换浏览器原生滚动条。

比起原生滚动条的优点是,在鼠标移入时才会展示滚动条,以及选定滚动的区域更简单

注意只要当元素高度超过最大高度,才会起作用

1
2
3
4
5
<template>
<el-scrollbar height="400px">
<p v-for="item in 20" :key="item" class="scrollbar-demo-item">{{ item }}</p>
</el-scrollbar>
</template>

动态引入图标

vue3.0+新版elementPlus中如何动态插入icon图标的问题_vue3.0 添加图标-CSDN博客

注意is中的内容如下:icon: ‘GoodsFilled’(用图表的名字即可)

1
2
3
<el-icon>
<component :is="menu.icon"></component>
</el-icon>

el-table

基础使用

1
2
3
<el-table :data="tableData" stripe class="tableBox" table-layout="fixed" @selection-change="handleSelectionChange">
<el-table-column prop="itemName" label="藏品名称"></el-table-column>
</el-table>

数据设置

1
2
3
4
5
6
7
onMounted(async () => {
loading.value = true;
allData.value = await getAllBlindBoxs();
counts.value = allData.value.length;
// tableData.value 为 res 中选择 pageSize.value 行数据
tableData.value = allData.value;
});

排序按钮

1
2
3
<el-table-column prop="createTime" label="日期" sortable>

</el-table-column>

插入自定义内容

1
2
3
4
5
6
<el-table-column prop="createTime" label="日期" sortable>
<template v-slot="{ row }">
<!-- 解析日期,格式如下:2024-04-01T16:33:00.127+08:00,精确到秒 -->
{{ new Date(row.createdAt).toLocaleString() }}
</template>
</el-table-column>

行高

原文参考:el-table-column设置高度/指定高度-CSDN博客

:row-style=”{ height: ‘100px’ }”

固定表头且高度占满

原文参考:Element表格el-table固定表头且高度占满-阿里云开发者社区 (aliyun.com)

el-pagination中文实现

原文链接:el-pagination 分页组件 ‘英文’ 修改为 ‘中文’(Vue3+ElementPlus实现) - 掘金 (juejin.cn)

vue2版好像直接就是中文

vue3版需要在外面套一层el-config-provider标签进行翻译,并在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
<template>
<el-config-provider :locale="zhCn">
<el-pagination
class="h-46 mr-50 flex justify-end"
v-if="pagination.isShow && pagination.total > 0"
v-model="pagination.current"
:layout="pagination.layout"
:pager-count="5"
:page-sizes="pagination.pageSizes"
:total="pagination.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</el-config-provider>
<template/>
<script setup>
// ElConfigProvider 组件
import { ElConfigProvider } from 'element-plus';
// 引入中文包
import zhCn from 'element-plus/es/locale/lang/zh-cn';
// 更改分页文字
// zhCn.el.pagination.total = '共 `{total} 条`';
// zhCn.el.pagination.goto = '跳至';
// zhCn.el.pagination.pagesize = '条/页';
// zhCn.el.pagination.pageClassifier = '页';
<script/>

el-button新版变化

主要是改变颜色的方式不一样了,现在使用type改变颜色,之前是用type改变按钮的类型

text表示是文字按钮

bg为增加按钮灰色背景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<el-button
text bg type="primary"
size="small"
>
上架
</el-button>

<el-button
type="text" size="small" class="blueBug"
size="small"
>
下架
</el-button>

el-switch学习

需要注意v-model必须是Boolean类型

size控制大小,参数有large、small、默认(不需要写size属性)

inline-prompt,控制文本是否显示在点内

inactive-iconactive-icon 属性,来添加关闭以及开启时的图标。

1
2
3
4
5
6
7
<el-switch
v-model="row.availability"
size="large"
inline-prompt
:active-icon="Check"
:inactive-icon="Close"
/>

el-check样式改变

注意要使用:deep(类名)(/deep/会报错)

1
2
3
4
:deep(.el-checkbox__inner){
border-radius: 50%;
zoom: 150%;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
:deep(.el-checkbox__inner){
width: 19px;
height: 19px;
border-radius: 22.5px;
}

:deep(.el-checkbox__inner::after) {
border: 1px solid #fff;
border-left: 0;
border-top: 0;
left: 7px;
top: 4px;
}

:deep(.el-checkbox__input.is-checked .el-checkbox__inner::after) {
transform: rotate(50deg) scaleY(1.5);
}

el-progress学习

注意percentage必须是number类型

status属性控制颜色:success(绿)、warning(黄)、exception(红)

stroke-width 属性更改进度条的高度

text-inside 属性,通过布尔值,来改变进度条内部的文字

type 属性来指定使用环形进度条:circle

1
2
3
4
5
6
<el-progress
:status="row.status"
:text-inside="true"
:stroke-width="18"
:percentage="row.salesPercentage"
/>

el-input样式改变(‼️)

目前内部的placeholder还没法改颜色

效果展示:

![截屏2024-03-06 09.09.46](/Users/tec/Library/Application Support/typora-user-images/截屏2024-03-06 09.09.46.png)

1
2
3
4
5
6
7
8
9
<!-- 下面为藏品名称搜索框 -->
<el-input v-model="name" placeholder="搜索" class="mt-4">
<template #prefix>
<el-icon color="var(--text-100)" class="el-input__icon">
<search />
</el-icon>
</template>
</el-input>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.el-input {
height: 50px;

border-radius: 12px;
border: 0.5px solid var(--text-200);
border: 0;
background-color: var(--bg-200);

font-size: 18px;
font-weight: bold;


:deep(.el-input__wrapper) {
border-radius: 12px;
background-color: var(--bg-200);

}


:deep(.is-focus) {
box-shadow: 0 0 0 1px var(--accent-200)
}
}

el-tag使用

type 属性可以改变颜色(color也可以),size改变尺寸

1
2
3
4
5
6
7
8
9
<template>
<div class="flex gap-2">
<el-tag type="primary" size="large">Tag 1</el-tag>
<el-tag type="success" size="Small">Tag 2</el-tag>
<el-tag type="info">Tag 3</el-tag>
<el-tag type="warning">Tag 4</el-tag>
<el-tag type="danger">Tag 5</el-tag>
</div>
</template>

实际使用:

1
<el-tag :type="row.orderType">{{ row.orderStatus }}</el-tag>

圆角改变:

1
2
3
:deep(.el-tag){
border-radius: 9px;
}

v-loading使用

v-loading可以在任何HTML标签上使用,作用对象是其对应的子节点

使用element-loading-text来控制加载时显示的文字

样式修改在下面的笔记中

HTML部分:

1
2
3
4
5
6
7
8
9
<div v-loading="loading" element-loading-text="生成中...">
<p class="text-left text-xs font-medium px-4 py-2">系列</p>
<div >

</div>
<div class="py-4" v-else>
<p class="font-semibold text-lg">暂无搜索结果</p>
</div>
</div>

v-loading样式修改

需要注意一点:deep过的样式,就不在遵从scoped

注意修改图标的颜色不是使用color而是使用stroke

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 下面为loading的样式
:deep(.el-loading-mask) {
border-radius: 16px;
}

// 修改图标的颜色
:deep(.el-loading-spinner .path) {
stroke: var(--accent-200);
}

// 修改文字的颜色
:deep(.el-loading-spinner .el-loading-text) {
color: var(--accent-200);
}

el-select样式修改

HTML部分:

下面的

需要注意的属性是:teleported必须改为false,这个属性控制是否直接挂载到body上,如果不关这个选项会导致使用:deep也获取不到下拉框的样式

还有需要注意的属性是style="width: 360px;",这个可以更方便的控制宽度

1
2
3
<el-select v-model="category" placeholder="请点击选择分类" size="large" clearable :teleported="false" style="width: 360px;">
<el-option v-for="item in allType" :key="item.objectId" :label="item.name" :value="item.objectId" />
</el-select>

CSS部分

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
// 下面为el-select部分
@mixin select_radius {
border-radius: 12px;
}


// 控制el-select的长度以及圆角
:deep(.el-select__wrapper) {
width: 360px;
height: 50px;
@include select_radius;
}
// 控制el-select中文字的样式
:deep(.el-select__placeholder) {
color: var(--text-200);
font-size: 18px;
font-weight: bold;
}

// 控制点击后的边框颜色
:deep(.el-select__wrapper.is-focused) {
box-shadow: 0 0 0 1px var(--accent-100);
}

// 下面为下拉框部分
// 下面用于控制整体的下拉框圆角
:deep(.el-select__popper.el-popper) {
@include select_radius;
}


//下拉框的文本未选中的样式
// .el-select-dropdown__item {

// }
//下拉框的文本颜色选中之后的样式
.el-select-dropdown__item.is-selected {
color: var(--accent-200);
}

el-input-number样式修改

1
2
<el-input-number :disabled="ifDisabled" v-model="num[index]" :min="0" :max="maxNumber[index]"
@change="handleChange(index, $event)" :step="1" />
1
2
3
4
5
6
7
8
let num = ref(1);
const handleChange = (value: number) => {
console.log(value);
};
// 实现handleChange方法
const handleChange = (index: number, value: number) => {

};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
:deep(.el-input){
box-shadow: 0 0 0 1px var(--accent-200, var(--accent-100)) inset;
border-radius: 12px;
}
:deep(.el-input-number__increase:hover~.el-input:not(.is-disabled) .el-input__wrapper){
box-shadow: 0 0 0 1px var(--accent-200, var(--accent-100)) inset;
}
:deep(.el-input-number__decrease:hover~.el-input:not(.is-disabled) .el-input__wrapper){
box-shadow: 0 0 0 1px var(--accent-200, var(--accent-100)) inset;
}
:deep(.el-input-number__increase:hover){
color: var(--accent-200);
}
:deep(.el-input-number__decrease:hover){
color: var(--accent-200);
}
:deep(.el-input__wrapper.is-focus){
box-shadow: 0 0 0 1px var(--accent-200, var(--accent-100)) inset !important;
}

el-avatar使用

使用 shapesize 属性来设置 Avatar 的形状和大小。

圆形:circle

矩形:square

1
<el-avatar shape="square" :size="50" :src="circleUrl" />

Vue基础

路由使用

路由的下载在第一部分讲过了

1、ts中使用

1
2
3
4
5
6
7
8
9
import { useRouter } from 'vue-router'

const router = useRouter()

const toIndex = () => {
router.push({
name: 'IndexView',
})
}

2、html中使用,主要是可以唤起鼠标的手部图标

1
<router-link to="/user">TEC</router-link>

多个router-view使用

原文链接:vue 路由的内置组件 router-view 详细介绍(有图有真相)-CSDN博客

放到子组件里就行

默认进入某一页面,把children路由path设置与父路由相同就行

实际上就是,router-view 当你的路由path 与访问的地址相符时,会将指定的组件替换该 router-view

本身就是最底层的/

1
2
3
4
5
6
7
8
9
10
11
12
const routes: Array<RouteRecordRaw> = [
{
path: "/",
component: () => import("../views/IndexView.vue"),
children: [
{
path: "/",
component: () => import("../views/HomeView.vue"),
},
],
},
];

本身不是最底层,是/同级的一层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";

// 2. 配置路由
const routes: Array<RouteRecordRaw> = [
{
path: "/setting",
name:"SettingView",
component: () => import("../views/SettingView.vue"),
children: [
{
path: "/setting",
component: () => import("../components/SettingUserInfo.vue"),
},
],
},

];

注意这种情况下跳转路由方法也要变下,需要加上父路由的路径

1
router.push(`/setting${path}`); // 使用父路由+子路由的路径

route router区别

route是路由信息对象,里面主要包含路由的一些基本信息,包含当前的路径,参数,query对象等。(包括name、meta、path、hash、query、params、fullPath、matched、redirectedFrom)

router对象是全局路由的实例,是router构造方法的实例,包含了一些路由的跳转方法,钩子函数等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script setup lang="ts">
import { ref, watch, onMounted } from "vue"
import { useRouter,useRoute } from 'vue-router'


const router = useRouter()
const route = useRoute()
onMounted(() => {
// 在组件挂载后检查 localStorage 中是否存在 token
hasToken.value = localStorage.getItem('token') !== "";
if (!hasToken.value && route.path === '/user') {
// 跳转到首页
router.replace({
path: '/'
});
}


});
</script>

动态路由

首先在路由配置中,修改路径为带/:id的动态路由

1
2
3
4
5
6
7
8
9
// 在路由配置中
const routes = [
{
path: '/nft/:id', // 使用动态路由参数 :id
name: 'NftView',
component: () => import('../views/NftView.vue'),
},
// 其他路由配置
];

然后是通过route.params.id获取路由参数

1
2
3
4
5
6
7
8
9
10
11
12
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useRoute } from 'vue-router';

const nftId = ref<string>('');

onMounted(() => {
// 获取路由参数
const route = useRoute();
nftId.value = route.params.id as string;
});
</script>

最后是跳转加上参数params

1
2
3
4
5
6
const toNft = (objectId: string) => {
router.push({
name: 'NftView',
params: { id: objectId }, // 传递动态路由参数
});
};

父组件向字组件传值

上面的Pinia作为全局管理也可以实现一样的效果,但是如果是比较简单的数据,则可以使用下面的方法

1
const props = defineProps<{ msg: string }>()

父组件使用:

1
<CollectionList msg="热门动画数字藏品" />

子组件使用:

1
const props = defineProps<{ msg: string }>()

父子组件通讯

使用prop/emit来实现,

当使用 propemit 进行父子组件通信时,主要涉及两个概念:props 和自定义事件。

下面我将分别从父组件以及子组件介绍用法

父组件

Props(属性)

在父组件中,通过 props 可以将数据传递给子组件。在子组件中,可以通过定义 props 属性来接收这些数据。

下面的例子中props名为ifShow,实际的值来自于isLoginBoxVisible,注意是可以传递多个属性的

Emit(自定义事件)

在父组件中,Emit并不是被直接使用,而是定义Emit所需要的事件,这里的事件为updateIfShow,而对应的方法为updateIsLoginBoxVisible,这里注意事件与方法的区别,以及方法的格式是需要参数的

父组件中的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!-- ParentComponent.vue -->
<template>
<div>
<LoginBox :ifShow="isLoginBoxVisible" @updateIfShow="updateIsLoginBoxVisible" />
</div>
</template>

<script setup lang="ts">
// 引入LoginBox
import LoginBox from '../components/LoginBox.vue'

// isLoginBoxVisible设置默认为false
const isLoginBoxVisible = ref(false);

// hideMaskLayer方法控制更新isLoginBoxVisible
const updateIsLoginBoxVisible = (value: boolean) => {
isLoginBoxVisible.value = value;
};

const showLogin = () => {
updateIsLoginBoxVisible(true);
}
</script>

子组件

Props(属性)

在子组件中,使用defineProps方法接收父组件传进来的Props,使用方法为props.(属性名)(props.ifShow

Emit(自定义事件)

在子组件中,通过 emit 方法可以触发自定义事件,并将数据传递给父组件。使用方法:首先通过defineEmits实例化一个emit,接着需要按照emit(‘事件名’,事件的方法参数)的方式调用父组件的事件

子组件中的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- ChildComponent.vue -->
<template>
<div class="LoginBox" v-if="props.ifShow">
<!-- 子组件的其他内容 -->
</div>
</template>

<script setup lang="ts">
import { } from 'vue';

const props = defineProps(['ifShow']);
const emit = defineEmits();

const toggleVisibility = () => {
emit('updateIfShow', false);
};

</script>

<script setup lang="ts">部分的代码规范(‼️)

注意不同的部分可以用两行隔开

import部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script setup lang="ts">
// import部分
// 1、引入外部包提供的各种模块
import { ref, watch, onMounted,Ref } from "vue"
import { useRouter, useRoute } from 'vue-router'
import { AxiosError } from "axios"


// 2、引入自己写的组件
import MaskLayer from '../components/MaskLayer.vue'


// 3、引入自己写的类型接口
import { Collection } from '../interfaces/Collection';


// 4、引入store
import { userInfoStore } from '../stores/UserInfoStore';


// 5、引入API
import { searchCollections } from '../api/collections'
</script>

定义变量部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script setup lang="ts">
// 定义变量部分
// 1、实例化外部包提供的各种模块
const router = useRouter()
const route = useRoute()


// 2、实例化store
const userInfo = userInfoStore();


// 3、定义一般变量
let maxLength = 10
let loginForm = new FormData();


// 4、定义ref变量
let name = ref('')

</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
34
35
<script setup lang="ts">
// 定义函数部分
// 1、生命周期函数
onMounted(() => {
hasToken.value = localStorage.getItem('token') !== "";
if (!hasToken.value && route.path === '/user') {
router.replace({
path: '/'
});
}
});


// 2、外部包提供的函数
watch(() => userInfo.token, (newToken) => {
hasToken.value = newToken !== "";
});


// 3、自己写的函数
const toCreate = () => {
router.push({
name: 'CreateView',
})
}


// 4、自己写的函数所引入的函数
const toggleVisibility = () => {
emit('updateIfShow', false);
};
function isValidPhone(phone: string) {
return /^1[3-9]\d{9}$/.test(phone);
}
</script>

onMounted的进一步理解

在刷新时会进行执行

执行顺序位于外层的代码之后

鼠标划动事件

不是hover,而是@mouseover="handleHover"

1
<div class="Detail" v-if="!isCartNullVisible" @mouseover="handleHover"></div>

watch理解

watch监听数据的变化,只有ref和reactive才行,但是无法直接访问reactive中的属性,需要加上getter,() => nameObj.name

watch与watchEffect区别和使用场景:

首先是第一次执行,watch是惰性的,第一次是不会执行的,只有监听值改变时才会执行,watchEffect则相反,其次是函数使用上,watch需要指定监听对象,watchEffect不需要,最后是使用用途上,watchEffect更适合使用api时使用

ref问题

使用ref时注意要用.value

1
2
3
4
5
6
// 定义一个变量isDeleteVisible
let isDeleteVisible = ref(false);
// 实现showDelete方法
const showDelete = () => {
isDeleteVisible.value = true;
}

transition的使用

官网:Transition | Vue.js (vuejs.org)

在vue3+ts+组合式api中,下面的过渡给我改为img出现时为放大出现,消失时为缩小消失,svg出现时为向右转180度出现,消失时为向左180度消失:(有时间研究一下,没时间哈哈哈)

1、使用Vue提供的transition组件

前提是transition中存在v-if来控制组件的出现与否,而且注意v-if的组件必须就位于transition的下一个包裹代码汇总

这个transition中的过渡时间不生效一直不生效(‼️)

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
<div class="MainNavbarUserInfo" @mouseover="showUserMenu" @mouseleave="hideUserMenu">
<el-icon :size="20">
<User />
</el-icon>
</div>

<transition name="fade">
<div class="MainNavbarUserMenu" v-if="isUserMenuVisible" @mouseover="showUserMenu" @mouseleave="hideUserMenu">

</div>
</transition>


<style lang="scss" scoped>
/* 整个导航栏容器 */

.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease;
}

.fade-enter-from,
.fade-leave-to {
opacity: 0;
}


</style>

插槽(slot)学习

原文参考:Vue3中slot插槽的使用 详细!! - 掘金 (juejin.cn)

基本概念:

使得组件中可以被插入内容,如同HTML 标签之间是可以插入内容的

虽然 child 不是 HTML 自带的标签,插槽可以使其内部被插入但是它却有着类似的特征,比如我们往<child></child>之间插入一点内容

基础使用:

子组件:

1
2
3
4
5
6
7
<template>
<div class="child-box">
<p>我是子组件</p>
<!-- 插槽 -->
<slot></slot>
</div>
</template>

父组件App.vue

1
2
3
4
5
<template>
<child>
<div>小猪课堂</div>
</child>
</template>

效果如下:

截屏2024-02-21 14.45.16

进阶使用:

1、插槽默认内容:

slot中加入内容就行,这样会在父组件使用<child></child>不加入更多内容时默认出现,<child><div>小猪课堂</div></child>时则会消失

子组件:

1
2
3
4
5
6
7
8
9
<template>
<div class="child-box">
<p>我是子组件</p>
<!-- 插槽 -->
<slot>
<p>我是默认内容</p>
</slot>
</div>
</template>

2、具名插槽

当子组件中使用多处插槽时,就会需要使用到具名插槽

子组件:

注意外层这个header组件,根据我的实验来说是不需要的,不过官网的例子是加上了的,但是估计是可以简写的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<div class="child-box">
<p>我是子组件</p>
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
</template>

3、动态插槽名

外层加一个中括号[ ]就行

1
2
3
4
5
<base-layout>
<template #[dynamicSlotName]>
...
</template>
</base-layout>

父组件:

注意外层必须嵌套一个template,带上名字,v-slot:header可以使用#header来简写

1
2
3
4
5
6
7
8
9
10
11
<template>
<child>
<template v-slot:header>
<div>我是 header:{{ message }}</div>
</template>
<div>我没有名字:{{ message }}</div>
<template v-slot:footer>
<div>我是 footer:{{ message }}</div>
</template>
</child>
</template>

4、默认插槽作用域传值

用于满足需要在插槽内容中获取子组件数据的需求

子组件:

1
2
3
4
5
6
<template>
<div class="child-box">
<p>我是子组件</p>
<slot text="我是子组件小猪课堂" :count="1"></slot>
</div>
</template>

父组件:

在父组件 App.vue 中通过 v-slot="slotProps"等形式接收子组件传毒过来的数据,slotProps 的名字是可以任意取的,它是一个对象,包含了所有传递过来的数据。

1
2
3
4
5
<template>
<child v-slot="slotProps">
<div>{{ slotProps.text }}---{{ slotProps.count }}</div>
</child>
</template>

5、具名插槽作用域传值

具名插槽作用域之间的传递其实默认插槽作用域传值原理是一样的,只不过写法略微不一样罢了

子组件:

1
2
3
4
5
6
<template>
<div class="child-box">
<p>我是子组件</p>
<slot name="header" text="我是子组件小猪课堂" :count="1"></slot>
</div>
</template>

父组件:

1
2
3
4
5
6
7
<template>
<child>
<template #header="{ text, count }">
<div>{{ text }}---{{ count }}</div>
</template>
</child>
</template>

CSS基础

CSS基础单位复盘

px

最基础的单位,通常用于有设计稿的情况下,或者不需要进行不同的适配

rem

根据字体的单位进行改变,和px兑换的比例为1:4

%

具体可看下面的auto与100%的对比

vh

根据视图高度进行改变

vw

根据视图宽度进行改变

!important

!important是提高样式规则优先级的声明。当你在样式规则中使用!important时,它会覆盖其他相同样式规则中的属性,并强制应用该规则,即使它的优先级较低。

1
2
3
p {
color: red !important;
}

width属性100%和auto的区别

参考文章:
width:100%和width:auto的区别_width:100%-CSDN博客

主要区别:

width:100%:子元素的宽度和父元素的宽度相等,其中并不包括子元素内外边距以及边框的值,为子元素真正的宽度

width:auto:auto表示子元素的 宽度+内边距+外边距+边框 才等于父元素的宽度

width: max-content:作用

其实就相当于是安卓中的match-content,主要用在文本的长度不会变成div的长度

max-content 尺寸关键字代表了内容的最大宽度或最大高度。对于文本内容而言,这意味着内容即便溢出也不会被换行。

修改字体

  1. 在项目assets文件下新建 font 文件夹,并将下载下来的字体拖入其中0

  2. 在css中实现修改字体

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @font-face {
    font-family: 'SmileySans';
    src: url('../font/static/Inter-Bold.ttf');
    font-weight: normal;
    font-style: normal;
    }
    #app {
    font-family: HelveticaNeue;
    min-height: 100vh;
    min-width: 100vh;

    max-width: 2880px;

    height: 100%;
    width: 100%;

    padding: 0 50px;
    padding-top: 15px;
    position: relative;
    }

JS基础

FormData 对象的使用

新建:new FormData()

添加:loginForm.append("username", username.value);

1
2
3
4
5
6
7
8
9
10
11
// 实现Login方法
const handleLogin = async () => {
// 清空loginForm
let loginForm = new FormData();

loginForm.append("username", username.value);
loginForm.append("password", password.value)

await login(loginForm).then(response => {
})
}

Promise理解

Promise 是异步编程的一种解决方案

所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。

从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理

Promise对象有以下两个特点。

(1)对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。

(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。

Bug合集

由于父元素存在padding,导致UserBackground无法铺满横向解决

margin-left: -50px;

margin-right: -50px;

父组件存在padding,子组件背景颜色想铺满的话,需要加上负的margin

使用overflow-y: Auto;导致的Bug

overflow-y: scroll;

使用overflow-y: Auto容易出现的问题是,同一个界面中,在切换字组件时,部分子组件长度超出父组件,overflow-y:会变为scroll,而部分子组件长度未超出父组件,overflow-y:会变为hidden,这样的情况下,会导致界面布局变化,比如宽度伸长缩短

解决的方法就是,在决定一定会超父组件时直接使用overflow-y: scroll;

el-image组件自定义加载失败内容无效的BUG

问题描述:

下面的有关自定义加载失败内容代码,跟官网一样,但是却无法正确显示自定义的加载失败内容

解决关键:

注意复制的imageUrl的URL本身是否就带有加载失败URL

解决过程:

首先本来使用的是vue2的版本,先去官网找到最新的使用方法

但是还是无法显示,于是接着看,是不是真的与官网一致,于是发现官网的el-image什么属性都没有加,于是我这样照做后,发现自定义加载失败内容出来了

接着就是分别去掉我的属性,看看究竟是什么属性导致的错误

最后发现是来自于src里面的imageUrl由于是直接复制别的网站的URL,该网站自己又有自定义加载失败图片,所以我自己的自定义没有成功,因为实际上图片并没有加载失败

1
2
3
4
5
6
7
8
9
10
11
<el-table-column prop="imageUrl" label="图片">
<template v-slot="{ row }">
<el-image style="width: auto; height: 40px; border: none; cursor: pointer;" :src="row.imageUrl">
<template #error>
<div class="image-slot">
<img src="../assets/images/no-image.png" style="width: auto; height: 40px; border: none;">
</div>
</template>
</el-image>
</template>
</el-table-column>

el-image预览图片穿透的BUG

参考文章:el-image在el-table中使用时层级问题 - 掘金 (juejin.cn)

问题描述

截屏2024-02-21 19.30.08

解决关键:设置preview-teleported为true

1
2
3
4
5
6
7
<el-image
style="height: 60px; width: 60px; border-radius: 12px; object-fit: cover; aspect-ratio: 1/1;"
:src="row.imageUrl"
:preview-src-list="[row.imageUrl]"
:preview-teleported="true"
>
</el-image>

解决过程:首先是怀疑自己的z-index写多了,然后改掉了几个,问题没解决,然后发现el-image有一个index属性,将其调到无敌高,还是没有解决,最后网上搜下,解决问题

父元素的padding挡住子元素,导致子元素无法click的BUG

原文参考:CSS - 元素遮挡(层级/定位等等)导致无法点击下层元素解决方案_css元素被遮挡-CSDN博客

为父元素加上使用 鼠标穿透属性 pointer-events就行

上面的解决方法也有bug,这样调整完后,父元素本来可以点击的内容又不能点击了

最后发现改为margin就行

1
2
3
4
/* 给父元素(遮挡的元素) */
.div{
pointer-events: none;
}

ElMessage出现在右侧的BUG

原文链接:element plus 使用ElMessage不生效或样式出现问题或出现在最底部_element-plus的elmessage提示总是从最下面-CSDN博客

重复引入的问题(不能局部引入)

产生的原因是:使用了element plus的按需引入,然后在组件中 又import { ElMessage } from ‘element-plus’ 引入了一次,就会出现这个问题,

解决的方法:将组件中的引入删除,直接使用 ElMessage.success(‘Successfully sent !’)

delete无法传递FormData参数的BUG

注意看负载

1
2
3
4
5
6
7
delete<T>(url: string, params?: object): Promise<T> { 
return this.service.delete(url, { params });
}
// 修改delete方法,允许传递FormData格式的数据
delete<T>(url: string, data?: FormData): Promise<T> {
return this.service.delete(url, { data });
}

不能将类型Ref<Collection | undefined>分配给类型Ref<Collection>的BUG

BUG复现:

1
let collectionItem:Ref<Collection>= ref()

BUG原因:

ref()可能为undefined类型

BUG解决:

1
2
// 使用类型断言或者创建默认数组
let collectionItem: Ref<Collection> = ref() as Ref<Collection>;

Uncaught (in promise) TypeError: Cannot read properties of undefined (reading ‘value’)(‼️)

位于CartList的错误,触发条件为商品添加进全局状态CartListCollection

错误报错位于

1
<p v-if="!isDeleteVisible[index].value">{{ item.price }}</p>

补充代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 定义一个数组用于储存购物车的内容,并且为响应式
// TODO:记得cartList一定是要放在全局变量里面的,否则清除所有就是清除不了的
const cartList = ref<Collection[]>([])

// cartList赋值给CartListCollection
cartList.value=CartListCollection.collections;

// 定义一个变量isDeleteVisible
let isDeleteVisible = cartList.value.map(() => ref(false));
// 实现showDelete方法
const showDelete = (index: number) => {
isDeleteVisible.forEach((item, i) => (item.value = i === index));
};
// 实现hideDelete方法
const hideDelete = () => {
isDeleteVisible.forEach((item) => (item.value = false));
};

初步想带到的解决方法为去掉.value

1
<p v-if="!isDeleteVisible[index]">{{ item.price }}</p>

离奇的点在于isDeleteVisible确实是ref属性啊,以及在把商品添加进去之前是可以的,但是添加之后就报错

接着想到的方法为加上判断

1
<p v-if="isDeleteVisible[index] && !isDeleteVisible[index].value">{{ item.price }}</p>

但是只是不报错,它原本的功能没了

最后发现其实是数组越界,把商品添加进去后,cartList更新了,但是没有更新isDeleteVisible数组,导致同样的indexisDeleteVisible数组会超出范围

正确的方法为增加watch,在cartList更新时一同更新isDeleteVisible数组,避免数组越界:

1
2
3
4
5
6
7
8
const cartList = ref<Collection[]>([])

cartList.value = CartListCollection.collections;

watch(cartList.value, (newValue, oldValue) => {
isDeleteVisible = newValue.map(() => ref(false));
console.log('watch 已触发', oldValue)
})

watch监听失效

失效的原因是在监听ref对象时,没有加上.value

1
2
3
4
5
6
7
8
const cartList = ref<Collection[]>([])

cartList.value = CartListCollection.collections;

watch(cartList.value, (newValue, oldValue) => {
isDeleteVisible = newValue.map(() => ref(false));
console.log('watch 已触发', oldValue)
})

在循环中使用API,导致的异步的BUG(‼️)

BUG复现:

虽然下面的代码也可以运行,但是注意看我的FavoriteCollection.collections所放的位置,明显顺序位置是错误的,但是放在forEach前面后触发collectionItemsLength === 0,导致无法显示正确的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
onMounted(async () => {
// 通过check获取到用户收藏的藏品
await check().then((res) => {
userInfo.user = res
})
// 使用getCollectionById通过数组userInfo.user.favoriteCollection中的objectId获取收藏的项目
userInfo.user!.favoriteCollection.forEach(async (item) => {
await getCollectionById(item).then((res) => {
FavoriteCollection.collections.push(res)
})
})

// 定义变量表示collectionItems的长度
let collectionItemsLength = FavoriteCollection.collections.length
// 如果collectionItemsLength=0时,isCollectionNullVisible为true

if (collectionItemsLength === 0) {
isCollectionNullVisible.value = true
}
FavoriteCollection.collections = []

})

BUG原因:

在 JavaScript 中,异步操作是非阻塞的,这意味着代码会继续执行而不等待异步操作完成。在这段代码中,由于getCollectionById是异步函数,它会立即返回一个Promise,而不会等待其内部的异步操作完成。因此,在遍历userInfo.user!.favoriteCollection数组并发起多个getCollectionById请求时,这些请求会同时被触发。

由于异步操作是并发执行的,代码会继续执行到下一个步骤,即计算FavoriteCollection.collections的长度。在这一点上,之前发起的异步请求可能尚未完成,因此FavoriteCollection.collections的实际长度可能并不反映所有异步操作的结果。

BUG解决:

加上Promise.all并将其指定为awiat,就可以在全部执行完之后,在去判断数组是否为空

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
onMounted(async () => {
// 通过check获取到用户收藏的藏品
await check().then((res) => {
userInfo.user = res
})

// 设置通过空数组防止重复存入
FavoriteCollection.collections = []
// 使用getCollectionById通过数组userInfo.user.favoriteCollection中的objectId获取收藏的项目
await Promise.all(userInfo.user!.favoriteCollection.map(async (item) => {
const res = await getCollectionById(item);
FavoriteCollection.collections.push(res);
}));

// 定义变量表示collectionItems的长度
let collectionItemsLength = FavoriteCollection.collections.length
// 如果collectionItemsLength=0时,isCollectionNullVisible为true

if (collectionItemsLength === 0) {
isCollectionNullVisible.value = true
}
})

el-select样式无法修改的BUG

一开始以为是:popper-append-to-body,结果发现最新版,该属性已经没了,取而代之的是teleported

同时最后发现有了teleported后,popper-class="mySelectStyle"也没啥用了,同时发现很多样式也不需要通过:deep获取

下面为原先错误的笔记:

需要注意的属性是:popper-append-to-body必须改为false,这个属性控制是否直接挂载到body上,如果不关这个选项会导致使用:deep也获取不到下拉框的样式

除了上面改:popper-append-to-body属性的方法,还可以通过属性popper-class="mySelectStyle",如果使用这种方法的话,下面的有关下拉框的样式都要包裹在.mySelectStyle

1
2
3
4
<el-select v-model="category" placeholder="Select" size="large" :popper-append-to-body="false">
<el-option v-for="item in allType" :key="item.objectId" :label="item.name" :value="item.objectId" />
</el-select>

点击穿透的BUG

BUG描述:
我希望只点击handleText2img,但是于此同时会调用openFileInput方法

1
2
3
4
5
6
7
8
9
10
11
12
<div class="CreateViewBodyLeft">
<p style="font-size: 36px;font-weight: bold;">创建NFT</p>
<p style="font-size: 20px;margin-top: 10px;"> 铸造项目后,您将无法更改其任何信息。</p>
<div v-if="!uploadedImage" @click="openFileInput">
<div class="flex justify-start items-center gap-2 bg-accent-100 text-black border rounded-2xl cursor-pointer p-2" @click="handleText2img">
<el-icon>
<Promotion />
</el-icon>
<p class="font-medium">AI辅助生图</p>
</div>
</div>
</div>

BUG解决方法:

@click.native.stop.prevent=”handleText2img”修饰符组合使用了.native来监听原生点击事件,并且使用.stop和.prevent修饰符来停止事件的传播和阻止默认行为。这样就可以确保只有点击”AI辅助生图”按钮时才会调用handleText2img方法,而不会触发openFileInput方法。

1
2
3
4
5
6
7
<div class="flex justify-start items-center gap-2 bg-accent-100 text-black border rounded-2xl cursor-pointer p-2" @click.native.stop.prevent="handleText2img">
<el-icon>
<Promotion />
</el-icon>
<p class="font-medium">AI辅助生图</p>
</div>

TS部分:

注意改变loading.value的位置,之前遇到过一个BUG,loading一直展示不出来,主要原因就是loading.valuefalsetrue切换时间太短导致loading不出现
解决办法就是注意在await函数部分最后再修改为false,而在await函数部分之前去修改为true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const loading = ref(false)
// search方法
const search = async () => {
loading.value = true
// 清空search表单
const searchForm = new FormData();
searchForm.append('name', name.value);
await searchCollections(searchForm).then(res => {
hasSearchInput.value = true
searchCollectionsArray.value = res
if(res.length===0){
ifSearchNull.value = true
}else{
ifSearchNull.value = false
}
loading.value = false
}).catch(err => {
console.log(err);
})
}

BUG部分:
1、loading一直展示不出来,主要原因就是loading.valuefalsetrue切换时间太短导致loading不出现

2、falsetrue搞翻了

API返回值无法找到的BUG

问题描述:

无法获取到API中返回到ResultImage属性

报错:

1
TypeError: Cannot read properties of undefined (reading 'ResultImage" )

代码:

问题在于(res as any)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const handleText2Img = async () => {
console.log("被点击")
const requestData = {
"Prompt": "女孩",
"RspImgType": "url"
};


await text2Img(requestData).then((res) => {
console.log(res)
uploadedImage.value = (res as any).data.Response.ResultImage as string;
}).catch((err) => {
console.log(err);
})
}

解决方法:

(res as any)以及后续属性的后面改为?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const handleText2Img = async () => {
console.log("被点击")
const requestData = {
"Prompt": "女孩",
"RspImgType": "url"
};


await text2Img(requestData).then(res => {
uploadedImage.value = res?.data?.Response?.ResultImage;
console.log("uploadedImage.value:", uploadedImage.value)
}).catch((err) => {
console.log(err);
})
}

问题解决过程:
首先我是先注意到了,之前一直是这么使用的,但是今天却出了问题

接着就想起来可能是自己之前封装的axios已经指定data的问题,毕竟这次的API没有使用自己封装的axios,而是原装的配置文件

指定data部分的配置

注意下面的const { data, config } = response;以及最后返回的是data

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
this.service.interceptors.response.use(
(response: AxiosResponse) => {
const { data, config } = response; // 解构

return data;
},
(error: AxiosError) => {
const { response } = error;
if (response) {
console.log("response:" + (response.data as ErrorResult).status);
this.handleCode((response.data as ErrorResult).status)
}
if (!window.navigator.onLine) {
ElMessage.error('网络连接失败');
}
// 处理错误的响应
return Promise.reject(error);
}
)

原生的AxiosResponse属性

1
2
3
4
5
6
7
8
export interface AxiosResponse<T = any, D = any> {
data: T;
status: number;
statusText: string;
headers: RawAxiosResponseHeaders | AxiosResponseHeaders;
config: InternalAxiosRequestConfig<D>;
request?: any;
}

JSON格式导致的BUG

问题描述:

在使用腾讯云盲水印API时,一直报错InvalidPicOperations: Pic operations param invalid,后来发现是JSON内部的JSON格式没有遵守,需要key都加上双引号""

问题复现:

下面是封装的部分,问题源头就是picOperations内部的JSON格式错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export async function addWatermark(path: string, fileObject: File, picOperations: string) {
// 使用 Promise 封装异步操作
return new Promise((resolve, reject) => {
cos.putObject({

// 支持自定义headers 非必须
Headers: {
'Pic-Operations': picOperations
},
}, function (err, data) {
if (err) {
// 处理请求出错
reject(err);
} else {
console.log(data);
// 处理请求成功
resolve(data);
}
});
});
}

下面是具体的错误源头:

注意key都忘记加上双引号了

1
2
3
4
5
6
7
8
9
const picOperations = {
is_pic_info: 1,
rules: [
{
fileid: path.value,
rule: `watermark/3/type/2/image/${base64Image.value}/level/3`,
},
],
};

问题解决:

为内部的key都加上双引号

1
2
3
4
5
6
7
8
9
const picOperations = {
"is_pic_info": 1,
"rules": [
{
"fileid": path.value,
"rule": `watermark/3/type/2/image/${base64Image.value}/level/3`,
},
],
};

space-between 失效的bug

原因:flex作为外层,没有设置足够的width

解决方法:增加自己想要的长队,比如w-full

1
2
3
4
5
6
7
8
9
10
11
<div class="flex justify-center items-center flex-col gap-5 border-solid border-t-[0.5px] border-text-200 -mx-5 px-5 pt-5">
<div class="flex justify-between items-center w-full">
<p class="text-lg font-medium">合约地址</p>
<p class="text-lg font-medium">0x1234567890</p>
</div>
<div class="flex justify-between items-center">
<p class="text-lg font-medium">合约地址</p>
<p class="text-lg font-medium">0x1234567890</p>
</div>
</div>

跳转同一路由组件不同参数时页面不变的BUG

原文参考:功能问题:如何解决跳同一路由组件时页面不变? - 知乎 (zhihu.com)

问题描述:跳转同一路由组件不同参数时页面不变

1
2
3
4
5
6
const toNft = (objectId: string) => {
router.push({
name: 'NftView',
params: { id: objectId }, // 传递动态路由参数
});
}

问题原因:由于是同一路由组件,无法触发push

解决方法:给router-view加上唯一key就行

1
2
3
4
5
6
7
8
9
10
11
12
13
<script setup lang="ts">
// 引入useRoute
import { useRoute } from 'vue-router'
const route = useRoute()
</script>

<template>
<router-view :key="route.fullPath"></router-view>
</template>

<style scoped>

</style>

video中的播放源,明明已经改变,但是页面还是播发之前的视频的BUG

原文地址:解决video动态切换src视频不改变问题_js video动态改变src-CSDN博客

错误代码:

1
2
3
4
5
<video v-else
style="height: 100%; width: 100%; border-radius: 20px 20px 0px 0px; object-fit: cover;"
autoplay muted loop>
<source :src="item.file" type="video/mp4">
</video>

source改为为src就行

正确代码:

1
2
3
4
5
<video v-else :src="item.file" type="video/mp4"
style="height: 100%; width: 100%; border-radius: 20px 20px 0px 0px; object-fit: cover;"
autoplay muted loop>
</video>

npm run build出现的bug

原文链接:vue3.x从打包、部署到上线_vue3 部署-CSDN博客

TS问题以及element-plus问题,主要还是检查包的问题,修改配置文件,不进行检查就行

tsconfig.json:

1
2
3
4
5
6
7
8
{
"compilerOptions": {
"esModuleInterop": true, // 启用了模块间的默认导入导出转换
"allowJs": true, // 允许编译 JavaScript 文件
"noEmit": true, // 禁止编译器输出任何文件
"skipLibCheck": true // 跳过对库文件的检查,加快编译速度
},
}

axios中的baseURL导致的无法在服务器正常请求网络请求的BUG

/api/index.ts

注意需要改为服务器对应的URL

1
2
const URL: string = 'http://42.192.90.134:5173'
const URL: string = 'http://localhost:5173'

路由模式导致刷新就会403的BUG

原文地址:Vue3:刷新页面报错404的解决方法 - 掘金 (juejin.cn)

改为hash的方式

1
2
3
4
const router = createRouter({
history: createWebHistory(),
routes,
});
1
2
3
4
5
import { createRouter, createWebHistory, RouteRecordRaw,createWebHashHistory } from "vue-router";
const router = createRouter({
history: createWebHashHistory(),
routes,
});

腾讯云在服务器端部署时出现的BUG

注意第一个是正确的写法,使用这种写法,需要去tsconfig.json里面修改一些配置

1
2
3
import COS from 'cos-js-sdk-v5';

import * as COS from 'cos-js-sdk-v5'; // 使用 import 语法导入 COS 模块

tsconfig.json:

好像是加下面这个,之后出现问题再搜索bug吧

1
2
3
{
"esModuleInterop": true, // 启用了模块间的默认导入导出转换
}

nginx 一个端口配置多个项目 出现的BUG

原文链接:使用nginx部署多个前端项目(三种方式)-CSDN博客

无法正确访问:

原因在于子目录指定代码的方式是使用alias,而不是root

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
worker_processes auto;
worker_rlimit_nofile 51200;

events {
worker_connections 51200;
multi_accept on;
}

http {

server {
listen 5173;
server_name localhost;

#charset koi8-r;

#access_log logs/host.access.log main;

location / {
root C:/wwwroot/42.192.90.134/NFT-Platform;
}
location /admin {
alias C:/wwwroot/42.192.90.134/admin;
}
}

include vhost/*.conf;
#加载vhost目录下的虚拟主机配置文件
}

上传Blog的Bug

使用hexo d命令时,出现! [remote rejected] HEAD -> main (push declined due to repository rule violations)error: failed to push some refs to 'https://github.com/TECNB/TECNB.github.io.git'

解决:

把github中仓库Settings -> Code security & analysis -> Secret scanning -> Disable

以及个人设置中的Code security and analysis -> Push protection for yourself -> Disable

absolute布局对应偏移对象理解错误

这个其实是相当基础的问题,但是确实前端这块基础不够扎实

absolute布局对应偏移对象实际上是在相对于static定位以外的第一个父元素进行定位

其实就是他自己向上一层一层的找自己的父元素,
然后看他们的position属性,谁的position属性不是static他就以谁为标准偏移.
如果一直没有的话就会找到body,body也不是的话,但是已经是最后一层了,
所以他就只能以body的初始位置为基准了.这就是之前为什么没对齐的原因.
注:(所有的块属性的position默认为static)

后面在写前后翻页的按钮时,发现了妙用,当最外层的app具有padding时,内部的元素无法处于padding的位置上,这时候将appposition设置为relative,就能够避免padding裁掉元素

因为nginx的cookie导致腾讯云接口无法使用的BUG

解决方法:

nginx配置后端的真实接口时,别加上其他关于cookie的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
worker_processes auto;
worker_rlimit_nofile 51200;

events {
worker_connections 51200;
multi_accept on;
}

http {

server {
listen 5173;
server_name localhost;

location /tencent-api {
# 后端的真实接口
proxy_pass https://aiart.tencentcloudapi.com/;
}
}

include vhost/*.conf;
#加载vhost目录下的虚拟主机配置文件
}

baseUrl错误而导致出现跨域BUG

问题复现:在部署到服务器时,发现出现跨域的报错Access to XMLHttpRequest at 'http://42.192.90.134:5173/api/categories/all' from origin 'http://localhost:5173' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.,但是在本地没有这个问题,同时跨域部分早就做好了设置

问题原因:项目中的跨域设置是针对/api的,而并没有对http://localhost:5173进行跨域的转发,所以在本地能够实现,但是转到服务器上面就不行了

解决过程:先是去指定了header发现没用,接着去翻bug的原因,发现别人的baseUrl为''

问题解决:在axios的index.ts文件中,URL不要进行指定,直接使用'',这样就能符合/api的转发配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import axios, { AxiosInstance, AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios'

// 引入ErrorResult接口
import {ErrorResult} from '../interfaces/ErrorResult';

const URL: string = ''
// const URL: string = 'http://42.192.90.134:5173'
// const URL: string = 'http://localhost:5173'
enum RequestEnums {
TIMEOUT = 20000,
OVERDUE = 600, // 登录失效
FAIL = 999, // 请求失败
SUCCESS = 200, // 请求成功
}
const config = {
// 默认地址
baseURL: URL as string,
// 设置超时时间
timeout: RequestEnums.TIMEOUT as number,
// 跨域时候允许携带凭证
withCredentials: true,
}

功能实战

点击全屏功能

这个功能比较死,可以直接copy

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
<template>
<el-icon size="20" @click="toggleFullScreen" style="cursor: pointer;">
<FullScreen />
</el-icon>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
const isFullScreen = ref(false);

const toggleFullScreen = () => {
if (isFullScreen.value) {
exitFullScreen();
} else {
enterFullScreen();
}
};

const enterFullScreen = () => {
const element = document.documentElement;

if (element.requestFullscreen) {
element.requestFullscreen();
}

isFullScreen.value = true;
};

const exitFullScreen = () => {
if (document.exitFullscreen) {
document.exitFullscreen();
}

isFullScreen.value = false;
};

watch(isFullScreen, (newValue) => {
// 在这里可以处理全屏状态变化后的逻辑
console.log('全屏状态变化:', newValue);
});
</script>

等待图片加载

1
2
3
4
5
6
7
8
9
await new Promise((resolve) => {
const img = new Image();
img.src = collectionItem.value.cover;
img.onload = () => {
imageLoading.value = false;
resolve(null);
};
});

从URL中获取blob对象再转化为File对象

使用场景:

获得的图片数据是一个URL,但是API需求的参数是File对象

使用方法:

首先是get请求获取到需求的URL,注意await别忘了

这里注意responseType设置为'blob'每一集设置headers的跨域,以及注意它们处于参数的位置

最需要注意的是跨域问题,上面的headers中设置跨域还不够,需要在vite.config.ts中配置到对应的服务器域名(具体请见 腾讯云AI制图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
33
34
35
36
37
38
39
40
// 定义一个 ref 变量来存储转化后的 file 对象
const fileData = ref<File | null>(null);

const handleSave = async () => {
const formData = new FormData();
// 发送 GET 请求获取文件内容并转化为 file 对象
await axios.get('在这里放需要获取的URL', {
responseType: 'blob', // 设置响应类型为 blob

headers:{
'Access-Control-Allow-Origin': '*', // 允许跨域
}
})
.then(response => {
console.log(response.data);
const blob = response.data; // 获取 blob 对象
const filename = 'example.jpg'; // 可以自定义文件名
const file = new File([blob], filename, { type: blob.type });
console.log(file);
fileData.value = file; // 将转化后的 file 对象存储到 ref 变量中
})
.catch(error => {
console.error('Error fetching file:', error);
});

console.log(fileData.value);

formData.append('file', fileData.value!);
formData.append('type', 'avatar');

await uploadImage(formData)
.then(res => {
console.log(res);
ElMessage.success("保存成功");
emit('saveSuccess', res); // 保存成功后,将图片URL传递给父组件
})
.catch(err => {
console.log(err);
});
};

优化后封装写法

参数为imgUrl,返回值为File对象

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 { ref } from 'vue';
import axios from 'axios';

/**
* 从给定的图片URL获取文件对象
* @param imgUrl 图片的URL
* @returns 返回一个Promise,该Promise在成功时解析为文件对象,失败时解析为null
*/
export async function getFileObject(imgUrl: string): Promise<File | null> {
// 创建一个响应式的文件对象引用
const fileData = ref<File>();

return new Promise((resolve, reject) => {
// 发起GET请求获取图片数据
axios
.get(imgUrl, {
responseType: 'blob',
headers: {
'Access-Control-Allow-Origin': '*',
},
})
.then(response => {
// 从响应中获取Blob数据
const blob = response.data;
// 设置文件名为example.jpg
const filename = 'example.jpg';
// 创建一个新的文件对象
const file = new File([blob], filename, { type: blob.type });
// 将文件对象赋值给fileData
fileData.value = file;
// 解析Promise并返回文件对象
resolve(fileData.value);
})
.catch(error => {
// 打印错误信息并拒绝Promise
console.error('获取文件时出错:', error);
reject(error);
});
});
};

优化后使用方法

1
2
3
4
5
6
// 通过对象存储的URL获取文件对象
await getFileObject(LocationUrl.value as string).then((res) => {
tempFile.value = res;
}).catch((err) => {
console.log(err);
});

URL转化为baseURL的方法

BASE64URL编码的流程:

1、明文使用BASE64进行加密

2、在BASE64的基础上进行以下的编码:

2.1)去除尾部的”=”

2.2)把”+”替换成”-“ 2.3)把”/“替换成”_”

1
2
3
4
5
6
7
8
9
10
11
12
13
import { ref } from 'vue';

export function tobase64Url(url: string): string {
// 1. 使用 BASE64 进行加密
const base64 = btoa(url);

// 2. 编码规则:去除尾部的"=",把"+"替换成"-",把"/"替换成"_"
const base64Url = base64.replace(/=+$/, '')
.replace(/\+/g, '-')
.replace(/\//g, '_');

return base64Url;
}

腾讯云获取对象储存预签名链接的方法

API文档:对象存储 使用预签名 URL 访问 COS-开发者指南-文档中心-腾讯云 (tencent.com)
SDK文档:对象存储 生成预签名链接-SDK 文档-文档中心-腾讯云 (tencent.com)

封装部分:

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
// 通过 npm 安装 sdk npm install cos-nodejs-sdk-v5
// SECRETID 和 SECRETKEY 请登录 https://console.cloud.tencent.com/cam/capi 进行查看和管理
// nodejs 端可直接使用 CAM 密钥计算签名,建议用限制最小权限的子用户的 CAM 密钥
// 最小权限原则说明 https://cloud.tencent.com/document/product/436/38618
import config from '../constant/config';
import * as COS from 'cos-js-sdk-v5'; // 使用 import 语法导入 COS 模块

const cos = new COS({
SecretId: config.SECRET_ID, // 推荐使用环境变量获取;用户的 SecretId,建议使用子账号密钥,授权遵循最小权限指引,降低使用风险。子账号密钥获取可参考https://cloud.tencent.com/document/product/598/37140
SecretKey: config.SECRET_KEY, // 推荐使用环境变量获取;用户的 SecretKey,建议使用子账号密钥,授权遵循最小权限指引,降低使用风险。子账号密钥获取可参考https://cloud.tencent.com/document/product/598/37140
});

const bucketConfig = {
// 需要替换成您自己的存储桶信息
Bucket: 'tec-1312799453', // 存储桶,必须
Region: 'ap-shanghai', // 存储桶所在地域,必须字段
};

export async function getObjectUrl(path: string) {
// 使用 Promise 封装异步操作
return new Promise((resolve, reject) => {
cos.getObjectUrl(
{
Bucket: bucketConfig.Bucket, // 必须
Region: bucketConfig.Region, // 存储桶所在地域,必须字段
Key: path, /* 存储在桶里的对象键(例如1.jpg,a/b/test.txt),支持中文,必须字段 */
Sign: true,
Expires: 3600, // 单位秒
},
function (err, data) {
// 修改data.Url
// 将https改为http
data.Url = data.Url.replace('https', 'http');
resolve(data.Url);
console.log(err || data.Url);
}
);
});
}

使用部分:

1
2
3
4
5
6
// 将打上水印的图片转化为URL
await getObjectUrl(path.value).then((res) => {
LocationUrl.value = res as string;
}).catch((err) => {
console.log(err);
});

腾讯云AI制图API的使用

首先给出几个官网学习的网站:
大模型图像创作引擎总体介绍:大模型图像创作引擎_AI绘画_AI作画_腾讯云 (tencent.com)

API调用工作台:API Explorer - 云 API - 控制台 (tencent.com)

API密钥管理:访问密钥 - 控制台 (tencent.com)

API调用情况查看:智能文生图 - 大模型图像创作引擎 - 控制台 (tencent.com)

API收费方式:大模型图像创作引擎 计费概述-购买指南-文档中心-腾讯云 (tencent.com)

API文档:大模型图像创作引擎 智能文生图-文生图相关接口-API 中心-腾讯云 (tencent.com)

公共参数文档:大模型图像创作引擎 公共参数-调用方式-API 中心-腾讯云 (tencent.com)

签名方法文档:大模型图像创作引擎 签名方法 v3-调用方式-API 中心-腾讯云 (tencent.com)

后付费设置:设置 - 大模型图像创作引擎 - 控制台 (tencent.com)

接着介绍如何使用:

签名部分

使用关键点在于headers中所携带的签名,腾讯云有自己的一套算法,而下面是我根据官网的nodejs封装出来的ts文件

封装自动签名的工具类部分

utils/V3.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
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
// 导入 crypto-js 库中的 SHA256, HmacSHA256, enc 方法
import { SHA256, HmacSHA256, enc } from 'crypto-js';

// 定义 V3Options 接口,包含了一些配置项
interface V3Options {
SECRET_ID: string; // 密钥 ID
SECRET_KEY: string; // 密钥
action: string; // 操作类型
host: string; // 主机地址
service: string; // 服务类型
region: string; // 区域
version: string; // 版本
ContentType: string;// 内容类型
}

// 定义 V3RequestPayload 接口,用于描述请求的 payload
interface V3RequestPayload {
[key: string]: any; // key-value 形式的任意数据
}

// 定义 sha256 函数,用于生成 HMAC SHA256 摘要
function sha256(message: string, secret: string = '', hex: boolean = false) {
const hmac = HmacSHA256(message, secret); // 使用 HmacSHA256 方法生成摘要
let hexHmac = hmac;
if (hex) {
hexHmac = hmac.toString(enc.Hex) as any; // 如果 hex 参数为 true,则将摘要转为十六进制字符串
}
return hexHmac;
}

// 定义 getHash 函数,用于生成 SHA256 摘要
function getHash(message: string) {
const hash = SHA256(message); // 使用 SHA256 方法生成摘要
const hexHash = hash.toString(enc.Hex); // 将摘要转为十六进制字符串
return hexHash;
}

// 定义 getDate 函数,用于将时间戳转为日期字符串
function getDate(timestamp: number) {
const date = new Date(timestamp * 1000); // 将时间戳转为 Date 对象
const year = date.getUTCFullYear(); // 获取年份
const month = ('0' + (date.getUTCMonth() + 1)).slice(-2); // 获取月份,并转为两位数的字符串
const day = ('0' + date.getUTCDate()).slice(-2); // 获取日期,并转为两位数的字符串
return `${year}-${month}-${day}`; // 返回 yyyy-mm-dd 格式的日期字符串
}

// 定义 V3 函数,用于生成 V3 签名
export function V3(config: V3Options, body: V3RequestPayload) {
// 从配置中解构出各个参数
const SECRET_ID = config.SECRET_ID;
const SECRET_KEY = config.SECRET_KEY;
const host = config.host;
const service = config.service;
const region = config.region;
const action = config.action;
const version = config.version;
const ContentType = config.ContentType;

const timestamp = Math.floor(Date.now() / 1000); // 获取当前时间的时间戳(秒)
const date = getDate(timestamp); // 将时间戳转为日期字符串
const payload = JSON.stringify(body); // 将请求体转为 JSON 字符串
const hashedRequestPayload = getHash(payload); // 对请求体生成 SHA256 摘要
const httpRequestMethod = "POST"; // HTTP 请求方法
const canonicalUri = "/"; // 规范 URI
const canonicalQueryString = ""; // 规范查询字符串
// 规范头部字符串
const canonicalHeaders =
`content-type:${ContentType}\n` +
`host:${host}\n` +
`x-tc-action:${action.toLowerCase()}\n`;
const signedHeaders = "content-type;host;x-tc-action"; // 签名头部字符串

// 构造规范请求字符串
const canonicalRequest =
`${httpRequestMethod}\n` +
`${canonicalUri}\n` +
`${canonicalQueryString}\n` +
`${canonicalHeaders}\n` +
`${signedHeaders}\n` +
`${hashedRequestPayload}`;

const algorithm = "TC3-HMAC-SHA256"; // 算法
const hashedCanonicalRequest = getHash(canonicalRequest); // 对规范请求字符串生成 SHA256 摘要
const credentialScope = `${date}/${service}/tc3_request`; // 凭证范围
// 构造待签名字符串
const stringToSign = `${algorithm}\n${timestamp}\n${credentialScope}\n${hashedCanonicalRequest}`;

// 生成签名密钥
const kDate = sha256(date, 'TC3' + SECRET_KEY);
const kService = sha256(service, kDate as any);
const kSigning = sha256('tc3_request', kService as any);
const signature = sha256(stringToSign, kSigning as any, true); // 生成签名

// 构造授权字符串
const authorization =
`${algorithm} Credential=${SECRET_ID}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;

// 构造 curl 命令
const curlcmd =
`curl -X POST https://${host}` +
` -H "Authorization: ${authorization}"` +
` -H "Content-Type: ${ContentType}"` +
` -H "Host: ${host}"` +
` -H "X-TC-Action: ${action}"` +
` -H "X-TC-Timestamp: ${timestamp}"` +
` -H "X-TC-Version: ${version}"` +
` -H "X-TC-Region: ${region}"` +
` -d '${payload}'`;

// 返回相关信息
return {
SECRET_ID,
SECRET_KEY,
authorization,
ContentType,
timestamp,
region,
action,
version,
host,
curlcmd
};
}

封装接口部分

注意headers部分将传入的自定义头部信息与默认头部信息合并,这样方便调用时根据params修改headers中的签名

其他参数都是腾讯云要求的公共参数

注意通过tencent-api来解决的跨域问题所配置的内容,在下面的获取图片部分有展示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import Axios from 'axios'
export const text2Img = (params: any, headers?: Record<any, any>) => {
return Axios.post('tencent-api', params, {
headers: {
...headers, // 将传入的自定义头部信息与默认头部信息合并
// 在这里添加你需要的自定义头部信息
// Authorization: authorization,
// Host: 'aiart.tencentcloudapi.com',
'X-TC-Action': 'TextToImage',
// 'X-TC-Timestamp': timestamp,
'X-TC-Version': '2022-12-29',
'X-TC-Region': 'ap-shanghai',

},
}).catch(error => {
// 错误处理
ElMessage.error('请求失败,请稍后重试');
return Promise.reject(error);
});
}

封装除请求参数其他配置为常量部分

constant/config.ts

1
2
3
4
5
6
7
8
9
10
11
12
// config.ts 文件
// 使用 export default 导出一个对象,这个对象包含了一些配置参数
export default {
SECRET_ID: "******", // 密钥 ID
SECRET_KEY: "", // 密钥
action: "TextToImage", // 操作类型
host: "aiart.tencentcloudapi.com", // 主机地址
service: "aiart", // 服务类型
region: "ap-shanghai", // 区域
version: "2022-12-29", // 版本
ContentType: "application/json" // 内容类型
};

如何调用接口部分

注意如何调用V3工具类以及如何增加headers

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
import { ref } from 'vue';
import axios from 'axios';

import config from "../constant/config";
import { V3 } from "../utils/V3";

import { text2Img } from "../api/collections"

// 定义prompt
let prompt = ref('')
// 定义negativePrompt
let negativePrompt = ref('')
let loading = ref(false);
let category = ref("");
// 定义上传后的图片URL
const uploadedImage = ref<string | null>(null);

// 实现handleText2Img方法
const handleText2Img = async () => {
if (isEmpty()) {
ElMessage.error("prompt以及negativePrompt不能为空")
return;
}
loading.value = true;
const requestData = {
"Prompt": prompt.value,
"NegativePrompt": negativePrompt.value,
"RspImgType": "url",
"Styles": [category.value]
};


// 调用V3接口
// body参数为requestData
const { authorization, timestamp } = V3(config, requestData)
const headers = {
Authorization: authorization,
"X-TC-Timestamp": timestamp
}


await text2Img(requestData, headers).then(res => {
uploadedImage.value = res?.data?.Response?.ResultImage;
loading.value = false;
ElMessage.success("生成图片成功,点击保存后返回")
}).catch((err) => {
console.log(err);
})
}


// 判断prompt以及negativePrompt是否为空
const isEmpty = () => {
return prompt.value === '' || negativePrompt.value === '';
}

获取图片部分

从URL中获取blob对象再转化为File对象部分

这点有专门的笔记解释,可进行查看

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
// 定义一个 ref 变量来存储转化后的 file 对象
const fileData = ref<File | null>(null);

const handleSave = async () => {
const formData = new FormData();
// 发送 GET 请求获取文件内容并转化为 file 对象
await axios.get('在这里放需要获取的URL', {
responseType: 'blob', // 设置响应类型为 blob

headers:{
'Access-Control-Allow-Origin': '*', // 允许跨域
}
})
.then(response => {
console.log(response.data);
const blob = response.data; // 获取 blob 对象
const filename = 'example.jpg'; // 可以自定义文件名
const file = new File([blob], filename, { type: blob.type });
console.log(file);
fileData.value = file; // 将转化后的 file 对象存储到 ref 变量中
})
.catch(error => {
console.error('Error fetching file:', error);
});

console.log(fileData.value);

formData.append('file', fileData.value!);
formData.append('type', 'avatar');

await uploadImage(formData)
.then(res => {
console.log(res);
ElMessage.success("保存成功");
emit('saveSuccess', res); // 保存成功后,将图片URL传递给父组件
})
.catch(err => {
console.log(err);
});
};

跨域配置部分

注意下面的配置是必须的,否则一定会报跨域问题

vite.config.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
// https://vitejs.dev/config/
export default defineConfig({
server: {
host: '0.0.0.0',
port: 5173,
proxy: {
'/api': {
target: 'http://qexo.moefish.net:5409', //实际请求地址
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ""),
// 显示请求代理后的真实地址

},
'/tencent-api': {
target: 'https://aiart.tencentcloudapi.com/',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/tencent-api/, ''),
},
'/tencent-download-api': {
target: 'https://aiart-1258344699.cos.ap-guangzhou.myqcloud.com/text_to_img',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/tencent-download-api/, ''),
},
}
},
})

腾讯云大模型审核API的使用

免费额度:数据万象 免费额度-产品计费-文档中心-腾讯云 (tencent.com)

选购界面:数据万象CI购买_数据万象CI选购 - 腾讯云 (tencent.com)

收费标准:数据万象 资源包使用-产品计费-文档中心-腾讯云 (tencent.com)

查看使用情况:概览 - 数据万象 - 控制台 (tencent.com)

桶跨域文档:对象存储 设置跨域访问-控制台指南-文档中心-腾讯云 (tencent.com)

桶跨域控制台:存储桶列表 - 对象存储 - 控制台 (tencent.com)

创建审核策略(biz-type):存储桶 tec-1312799453 - 对象存储 - 控制台 (tencent.com)

API调试台:API Explorer - 云 API - 控制台 (tencent.com)

生成签名的文档(包含直接生成签名以及生成预签名链接,这次的API都没使用到,以后可能会用到,所以放在这里):对象存储 请求签名-API 文档-文档中心-腾讯云 (tencent.com)

请求URL:<BucketName-APPID>.cos.<Region>.myqcloud.com

跨域问题

第一次使用时会遇到访问桶被跨域阻拦的问题,这时候需要去桶跨域控制台,增加跨域设置,不过设置完之后就不用管了,不过具体的API访问倒是不需要配置

封装部分

直接使用腾讯云封装好的API:

下面会解释步骤,并说出与官方给的实例中我所改变的部分

注意首先第一步是安装npm install cos-js-sdk-v5

接着是引入COS,官方是使用require来引入,这里必须使用import * as COS from 'cos-js-sdk-v5';的方式来引入,于此同时由于我新封装的文件是ts类型,所以还需要更多的引入以及配置,具体请看配置部分,

再接下来是new COS部分,新获得的实例会包含签名部分Authorization

然后配置桶的信息bucketConfig

最后是export方法getImageAuditing方便其他文件进行调用

与官方不同的是我增加了return,方便调用的时候获取到数据

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
// 通过 npm 安装 sdk npm install cos-nodejs-sdk-v5
// SECRETID 和 SECRETKEY 请登录 https://console.cloud.tencent.com/cam/capi 进行查看和管理
// nodejs 端可直接使用 CAM 密钥计算签名,建议用限制最小权限的子用户的 CAM 密钥
// 最小权限原则说明 https://cloud.tencent.com/document/product/436/38618
import config from '../constant/config';
import * as COS from 'cos-js-sdk-v5'; // 使用 import 语法导入 COS 模块
import { RecognitionResult } from '../interfaces/RecognitionResult';

const cos = new COS({
SecretId: config.SECRET_ID, // 推荐使用环境变量获取;用户的 SecretId,建议使用子账号密钥,授权遵循最小权限指引,降低使用风险。子账号密钥获取可参考https://cloud.tencent.com/document/product/598/37140
SecretKey: config.SECRET_KEY, // 推荐使用环境变量获取;用户的 SecretKey,建议使用子账号密钥,授权遵循最小权限指引,降低使用风险。子账号密钥获取可参考https://cloud.tencent.com/document/product/598/37140
});

const bucketConfig = {
// 需要替换成您自己的存储桶信息
Bucket: 'tec-1312799453', // 存储桶,必须
Region: 'ap-shanghai', // 存储桶所在地域,必须字段
};

export async function getImageAuditing(detectUrl: string) {
// 使用 Promise 封装异步操作
return new Promise((resolve, reject) => {
cos.request({
Bucket: bucketConfig.Bucket,
Region: bucketConfig.Region,
Method: 'GET',
Key: '', // 存储桶内的图片文件,必须字段
Query: {
'ci-process': 'sensitive-content-recognition', // 图片审核的处理接口,必须字段
'biz-type': 'bd4575d0e81311ee8556525400662d48', // 敏感内容识别的 biztype,必须字段
'detect-url': detectUrl, // 图片的 URL 地址,必须字段
},
}, function (err:any, data:any) {
if (err) {
// 处理请求出错
reject(err);
} else {
// 处理请求成功
resolve(data as RecognitionResult);
}
});
});
}

配置部分

因为封装的文件是ts类型,所以需要增加下面的配置防止报错

首先是下载类型包npm install --save-dev @types/node

接着是去修改tsconfig.json,修改的部分为增加"node"以及esModuleInterop以及allowSyntheticDefaultImports都设置为true

1
2
3
4
5
6
7
8
9
{
" ": {
"types": ["element-plus/global","node"],
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,

},
}

总体返回类型接口AuditResult

1
2
3
4
5
6
7
8
9
10
11
import { RecognitionResult } from './RecognitionResult';
export interface AuditResult {
RecognitionResult: RecognitionResult;
statusCode: number;
headers: {
"content-length": string;
"content-type": string;
"x-cos-request-id": string;
};
RequestId: string;
}

审核数据类型接口RecognitionResult

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
export interface RecognitionResult {
DataId: string;
JobId: string;
State: string;
Object: string;
Url: string;
CompressionResult: number;
Result: number;
Label: string;
Category: string;
SubLabel: string;
Score: number;
Text: string;
PornInfo?: {
Code: number;
Msg: string;
HitFlag: number;
Score: number;
Label: string;
Category: string;
SubLabel: string;
OcrResults?: {
// Define OCR results structure if needed
}[];
LibResults?: {
// Define LibResults structure if needed
}[];
};
AdsInfo?: {
Code: number;
Msg: string;
HitFlag: number;
Score: number;
Label: string;
Category: string;
SubLabel: string;
OcrResults?: {
// Define OCR results structure if needed
}[];
LibResults?: {
// Define LibResults structure if needed
}[];
};
QualityInfo?: {
Code: number;
Msg: string;
HitFlag: number;
Score: number;
Label: string;
Category: string;
SubLabel: string;
OcrResults?: {
// Define OCR results structure if needed
}[];
LibResults?: {
// Define LibResults structure if needed
}[];
};
}

使用部分

需要注意的是await的使用

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
import { getImageAuditing } from "../utils/ImageAuditing"
// 上传图片
const uploadFile = async () => {
const fileInput = document.getElementById('fileInput') as HTMLInputElement;
// 确保存在文件
if (fileInput && fileInput.files && fileInput.files.length > 0) {
// 此处省略上传API的使用

// 图片审核
const result = await getImageAuditing(uploadedImage.value!)
// 审核结果
const resultData = (result as AuditResult).RecognitionResult.Result
// 审核内容
const resultLabel = (result as AuditResult).RecognitionResult.Label

// 如果审核不通过,弹出提示
if(resultData == 1){
uploadedImage.value! = ""
switch (resultLabel) {
case "Porn":
ElMessage.error("图片审核不通过,图片中包含色情内容")
break;
case "Terrorism":
ElMessage.error("图片审核不通过,图片中包含暴力内容")
break;
}
}else if(resultData == 2){
ElMessage.info("图片等待人工审核")
}else if(resultData == 0){
ElMessage.success("图片审核通过")
}
}
};

腾讯云盲水印烙印以及提取API的使用

API文档:数据万象 图片盲水印-API 文档-文档中心-腾讯云 (tencent.com)

对象存储文档:对象存储 PUT Object-API 文档-文档中心-腾讯云 (tencent.com)

对象存储API调试控制台:API Explorer - 云 API - 控制台 (tencent.com)

对象存储SDK文档:对象存储 上传对象-SDK 文档-文档中心-腾讯云 (tencent.com)

错误码文档:对象存储 错误码-API 文档-文档中心-腾讯云 (tencent.com)

对象存储台:存储桶 tec-1312799453 - 对象存储 - 控制台 (tencent.com)

配置解释部分

is_pic_info:是否返回原图信息。0表示不返回原图信息,1表示返回原图信息,默认为0

rules:处理规则,一条规则对应一个处理结果(目前最多支持五条规则),不填则不进行图片处理

rules(json 数组)中每一项具体参数如下:

fileid:处理后文件的保存路径及名称。

rule:处理参数

使用盲水印需在 rule 中添加水印图参数(watermark),示例格式如下:

注意level参数,建议直接上最强的3级,否则很容易出现水印提取不出来(‼️)

1
watermark/3/type/<type>/image/<imageUrl>/text/<text>/level/<level>

type:盲水印类型,有效值:1为图片半盲水印;2为图片全盲水印;3为文字盲水印

image:盲水印图片地址,需要经过 URL 安全的 Base64 编码。 指定的水印图片必须同时满足如下条件:

text:盲水印文字,需要经过 URL 安全的 Base64 编码

level:只对图片全盲水印(type=2)有效。level 的取值范围为{1,2,3},默认值为1,level 值越大则图片受影响程度越大、盲水印效果越好。

最后注意提取的格式,示例格式如下:

注意不需要加level

以及注意image地址填的究竟是啥:

当 type 为1,则 image 必填,且为原图图片地址

当 type 为2,则 image 必填,且为水印图地址

当 type 为3,则 image 无需填写(无效)

1
watermark/4/type/<type>/image/<imageUrl>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"is_pic_info": 1,
"rules": [
{
"fileid": "/p3/test1.jpg",
"rule": "watermark/3/type/2/image/aHR0cDovL3RlYy0xMzEyNzk5NDUzLmNvcy5hcC1zaGFuZ2hhaS5teXFjbG91ZC5jb20vd2F0ZXJtYXJrLnBuZz9xLXNpZ24tYWxnb3JpdGhtPXNoYTEmcS1haz1BS0lEaTFHYXVXUE1LTFZnbnFvdDhRcENVa3V6MkRZdkdsSW0mcS1zaWduLXRpbWU9MTcxMTMyNjc0MDsxNzExMzMwMzQwJnEta2V5LXRpbWU9MTcxMTMyNjc0MDsxNzExMzMwMzQwJnEtaGVhZGVyLWxpc3Q9aG9zdCZxLXVybC1wYXJhbS1saXN0PSZxLXNpZ25hdHVyZT1kZmMyZTg1MTFiZGNkNmMwMTUxZmU3YWVlMDkxYmIzYjMzZWQxZDg5/level/3"
}
]
}
{
"is_pic_info": 1,
"rules": [
{
"fileid": "/p3/test2.jpg",
"rule": "watermark/4/type/2/image/aHR0cDovL3RlYy0xMzEyNzk5NDUzLmNvcy5hcC1zaGFuZ2hhaS5teXFjbG91ZC5jb20vd2F0ZXJtYXJrLnBuZz9xLXNpZ24tYWxnb3JpdGhtPXNoYTEmcS1haz1BS0lEaTFHYXVXUE1LTFZnbnFvdDhRcENVa3V6MkRZdkdsSW0mcS1zaWduLXRpbWU9MTcxMTMyNjc0MDsxNzExMzMwMzQwJnEta2V5LXRpbWU9MTcxMTMyNjc0MDsxNzExMzMwMzQwJnEtaGVhZGVyLWxpc3Q9aG9zdCZxLXVybC1wYXJhbS1saXN0PSZxLXNpZ25hdHVyZT1kZmMyZTg1MTFiZGNkNmMwMTUxZmU3YWVlMDkxYmIzYjMzZWQxZDg5"
}
]
}

封装部分

直接使用腾讯云封装好的API:

下面会解释步骤,并说出与官方给的实例中我所改变的部分

onProgress获取上传的进度

Headers内部的'Pic-Operations'是配置的关键步骤,格式在上面的配置解释部分

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
// 通过 npm 安装 sdk npm install cos-nodejs-sdk-v5
// SECRETID 和 SECRETKEY 请登录 https://console.cloud.tencent.com/cam/capi 进行查看和管理
// nodejs 端可直接使用 CAM 密钥计算签名,建议用限制最小权限的子用户的 CAM 密钥
// 最小权限原则说明 https://cloud.tencent.com/document/product/436/38618
import config from '../constant/config';
import * as COS from 'cos-js-sdk-v5'; // 使用 import 语法导入 COS 模块
import { PicOperation } from '../interfaces/PicOperation';

const cos = new COS({
SecretId: config.SECRET_ID, // 推荐使用环境变量获取;用户的 SecretId,建议使用子账号密钥,授权遵循最小权限指引,降低使用风险。子账号密钥获取可参考https://cloud.tencent.com/document/product/598/37140
SecretKey: config.SECRET_KEY, // 推荐使用环境变量获取;用户的 SecretKey,建议使用子账号密钥,授权遵循最小权限指引,降低使用风险。子账号密钥获取可参考https://cloud.tencent.com/document/product/598/37140
});

const bucketConfig = {
// 需要替换成您自己的存储桶信息
Bucket: 'tec-1312799453', // 存储桶,必须
Region: 'ap-shanghai', // 存储桶所在地域,必须字段
};

export async function addWatermark(path: string, fileObject: File, picOperations: string) {
// 使用 Promise 封装异步操作
return new Promise((resolve, reject) => {
cos.putObject({
Bucket: bucketConfig.Bucket, // 必须
Region: bucketConfig.Region, // 存储桶所在地域,必须字段
Key: path, // 必须
StorageClass: 'STANDARD',
Body: fileObject, // 上传文件对象
onProgress: function (progressData) {
console.log(JSON.stringify(progressData));
},
// 支持自定义headers 非必须
Headers: {
'Pic-Operations': picOperations
},
}, function (err, data) {
if (err) {
// 处理请求出错
reject(err);
} else {
console.log(data);
// 处理请求成功
resolve(data);
}
});
});
}

使用部分

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
// 获取水印图片
watermarkUrl.value = await getObjectUrl("watermark.png") as string;
base64Image.value = tobase64Url(watermarkUrl.value as string);
picOperations.value = {
"is_pic_info": 1,
"rules": [
{
"fileid": path.value,
"rule": `watermark/4/type/2/image/${base64Image.value}`,
},
],
};

// 转化为JSON类型
picOperations.value = JSON.stringify(picOperations.value);
console.log("picOperations.value:", picOperations.value);

// 检测是否已经存在水印
loading.value = true;
await addWatermark(path.value, fileInput.files[0], picOperations.value).then((res: WatermarkResult) => {
watermarkStatus = res.UploadResult.ProcessResults.Object.WatermarkStatus;
}).catch((err) => {
console.log(err);
});
if (watermarkStatus >= 75) {
ElMessage.error("尊重版权,禁止上传已存在的藏品");
loading.value = false;
return;
}

picOperations.value = {
"is_pic_info": 1,
"rules": [
{
"fileid": path.value,
"rule": `watermark/3/type/2/image/${base64Image.value}/level/3`,
},
],
};
// 转化为JSON类型
picOperations.value = JSON.stringify(picOperations.value);
console.log("picOperations.value:", picOperations.value);
// 添加水印
await addWatermark(path.value, fileInput.files[0], picOperations.value).then((res: WatermarkResult) => {
// 加上http//
// LocationUrl.value = "http//" + res.UploadResult.OriginalInfo.Location;
}).catch((err) => {
console.log(err);
});

调试与部署

网页调试器获取到点击后才出现的元素

原文链接:如何使用chrome浏览器进行js调试找出元素绑定的点击事件-CSDN博客

主要是在源代码这个菜单中,右侧有一个事件侦听器断点子菜单,选中里面的鼠标-click就行

跨域问题

vite.config.ts文件中多加上server的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
// 省略
],
// 下面是重点
server: {
host: '0.0.0.0',
port: 5173,
proxy: {
'/api': {
target: 'http://qexo.moefish.net:5409', //实际请求地址
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ""),
// 显示请求代理后的真实地址(在header中的x-res-proxyUrl显示)
bypass(req, res, options) {
const proxyUrl = new URL(req.url || "", options.target)?.href || "";
res.setHeader("x-res-proxyUrl", proxyUrl);
},
},
}
},
})

如何nginx 一个端口配置多个项目

原文链接:使用nginx部署多个前端项目(三种方式)-CSDN博客

最简单Vue3中配置二级目录的方法,不需要设置系统变量原文链接:vite+vue3项目配置二级访问目录 - 个人文章 - SegmentFault 思否

第一步配置二级目录目录:

本身使用一级目录的项目不需要进行下面的配置

1
2
3
4
5
// 返回一个 router 实例,为函数,里面有配置项(对象) history
const router = createRouter({
history: createWebHashHistory(('/admin/')),
routes,
});

第二步配置nginx

注意listen应该是要进行修改成对应本地运行时的端口

注意root以及alias记得上传对应的打包后的文件

这里后续学到了root底下的index的作用(index xxx.html),修改Nginx默认会找的文件index.html为指定的文件名,不过一般打包出来就是index.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
worker_processes auto;  # 自动设置工作进程数

worker_rlimit_nofile 51200; # 每个工作进程的最大文件描述符数

events {
worker_connections 51200; # 每个工作进程的最大连接数
multi_accept on; # 允许一个工作进程同时接受多个新连接
}

http {
include mime.types;
server {
listen 5173; # 监听端口号
server_name localhost; # 服务器名称为本地主机

location / {
root C:/wwwroot/42.192.90.134/NFT-Platform; # 对路径 / 的请求映射到文件系统路径
}
location /admin {
alias C:/wwwroot/42.192.90.134/admin; # 对路径 /admin 的请求映射到文件系统路径
}

location /api/ {
# 后端的真实接口
proxy_pass http://124.220.75.222:8080/; # 代理转发请求到指定后端服务器地址
}
location /tencent-api {
# 后端的真实接口
proxy_pass https://aiart.tencentcloudapi.com/; # 代理转发请求到指定后端服务器地址
}
location /tencent-download-api {
# 后端的真实接口
proxy_pass https://aiart-1258344699.cos.ap-guangzhou.myqcloud.com/text_to_img; # 代理转发请求到指定后端服务器地址
}
location /nginx_status {
allow 127.0.0.1; # 允许本地访问
deny all; # 拒绝所有其他地址的访问
stub_status on; # 启用 Nginx 状态信息的展示
access_log off; # 关闭对这个路径的访问日志记录
}
}

include vhost/*.conf; # 加载虚拟主机配置文件
}

完整代码:

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
worker_processes auto;
worker_rlimit_nofile 51200;

events {
worker_connections 51200;
multi_accept on;
}

http {
include mime.types;
#include luawaf.conf;
include proxy.conf;
default_type application/octet-stream;

server_names_hash_bucket_size 512;
client_header_buffer_size 32k;
large_client_header_buffers 4 32k;
client_max_body_size 50m;

sendfile on;
tcp_nopush on;

keepalive_timeout 60;

tcp_nodelay on;

fastcgi_connect_timeout 300;
fastcgi_send_timeout 300;
fastcgi_read_timeout 300;
fastcgi_buffer_size 64k;
fastcgi_buffers 4 64k;
fastcgi_busy_buffers_size 128k;
fastcgi_temp_file_write_size 256k;
fastcgi_intercept_errors on;

gzip on;
gzip_min_length 1k;
gzip_buffers 4 16k;
gzip_http_version 1.1;
gzip_comp_level 2;
gzip_types text/plain application/javascript application/x-javascript text/javascript text/css application/xml;
gzip_vary on;
gzip_proxied expired no-cache no-store private auth;
gzip_disable "MSIE [1-6]\.";

limit_conn_zone $binary_remote_addr zone=perip:10m;
limit_conn_zone $server_name zone=perserver:10m;

server_tokens off;
access_log off;

server {
listen 5173;
server_name localhost;

#charset koi8-r;

#access_log logs/host.access.log main;

location / {
root C:/wwwroot/42.192.90.134/NFT-Platform;
}
location /admin {
alias C:/wwwroot/42.192.90.134/admin;
}
location /api/ {
# 后端的真实接口
proxy_pass http://124.220.75.222:8080/;
}
location /tencent-api {
# 后端的真实接口
proxy_pass https://aiart.tencentcloudapi.com/;
}
location /tencent-download-api {
# 后端的真实接口
proxy_pass https://aiart-1258344699.cos.ap-guangzhou.myqcloud.com/text_to_img;
}
location /nginx_status {
allow 127.0.0.1;
deny all;
stub_status on;
access_log off;
}
}

include vhost/*.conf;
#加载vhost目录下的虚拟主机配置文件
}

nginx负载均衡

负载均衡是指将流量分发到多个服务器上,以确保请求可以高效地处理并且不会因为某一台服务器的故障而中断服务。Nginx的负载均衡功能通过代理模块实现,可以实现多种负载均衡策略。

下面代码实现的效果为每次刷新页面会根据nginx的设置,来自动分配到不同的服务器中localhost:3000或者是localhost:3001,并且使用weight来控制究竟选择分配到哪个服务器多一点

当然负载均衡是很复杂的问题,还要涉及到不同服务器之间状态的统一,所以这里只是暂时去了解下

分配前

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
server {
listen 80;
server_name localhost;
root /var/www/localhost;
index index.html;
error_page 404 /404.html;

location /nextjsapp1 {
proxy_pass http://localhost:3000;
}

location /nextjsapp2 {
proxy_pass http://localhost:3001;
}
}

分配后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
upstream backend-servers {
server localhost:3000 weight=3;
server localhost:3001 weight=8;
}

server {
listen 80;
server_name localhost;
root /var/www/localhost;
index index.html;
error_page 404 /404.html;

location / {
proxy_pass http://backend-servers;
}
}

Git

Gitee初始化仓库并提交

首先根据上面的步骤初始化vue项目之后

1、初始化本地git仓库以及

打开vscode的SOURCE CONTROL图标,第一次进去会会显示Initialize repository点击这个按钮,也就相当于进行了下面的步骤

1
2
git init
git add .

下面的是必要的连接远程仓库的命令行命令

1
git remote add origin 远程仓库URL

2、重命名分支

注意这个时候别提交,应该先更改分支的名字,因为大部分远程仓库主分支名为master,但是本地仓库默认为main为主分支,这时候可以使用vscode里面的Rename Branch...更改名字为master,同等效果的git命令我也放到下面了

截屏2024-02-15 18.36.40

1
git branch -m 旧分支名 新分支名

3、提交到远程仓库

这里要注意如果远程仓库本来就已经有代码时哦,要先拉取代码

1
git pull origin master

然后再进行提交,这里直接使用vscode的提交就行,当然相关命令我也放在下面,防止忘记

截屏2024-02-15 19.01.41

1
2
git commit -m "提交消息"
git push origin master

Git提交信息规范

  1. 提交信息结构:

    • 提交信息分为类型、可选的范围和描述三个部分。
    • 示例:feat(user-auth): 增加密码重置功能
  2. 类型(Type):

    • feat: 新功能
    • fix: 修复问题
    • docs: 文档变更
    • style: 代码格式、空格等非逻辑性的变更
    • refactor: 重构代码,既不是新增功能也不是修复bug
    • test: 添加或修改测试
    • chore: 构建过程或辅助工具的变更
  3. 可选的范围(Scope):

    • 描述变更影响的范围,可以是模块、组件、功能等。例如:(user-auth)
  4. 描述(Description):

    • 提供详细的变更描述,解释为什么做出这个变更以及变更的具体内容。
  5. 提交信息长度:

    • 保持简短,最好不超过50个字符。如果需要更详细的描述,可以使用空行后添加更多信息。
  6. 动词使用:

    • 使用动词的现在时来描述变更,例如:增加修复更新等。
  7. 参考任务或问题编号:

    • 如果有相关的任务、问题或需求编号,可以在提交信息中引用,以便于跟踪。例如:修复 #123

示例提交信息:

1
2
3
4
5
6
feat(user-auth): 增加密码重置功能

- 增加密码重置的新接口
- 实现了密码重置的邮件通知功能
- 更新用户认证服务以处理密码重置请求
chore(init): 初始化项目结构

这样的规范有助于更清晰地表达每次提交的目的和内容,有助于团队协作和代码维护。

Git怎么修改提交的注释信息

原文链接:[Git使用小技巧【修改commit注释, 超详细】_git更改commit描述-CSDN博客](https://blog.csdn.net/xiaoyulike/article/details/119176756#:~:text=二、修改以前提交的注释 1 (1)git rebase -i HEAD~2 【 2,(4)%3Awq 【保存退出】 5 (5)git commit –amend 【同上有提示,第一行进行你真正需要的修改%2C 修改完后,保存退出】)

如果仅仅是想修改最后一次注释

(1)git commit –amend 【第一行出现注释界面】

(2)i 【进入修改模式, 修改完成】

(3)Esc 【退出编辑模式】

(4):wq 【保存并退出即可】

(5)git log 【 查看提交记录】

注意按住q可以退出,不要使用关掉终端的方式,否则会报以下的错误,主要是因为上个打开的文件没关,形成了一个临时文件

参考链接:成功解决:使用vim修改文件时报错Another program may be editing the same file. If this is the case的问题_another program may be editing the same file. if t-CSDN博客

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
E325: ATTENTION
Found a swap file by the name "~/NFT-Platform/.git/.COMMIT_EDITMSG.swp"
owned by: tec dated: 一 1 29 22:25:26 2024
file name: ~tec/NFT-Platform/.git/COMMIT_EDITMSG
modified: YES
user name: tec host name: TECdeMacBook-Pro.local
process ID: 15259
While opening file "/Users/tec/NFT-Platform/.git/COMMIT_EDITMSG"
dated: 一 1 29 22:27:40 2024
NEWER than swap file!

(1) Another program may be editing the same file. If this is the case,
be careful not to end up with two different instances of the same
file when making changes. Quit, or continue with caution.
(2) An edit session for this file crashed.
If this is the case, use ":recover" or "vim -r /Users/tec/NFT-Platform/.git/COMMIT_EDITMSG"

解决方法为删除该临时文件就行

1
sudo rm -rf /Users/tec/NFT-Platform/.git/.COMMIT_EDITMSG.swp

然后提交到远程仓库就行

1
git push --force origin master

后端

后端初始项目(⚠️)

pom.xml

关键是不同版本之间的兼容问题

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
149
150
151
152
153
154
155
156
157
158
159
160
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>nftadmin</groupId>
<artifactId>nftadmin</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>nftadmin</name>
<description>nftadmin</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>

<dependency>
<groupId>com.belerweb</groupId>
<artifactId>pinyin4j</artifactId>
<version>2.5.1</version>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>4.4.0</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>4.1.2</version> <!-- 请根据最新版本进行更改 -->
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version> <!-- 请根据最新版本进行更改 -->
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<!-- Sa-Token 权限认证,在线文档:https://sa-token.cc -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>1.36.0</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>compile</scope>
</dependency>

<!-- 引入aop支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>

<!-- <dependency>-->
<!-- <groupId>com.baomidou</groupId>-->
<!-- <artifactId>mybatis-plus-boot-starter</artifactId>-->
<!-- <version>3.5.4.1</version>-->
<!-- <exclusions>-->
<!-- <exclusion>-->
<!-- <artifactId>mybatis-spring</artifactId>-->
<!-- <groupId>org.mybatis</groupId>-->
<!-- </exclusion>-->
<!-- </exclusions>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>org.mybatis</groupId>-->
<!-- <artifactId>mybatis-spring</artifactId>-->
<!-- <version>3.0.3</version>-->
<!-- </dependency>-->

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
</dependency>

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>

<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>

<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.23</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.5.16</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-dysmsapi</artifactId>
<version>1.1.0</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.6.11</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.16</version>
</dependency>



</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.5.0</version>
</plugin>
</plugins>
</build>

</project>

运行别人的JAVA项目报错

重新导入一下maven就行

截屏2024-03-04 08.48.29

其他

Copilot使用

![截屏2024-01-24 10.35.54](/Users/tec/Library/Application Support/typora-user-images/截屏2024-01-24 10.35.54.png)