Vue Cli+Vue3项目使用v-bind动态控制img的src属性值踩坑记录
之前自己写项目都是用Vite+Vue3,突然接了个需要维护的项目使用Vue Cli+Vue3构建的,因为本人之前很少接触webpack流,所以对其特性不是很了解,导致踩了一些坑。
- 踩坑记录-webpack项目img标签:src=“urlPath“拿不到图片
- 吐槽:都2025年了怎么还有项目没用Vite呢,简直比webpack流好用几十倍啊,不论是启动还是热更新都遥遥领先。
回到正题
踩坑点:使用
<img :src="imgs.img">居然无法正常获取到图片。不使用v-bing绑定正常写入imgs.img的值时又可以正常显示图片。分析:因为个人实在不想去了解过时的框架特性,在C站上简单搜了下相关案例,发现是脚手架的机制问题,因为在Vite中就不会出现这种问题,于是只能通过require动态导入资源。
案例代码
<template><!-- 一个3行2列的虚拟表格 --><divclass="table_box"><divv-for="item in data":key="item.key"class="col"><img:src="item.img"alt=""class="col_left_icon"><divclass="col_name">{{ item.label }}</div><divclass="col_num">{{ item.num + item.unit }}</div></div></div></template><scriptsetuplang="ts">import{ref}from'vue'constdata=ref([{img:require('../../assets/img/ck-icon1.png'),key:'key1',label:'标签1',num:0,unit:'个'},{img:require('../../assets/img/ck-icon2.png'),key:'key2',label:'标签2',num:0,unit:'个'},{img:require('../../assets/img/ck-icon3.png'),key:'key3',label:'标签3',num:0,unit:'条'},// ... 共6条,再次吐槽,如果是Vite构建的项目,此处无需使用require动态导入资源,直接写相对路径即可。])</script><stylelang="scss"scoped>.table_box { height: 100%; width: 100%; display: flex; flex-wrap: wrap; //这个属性可以让弹性盒在适当的时候(一般是宽度达到100%)自动换行 justify-content: space-between; //结合上一条属性,可以实现类似表格的效果 margin-top: 0.5vh; .col { height: 29%; width: 47%; margin: 0.4vh 0.5vh; display: flex; align-items: center; text-align: left; .col_left_icon { height: 60%; } .col_name { width: 38%; margin-left: 0.5vh; color: #fff; font-size: 0.7vw; white-space: nowrap; //强制不换行 } .col_num { width: 38%; margin-right: 0.5vh; color: aqua; font-size: 1vw; font-weight: 500; text-align: right; } } }使用FastApi+PostgreSQL开发一个web服务端
主要使用技术
fastapi创建web服务器psycopg2连接数据库工具
第三方库安装
pip install fastapi搭建服务器pip install fastapi_cdn_host接口文档本地化部署pip install psycopg2连接pg数据库用pip install uvicorn启动服务器
源码
importosimporttimeimportjsonimportfastapi_cdn_host# 自动文档cdnimportconfigparser# 读取配置importpsycopg2# pg库工具fromdatetimeimportdatetime# 系统时间fromfastapiimportFastAPI,HTTPExceptionfrompydanticimportBaseModel,ConfigDictfromcontextlibimportasynccontextmanager# 创建一个读取配置文件的对象config=configparser.ConfigParser()config.read('config.ini')# 获取配置信息db_host=config.get('pgdb','host')db_name=config.get('pgdb','database')db_user=config.get('pgdb','user')db_pass=config.get('pgdb','password')db_port=config.get('pgdb','port')# 创建连接对象conn=psycopg2.connect(database=db_name,user=db_user,password=db_pass,host=db_host,port=db_port)# print('Connected to PostgreSQL')cur=conn.cursor()# 获取游标# 使用asynccontextmanager装饰器来定义一个异步上下文管理器,这将作为你的生命周期事件处理器@asynccontextmanagerasyncdeflifespan(app:FastAPI):# 应用启动时执行的代码print(f'FastAPI server started:{datetime.now()}')yield# 应用关闭时执行的代码,用于清理未关闭连接conn.close()print(f'FastAPI server stopped:{datetime.now()}')# 创建API接口app=FastAPI(lifespan=lifespan)# 引入本地cdn资源fastapi_cdn_host.patch_docs(app)# 参数类classQuery(BaseModel):# 借助 Pydantic 的BaseModel方法来使用标准的 Python 类型声明请求体sql:str# sql语句传参,前端提供详细语句,后端负责连接数据库执行dataSource:str# 数据源传参,根据原始表的所属租户填写@app.post("/pyapi/test/openQuery")deftest_open_query(query:Query):try:ifquery.dataSourcenotNone:print(query.dataSource,query.toJson)cur.execute(query.sql)return{'status':200,'msg':'Operations Success!','data':cur.fetchall()}exceptExceptionase:raiseHTTPException(status_code=500,detail=e)classAnyParam(BaseModel):model_config=ConfigDict(extra='allow')# 允许接收额外的字段@app.post("/pyapi/test/pushPageVisits")deftest_push_page_visits(param:AnyParam):try:# 执行数据库插入数据操作cur.execute('INSERT INTO "public"."t_a_test_visits" ("username", "employeeId", "pageName", "pageRoute", "startTime", "endTime", "loginId") VALUES (%s, %s, %s, %s, %s, %s, %s)',(param.username,param.employeeId,param.pageName,param.pageRoute,param.startTime,param.endTime,param.loginId))conn.commit()# 提交事务return{'status':200,'msg':'Operations Success!'}exceptExceptionase:conn.rollback()# 回滚事务raiseHTTPException(status_code=500,detail=e)@app.post("/pyapi/test/getPageVisits")deftest_get_page_visits(param:AnyParam):# 执行数据库查询操作try:cur.execute('SELECT * FROM "public"."t_a_test_visits" WHERE username = coalesce(%s, username)',(param.username,))return{'status':200,'msg':'Operations Success!','data':cur.fetchall()}exceptExceptionase:raiseHTTPException(status_code=500,detail=e)# 启动服务器if__name__=="__main__":os.system('chcp & cls')importuvicorn uvicorn.run("main:app",host="127.0.0.1",port=8000,reload=True)# reload在开发时可设置为True,在修改代码后会自动重启项目,但打包时必须设置为False,否则会导致应用被重复启动的问题原生video元素自定义控件
背景:
项目需要对video元素做一些样式定义,原生的很难修改,于是借助video标签的事件进行自定义控件改造。
本文内容基于网上大神分享的vue2方案,改写为适配vue3的方案。原文链接:掘金_原生video元素自定义播放控件controls
开发环境:
- Vue3 3.2+
- Nodejs 18+
- ElementPlus 2.3+
基础功能:
- 播放、暂停按钮
- 当前播放时长/视频总时长
- 进度条(点击或拖动滑块可跳转进度并续播)
- 音量控制
- 控件显隐
源码:
<template><divclass="customer_video_wrap"@mouseleave="hideControls"@mouseover="showControls"><videoref="videoPlayer"src="http://vjs.zencdn.net/v/oceans.mp4"width="100%":class="['answerVideo', 'isMirror']"preload="auto"@timeupdate="updateProgressBar"@loadedmetadata="getDuration"@ended="onVideoEnded"@click="togglePlay"></video><!-- 自定义视频控件 --><transitionname="el-fade-in-linear"><divclass="video_control"v-show="isShowingControls"style="transition:opacity .5s;"><divclass="video_control_top"><div><el-iconv-if="isPlaying"@click="togglePlay"><VideoPause/></el-icon><el-iconv-else@click="togglePlay"><VideoPlay/></el-icon><spanclass="video_time">{{ formatTime(currentTime) }}<span>/ {{ formatTime(duration) }}</span></span></div><spanclass="video_volume_wrap"><el-sliderclass="video_volume"v-model="volume"@change="changeVolume"></el-slider>音量</span></div><el-sliderref="progressSlider"class="video_control_progress"v-model="currentTime":show-tooltip="false":max="duration"@mousedown="handleDragStart"@touchstart="handleDragStart"@change="changeProgress"></el-slider></div></transition></div></template><scriptsetup>import{VideoPause,VideoPlay}from'@element-plus/icons-vue'import{ref}from'vue'// 自定义视频控件功能逻辑constisPlaying=ref(false)constcurrentTime=ref(0)constduration=ref(1)constvolume=ref(30)constisShowingControls=ref(true)constisDraging=ref(false)constvideoPlayer=ref(null)letvideo/** 视频播放/暂停 */consttogglePlay=()=>{video=videoPlayer.valueif(video.paused){video.play()isPlaying.value=true}else{video.pause()isPlaying.value=false}}/** 视频播放结束 */constonVideoEnded=()=>{isPlaying.value=false}/** 获取视频时长 */constgetDuration=()=>{video=videoPlayer.valueif(video){duration.value=Number.isFinite(video.duration)?video.duration:5}}/** 更新进度条 */constupdateProgressBar=()=>{getDuration()if(isDraging.value){return}if(video){currentTime.value=video.currentTime}}/** 拖拽进度条结束时 */constchangeProgress=(value)=>{isDraging.value=falsevideo.currentTime=value video.play()isPlaying.value=true}/** 拖拽进度条开始与进行中时 */consthandleDragStart=()=>{isDraging.value=truevideo.pause()}/** 音量变化 */constchangeVolume=(value)=>{video.volume=value/100}/** 格式化时间 */constformatTime=(seconds)=>{constformat=val=>`0${Math.floor(val)}`.slice(-2)consthours=seconds/3600constminutes=(seconds%3600)/60seconds=seconds%60if(hours<1){return[minutes,seconds].map(format).join(":")}return[hours,minutes,seconds].map(format).join(":")}/** 显示控制组件 */constshowControls=()=>{isShowingControls.value=true}/** 隐藏控制组件 */consthideControls=()=>{isShowingControls.value=false}</script><stylescopedlang="less">.customer_video_wrap{position:relative;width:100%;}.answerVideo{z-index:1;}video.isMirror{transform:scale(-1,1);}// 自定义视频控件 .video_control{position:absolute;bottom:0;left:0;right:0;padding:8px 20px;background:linear-gradient(0deg,#000000 5%,#00000073,transparent);.video_control_top{display:flex;justify-content:space-between;align-items:center;color:#fff;i{font-size:20px;color:#fff;}.video_time{transform:translateY(-2px);display:inline-block;margin-left:8px;}.video_volume_wrap{display:flex;align-items:center;font-size:13px;}.video_volume{width:70px;margin-right:8px;}}:deep(.el-slider){.el-slider__runway{background-color:#cbcbcb;height:4px;margin:4px 0;}.el-slider__bar{height:4px;background-color:#fff;}.el-slider__button-wrapper{top:-16px;}.el-slider__button{width:10px;height:10px;border:none;}}}</style>