工作中的使用
浏览器兼容性问题
Autoprefixer自动添加
- 前置依赖安装
npm install -D css-loader style-loader - 安装postcss依赖相关依赖
npm install -D postcss-loader autoprefixer postcss - 在webpack.config.js中进行配置
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: ["style-loader", "css-loader", "postcss-loader"]
}
]
}
}
- 配置
autoprefixer
- 方式一:在postcss.config.js进行配置
module.exports = {
plugins: {
// 兼容浏览器,添加前缀
'autoprefixer':{
overrideBrowserslist: [
"Android 4.1",
"iOS 7.1",
"Chrome > 31",
"ff > 31",
"ie >= 8"
//'last 10 versions', // 所有主流浏览器最近2个版本
],
grid: true
}
}
}
- 方式二:在postcss.config.js进行配置
module.exports = {
plugins: [
// 兼容浏览器,添加前缀
require('autoprefixer')({
overrideBrowserslist: [
"Android 4.1",
"iOS 7.1",
"Chrome > 31",
"ff > 31",
"ie >= 8"
//'last 2 versions', // 所有主流浏览器最近2个版本
],grid: true})
]
}
小程序图片转Background_base64
const Background_base64 = (path) => {
return 'data:image/jpg;base64,' + wx.getFileSystemManager().readFileSync(path, 'base64');
}
响应式适配问题
- 使用responsively软件查询效果 响应式应用程序 - Web 开发人员的浏览器
- 使用媒体查询(Media Queries)使用媒体查询 - CSS:层叠样式表 | MDN
body {
background-color:#fff;
}
// max-height,max-width,min-height,min-width
@media screen and (max-width: 500px) {
// 视口小于500px时背景色变红色
body {
background-color:red;
}
}
- 使用弹性布局(Flexbox)或栅格系统(如 Bootstrap、Tailwind)。
.box{
display: flex;//容器都指定为Flex布局。
flex-direction: row | row-reverse | column | column-reverse;//flex-direction属性决定主轴的方向
- row(默认值):主轴为水平方向,起点在左端。
- row-reverse:主轴为水平方向,起点在右端。
- column:主轴为垂直方向,起点在上沿。
- column-reverse:主轴为垂直方向,起点在下沿。
flex-wrap: nowrap | wrap | wrap-reverse; //如果一条轴线排不下,如何换行
- nowrap(默认):不换行。
- wrap:换行,第一行在上方。
- wrap-reverse:换行,第一行在下方。
justify-content: flex-start | flex-end | center | space-between | space-around; //主轴上的对齐方式。
- flex-start(默认值):左对齐
- flex-end:右对齐
- center: 居中
- space-between:两端对齐,项目之间的间隔都相等。
- space-around:每个项目两侧的间隔相等。所以,项目之间的间隔比项目与边框的间隔大一倍。
align-items: flex-start | flex-end | center | baseline | stretch; //交叉轴上如何对齐
- flex-start:交叉轴的起点对齐。
- flex-end:交叉轴的终点对齐。
- center:交叉轴的中点对齐。
- baseline: 项目的第一行文字的基线对齐。
- stretch(默认值):如果项目未设置高度或设为auto,将占满整个容器的高度。
}
- 尽量使用相对单位(如 %, vw, rem)代替绝对单位(px)
- Viewport(视口)
- 是用户在网页上能够看到的区域。而viewport meta标记则是HTML中用于控制视口的设置。它位于
<head>标签内部,通过它我们可以告诉浏览器如何对页面进行缩放和布局,从而适配不同的设备屏幕。
-
viewport有以下几个属性:
-
width:视口的宽度,正整数或设备宽度device-width(width=device-width)
-
height:视口高度,正整数或device-height
-
initial-scale(initial-scale=1.0):网页初始缩放值,小数缩小,反之放大(initial-scale=1.0)
-
maximum-scale(maximum-scale=1.0):设置页面的最大缩放比例
-
minimum-scale(minimum-scale=1.0): 设置页面的最小缩放比例
-
user-scaleble(user-scalable=no):用户是否可以缩放 ————————————————
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:blog.csdn.net/djemx2025/a…
<meta name="viewport" content="width=device - width, initial - scale = 1.0, maximum - scale = 1.5, minimum - scale = 0.8, user - scalable = yes">
这样的设置可以确保网页在加载时以设备屏幕宽度显示,初始不缩放,用户可以在一定范围内缩放(最大1.5倍,最小0.8倍),从而在保证页面布局基本稳定的同时,也给用户一定的操作灵活性,方便用户查看商品的细节图片或者文字描述等内容。 viewport meta标记及其属性的合理使用能够大大提升用户在移动设备上浏览网页的体验,是每个前端开发者都需要熟练掌握的技能
- 安装
npm install v-scale-screen
# or
yarn add v-scale-screen
- 在 vue2 中我们使用插件方式导出,故而需要 Vue.use() 进行注册
// main.js
import Vue from 'vue'
import VScaleScreen from 'v-scale-screen'
Vue.use(VScaleScreen)
<template>
<v-scale-screen width="1920" height="1080">
<div>
<v-chart>....</v-chart>
<v-chart>....</v-chart>
<v-chart>....</v-chart>
<v-chart>....</v-chart>
<v-chart>....</v-chart>
</div>
</v-scale-screen>
</template>
- Vue3 or Vue2.7 版本
<template>
<v-scale-screen width="1920" height="1080">
<div>
<v-chart>....</v-chart>
<v-chart>....</v-chart>
<v-chart>....</v-chart>
<v-chart>....</v-chart>
<v-chart>....</v-chart>
</div>
</v-scale-screen>
</template>
<script>
import { defineComponent } from 'vue'
import VScaleScreen from 'v-scale-screen'
export default defineComponent({
name: 'Demo',
components: {
VScaleScreen
}
})
</script>
页面加载慢 / 首屏加载慢
- 资源优化
图片优化
- 使用现代图片格式:
WebP(比 JPG/PNG 更小)- 压缩图片:TinyPNG、ImageOptim、Webpack 插件(如
image-webpack-loader)- 图片懒加载:
<img loading="lazy">- 合理尺寸裁剪:避免加载超大图
JS/CSS 优化
- 使用 Tree Shaking 移除无用代码
- 拆包分包(Code Splitting):React 使用
React.lazy+Suspense- 开启 gzip / Brotli 压缩(后端配置或 Vite/Webpack 插件)
- 删除未使用的 CSS(PurgeCSS、Tailwind 的 tree-shaking)
- 使用模块化引入(如 lodash 按需加载)
- 加载策略优化
HTML 和资源加载顺序
- 使用
defer加载非关键 JS(避免阻塞 DOM 解析)- CSS 放
<head>、JS 放<body>底部或加defer- 尽量减少
<head>中的同步 JS/CSS 链接
懒加载(Lazy Load)
- 图片、视频、iframe 不在首屏区域就不加载
- 路由级代码分割(比如 React/Vue 的懒加载组件)
- 使用骨架屏提升感知速度
<template>
<div v-if="loading">
<div class="skeleton"></div>
</div>
<div v-else>
<RealContent />
</div>
</template>
// 成的 UI 库组件:如 Element Plus 的 `<el-skeleton />`
- 缓存与 CDN 优化
- 使用
localStorage/sessionStorage缓存部分数据 - 配置 HTTP 缓存头
- 将
js/css/font/image资源部署到 CDN 上,加快分发
前端实现签字效果+合同展示
安装
npm install signature_pad
<template>
<div class="contract-signature">
<!-- 合同展示区域 -->
<div class="contract-preview">
<!-- 方式一:PDF展示 -->
<!-- <pdf src="合同链接" /> -->
<!-- 方式二:HTML 富文本 -->
<div class="contract-content">
<div v-html="contractHtml"></div>
<img :src="Url" alt="" style="height: 50px;">
</div>
</div>
<!-- 签名区域 -->
<div class="signature-area">
<canvas ref="signatureCanvas" width="400" height="200" class="signature-canvas"></canvas>
<div class="btns">
<button @click="clearSignature">清除</button>
<button @click="submitSignature">提交签名</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import SignaturePad from 'signature_pad'
const contractHtml = ref('<h3>电子合同</h3><p>这是合同正文内容……</p>') // 替换为实际合同内容
const signaturePad = ref(null) // 用于保存 SignaturePad 实例
const signatureCanvas = ref(null) // 签名画布
let Url = ref('')
onMounted(() => {
signaturePad.value = new SignaturePad(signatureCanvas.value)
})
function clearSignature() {
signaturePad.value.clear()
}
function submitSignature() {
if (signaturePad.value.isEmpty()) {
alert('请先签名')
return
}
const dataUrl = signaturePad.value.toDataURL()
console.log('签名Base64:', dataUrl)
Url.value = dataUrl
// 可上传 dataUrl 到后端保存
}
</script>
<style scoped>
.contract-signature {
display: flex;
flex-direction: column;
gap: 20px;
}
.contract-content {
border: 1px solid #ccc;
padding: 20px;
max-height: 400px;
overflow-y: auto;
}
.signature-area {
border: 1px dashed #666;
padding: 10px;
width: fit-content;
}
.signature-canvas {
border: 1px solid #000;
background: #fff;
}
.btns {
margin-top: 10px;
display: flex;
gap: 10px;
}
</style>
页面刷新,让用户回到上次阅读的位置
子组件 ScrollMemory.vue
<template>
<!-- 如果使用滚动容器,就让 slot 内容渲染在容器里 -->
<div
v-if="useContainer"
ref="scrollRef"
class="scroll-container"
:style="{ maxHeight, overflowY: 'auto' }"
>
<slot />
</div>
<!-- 否则只处理 window 滚动,不包裹内容 -->
<slot v-else />
</template>
<script setup>
import { onMounted, onBeforeUnmount, ref } from 'vue'
const props = defineProps({
useContainer: { type: Boolean, default: false }, // 是否使用滚动容器
storageKey: { type: String, default: 'scroll-memory' }, // 存储的 key
maxHeight: { type: String, default: '500px' } // 容器最大高度
})
const scrollRef = ref(null)
function saveScroll(y) {
sessionStorage.setItem(props.storageKey, y.toString())
}
function restoreScroll(y) {
if (props.useContainer && scrollRef.value) {
scrollRef.value.scrollTop = y
} else {
window.scrollTo(0, y)
}
}
onMounted(() => {
const saved = sessionStorage.getItem(props.storageKey)
if (saved) {
restoreScroll(parseInt(saved))
}
if (props.useContainer && scrollRef.value) {
scrollRef.value.addEventListener('scroll', handleScroll)
} else {
window.addEventListener('scroll', handleScroll)
window.addEventListener('beforeunload', saveOnUnload)
}
})
onBeforeUnmount(() => {
if (props.useContainer && scrollRef.value) {
scrollRef.value.removeEventListener('scroll', handleScroll)
} else {
window.removeEventListener('scroll', handleScroll)
window.removeEventListener('beforeunload', saveOnUnload)
}
})
function handleScroll() {
const y = props.useContainer && scrollRef.value
? scrollRef.value.scrollTop
: window.scrollY
saveScroll(y)
}
function saveOnUnload() {
const y = window.scrollY
saveScroll(y)
}
</script>
<style scoped>
.scroll-container {
border: 1px solid #ccc;
}
</style>
父组件 index.vue
<template>
<ScrollMemory storageKey="contract-001" useContainer>
<div class="contract-content">
<!-- 合同内容在这里 -->
</div>
</ScrollMemory>
</template>
<script setup>
</script>
设置全局样式
*{
margin:0;
padding:0;
}
body{
font-size:12px;
color:#000;
}
a{
color:#333;
text-decoration: none;
}
ul,li {
list-style-type:none;
}
获取时间戳
new Date().getTime() 可以获取当前时间的时间戳
console.log(new Date().getTime()); // 1696871204353
console.log(+new Date()); // 1696871204353
秒转时分秒
时间格式化可以使用day.js进行处理(Day.js中文网)
// 秒数格式为 HH:mm:ss 的格式。
const formatSeconds = (s) => {
return [parseInt(s / 60 / 60), parseInt((s / 60) % 60), parseInt(s % 60)]
.map(num => num.toString().padStart(2, '0')) // 补0 padStart() 方法用另一个字符串填充当前字符串(如果需要的话,会重复多次),直到当前字符串达到给定的长度。
.join(':'); // 拼接 join() 方法用于把数组中的所有元素放入一个字符串。
}
console.log('time', formatSeconds(60));// 00:01:00
Day.js的简单使用
- 安装 npm install dayjs
- 引入 var dayjs = require('dayjs') // import dayjs from 'dayjs' // ES 2015
- 使用 dayjs().format()
// 格式化
var dayjs = require('dayjs')
console.log(dayjs('2019-01-2 8:30:30').format('DD/MM/YYYY HH小时mm分钟ss秒')) // 02/01/2019 08小时30分钟30秒
// 返回现在到当前实例的相对时间。
// 相对当前时间(前)
console.log(dayjs('2025-3-20').fromNow()); //12 天前
// 相对指定时间(前)
console.log(dayjs('2024-3-20').from(dayjs('2025-3-20'))); // 1 年前
//获取月天数
console.log(dayjs('2025-02-25').daysInMonth()); //28
取整的小技巧
~~ 双取反
console.log(~~4.9); // 4
console.log(~~-4.9); // -4
按位或 | 0
console.log(4.9 | 0); // 4
console.log(-4.9 | 0); // -4
Math.trunc()
console.log(Math.trunc(4.9)); // 4
console.log(Math.trunc(-4.9)); // -4
常见的校验
身份证的校验
const sfzReg = /^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/
if (sfzReg.test(value)) { //匹配成功返回
console.log('校验成功返回');
}
手机号校验
const checkPhone = /^[1][3,4,5,6,7,8,9][0-9]{9}$/;
if (checkPhone.test(value)) { //匹配成功返回
console.log('校验成功返回');
}
密码
const checkPassword = /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{6,20}$/;;//密码(6-20位,包含字母和数字)
if (checkPassword.test(value)) { //匹配成功返回
console.log('校验成功返回');
}
密码强度的校验
const passwordReg = /(?=.*[0-9])(?=.*[a-zA-Z])(?=.*[^a-zA-Z0-9]).{8,30}/
const password1 = 'sunshine_Lin12345..'
console.log(passwordReg.test(password1)) // true
const password2 = 'sunshineLin12345'
console.log(passwordReg.test(password2)) // false
邮箱的校验
const emailReg = /^([A-Za-z0-9_\-\.])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/
if (emailReg.test(value)) { //匹配成功返回
console.log('校验成功返回');
}
字符串长度n的校验
function checkStrLength(n) {
return new RegExp(`^.{${n}}$`)
}
// 校验长度为3的字符串
const lengthReg = checkStrLength(3)
const str1 = 'hhh'
console.log(lengthReg.test(str1)) // true
const str2 = 'hhhhh'
console.log(lengthReg.test(str2)) // false
文件拓展名的校验
function checkFileName (arr) {
arr = arr.map(name => `.${name}`).join('|')
return new RegExp(`(${arr})$`)
}
const filenameReg = checkFileName(['jpg', 'png', 'txt'])
const filename1 = 'sunshine.jpg'
console.log(filenameReg.test(filename1)) // true
const filename2 = 'sunshine.png'
console.log(filenameReg.test(filename2)) // true
const filename3 = 'sunshine.txt'
console.log(filenameReg.test(filename3)) // true
const filename4 = 'sunshine.md'
console.log(filenameReg.test(filename4)) // false
URL的校验
const urlReg = /^((https?|ftp|file):\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/
if (urlReg.test(value)) { //匹配成功返回
console.log('校验成功返回');
}
整数的校验
const intReg = /^[-+]?\d*$/
if (intReg.test(value)) { //匹配成功返回
console.log('校验成功返回');
}
小数的校验
const floatReg = /^[-\+]?\d+(\.\d+)?$/
const floatNum = 1234.5
console.log(floatReg.test(floatNum)) // true
保留两位小数(不四舍五入)
let num = 100;
console.log(num.toFixed(3).slice(0, -1)); //100.00
页面查看PDF文件
- 使用iframe
- vue-pdf 官方文档:较为完善的 vue-PDF 解决方案。
使用iframe查看PDF
<iframe style="width: 100%; height: 100%;" src="/static/test1.pdf"></iframe>
使用vue-PDF查看PDF
安装 npm install --save vue-pdf
<template>
<div id="container">
<!-- 上一页、下一页 -->
<div class="right-btn">
<!-- 输入页码 -->
<div class="pageNum">
<input v-model.number="currentPage"
type="number"
class="inputNumber"
@input="inputEvent()"> / {{pageCount}}
</div>
<div @click="changePdfPage('first')"
class="turn">
首页
</div>
<!-- 在按钮不符合条件时禁用 -->
<div @click="changePdfPage('pre')"
class="turn-btn"
:style="currentPage===1?'cursor: not-allowed;':''">
上一页
</div>
<div @click="changePdfPage('next')"
class="turn-btn"
:style="currentPage===pageCount?'cursor: not-allowed;':''">
下一页
</div>
<div @click="changePdfPage('last')"
class="turn">
尾页
</div>
</div>
<div class="pdfArea">
<pdf :src="src"
ref="pdf"
v-show="loadedRatio===1"
:page="currentPage"
@num-pages="pageCount=$event"
@progress="loadedRatio = $event"
@page-loaded="currentPage=$event"
@loaded="loadPdfHandler"
@link-clicked="currentPage = $event"
style="display: inline-block;width:100%"
id="pdfID"></pdf>
</div>
<!-- 加载未完成时,展示进度条组件并计算进度 -->
<div class="progress"
v-show="loadedRatio!==1">
<el-progress type="circle"
:width="70"
color="#53a7ff"
:percentage="Math.floor(loadedRatio * 100)"></el-progress>
<br>
<!-- 加载提示语 -->
<span>{{remindShow}}</span>
</div>
</div>
</template>
<script>
import pdf from 'vue-pdf'
export default {
components: {
pdf
},
computed: {
},
created () {
this.prohibit()
},
destroyed () {
// 在页面销毁时记得清空 setInterval
clearInterval(this.intervalID)
},
mounted () {
// 更改 loading 文字
this.intervalID = setInterval(() => {
this.remindShow === this.remindText.refresh
? this.remindShow = this.remindText.loading
: this.remindShow = this.remindText.refresh
}, 4000)
// 监听滚动条事件
this.listenerFunction()
},
data () {
return {
// ----- loading -----
remindText: {
loading: '加载文件中,文件较大请耐心等待...',
refresh: '若卡住不动,可刷新页面重新加载...'
},
remindShow: '加载文件中,文件较大请耐心等待...',
intervalID: '',
// ----- vuepdf -----
// src静态路径: /static/xxx.pdf
// src服务器路径: 'http://.../xxx.pdf'
src: '你的pdf路径',
// 当前页数
currentPage: 0,
// 总页数
pageCount: 0,
// 加载进度
loadedRatio: 0
}
},
methods: {
// 监听滚动条事件
listenerFunction (e) {
document.getElementById('container').addEventListener('scroll', true)
},
// 页面回到顶部
toTop () {
document.getElementById('container').scrollTop = 0
},
// 输入页码时校验
inputEvent () {
if (this.currentPage > this.pageCount) {
// 1. 大于max
this.currentPage = this.pageCount
} else if (this.currentPage < 1) {
// 2. 小于min
this.currentPage = 1
}
},
// 切换页数
changePdfPage (val) {
if (val === 'pre' && this.currentPage > 1) {
// 切换后页面回到顶部
this.currentPage--
this.toTop()
} else if (val === 'next' && this.currentPage < this.pageCount) {
this.currentPage++
this.toTop()
} else if (val === 'first') {
this.currentPage = 1
this.toTop()
} else if (val === 'last' && this.currentPage < this.pageCount) {
this.currentPage = this.pageCount
this.toTop()
}
},
// pdf加载时
loadPdfHandler (e) {
// 加载的时候先加载第一页
this.currentPage = 1
},
// 禁用鼠标右击、F12 来禁止打印和打开调试工具
prohibit () {
// console.log(document)
document.oncontextmenu = function () {
return false
}
document.onkeydown = function (e) {
if (e.ctrlKey && (e.keyCode === 65 || e.keyCode === 67 || e.keyCode === 73 || e.keyCode === 74 || e.keyCode === 80 || e.keyCode === 83 || e.keyCode === 85 || e.keyCode === 86 || e.keyCode === 117)) {
return false
}
if (e.keyCode === 18 || e.keyCode === 123) {
return false
}
}
}
}
}
</script>
<style scoped>
#container {
overflow: auto;
height: 800px;
font-family: PingFang SC;
width: 100%;
display: flex;
/* justify-content: center; */
position: relative;
}
/* 右侧功能按钮区 */
.right-btn {
position: fixed;
right: 5%;
bottom: 15%;
width: 120px;
display: flex;
flex-wrap: wrap;
justify-content: center;
z-index: 99;
}
.pdfArea {
width: 80%;
}
/* ------------------- 输入页码 ------------------- */
.pageNum {
margin: 10px 0;
font-size: 18px;
}
/*在谷歌下移除input[number]的上下箭头*/
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none !important;
margin: 0;
}
/*在firefox下移除input[number]的上下箭头*/
input[type='number'] {
-moz-appearance: textfield;
}
.inputNumber {
border-radius: 8px;
border: 1px solid #999999;
height: 35px;
font-size: 18px;
width: 60px;
text-align: center;
}
.inputNumber:focus {
border: 1px solid #00aeff;
background-color: rgba(18, 163, 230, 0.096);
outline: none;
transition: 0.2s;
}
/* ------------------- 切换页码 ------------------- */
.turn {
background-color: #888888;
opacity: 0.7;
color: #ffffff;
height: 70px;
width: 70px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 5px 0;
}
.turn-btn {
background-color: #000000;
opacity: 0.6;
color: #ffffff;
height: 70px;
width: 70px;
border-radius: 50%;
margin: 5px 0;
display: flex;
align-items: center;
justify-content: center;
}
.turn-btn:hover,
.turn:hover {
transition: 0.3s;
opacity: 0.5;
cursor: pointer;
}
/* ------------------- 进度条 ------------------- */
.progress {
position: absolute;
right: 50%;
top: 50%;
text-align: center;
}
.progress > span {
color: #199edb;
font-size: 14px;
}
</style>
获取键盘高度
// 监听键盘高度变化
uni.onKeyboardHeightChange(res => {
console.log('获取键盘高度', res.height);
})
小程序列表页(下拉刷新,上拉触底)
<template>
<view class="OxygenCabinBox" v-for="(item,index) in List" :key="index">{{item.name</view>
<u-loadmore :marginTop="20" :marginBottom="20" :load-text="loadText" bg-color="rgba(0, 0, 0, 0)" :status="loadStatus"></u-loadmore>
</template>
<script setup>
import {ref,reactive} from 'vue';
import {onShow,onLoad,onHide,onReachBottom,onPullDownRefresh} from '@dcloudio/uni-app'
onLoad(async () => {
loadStatus.value = 'loading';
await GetList()
})
let loadText = ref({
loadmore: '轻轻上拉',
loading: '努力加载中',
nomore: '拉到底了'
})
// 下拉刷新的事件
onPullDownRefresh(async () => {
// 1. 重置关键数据
loadStatus.value = 'loadmore';
PageIndex.value = 1
total.value = 0
List.value = []
// 2. 重新发起请求
await GetList(() => uni.stopPullDownRefresh())
})
// 上拉触底
onReachBottom(() => {
if (PageIndex.value * PageSize.value > total.value) {
loadStatus.value = 'nomore';
} else {
loadStatus.value = 'loadmore';
PageIndex.value = ++PageIndex.value;
GetList();
}
console.log("触底");
})
let List = ref([])
let PageIndex = ref(1)
let PageSize = ref(10)
let total = ref(0)
let loadStatus = ref('')
let query = reactive({})
const GetList = async (cb) => {
loadStatus.value = 'loading';
let parm = {
PageIndex: PageIndex.value,
PageSize: PageSize.value,
...query
}
const res = await OxygenCabin(parm)
console.log("获取列表", res);
cb && cb()
const StatusMap = {
Connected: "在线",
Disconnected: "离线",
};
res.data.map(item => {
item.DeviceStatus = DeviceStatusMap[item.DeviceStatus];
})
List.value = [
...List.value,
...res.data
]
total.value = res.dataCount
loadStatus.value = 'nomore';
}
</script>
限制并发
async function asyncPool(poolLimit, iterable, iteratorFn) {
// 用于保存所有异步请求
const ret = [];
// 用户保存正在进行的请求
const executing = new Set();
for (const item of iterable) {
// 构造出请求 Promise
const p = Promise.resolve().then(() => iteratorFn(item, iterable));
ret.push(p);
executing.add(p);
// 请求执行结束后从正在进行的数组中移除
const clean = () => executing.delete(p);
p.then(clean).catch(clean);
// 如果正在执行的请求数大于并发数,就使用 Promise.race 等待一个最快执行完的请求
if (executing.size >= poolLimit) {
await Promise.race(executing);
}
}
// 返回所有结果
return Promise.all(ret);
}
// 使用方法
const timeout = i => new Promise(resolve => setTimeout(() => resolve(i), i));
asyncPool(2, [1000, 5000, 3000, 2000], timeout).then(results => {
console.log(results)
})
文件上传到oss
// upload.vue
<template>
<el-upload class="upload-demo" drag action multiple :http-request="Upload" :before-upload="beforeAvatarUpload"
:on-remove="handleRemove">
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">
拖拽上传文件或 <em>点击上传</em>
</div>
</el-upload>
<el-progress :text-inside="true" :stroke-width="26" :percentage="percent" />
</template>
<script setup lang="ts">
import { UploadFilled } from '@element-plus/icons-vue'
import { client, getFileNameUUID, } from './ali-oss'
const beforeAvatarUpload = (file: any) => {
console.log('上传前', file);
// const isJPG = file.type === 'image/jpeg' || file.type === 'image/png'
// const isLt2M = file.size / 1024 / 1024 < 5
// if (!isJPG) {
// ElMessage.error('上传头像图片只能是 JPG/PNG 格式!')
// } else if (!isLt2M) {
// ElMessage.error('上传头像图片大小不能超过 5MB!')
// }
// return isJPG && isLt2M
}
const percent = ref(0)
const Upload = async (file: any) => {
// commonUpload(file);
multipartUpload(file);
}
// 普通上传
const commonUpload = async (file) => {
const fileName = file.file.name;
client().put(`Date/${fileName}`, file.file).then(result => {
console.log(`上传结果`, result)
}).catch(err => {
console.log(`上传结果err`, err);
});
}
//分片上传
const multipartUpload = async (file) => {
let fileName = file.file.name
const { res } = await client().multipartUpload(`Date/${fileName}`, file.file, {
partSize: 5 * 1024 * 1024, // 分片大小
parallel: 3, // 并行上传个数
timeout: 3 * 60 * 1000, // 超时时间
progress: async function (p) {
console.log('上传进度', p);
percent.value = Math.floor(p * 100).toFixed(0);
// client().cancel(); // 取消上传
},
})
console.log('上传结果', res)
}
// 删除
const handleRemove: UploadProps['onRemove'] = (uploadFile, uploadFiles) => {
console.log(uploadFile, uploadFiles)
client().delete(`Date/${uploadFile.name}`).then(res => {
console.log(`删除结果`, res)
}).catch(err => {
console.log(`删除结果err`, err)
});
}
</script>
<style scoped lang="scss"></style>
npm i ali-oss
// 引入ali-oss
// const OSS = require('ali-oss')
import OSS from 'ali-oss';
/**
* [accessKeyId] {String}:通过阿里云控制台创建的AccessKey。
* [accessKeySecret] {String}:通过阿里云控制台创建的AccessSecret。
* [bucket] {String}:通过控制台或PutBucket创建的bucket。
* [region] {String}:bucket所在的区域, 默认oss-cn-hangzhou。
*/
export function client() {//data后端提供数据
return new OSS({
region: 'oss-cn-hangzhou', // 地区
accessKeyId: '', // AccessKey管理那里的数据
accessKeySecret: '', //ccessKey管理那里的数据
bucket: 'xmydate', // 就是创建的bucket名字
timeout: 600000, // 设置超时时间
})
}
/**
* 生成随机文件名称
* 规则八位随机字符,加下划线连接时间戳
*/
export const getFileNameUUID = () => {
function rx() {
return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1)
}
return `${+new Date()}_${rx()}${rx()}`
}
文件下载
// 下载视频
download(item) {
const fileUrl = item.videoUrl; // 文件的URL地址
axios.get(fileUrl, { responseType: 'blob' }) // // 注意:这里需要设置响应类型为 blob
.then(response => {
const url = window.URL.createObjectURL(new Blob([response.data])); // 创建一个URL对象,
const link = document.createElement('a'); // 创建一个<a>标签
link.href = url; // 设置<a>标签的href属性为URL对象
link.setAttribute('download', fileUrl); // 设置<a>标签的download属性为文件名
document.body.appendChild(link); // 将<a>标签添加到文档中
link.click(); // 模拟点击<a>标签,触发下载
link.remove(); // 下载完成后移除a元素
URL.revokeObjectURL(url); // 释放URL对象
})
.catch(error => {
console.error("下载视频失败",error);
});
},
点击复制内容
npm install --save vue-clipboard2
// mian/js
npm install clipboard --save
Vue.use(VueClipBoard);
// hpme.vue
<template>
<span v-clipboard:copy="`${UpgradeForm.URL}`" v-clipboard:success="firstCopySuccess">复制下载链接</span>
</template>
<script>
firstCopySuccess(row) {
},
</script>
把dom元素转图片并下载
npm install html2canvas
<template>
<div id="imgCard">
需要转的内容
</div>
<div @click="generateImages('imgCard')">Dom转图片</div>
<!-- 要生成的图片的内容区域 -->
<div id="content"></div>
</template>
<script>
import html2canvas from 'html2canvas'
methods: {
generateImages(id) {
html2canvas(document.getElementById(id)).then(canvas => {
const imgUrl = canvas.toDataURL('image/jpeg')
const image = document.createElement('img')
image.src = imgUrl
// 将生成的图片放到 类名为 content 的元素中
document.getElementById('content').appendChild(image)
const a = document.createElement('a')
a.href = imgUrl
// a.download 后面的内容为自定义图片的名称
a.download = '健康报告'
a.click()
})
},
}
</script>
<style>
#content {
img {
width: 300px;
height: 300px;
}
}
</style>
Nginx 部署配置
// 解决刷新404
location / {
try_files $uri $uri/ /index.html;
}
// api代理重写
location /api/ {
rewrite ^/api/(.*)$ /$1 break;
proxy_pass http://localhost:8000/;
}
vue -qr 生成二维码并且下载
npm install vue-qr --save
- text 二维码内容
- size 二维码宽高大小,因为是正方形,所以设一个参数即可
- margin 默认边距20px,不喜欢的话自己设为0
- colorDark 实点的颜色,注意要和colorLight一起设置才有效
- colorLight 空白的颜色,注意要和colorDark一起设置才有效
- bgSrc 嵌入背景图地址,没什么卵用,不建议设置
- logoSrc 二维码中间的图,这个是好东西,设置一下显得专业点
- logoScale 中间图的尺寸,不要设太大,太大会导致扫码失败的
- dotScale 那些小点点的大小,这个也没什么好纠结的,不建议设置了
<template>
<vue-qr id="payQR" v-if="codeText" :text="codeText" :size="248" colorDark="#5559FF" colorLight="#ffffff"
:logoSrc="logoImg" :callback="getImgInfo">
</vue-qr>
</template
<script setup>
import { ref } from 'vue'
import vueQr from 'vue-qr/src/packages/vue-qr.vue' // 引入vue-qr组件
const codeText = ref('123'); // 二维码内容 可以是一个URL、文本或其他数据
const logoImg = ref('https://img-s-msn-com.akamaized.net/tenant/amp/entityid/AA1oEjiP.img?w=768&h=480&m=6&x=394&y=103&s=381&d=381'); // 二维码logo图片地址
const qrDownloadUrl = ref('')// 下载数据base64
const canvasImg = ref('') //canvas绘制的图片
// 获取二维码图片信息
const getImgInfo = (dataUrl) => {
qrDownloadUrl.value = dataUrl // 将二维码图片信息保存到下载变量中
console.log(dataUrl);
CanvasQr() // 调用canvas绘制二维码
}
// 利用canvas绘制二维码
const CanvasQr = () => {
const img = new Image();
img.src = qrDownloadUrl.value;
img.onload = () => {
const text = '文字文字'
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, img.width, img.height);
// 设置轮廓颜色和宽度
ctx.strokeStyle = '#994AE7';
ctx.lineWidth = 3;
// 绘制矩形
ctx.strokeRect(0, 0, img.width, img.height);
// 设置文本样式
ctx.fillStyle = 'red';
ctx.font = '20px bold';
// 计算文本位置
const textWidth = ctx.measureText(text).width;
const x = (img.width - textWidth) / 2;
const y = img.height - 20;
// 绘制文本
ctx.fillText(text, x, y);
const dataURL = canvas.toDataURL('image/png'); // 获取canvas的base64数据
canvasImg.value = dataURL;
}
}
// 下载二维码
const downloadQrcode = () => {
const image = new Image(); // 创建一个img元素
image.setAttribute("crossOrigin", "anonymous");// 解决跨域问题
image.src = qrDownloadUrl.value;//地址
image.onload = () => {
const canvas = document.createElement("canvas"); // 创建一个canvas元素
canvas.width = image.width; // 设置canvas的宽高与图片一致
canvas.height = image.height; // 设置canvas的宽高与图片一致
const context = canvas.getContext("2d"); // 获取canvas的2d上下文
context.drawImage(image, 0, 0, image.width, image.height); // 将图片绘制到canvas上
canvas.toBlob((blob) => {
const url = URL.createObjectURL(blob); // 创建一个URL对象
// 下载
const a = document.createElement("a");
a.download = "文件名称"; // 下载的文件名称
a.href = url; // 下载的文件地址
a.click(); // 触发下载
a.remove(); // 下载完成后移除a元素
URL.revokeObjectURL(url); // 释放URL对象
});
};
}
</script>
点击按钮页面滚动到对应的位置
<div style="height: 90vh;"> </div>
<div style="background-color: red" id="testid">跳到这里</div>
<button @click="scrollToElement('testid')">跳转</button>
<script setup>
const scrollToElement = (id) => {
let anchor = document.getElementById(id);
document.documentElement.scrollTo({ // 平滑滚动到指定元素
top: anchor.offsetTop - 100, // 滚动到指定元素,并距离顶部100px
behavior: 'smooth' // 平滑滚动
})
}
</script>
点击移除盒子
<template>
<div class="swipe-box">
<transition-group name="list" tag="div">
<div
v-for="(item, index) in contents"
:key="item.id"
class="swipe-content"
:class="{ 'slide-out': item.removing }"
@click="handleRemove(index)"
>
{{ item.text }}
</div>
</transition-group>
</div>
</template>
<script>
export default {
data() {
return {
contents: [
{ id: 1, text: '内容1', removing: false },
{ id: 2, text: '内容2', removing: false },
{ id: 3, text: '内容3', removing: false },
],
};
},
methods: {
handleRemove(index) {
// 标记为移除状态,触发 slide-out 动画
this.contents[index].removing = true;
// 等动画执行完后再从数组中移除
setTimeout(() => {
this.contents.splice(index, 1);
}, 300); // 300ms 要和 CSS 动画时长一致
},
},
};
</script>
<style scoped>
.swipe-box {
margin-top: 100rpx;
overflow: hidden;
position: relative;
}
.swipe-content {
height: 100px;
background-color: #f0f0f0;
margin-bottom: 20rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
transition: transform 0.3s ease, opacity 0.3s ease;
}
/* 左滑动画 */
.slide-out {
transform: translateX(-100%);
opacity: 0;
}
/* 元素删除后的上移动画 */
.list-leave-active,
.list-enter-active {
transition: all 0.3s ease;
}
.list-leave-to {
opacity: 0;
transform: translateY(-20px);
}
.list-enter-from {
opacity: 0;
transform: translateY(20px);
}
</style>
鼠标滚动到指定div处,出现动画
<h2 id="text" class="test" :class="{ testenter: show }">首席健康专家</h2>
<script setup>
import { onMounted } from 'vue'
const show = ref(false)
const handleScroll = () => {
let scrollTop = document.documentElement.scrollTop // 获取滚动条距离顶部的距离
if (scrollTop >= document.getElementById('text').offsetTop - 1200) { //当滚动条距离顶部的距离大于等于某个元素距离顶部的距离时,显示该元素
show.value = true
console.log(show.value);
} else {
show.value = false
}
}
onMounted(() => {
window.addEventListener('scroll', handleScroll)
})
</script>
<style>
.test {
transform: translateY(70%);
opacity: 0;
}
.testenter {
transform: translateY(0%) !important;
/* 滚动后的位置 */
opacity: 1 !important;
/* 滚动后显现 */
transition: all 1s ease;
</style>
回到顶部
export const back = () => {
let top = document.documentElement.scrollTop || document.body.scrollTop;
// 实现滚动效果
const timeTop = setInterval(() => {
document.body.scrollTop = document.documentElement.scrollTop = top -= 100;
if (top <= 0) {
clearInterval(timeTop);
}
}, 10);
};
单行或多行省略号
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.truncate {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
选中最后几个元素
// 前三个
li:nth-child(-n + 3) {
text-decoration: underline;
}
// 选中 2-5 的列表项
li:nth-child(n + 2):nth-child(-n + 5) {
color: #2563eb;
}
// 倒数两个
li:nth-last-child(-n + 2) {
text-decoration-line: line-through;
}
/* 奇数行 */
.ul li:nth-child(odd) {
background: red;
list-style-type: none;
}
/* 偶数行 */
.ul li:nth-child(even) {
background: #000;
list-style-type: none;
padding: 12px 2px;
}
vur路由跳转动画
transition 参考 前端 - 用了很多动效,介绍 4个很 Nice 的 Veu 路由过渡动效! - 终身学习者 - SegmentFault 思否
<RouterView v-slot="{ Component }">
<transition name="fade">
<KeepAlive>
<component :is="Component" />
</KeepAlive>
</transition>
</RouterView>
<script setup>
import Header from './components/Header.vue';
import Footer from './components/Footer.vue';
import MobileHeader from './components/MobileHeader.vue';
import { ref, onMounted, onUnmounted, computed } from 'vue'
import { RouterView, useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
let seed = ref(false)
// 根据窗口大小判断是否显示seed
const handleResize = () => {
if (window.innerWidth > 768) {
seed.value = true
} else {
seed.value = false
}
}
onMounted(() => {
handleResize()
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
})
</script>
<template>
<header>
<Header v-if="seed"></Header>
<MobileHeader v-else></MobileHeader>
</header>
<main>
<RouterView v-slot="{ Component }">
<transition name="fade">
<KeepAlive>
<component :is="Component" />
</KeepAlive>
</transition>
</RouterView>
</main>
<footer>
<Footer></Footer>
</footer>
</template>
<style scoped>
/* body {
scroll-behavior: smooth;
} */
.fade-enter-from {
/* 进入时的透明度为0 和 刚开始进入时的原始位置通过active透明度渐渐变为1 */
opacity: 0;
transform: translateX(-100%);
}
.fade-enter-to {
/*定义进入完成后的位置 和 透明度 */
transform: translateX(0%);
opacity: 1;
}
.fade-leave-active,
.fade-enter-active {
transition: all 0.5s ease-out;
}
.fade-leave-from {
/* 页面离开时一开始的css样式,离开后为leave-to,经过active渐渐透明 */
transform: translateX(0%);
opacity: 1;
}
.fade-leave-to {
/* 这个是离开后的透明度通过下面的active阶段渐渐变为0 */
transform: translateX(100%);
opacity: 0;
}
</style>