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列的虚拟表格 -->
<div class="table_box">
<div v-for="item in data" :key="item.key" class="col">
<img :src="item.img" alt="" class="col_left_icon">
<div class="col_name">{{ item.label }}</div>
<div class="col_num">{{ item.num + item.unit }}</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const data = 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>
<style lang="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 启动服务器
源码
import os
import time
import json
import fastapi_cdn_host # 自动文档cdn
import configparser # 读取配置
import psycopg2 # pg库工具
from datetime import datetime # 系统时间
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, ConfigDict
from contextlib import asynccontextmanager
# 创建一个读取配置文件的对象
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装饰器来定义一个异步上下文管理器,这将作为你的生命周期事件处理器
@asynccontextmanager
async def lifespan(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)
# 参数类
class Query(BaseModel): # 借助 Pydantic 的BaseModel方法来使用标准的 Python 类型声明请求体
sql: str # sql语句传参,前端提供详细语句,后端负责连接数据库执行
dataSource: str # 数据源传参,根据原始表的所属租户填写
@app.post("/pyapi/test/openQuery")
def test_open_query(query: Query):
try:
if query.dataSource not None:
print(query.dataSource, query.toJson)
cur.execute(query.sql)
return {'status': 200, 'msg': 'Operations Success!', 'data': cur.fetchall()}
except Exception as e:
raise HTTPException(status_code=500, detail=e)
class AnyParam(BaseModel):
model_config = ConfigDict(extra='allow') # 允许接收额外的字段
@app.post("/pyapi/test/pushPageVisits")
def test_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!'}
except Exception as e:
conn.rollback() # 回滚事务
raise HTTPException(status_code=500, detail=e)
@app.post("/pyapi/test/getPageVisits")
def test_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()}
except Exception as e:
raise HTTPException(status_code=500, detail=e)
# 启动服务器
if __name__ == "__main__":
os.system('chcp & cls')
import uvicorn
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>
<div class="customer_video_wrap" @mouseleave="hideControls" @mouseover="showControls">
<video ref="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>
<!-- 自定义视频控件 -->
<transition name="el-fade-in-linear">
<div class="video_control" v-show="isShowingControls" style="transition: opacity .5s;">
<div class="video_control_top">
<div>
<el-icon v-if="isPlaying" @click="togglePlay">
<VideoPause />
</el-icon>
<el-icon v-else @click="togglePlay">
<VideoPlay />
</el-icon>
<span class="video_time">
{{ formatTime(currentTime) }}<span>/ {{ formatTime(duration) }}</span>
</span>
</div>
<span class="video_volume_wrap">
<el-slider class="video_volume" v-model="volume" @change="changeVolume"></el-slider>
音量
</span>
</div>
<el-slider ref="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>
<script setup>
import { VideoPause, VideoPlay } from '@element-plus/icons-vue'
import { ref } from 'vue'
// 自定义视频控件功能逻辑
const isPlaying = ref(false)
const currentTime = ref(0)
const duration = ref(1)
const volume = ref(30)
const isShowingControls = ref(true)
const isDraging = ref(false)
const videoPlayer = ref(null)
let video
/** 视频播放/暂停 */
const togglePlay = () => {
video = videoPlayer.value
if (video.paused) {
video.play()
isPlaying.value = true
} else {
video.pause()
isPlaying.value = false
}
}
/** 视频播放结束 */
const onVideoEnded = () => {
isPlaying.value = false
}
/** 获取视频时长 */
const getDuration = () => {
video = videoPlayer.value
if (video) {
duration.value = Number.isFinite(video.duration) ? video.duration : 5
}
}
/** 更新进度条 */
const updateProgressBar = () => {
getDuration()
if (isDraging.value) {
return
}
if (video) {
currentTime.value = video.currentTime
}
}
/** 拖拽进度条结束时 */
const changeProgress = (value) => {
isDraging.value = false
video.currentTime = value
video.play()
isPlaying.value = true
}
/** 拖拽进度条开始与进行中时 */
const handleDragStart = () => {
isDraging.value = true
video.pause()
}
/** 音量变化 */
const changeVolume = (value) => {
video.volume = value / 100
}
/** 格式化时间 */
const formatTime = (seconds) => {
const format = val => `0${Math.floor(val)}`.slice(-2)
const hours = seconds / 3600
const minutes = (seconds % 3600) / 60
seconds = seconds % 60
if (hours < 1) {
return [minutes, seconds].map(format).join(":")
}
return [hours, minutes, seconds].map(format).join(":")
}
/** 显示控制组件 */
const showControls = () => {
isShowingControls.value = true
}
/** 隐藏控制组件 */
const hideControls = () => {
isShowingControls.value = false
}
</script>
<style scoped lang="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>
5888



被折叠的 条评论
为什么被折叠?



