上传图片【选择文件📁+上传图片🎨】
1. 上传图片的流程分析:【选择文件📁+上传图片🎨】
2.1 功能步骤
2.1 页面基本布局
基本布局
<template>
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>更换头像</span>
</div>
<div>
<!-- 图片,用来展示用户选择的头像 -->
<img src="../../../assets/images/avatar.jpg" alt="" class="preview">
<!-- 按钮区域 -->
<div class="btn-box">
<!-- accept: 当前文件选择框可以接受的文件类型 -->
<input type="file" accept="image/*" ref="inp" name="" id="" style="display: none">
<el-button type="primary" @click="$refs.inp.click()" icon="el-icon-circle-plus-outline">选择图片</el-button>
<el-button type="success" icon="el-icon-upload" :disabled="true">上传图片</el-button>
</div>
</div>
</el-card>
</template>
2.2 绑定点击事件
2.3 选择文件
accept="image/*" 是可以选择索引图片 格式,可以选择其他类型
2.4 转换base64图片格式
<input @click="changeFile" type="file" accept="image/*" ref="inp" name="" id="" style="display: none">
绑定事件@click="changeFile"可以实现base64图片转换,渲染图片或提交图片的时候都需要用base64格式
2.5 转换base64格式方法
获取上传图片信息: const files = e.target.files
>1 创建 FileReader 对象
>2 读取文件转成 BASE64
>3 监听事件, 得到结果:必须使用箭头函数
data () {
return {
avatar:''
}
},
methods: {
changeFile (e) {
// console.log(e.target.files[0])
// this.avatar = e.target.files[0]
const files = e.target.files
if (files.length > 0) {
// this.avatar = e.target.files[0]
// files[0] : 是一个对象
// 目标: 将这个对象转成 BASE64 格式的字符串
// FileReader 文件读取器
// 1. 创建 FileReader 对象
const fr = new FileReader()
// 2. 读取文件转成 BASE64
fr.readAsDataURL(files[0])
// 3. 监听事件, 得到结果
// 必须使用箭头函数
fr.onload = (e) => {
// console.log(e.target.result)
this.avatar = e.target.result
}
} else {
this.avatar = ''
}
},
2.5 图片渲染
console.log(e.target.result) 转换成功后的base64图片信息,点击取消可以返回原来的默认值
3. 上传提交~
提交按钮绑定事件
提交
// 点击提交
async upload(){
// 1.发送请求
const {data:res}= await this.$http.patch('/my/update/avatar',{avatar:this.avatar})
// 2.根据结果提示用户
if (res.code !== 0) return this.$message.error(res.message)
this.$message.success(res.message)
// 3.重新获取用户信息
await this.$store.dispatch('user/getUserInfo')
}
4. 完整的项目展示
完整的code
<template>
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>更换头像</span>
</div>
<div>
<!-- 图片,用来展示用户选择的头像 -->
<img v-if="avatar" :src="avatar" alt="" class="preview" />
<img v-else src="../../../assets/images/avatar.jpg" alt="" class="preview" />
<!-- 按钮区域 -->
<div class="btn-box">
<!-- accept: 当前文件选择框可以接受的文件类型 -->
<!-- MIME_TYPE: application/json text/plain text/html text/css Content-Type: application/json -->
<!-- 为什么 MIME Type 会出现? 使用 http 协议在互联网上通信, 只支持两种格式的数据: 文本 / 二进制 -->
<!-- 用于标识在互联网上传输的文件类型 -->
<input @change="changeFile" accept="image/*" ref="inp" style="display: none" type="file">
<el-button @click="$refs.inp.click()" type="primary" icon="el-icon-plus">选择图片</el-button>
<el-button @click="upload" type="success" icon="el-icon-upload" :disabled="!avatar">上传头像</el-button>
</div>
</div>
</el-card>
</template>
<script>
export default {
name: 'UserAvatar',
data () {
return {
avatar: ''
}
},
methods: {
changeFile (e) {
// 我要一个 Object, 可以传入一个 Array
// Blob 是父类对象 File 是子类对象
// files
// console.log(e.target.files[0])
// img 标签的 src 属性只能设置两种值:
// URL 和 BASE64 (Data URL)
// this.avatar = e.target.files[0]
const files = e.target.files
if (files.length > 0) {
// this.avatar = e.target.files[0]
// files[0] : 是一个对象
// 目标: 将这个对象转成 BASE64 格式的字符串
// FileReader 文件读取器
// 1. 创建 FileReader 对象
const fr = new FileReader()
// 2. 读取文件转成 BASE64
fr.readAsDataURL(files[0])
// 3. 监听事件, 得到结果
// 必须使用箭头函数
fr.onload = (e) => {
console.log(e.target.result)
this.avatar = e.target.result
}
} else {
this.avatar = ''
}
},
// 点击提交
async upload(){
// 1.发送请求
const {data:res}= await this.$http.patch('/my/update/avatar',{avatar:this.avatar})
// 2.根据结果提示用户
if (res.code !== 0) return this.$message.error(res.message)
this.$message.success(res.message)
// 3.重新获取用户信息
await this.$store.dispatch('user/getUserInfo')
}
}
}
</script>
<style lang="less" scoped>
.btn-box {
margin-top: 10px;
}
.preview {
object-fit: cover;
width: 350px;
height: 350px;
}
</style>
vuexstore/user
// 引入axios
import axios from 'axios'
export default {
namespaced: true,
state: () => ({
// token: localStorage.getItem('token')
token: '',
userInfo: {},
}),
mutations: {
updateToken (state, token) {
state.token = token
// 存入 localStorage
// localStorage.setItem('token', token)
},
updateUserInfo(state,userInfo){
state.userInfo = userInfo
}
},
actions: {
async getUserInfo (context) {
// console.log(context.state.token)
// this.$http.get('my/userInfo')
// console.log(this)
// get 第一个参数是 url
// 第二个参数是 config 配置对象
// 配置对象中有一个设置请求头的属性: headers
const {data:res } = await axios.get('my/userInfo')
// console.log(res.data)
context.commit('updateUserInfo',res.data)
}
},
getters: {}
}
5.附加功能:上传图片和删除已上传图片
5.1 核心布局代码
1.如果是用户选择了文件,那么先转换base64字符串格式,标签赋值给img src地址,渲染图片
2.如果是用户没选图片就显示默认图片: v-if v-else 应用
3.先布局原生的选择文件的按钮,绑定change事件,UI不好看。所有先隐藏调display:none,我们需要的是她的元素事件change,为了获取元素 ref="iptFileRef"
<input @change="changeImg" type="file" style="display: none;" accept="image/*" ref="iptFileRef" />
4.新的按钮身上代理绑定原生选择文件的change事件,代理绑定:
<el-button @click="$refs.iptFileRef.click()" type="info" plain icon="el-icon-upload" size="mini" >选中封面</el-button> <el-button @click="deleteImg" type="warning" plain icon="el-icon-delete" size="mini" >删除封面</el-button>
布局代码code
<el-form-item label="文章封面" prop="cover_img">
<!-- 用来显示封面的图片 -->
<img v-if="preview" :src="preview" alt="" class="cover-img" ref="imgRef">
<img v-else src="../../../assets/images/cover.jpg" alt="" class="cover-img" ref="imgRef">
<!-- 文件选择框,默认被隐藏 -->
<input @change="changeImg" type="file" style="display: none;" accept="image/*" ref="iptFileRef" /> <br>
<!-- 选择封面的按钮 将原生按钮事件代理绑定给性能的UI按钮上-->
<el-button @click="$refs.iptFileRef.click()" type="info" plain icon="el-icon-upload" size="mini" >选中封面</el-button>
<el-button @click="deleteImg" type="warning" plain icon="el-icon-delete" size="mini" >删除封面</el-button>
</el-form-item>
<el-form-item>
<el-button type="primary">发布文章</el-button>
<el-button type="info">存为草稿</el-button>
</el-form-item>
5.2 图片信息转换base64字符串格式
changeImg(e){
const files = e.target.files
if (files.length > 0){
//用户选择文件
//转换base64字符串
this.pubForm.cover_img = files[0]
const fr = new FileReader()
fr.readAsDataURL(files[0])
fr.onload = e => {
this.preview = e.target.result
}
//没选
}else {
//this.pubForm.cover_img = ''
//this.preview = ''
}
5.2.1 图片img>src能读取显示方法:URL和base64字符串
img 的 src 只能设置为两种: 1. BASE64 2. URL
// 将 File 对象转为 BASE64 字符串, 设置给 img 标签的 src
// 转为 BASE64 的好处和坏处
// 好处: 减少请求次数
// 坏处: 会额外占用 33% 左右的体积
// 结论: 小图片推荐转为 base64, 大图片不推荐所有强烈推荐使用:简单实用的方法是--URL.createObjectURL()
// URL.createObjectURL
// 作用: 将 Blob 或 File 对象转成 URL
// 参数: Blob 或 File 对象
// 返回值: URL
// 这个 URL 的生命周期和当前窗口一致, 窗口关闭后就不能访问该 URL 了
this.preview = URL.createObjectURL(files[0])
URL.createObjectURL()
changeImg(e){
// 1.给+选择文件的按钮绑定事件,点击出发选择文件事件
// 2.给文件选择框绑定change事件,当用户选择文件时触发
// 3.当change事件中获取用户选择的文件
const files = e.target.files
// 4.1准备好提交给后台的数据
// 4.2展示给用户看
if (files.length > 0){
// 用户选择了文件
// console.log(files[0])
this.pubForm.cover_img = files[0]
// 将file对象转换给base64字符串格式,设置给img的src里,才能img图片渲染出来
// ---转换base64格式
// const fr = new FileReader()
// fr.readAsDataURL(files[0])
// fr.onload = e => {
// this.preview = e.target.result
// }
// ----
// ***新的方法URL.createObjectURL()
// URL.createObjectURL
// 作用: 将 Blob 或 File 对象转成 URL
// 参数: Blob 或 File 对象
// 返回值: URL
// 这个 URL 的生命周期和当前窗口一致, 窗口关闭后就不能访问该 URL 了
this.preview = URL.createObjectURL(files[0])
}else {
// this.pubForm.cover_img = ''
// this.preview = ''
console.log('用户没选')
}
},
5.3 删除已上传图片
实现的时候把上传的事件中的用户没选文件分开写个独立的功能就可以
<el-button @click="deleteImg" type="warning" plain icon="el-icon-delete" size="mini" >删除封面</el-button>
deleteImg(){
this.pubForm.cover_img = ''
this.preview = ''
}
5.4完整代码(2)
完整代码(2)
<template>
<div>
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>文章列表</span>
</div>
<!-- 搜索区域 -->
<div class="search-box">
<el-form :inline="true" :model="q">
<el-form-item label="文章分类">
<el-select v-model="filter.cate_id" placeholder="请选择分类" size="small">
<el-option v-for="item in cateList" :key="item.id" :label="item.cate_name" :value="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="发布状态" style="margin-left: 15px;">
<el-select v-model="filter.state" placeholder="请选择状态" size="small">
<el-option label="已发布" value="已发布"></el-option>
<el-option label="草稿" value="草稿"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="filterArt" type="primary" size="small">筛选</el-button>
<el-button @click="reset" type="info" size="small">重置</el-button>
</el-form-item>
</el-form>
<!-- 发表文章的按钮 -->
<el-button @click="pubVisible = true" type="primary" size="small" class="btn-pub">发表文章</el-button>
</div>
<!-- 文章表格区域 -->
<el-table
stripe
border
:data="artList"
style="width: 100%">
<el-table-column
label="文章标题">
<template v-slot="scope">
<el-link @click="showDetail(scope.row.id)" type="primary">{{ scope.row.title }}</el-link>
</template>
</el-table-column>
<el-table-column
prop="cate_name"
label="文章分类">
</el-table-column>
<el-table-column
label="发表时间">
<template v-slot="scope">
{{ formatDate(scope.row.pub_date) }}
</template>
</el-table-column>
<el-table-column
prop="state"
label="状态">
</el-table-column>
<el-table-column
label="操作">
<template v-slot="scope">
<el-button @click="del(scope.row.id)" type="danger" size="mini">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页区域 -->
<!-- 分页组件是独立于数据组件的, 它只是辅助数据组件完成分页功能 -->
<!-- current-page: 分页组件显示的当前页码 -->
<!-- page-sizes: 控制每页显示多少条的控制器 -->
<!-- page-size: 控制每页显示多少条 -->
<!-- total: 总数量 -->
<!-- layout: 控制分页组件的布局, 用逗号分隔 -->
<!-- size-change 事件: 当每页显示的条数发生变化时触发 -->
<!-- current-change 事件: 当前页发生变化时触发 -->
<el-pagination
style="margin-top: 15px"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="q.pagenum"
:page-sizes="[2, 3, 5, 10]"
:page-size="q.pagesize"
layout="total, sizes, prev, pager, next, jumper"
:total="total">
</el-pagination>
</el-card>
<!-- 发表文章的 dialog -->
<!-- fullscreen: 全屏 dialog -->
<!-- before-close: 用于拦截用户关闭 -->
<el-dialog
@closed="dialogClose"
fullscreen
title="发表文章"
:visible.sync="pubVisible"
:before-close="handleClose">
<el-form :model="pubForm" :rules="pubRules" ref="pubForm" label-width="80px">
<el-form-item label="文章标题" prop="title">
<el-input placeholder="请输入标题" v-model="pubForm.title"></el-input>
</el-form-item>
<el-form-item label="文章分类" prop="cate_id">
<el-select style="width: 100%" v-model="pubForm.cate_id" placeholder="请选择分类">
<!-- label: 显示给用户看的标签 -->
<!-- value: 提交给后台的数据 -->
<el-option v-for="item in cateList" :key="item.id" :label="item.cate_name" :value="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="文章内容" prop="content">
<quill-editor v-if="pubVisible" @blur="checkContent" v-model="pubForm.content">
</quill-editor>
</el-form-item>
<el-form-item label="文章封面" prop="cover_img">
<!-- 用来显示封面的图片 -->
<img v-if="preview" :src="preview" alt="" class="cover-img" ref="imgRef" />
<img v-else src="../../../assets/images/cover.jpg" alt="" class="cover-img" ref="imgRef" />
<br />
<!-- 文件选择框,默认被隐藏 -->
<input v-if="pubVisible" @change="changeImg" type="file" style="display: none;" accept="image/*" ref="iptFileRef" />
<!-- 选择封面的按钮 -->
<el-button @click="$refs.iptFileRef.click()" type="text">+ 选择封面</el-button>
</el-form-item>
<el-form-item prop="state">
<el-button @click="pubArt('已发布')" type="primary">发布</el-button>
<el-button @click="pubArt('草稿')" type="info">存为草稿</el-button>
</el-form-item>
</el-form>
</el-dialog>
<!-- 预览文章的 dialog -->
<el-dialog
title="文章预览"
:visible.sync="detailVisible"
width="80%">
<h1 class="title">{{ artDetail.title }}</h1>
<div class="info">
<span>作者: {{ artDetail.nickname || artDetail.username }}</span>
<span>发布时间: {{ formatDate(artDetail.pub_date) }}</span>
<span>所属分类: {{ artDetail.cate_name }}</span>
<span>状态: {{ artDetail.state }}</span>
</div>
<el-divider content-position="left">我是华丽丽的分割线</el-divider>
<img :src="'http://www.liulongbin.top:3008' + artDetail.cover_img" alt="">
<div class="detail-box" v-html="artDetail.content"></div>
</el-dialog>
</div>
</template>
<script>
import dayjs from 'dayjs'
export default {
name: 'ArtList',
data () {
return {
preview: '',
pubVisible: false,
pubForm: {
title: '',
cate_id: '',
content: '',
cover_img: '',
state: ''
},
pubRules: {
title: [
{ required: true, message: '请输入文章标题', trigger: 'blur' },
{ min: 1, max: 30, message: '文章标题必须是 1 ~ 30 个字符', trigger: 'blur' }
],
cate_id: [
{ required: true, message: '请选择文章分类', trigger: 'change' }
],
content: [
{ required: true, message: '请输入文章内容' }
],
cover_img: [
{ required: true, message: '请选择文章封面' }
]
},
cateList: [],
// 查询参数对象
q: {
// select * from articles where cate_id = '' limit 3, 2
pagenum: 1, // 当前页码
pagesize: 2, // 一页显示多少条
cate_id: '',
state: ''
},
filter: {
cate_id: '',
state: ''
},
artList: [],
total: 0,
artDetail: {},
detailVisible: false
}
},
methods: {
async handleClose (done) {
// this.$confirm('此操作将导致文章信息丢失, 是否继续?', '提示', {
// type: 'warning'
// })
// .then(_ => {
// done()
// })
// .catch(_ => {})
const result = await this.$confirm('此操作将导致文章信息丢失, 是否继续?', '提示', {
type: 'warning'
}).catch(e => e)
// 用户点了确定
if (result === 'confirm') done()
},
async getCateList () {
// 发请求拿数据, 存入到 data 中
const { data: res } = await this.$http.get('/my/cate/list')
if (res.code === 0) this.cateList = res.data
},
checkContent () {
// console.log('我要校验 content')
// 由于 vue-quill-editor 组件不是 Element 的组件, 所以自带的表单校验功能, 无法准确的知道什么时候失去焦点
// 需要我们手动监听 quill-editor 的失去焦点事件
// 在失去焦点时手动调用 validateField 方法对 content 字段手动校验
this.$refs.pubForm.validateField('content')
},
changeImg (e) {
// if (this.pubForm.cover_img) {
// this.$refs.pubForm.clearValidate('cover_img')
// }
// 1. 给 +选择封面 按钮绑定点击事件, 触发文件选择框的点击事件
// 2. 给文件选择框绑定 change 事件, 当用户选择文件时触发
// 3. 在 change 事件中获取用户选择的文件
const files = e.target.files
if (files.length > 0) {
// 选择了文件
// 4.1 准备提交给后台的数据
// console.log(files[0])
this.pubForm.cover_img = files[0]
// 只要用户改过自新愿意选择图片, 就清除校验规则
this.$refs.pubForm.clearValidate('cover_img')
// 4.2 渲染给用户看
// 将 File 对象转为 BASE64 字符串, 设置给 img 标签的 src
// 转为 BASE64 的好处和坏处
// 好处: 减少请求次数
// 坏处: 会额外占用 33% 左右的体积
// 结论: 小图片推荐转为 base64, 大图片不推荐
// const fr = new FileReader()
// fr.readAsDataURL(files[0])
// fr.onload = e => {
// this.preview = e.target.result
// }
// img 的 src 只能设置为两种: 1. BASE64 2. URL
// 怎么将 File 对象转成 URL ?
// URL.createObjectURL
// 作用: 将 Blob 或 File 对象转成 URL
// 参数: Blob 或 File 对象
// 返回值: URL
// 这个 URL 的生命周期和当前窗口一致, 窗口关闭后就不能访问该 URL 了
this.preview = URL.createObjectURL(files[0])
} else {
// 没选文件
// console.log('用户没选文件')
this.pubForm.cover_img = ''
this.preview = ''
}
},
async pubArt (state) {
// 1. 修改当前状态
this.pubForm.state = state
// 2. 兜底校验
// this.$refs.pubForm.validate(valid => {
// if (!valid) return
// console.log('我要发请求了')
// })
// this.$refs.pubForm.validate().then(valid => {
// if (!valid) return
// console.log('我要发请求了')
// }).catch(e => { console.log(e) })
const valid = await this.$refs.pubForm.validate().catch(e => e)
if (!valid) return
// console.log('我要发请求了')
// 3. 发请求
// 将数据存储到 FormData 中再发请求给后台
const fd = new FormData()
// fd.append('title', this.pubForm.title)
// fd.append('cate_id', this.pubForm.cate_id)
// fd.append('content', this.pubForm.content)
// fd.append('state', this.pubForm.state)
// fd.append('cover_img', this.pubForm.cover_img)
// 错误的做法, 不要模仿!!!
// fd = { ...this.pubForm } // {} 在创建一个新对象
// for (const k in this.pubForm) {
// fd.append(k, this.pubForm[k])
// }
// console.log(fd)
// keys 可以获取对象的所有属性名, 返回值是一个数组
// const arr = Object.keys(this.pubForm)
// // console.log(arr)
// arr.forEach(k => {
// fd.append(k, this.pubForm[k])
// })
// 高级
Object.keys(this.pubForm).forEach(k => fd.append(k, this.pubForm[k]))
// http 只能传输文本或二进制, 如果要传递文件对象, 必须要使用 form-data 格式来传递
// axios 默认请求格式: application/json
// 如果传入的对象是 FormData 格式, axios 会自动将 ContentType 设置为: multipart/form-data
const { data: res } = await this.$http.post('/my/article/add', fd)
// console.log(res)
// 4. 根据响应提示用户
if (res.code !== 0) return this.$message.error(res.message)
this.$message.success(res.message)
// 5. 关闭 dialog
this.pubVisible = false
// 6. 更新文章列表数据 (等后期文章列表完成后再做)
this.getArtList()
},
dialogClose () {
// 清空表单
// resetFields: 是依据 prop 来重置的
this.$refs.pubForm.resetFields()
// 清空预览
this.preview = ''
},
async getArtList () {
// 发请求拿数据
const { data: res } = await this.$http.get('/my/article/list', {
params: this.q
})
// console.log(res.total)
this.total = res.total
this.artList = res.data
},
formatDate (date) {
return dayjs(date).format('YYYY-MM-DD HH:mm:ss')
},
handleSizeChange (pagesize) {
// console.log(pagesize)
this.q.pagesize = pagesize
this.getArtList()
},
handleCurrentChange (current) {
// console.log(current)
// 发请求
this.q.pagenum = current
this.getArtList()
},
reset () {
// Bug 现象: 选择最新分类数据后, 点击 31 页获取不到数据
// 分析原因: 选择最新分类数据会同步到 q.cate_id 上, 切换页面到 31 页时会自动将 q 携带发请求, 此时获取的就是最新分类的第 31 页数据, 而最新分类中没有第 31 页数据
// 解决思路:
// 1. 准备一个新的数据对象, 将分类的 v-model 绑定到新对象上
// 此时, 选择分类后, 点击 31 页不会出现之前的问题, 因为 q.cate_id 没有发生变化
// 2. 点击筛选按钮时, 将新对象的状态同步给 q, 再发请求
// 此时, Bug 已经基本修复, 但是选择分类后, 手动切换到 31 页, 再点筛选依然会出现 Bug
// 3. 点击筛选按钮时, 将 pagenum 设置为 1
// 将 q 的所有状态还原
// this.q = {
// pagenum: 1, // 当前页码
// pagesize: 2, // 一页显示多少条
// cate_id: '',
// state: ''
// }
this.filter = {
cate_id: '',
state: ''
}
this.q.pagenum = 1
this.q.cate_id = ''
this.q.state = ''
// 发请求
this.getArtList()
},
filterArt () {
// 将 filter 的状态同步给 q
this.q.cate_id = this.filter.cate_id
this.q.state = this.filter.state
// 将 pagenum 设置为 1
this.q.pagenum = 1
// 发请求
this.getArtList()
},
async showDetail (id) {
// 发送请求, 根据 id 获取文章详情
const { data: res } = await this.$http.get('/my/article/info', {
params: {
id
}
})
// console.log(res)
// 存到 data 中
this.artDetail = res.data
// 显示 dialog
this.detailVisible = true
},
async del (id) {
// 1. 绑定点击事件传入 id
// 2. 使用 confirm 提醒用户
const result = await this.$confirm('真的要删除吗?你忍心吗?', '提示').catch(e => e)
if (result !== 'confirm') return
// 3. 发请求根据 id 删除文章
const { data: res } = await this.$http.delete('/my/article/info', {
params: {
id
}
})
// 4. 根据结果提醒用户
if (res.code !== 0) return this.$message.error(res.message)
this.$message.success(res.message)
// 判断当前 artList 是不是只有一条数据, 如果只有一条就让 pagenum--
if (this.artList.length === 1) this.q.pagenum > 1 && this.q.pagenum--
// 5. 获取最新的文章列表数据
this.getArtList()
}
},
created () {
// 加载分类列表
this.getCateList()
// 加载文章列表
this.getArtList()
}
}
</script>
<style lang="less" scoped>
// scoped:
// 1. 给当前组件中所有元素加一个 data-v-hash, 如果用了组件, 只会给组件的根元素加这个属性
// 会导致组件内的元素样式无法生效
// 2. 给所有选择器加一个属性交集选择器
.search-box {
display: flex;
justify-content: space-between;
align-items: flex-start;
.btn-pub {
margin-top: 5px;
}
}
// 深度选择器: 让组件内的元素也可以生效
// 原理: 将之前的交集选择器变成 后代选择器, [data-v-hash] .ql-editor
/deep/ .ql-editor {
height: 300px
}
// 设置图片封面的宽高
.cover-img {
width: 400px;
height: 280px;
object-fit: cover;
}
.title {
font-size: 24px;
text-align: center;
font-weight: normal;
color: #000;
margin: 0 0 10px 0;
}
.info {
font-size: 12px;
span {
margin-right: 20px;
}
}
// 修改 dialog 内部元素的样式,需要添加样式穿透
/deep/ .detail-box {
img {
width: 500px;
}
}
</style>