前端 界面设计与开发 改变背景颜色动画效果 重点关注的其实是如何做到颜色加深或者变浅,参考下面的例子,关键就是调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 ); }
向上移动5px的动画 ransition: box-shadow 0.3s ease, transform 0.3s ease; /* 添加过渡效果 */
属性:box-shadow
和 transform
属性在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 ); }
图片按照比例缩放 object-fit
object-fit
是 CSS 中用于控制替换元素(如 <img>
、<video>
或 <object>
)的尺寸和裁剪的属性。这个属性允许你定义替换元素在其容器内的尺寸和位置,以及如何调整替换元素的内容以适应这些尺寸。
object-fit
属性有以下几个可能的取值:
fill
: 默认值。替换元素被拉伸以填满容器 ,可能导致元素的宽高比发生变化。
1 2 3 img { object-fit : fill; }
contain
: 替换元素被缩放以适应容器,保持其宽高比 ,可能在容器内留有空白 。
1 2 3 img { object-fit : contain; }
cover
: 替换元素被缩放以填满容器,保持其宽高比 ,可能裁剪超出容器的部分 。(最常用)
1 2 3 img { object-fit : cover; }
none
: 替换元素保持其原始尺寸 ,可能溢出容器。
1 2 3 img { object-fit : none; }
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部分:
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-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 ); transition : background-color 0.2s cubic-bezier (0.05 , 0 , 0.2 , 1 ) 0s ; } .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-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 .element { transition : all 0.3s ease 0.1s ; } .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
定位,使图片悬浮在左下角
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; 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 ); }
手写悬浮二级菜单 这个比较死可以直接拿来用
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 ); } } }
手写朝着点击方向移动的动画效果
方法一: 该方法是纯自己想,缺点在于开局就会转动,优点在于遇到开局需要运动的需求,可以用上这个
方法二:
该方法才是通用的方法,首先是大体的html结构部分,抛弃方法一中的,每个位置上都放置一个白色选择块,再通过点击显示点击位置的白色选择块的方法
实际上应该只放置一个白色选择块,点击后通过css中的translateX
,将该唯一白色选择块移动至点击位置,最后用transition
控制移动动画的长度
HTML部分:
1 2 3 4 5 6 7 8 9 <div class ="FilterSectionType" style ="flex: 2;" > <div :class ="{ 'Selected0': TypeIndex.index === 0, 'Selected1': TypeIndex.index === 1 }" > </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% ; top : 50% ; 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>
实现展开隐藏一段文字的效果
通过用不同的样式,实现该效果:
展开之前:
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 ); } input [type="checkbox" ] { transform : scale (1.5 ); }
媒体查询 注意max-width为在窗口宽度小于等于 1300px 时,min-width为在窗口宽度大于等于 1300px 时,启用样式
1 2 3 4 5 6 7 @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 ("" );const uploadedImage = ref<string | null >(null );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 ; 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'
解决方法为修改tsconfig.json 文件,然后关闭 VScode ,重新启动一下项目即可。
将bundler
改为node
1 2 "moduleResolution" : "node" ,
第二个是两个组件Volar
和Vetur
冲突
Vetur是针对vue2的,Volar是针对vue3的关一个就行
1 2 import HelloWorld from './components/HelloWorld.vue'
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' 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' 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" ;const routes : Array <RouteRecordRaw > = [ { path : "/" , component : () => import ("../views/index.vue" ), }, { path : "/hello" , component : () => import ("../components/HelloWorld.vue" ), }, ]; const router = createRouter ({ history : createWebHistory (), routes, }); 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 所需要加入的依赖少得多
8、引入tailwindcss
见下面的tailwindcss
学习部分
9、引入axios
见下面的axios学习
学习部分
Pinia学习 Pinia使用 1、首先安装Pinia
2、更改main.ts
文件
1 2 3 4 5 6 7 8 9 import { createApp } from 'vue' import { createPinia } from 'pinia' import App from '@/App.vue' const app = createApp (App )app .use (createPinia ()) .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 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' ;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' , }, ] 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' 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' ;import { Type } from '../interfaces/Type' ;export const TypeStore = defineStore ('TypeStore' , { state : () => ({ typeInfo : [] as Type [], }), 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' ] }, { storage : localStorage , paths : ['accessToken' ] }, ], }, });
上面的是错的(😅)
首先是下载sudo npm i pinia-plugin-persistedstate
然后在main.ts
中引入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import { createPinia } from 'pinia' const pinia = createPinia ()import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' pinia.use (piniaPluginPersistedstate) const app = createApp (App )app .use (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的配置文件
这将会在您的项目根目录创建一个最小化的 tailwind.config.js
文件:
1 2 3 4 5 6 7 8 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' export default defineConfig ({ css : { postcss : { plugins : [tailwindcss, autoprefixer] } } })
使用postcss
的tailwindcss
和autoprefixer
插件对,css进行处理
6.配置vscode的代码提示
这个步骤vscode配过一次就行,其实配置到这里我已经完成对tailwind
的安装,但在模板中仍没有智能的提示,此时需要去settings.json
中,在末尾添加以下代码段:
1 2 3 "editor.quickSuggestions" : { "strings" : true }
基本属性
布局
- 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
文本样式
- 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
背景和边框
- bg: background-color
- border: border-style, border-width, border-color
- rounded: border-radius
- shadow: box-shadow
弹性盒子布局
- flex: display: flex
- justify: justify-content
- items: align-items
- self: align-self
- order: order
- flex-grow: flex-grow
- flex-shrink: flex-shrink
网格布局
- grid-cols: grid-template-columns
- grid-rows: grid-template-rows
- gap: grid-gap
响应式设计
- sm, md, lg, xl: 分别对应移动设备、平板、桌面、大屏幕
- hover: 鼠标悬停时的样式
- focus: 元素获取焦点时的样式
除了上面列举的 Tailwind CSS 缩写和对应含义之外,Tailwind CSS 还提供了很多其他的实用程序类,以下是一些常用的 Tailwind CSS 缩写和对应含义:
边框和分隔符
- 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: 设置边框是否合并
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 子元素)之间的空间
Z-index
- z-{n}: 设置 z-index 的值,其中 n 为正整数
动画
- animate-{name}: 向元素添加动画(使用 @keyframes 中定义的动画名称)
列表样式
- list-style-{type}: 设置列表项的类型 (disc, decimal, decimal-leading-zero)
转换和过渡
- transform: 让元素旋转、缩放、倾斜、平移等
- transition-{property}: 用于添加一个过度效果 {property} 的值是必需的。
颜色
- text-{color}: 设置文本颜色
- bg-{color}: 设置背景颜色
- border-{color}: 设置边框颜色
字体权重
- font-thin: 字体细
- font-light: 字体轻
- font-normal: 字体正常
- font-medium: 字体中等
- font-semibold: 字体半粗
- font-bold: 字体粗
- font-extrabold: 字体特粗
- font-black: 字体黑
SVG
- fill-{color}: 设置 SVG 填充颜色
- stroke-{color}: 设置 SVG 描边颜色
显示和隐藏
- hidden: 隐藏元素(display: none)
- invisible: 隐藏元素,但仍保留该元素的布局和尺寸
- visible: 显示元素
清除浮动
- clear-{direction}: 清除某个方向的浮动效果
容器
- 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 module .exports = { corePlugins : { preflight : false , } }
我们需要在项目中的 tailwind.config.js
中 corePlugins
部分设置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 const options = { yAxis : { type : 'value' , position : 'right' , }, };
桩状增加圆角
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const options = { series : [ { itemStyle : { normal : { barBorderRadius : [8 , 8 , 0 , 0 ] } } }, ], };
桩状改变颜色
1 2 3 4 5 6 7 8 9 const options = { series : [ { color : ['#5DB1FF' ], }, ], };
颠倒柱状图,x轴变为y轴,y轴变为x轴
调换一下两个轴的type
就行
1 2 3 4 5 6 7 8 9 const options = { xAxis : { type : 'value' , }, yAxis : { type : 'category' , }, };
默认柱状图或折线图过小的问题
参考文章:echarts图表的大小调整的解决方案 - 简书 (jianshu.com)
参数具体含义如图所示:
1 2 3 4 5 6 7 8 9 10 11 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 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 const options = { series : [ { type : 'line' , symbol : 'circle' , symbolSize : 6 , }, ], };
饼图:
集大成的效果参考:
控制图例的位置,摆放方向等各类属性
原文参考:echarts 饼图以及图例的位置及大小,环图中间字_echarts饼状图文字位置-CSDN博客
1 2 3 4 5 6 7 8 9 10 11 12 13 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 const options = { series : [ { name : 'Access From' , type : 'pie' , radius : ['40%' , '70%' ], center : ['30%' , '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 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下载
封装的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' import { ErrorResult} from '../interfaces/ErrorResult'; const URL: string = '' 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) { this.service = axios.create(config); this.service.interceptors.request.use( (config: any) => { const token = localStorage.getItem('token') || ''; return { ...config, headers: { 'Token': token, } } } , (error: AxiosError) => { Promise.reject(error) } ) this.service.interceptors.response.use( (response: AxiosResponse) => { const { data, config } = response; if (data.code === RequestEnums.OVERDUE) { localStorage.setItem('token', ''); return Promise.reject(data); } 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<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' 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 './' ;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' ); }; export const getTypeById = (objectId : string ) => { return axios.get <Type >('api/categories/objects/' + objectId); }
API使用 注意定义的时候需要加上Ref<Type[]>
,防止出现类型报错
以及async
和await
的使用
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 >
用于替换浏览器原生滚动条。
比起原生滚动条的优点是,在鼠标移入时才会展示滚动条,以及选定滚动的区域更简单
注意只要当元素高度超过最大高度,才会起作用
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 = 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 分页组件 ‘英文’ 修改为 ‘中文’(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/>
主要是改变颜色的方式不一样了,现在使用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-icon
和 active-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" />
目前内部的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 :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 @mixin select_radius { border-radius : 12px ; } :deep (.el-select__wrapper) { width : 360px ; height : 50px ; @include select_radius; } :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 .is-selected { color : var (--accent-200 ); }
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); }; 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使用 使用 shape
和 size
属性来设置 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" ;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' , 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来实现,
当使用 prop
和 emit
进行父子组件通信时,主要涉及两个概念: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 <template > <div > <LoginBox :ifShow ="isLoginBoxVisible" @updateIfShow ="updateIsLoginBoxVisible" /> </div > </template > <script setup lang ="ts" > import LoginBox from '../components/LoginBox.vue' const isLoginBoxVisible = ref (false );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 <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 let isDeleteVisible = ref (false );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>
效果如下:
进阶使用:
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
尺寸关键字代表了内容的最大宽度或最大高度。对于文本内容而言,这意味着内容即便溢出也不会被换行。
修改字体
在项目assets文件下新建 font 文件夹,并将下载下来的字体拖入其中0
在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基础 新建:new FormData()
添加:loginForm.append("username", username.value);
1 2 3 4 5 6 7 8 9 10 11 const handleLogin = async ( ) => { 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)
问题描述
解决关键:设置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 !’)
注意看负载
1 2 3 4 5 6 7 delete <T>(url : string , params?: object ): Promise <T> { return this .service .delete (url, { params }); } 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 const cartList = ref<Collection []>([])cartList.value =CartListCollection .collections ; let isDeleteVisible = cartList.value .map (() => ref (false ));const showDelete = (index: number ) => { isDeleteVisible.forEach ((item, i ) => (item.value = i === index)); }; 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
数组,导致同样的index
在isDeleteVisible
数组会超出范围
正确的方法为增加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 () => { await check ().then ((res ) => { userInfo.user = res }) userInfo.user !.favoriteCollection .forEach (async (item) => { await getCollectionById (item).then ((res ) => { FavoriteCollection .collections .push (res) }) }) let collectionItemsLength = FavoriteCollection .collections .length 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 () => { await check ().then ((res ) => { userInfo.user = res }) FavoriteCollection .collections = [] await Promise .all (userInfo.user !.favoriteCollection .map (async (item) => { const res = await getCollectionById (item); FavoriteCollection .collections .push (res); })); let collectionItemsLength = FavoriteCollection .collections .length 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.value
的false
和true
切换时间太短导致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 )const search = async ( ) => { loading.value = true 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.value
的false
和true
切换时间太短导致loading不出现
2、false
和true
搞翻了
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 ) { return new Promise ((resolve, reject ) => { cos.putObject ({ 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 , "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' ;
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; location / { root C:/wwwroot/42.192.90.134/NFT-Platform; } location /admin { alias C:/wwwroot/42.192.90.134/admin; } } include vhost/*.conf; }
上传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的位置上,这时候将app
的position
设置为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; }
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' import {ErrorResult } from '../interfaces/ErrorResult' ;const URL : string = '' 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 const fileData = ref<File | null >(null );const handleSave = async ( ) => { const formData = new FormData (); await axios.get ('在这里放需要获取的URL' , { responseType : 'blob' , headers :{ 'Access-Control-Allow-Origin' : '*' , } }) .then (response => { console .log (response.data ); const blob = response.data ; const filename = 'example.jpg' ; const file = new File ([blob], filename, { type : blob.type }); console .log (file); fileData.value = file; }) .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); }) .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' ;export async function getFileObject (imgUrl: string ): Promise <File | null > { const fileData = ref<File >(); return new Promise ((resolve, reject ) => { axios .get (imgUrl, { responseType : 'blob' , headers : { 'Access-Control-Allow-Origin' : '*' , }, }) .then (response => { const blob = response.data ; const filename = 'example.jpg' ; const file = new File ([blob], filename, { type : blob.type }); fileData.value = file; resolve (fileData.value ); }) .catch (error => { console .error ('获取文件时出错:' , error); reject (error); }); }); };
优化后使用方法 1 2 3 4 5 6 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 { const base64 = btoa (url); 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 import config from '../constant/config' ;import * as COS from 'cos-js-sdk-v5' ; const cos = new COS ({ SecretId : config.SECRET_ID , SecretKey : config.SECRET_KEY , }); const bucketConfig = { Bucket : 'tec-1312799453' , Region : 'ap-shanghai' , }; export async function getObjectUrl (path: string ) { return new Promise ((resolve, reject ) => { cos.getObjectUrl ( { Bucket : bucketConfig.Bucket , Region : bucketConfig.Region , Key : path, Sign : true , Expires : 3600 , }, function (err, data ) { data.Url = data.Url .replace ('https' , 'http' ); resolve (data.Url ); console .log (err || data.Url ); } ); }); }
使用部分:
1 2 3 4 5 6 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 import { SHA256 , HmacSHA256 , enc } from 'crypto-js' ;interface V3Options { SECRET_ID : string ; SECRET_KEY : string ; action : string ; host : string ; service : string ; region : string ; version : string ; ContentType : string ; } interface V3RequestPayload { [key : string ]: any ; } function sha256 (message: string , secret: string = '' , hex: boolean = false ) { const hmac = HmacSHA256 (message, secret); let hexHmac = hmac; if (hex) { hexHmac = hmac.toString (enc.Hex ) as any ; } return hexHmac; } function getHash (message: string ) { const hash = SHA256 (message); const hexHash = hash.toString (enc.Hex ); return hexHash; } function getDate (timestamp: number ) { const date = new Date (timestamp * 1000 ); const year = date.getUTCFullYear (); const month = ('0' + (date.getUTCMonth () + 1 )).slice (-2 ); const day = ('0' + date.getUTCDate ()).slice (-2 ); return `${year} -${month} -${day} ` ; } 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); const hashedRequestPayload = getHash (payload); const httpRequestMethod = "POST" ; const canonicalUri = "/" ; 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); 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} ` ; 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, 'X-TC-Action' : 'TextToImage' , '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 export default { SECRET_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" let prompt = ref ('' )let negativePrompt = ref ('' )let loading = ref (false );let category = ref ("" );const uploadedImage = ref<string | null >(null );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 ] }; 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); }) } 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 const fileData = ref<File | null >(null );const handleSave = async ( ) => { const formData = new FormData (); await axios.get ('在这里放需要获取的URL' , { responseType : 'blob' , headers :{ 'Access-Control-Allow-Origin' : '*' , } }) .then (response => { console .log (response.data ); const blob = response.data ; const filename = 'example.jpg' ; const file = new File ([blob], filename, { type : blob.type }); console .log (file); fileData.value = file; }) .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); }) .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 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 import config from '../constant/config' ;import * as COS from 'cos-js-sdk-v5' ; import { RecognitionResult } from '../interfaces/RecognitionResult' ;const cos = new COS ({ SecretId : config.SECRET_ID , SecretKey : config.SECRET_KEY , }); const bucketConfig = { Bucket : 'tec-1312799453' , Region : 'ap-shanghai' , }; export async function getImageAuditing (detectUrl: string ) { 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' , 'detect-url' : detectUrl, }, }, 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 ?: { }[]; LibResults ?: { }[]; }; AdsInfo ?: { Code : number ; Msg : string ; HitFlag : number ; Score : number ; Label : string ; Category : string ; SubLabel : string ; OcrResults ?: { }[]; LibResults ?: { }[]; }; QualityInfo ?: { Code : number ; Msg : string ; HitFlag : number ; Score : number ; Label : string ; Category : string ; SubLabel : string ; OcrResults ?: { }[]; LibResults ?: { }[]; }; }
使用部分 需要注意的是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 ) { 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 import config from '../constant/config' ;import * as COS from 'cos-js-sdk-v5' ; import { PicOperation } from '../interfaces/PicOperation' ;const cos = new COS ({ SecretId : config.SECRET_ID , SecretKey : config.SECRET_KEY , }); const bucketConfig = { Bucket : 'tec-1312799453' , Region : 'ap-shanghai' , }; export async function addWatermark (path: string , fileObject: File, picOperations: string ) { 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 : { '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} ` , }, ], }; 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` , }, ], }; picOperations.value = JSON .stringify (picOperations.value ); console .log ("picOperations.value:" , picOperations.value );await addWatermark (path.value , fileInput.files [0 ], picOperations.value ).then ((res: WatermarkResult ) => { }).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 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/ , "" ), 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 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; } 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 ; }
完整代码:
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 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; 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 ; }
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 git remote add origin 远程仓库URL
2、重命名分支
注意这个时候别提交,应该先更改分支的名字,因为大部分远程仓库主分支名为master
,但是本地仓库默认为main
为主分支,这时候可以使用vscode里面的Rename Branch...
更改名字为master
,同等效果的git命令我也放到下面了
3、提交到远程仓库
这里要注意如果远程仓库本来就已经有代码时哦,要先拉取代码
然后再进行提交,这里直接使用vscode的提交就行,当然相关命令我也放在下面,防止忘记
1 2 git commit -m "提交消息" git push origin master
Git提交信息规范
提交信息结构:
提交信息分为类型、可选的范围和描述三个部分。
示例:feat(user-auth): 增加密码重置功能
类型(Type):
feat
: 新功能
fix
: 修复问题
docs
: 文档变更
style
: 代码格式、空格等非逻辑性的变更
refactor
: 重构代码,既不是新增功能也不是修复bug
test
: 添加或修改测试
chore
: 构建过程或辅助工具的变更
可选的范围(Scope):
描述变更影响的范围,可以是模块、组件、功能等。例如:(user-auth)
。
描述(Description):
提供详细的变更描述,解释为什么做出这个变更以及变更的具体内容。
提交信息长度:
保持简短,最好不超过50个字符。如果需要更详细的描述,可以使用空行后添加更多信息。
动词使用:
使用动词的现在时来描述变更,例如:增加
、修复
、更新
等。
参考任务或问题编号:
如果有相关的任务、问题或需求编号,可以在提交信息中引用,以便于跟踪。例如:修复 #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 /> </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 > <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 > <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 > 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就行
其他 Copilot使用 ![截屏2024-01-24 10.35.54](/Users/tec/Library/Application Support/typora-user-images/截屏2024-01-24 10.35.54.png)