UIKit学习笔记8-发送照片、拍摄照片并发送

现在要写这个部分,新开一页去放。

思路是利用for...in来逐个打印组件

设置两个数组,分别储存图片字符串和对应的文本字符串

记得数组取名要取复数,表示里面不止一个元素

新建"+"按钮的功能栏,并将其添加到视图

只做这三个:照片、拍摄、位置。

swift 复制代码
class MoreView: UIView {
    let imgs = ["photo.fill", "camera.fill", "location.fill"]
    let texts = ["照片", "相机", "位置"]
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupUI()
    }
    }

然后再setupUI()函数里设置图片按钮和文字标签,并为其添加事件

使用for...in来逐个打印组件

按钮

swift 复制代码
func setupUI(){
        let y = 16
        for idx in 0..<imgs.count {
            let x = 16 + (24 + 66) * idx //同行x是递增的
            let btn = UIButton(frame: .init(x: x, y: y, width: 66, height: 66))
            btn.setImage(UIImage(systemName: imgs[idx]), for: .normal)
            btn.backgroundColor = .white
            btn.layer.cornerRadius = 12
            btn.layer.masksToBounds = true
            btn.tag = 1000 + idx
            //为了防止tag重复,要在idx的基础上加100,1000等
            btn.tintColor = .black.withAlphaComponent(0.7)
            self.addSubview(btn)

文本

swift 复制代码
let lbl = UILabel(frame: .init(x: x, y: y, width: 66, height: 66))
            lbl.text = texts[idx]
            lbl.textColor = .black.withAlphaComponent(0.7)
            lbl.font = .systemFont(ofSize: 16)
            lbl.textAlignment = .center
            self.addSubview(lbl)

使用回调,把索引以tag传出去。

swift 复制代码
typealias BtnClick = ((Int) -> ())

class MoreView: UIView {
    var btnClick: BtnClick? }
swift 复制代码
@objc func handleAction(btn: UIButton) {
        let tag = btn.tag - 1000
        btnClick?(tag)
    }

写一个函数,控制闭包被传出?

获取底部栏高度:写在Public里

swift 复制代码
let bottomSafeArea = UIApplication.shared.windows.first?.safeAreaInsets.bottom ?? 0

在ChatViewController页面写一个moreView,这是创建界面实例,并控制点击对应按钮,然后操作相关功能的。

swift 复制代码
lazy var moreView: MoreView = {
        let moreView = MoreView(frame: .init(x: 0, y: SCREENHEIGHT - 98 - bottomSafeArea, width: SCREENWIDTH, height: 98 + bottomSafeArea))
        moreView.backgroundColor = .lightGray
        moreView.btnClick = { [weak self] idx in
            print(idx)
            if idx == 0 {
                //验证相册权限,并打开相册
                self?.openPhotoLib()
            } else if idx == 1 {
                //验证相机权限,并打开相机
                self?.openCamera()
            } else {
                //打开定位
            }
        }

把相关绑定到点击按钮MoreView的对应索引处

0对应的是第一个按钮,以此类推(openPhotoLib函数,openCamera函数待编写),使用self?调用是因为闭包会捕获内外参数,必须弱引用。weak self和?应该是一起用的。

然后写一个函数popMoreView,点击加号按钮时展开界面。

先为加号按钮添加事件。

swift 复制代码
chatInputView.plusButton.addTarget(self, action: #selector(popMoreView), for: .touchUpInside)

为了实现展开时注销键盘、收起时展开键盘的效果,所以用一个私有变量定义本页的状态。

聊天页面的默认状态是收起界面,在展开页面时将moreView添加到页面中,并将对话框顶上去。

swift 复制代码
private var isMore = false
swift 复制代码
@objc func popMoreView(){
        if isMore {
            
        } else {
            
        }
        isMore = !isMore
    }

isMore = !isMore来切换页面的状态,每次点击时切换一次

swift 复制代码
   @objc func popMoreView(){
        if isMore {
            self.moreView.removeFromSuperview()
            self.chatInputView.chattextField.becomeFirstResponder()
        } else {
            self.view.addSubview(self.moreView)
            self.chatInputView.chattextField.resignFirstResponder()
            UIView.animate(withDuration: 0.5) {
                self.chatInputView.frame = .init(origin: .init(x: 0, y: SCREENHEIGHT - 100 - 98), size: self.chatInputView.frame.size)
                self.chat_scrollView.frame = .init(origin: .init(x: 0, y: 100 - 98), size: self.chat_scrollView.frame.size)
            }
        }
        isMore = !isMore
    }

self.moreView.removeFromSuperview()

self.chatInputView.chattextField.becomeFirstResponder()

默认状态下,移除功能栏,并将输入框设置为第一响应器。

功能栏展开时,添加功能栏视图到界面,并重新设置输入框视图与滚动视图。

设置照片按钮功能

设置照片按钮功能,需要:

  1. 请求相册权限,检查相册权限,打开选择器获取照片,并关闭选择器。
  2. 设置照片的Cell,将照片转换成png并写入沙盒中。
  3. 根据照片比例重新渲染照片
  4. 在页面内创建照片的Cell,并将其发送到消息记录内
  5. 练习:点击照片消息时放大照片,再次点击则回到原来的页面

请求相册权限,检查相册权限,打开选择器获取照片,并关闭选择器

像请求语音权限一样设置请求与请求信息

Privacy - Photo Library Usage Description

我们需要您的允许来访问相册,以便选择照片。

使用相册/相机需要导入photos库

swift 复制代码
import Photos

检查相册权限时,有三种情况:

  1. 已授权相册权限:获取照片
  2. 未授权相册权限:请求权限,成功则获取照片,否则提示无权限
  3. 拒绝授权:提示无权限

获取相册权限

swift 复制代码
func openPhotoLib(){
        let status = PHPhotoLibrary.authorizationStatus()
    }

编写包括的所有情况

swift 复制代码
        let status = PHPhotoLibrary.authorizationStatus()
        if status == .authorized {
            //有权限时
            DispatchQueue.main.async {
             
            }
        } else if status == .denied {
            //拒绝(了)授权时
            
        } else {
            //无权限时,请求授权
            PHPhotoLibrary.requestAuthorization { [weak self] status in
                if status == .authorized {
                    
                } else {
                    
                }
            }
            
            
        }
    }
  • 使用 [weak self]:防止强引用循环,从而避免内存泄漏。
  • 这个status是闭包的参数,这里是要捕获status的状态才能继续操作,所以要加上参数。
  • 安全访问 :使用 self? 安全地访问 self,防止在 self 已被释放时触发崩溃。
有权限时,打开选择器获取照片,编写**presentImagePickerController函数,设置获取源为相册**
swift 复制代码
func presentImagePickerController(){
        let imagePickerController = UIImagePickerController()
        imagePickerController.delegate = self
        imagePickerController.sourceType = .photoLibrary //设置源为相册
        present(imagePickerController, animated: true, completion: nil)
    }

let imagePickerController = UIImagePickerController()创建一个照片选择器

imagePickerController.delegate = self将delegate属性设置为当前类的实例,实现这些协议,可以处理用户选择的图片或视频,并响应选择器中的操作(例如取消选择或完成选择)

系统提示,为遵循协议,这意味着当前的视图控制器需要遵循 UIImagePickerControllerDelegateUINavigationControllerDelegate 协议

imagePickerController.sourceType = .photoLibrary设置源为相册

present(imagePickerController, animated: true, completion: nil)将该选择器推入界面

无权限时,提示无权限,编写对应函数。

这个提示本质是一个提示框

swift 复制代码
func showPermissionAlert(){
        let alert = UIAlertController(title: "访问相册", message: "请在设置中允许访问相册", preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "好的", style: .default, handler: nil))
        present(alert, animated: true, completion: nil)
    }

let alert = UIAlertController(title: "访问相册", message: "请在设置中允许访问相册", preferredStyle: .alert)

创建一个 UIAlertController 的实例,命名为 alert。这个控制器是用于显示警告和提示对话框的

alert.addAction(UIAlertAction(title: "好的", style: .default, handler: nil))

alert 中添加一个操作按钮。这是 UIAlertAction 的实例

alert: 这是一个已经创建好的 UIAlertController 实例,用于显示警告或提示信息。

title: "好的": 这是按钮上显示的文本,当用户看到弹出框时,会看到这个中文"好的"按钮。

style: .default: 这是按钮的样式。.default 表示这是一个普通的按钮,用户点击后将触发相应的操作。还有其他样式,比如 .cancel 和 .destructive。

handler: nil: 这是一个闭包,在用户点击这个按钮时会被调用。由于这里设置为 nil,表示点击该按钮时不会执行任何操作。如果您需要在点击时执行特定的功能,可以传入一个闭包。

然后继续编写openPhotoLib(检查权限方法)

请求权限之后会进入异步线程,所以要回到主线程才能执行其他操作。

swift 复制代码
    func openPhotoLib(){
        let status = PHPhotoLibrary.authorizationStatus()
        if status == .authorized {
                self.presentImagePickerController()
        } else if status == .denied {
            //拒绝
            self.showPermissionAlert()
        } else {
            //未授权时,请求授权
            PHPhotoLibrary.requestAuthorization { [weak self] status in
                if status == .authorized {
                    DispatchQueue.main.async {
                        self?.presentImagePickerController()
                    }
                } else {
                    DispatchQueue.main.async {
                        self?.showPermissionAlert()
                    }
                }
            }
        }
    }
选择完毕之后关闭选择器,输入didcancel即可弹出该函数。
swift 复制代码
//关闭选择器
    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
        picker.dismiss(animated: true)
    }

设置照片的Cell,将照片转换成png、修改尺寸。

照片消息由头像和照片按钮组成。

头像不变

swift 复制代码
 lazy var user_head: UIImageView = {
        var user_head = UIImageView(frame: .init(origin: .init(x: SCREENWIDTH - 16 - 50, y: 0), size: .init(width: 50, height: 50)))
        user_head.layer.cornerRadius = 25
        user_head.layer.masksToBounds = true
        return user_head
    }()

图片按钮

swift 复制代码
lazy var imgBtn: UIButton = {
        var imgBtn = UIButton(frame: .init(origin: .init(x: SCREENWIDTH - (16 + 8 + 30), y: 0), size: .init(width: 30, height: 30)))
        imgBtn.layer.cornerRadius = 8
        imgBtn.layer.masksToBounds = true
        imgBtn.backgroundColor = .systemGreen
        //imgBtn.addTarget(self, action: #selector(expandThisImg), for: .touchUpInside)
        
        return imgBtn
    }()

16是边距,8是与消息框的距离,30是头像宽度

获取选择的图片地址,并用使用 Data(contentsOf:) 方法来获取图片,并对其重新渲染。

获取图片地址的方式同录音一致。该方法需要用到Model中存放的数据,所以把Model作为参数传进来。

swift 复制代码
func reDraw(with chatModel: ChatTextModel){
        
    }

获取资源名的函数非常常用,将其写在Public中。

swift 复制代码
static func getDocumentsDirectory() -> URL {
        return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
    }
    //获取存放文档的沙盒位置
    
    static func fullFilePath(file_name: String) -> URL {
        return getDocumentsDirectory().appendingPathComponent(file_name)
    }
    //获取文件名(URL)

那么我们需要先获取存储文档的沙盒地址,再拼起来获取图片文件名(当然,此时还未设置,但我们打算将文件名存在Model里的filePath中,就像语音一样,这里先写)。

Data(contentsOf:) 从指定的 URL 加载数据的一种方法。它非常适合快速读取文件内容,例如本地文件或网络资源。所以,contentsOf后面要传入的是一个URL。

顺手设置一下头像的图片,当然也可以在外面设。

swift 复制代码
user_head.image = UIImage(named: chatModel.mineHead)
let data = try? Data(contentsOf: URL)
swift 复制代码
let data = try? Data(contentsOf: Public.fullFilePath(file_name: chatModel.filePath))

使用try?避免获取文件失败。

现在获取了图片,使用从上一步加载的数据 (data),创建一个UIImage的实例。如果获取成功,则执行闭包内容。闭包就是重新设置图片属性的内容,暂不编写。

swift 复制代码
if let image = UIImage(data: data ?? Data()){
            
        }

data ?? Data() 确保如果 data 为 nil,则提供一个空的数据对象。避免了 UIImage(data:) 的崩溃,因为提供一个空的 Data 对象以防数据加载不成功。

根据图片比例重新设置图片宽高

内容比较复杂,额外开一个函数getSize来操作。既然是要根据图片重新设置宽高,需要用到的参数是图片img: UIImage返回的就是CGSize

使用static修饰,这样就可以在没有该类(ChatImageCell)实例的情况下,随意使用该方法。这里去掉static就不能在其他函数里随意调用它了,因为现在其实还没有ChatImageCell的实例。

swift 复制代码
static func getSize(with img: UIImage) -> CGSize {
        
    }

图片不可能无限大,先规定其最长的长度。使用static修饰因为这是一个不能被改变的量。

swift 复制代码
static let maxLength = 186.0

重新设置宽高思路:

如果宽(width)大于高(height),最大值为宽,高用宽高比计算。

如果高(height)大于宽(width),最大值为高,宽用宽高比计算。

返回reDraw方法继续渲染图片
swift 复制代码
if let image = UIImage(data: data ?? Data()){
            let size = ChatImageCell.getSize(with: image)
            self.imgBtn.frame = .init(origin: .init(x: SCREENWIDTH - 50 - 16 - 8 - size.width, y: 0), size: size)
            self.imgBtn.setImage(image, for: .normal)
        }

50是头像的宽,16是边距,8是头像与对话框的边距,size.width是自己的宽

返回ChatViewController,调用被用户选择的图片。

使用imagePickerController...didFinishPickingMediaWithInfo函数来调用这张图片。输入didFinish就能出来。

func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any])该方法在用户选择图片后调用,主要就处理这张图片。

swift 复制代码
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        picker.dismiss(animated: true)
    }

使用picker.dismiss(animated: true)把选择框关掉。

获取原始图片

获取成功则执行闭包内操作

swift 复制代码
if let image = info[.originalImage] as? UIImage { }
  1. info : 这是一个字典,包含用户在图像选择器中所选择的媒体的信息。其键为 UIImagePickerController.InfoKey 类型,值为任意类型(Any)。
  2. info[.originalImage] :
    • 这里使用了 info 字典的键 UIImagePickerController.InfoKey.originalImage 来访问用户所选的原始图像。
    • info[.originalImage] 将返回一个可选值(Optional),它可能包含用户选择的图像(如果用户选择了图像)或者为 nil(如果没有选择图像)。
  3. as? UIImage :
    • 这一部分尝试将 info[.originalImage] 的值转换为 UIImage 类型。
    • as? 是一个"条件类型转换"的操作符,表示如果转换成功,它将返回一个非可选的 UIImage;如果转换失败(即 info[.originalImage] 的值不是 UIImage 类型或者为 nil),则 image 将为 nil

将图片转换为PNG格式

let data = image.pngData() 用于将 UIImage 类型的图像转换为 PNG 格式的数据

swift 复制代码
if let image = info[.originalImage] as? UIImage {
            let data = image.pngData()

命名图片

前面Cell里我们需要获取这张图片的名字及URL,所以这里像命名语音一样命名用户选择的图片。

并将其拼成对应的URL。

swift 复制代码
 let img_name = Public.generateUniqueTimestamp() + ".png"
 let url = Public.getDocumentsDirectory().appendingPathComponent(img_name)
将其写入沙盒中

write(to: url):是 Data 类型的方法,试图将该数据写入指定的文件路径(url),创建或覆盖目标文件。

考虑到写入失败,或获取不到图片的情况,使用try?和data?

swift 复制代码
try? data?.write(to: url)

将图片添加到存储数据的Model中

将目标图片添加到Model中。注意其中type属性为"image",filePath是文件名,为计算得出的名字。

注:这里处理图片,并得出filePath名称之后,ChatImageCell中的reDraw方法就可以对图片进行重新渲染了。

swift 复制代码
let messageID = "\(self.chatModels.count + 1)"
            let model = chatModels[0]
            let newmodel = ChatTextModel(chatID: self.messCellmodel.chatID, messageID: messageID, content: "", target: "mine", mineHead: model.mineHead, otherHead: model.otherHead, type: "image", filePath: img_name)

图片消息之间也要设置高度,到heightForRowAt函数设置cell之间的间距。

要设置间距,必须获得图片的高,这里图片的高度不是固定的,而是随着图片尺寸变化的。

1.用Data(contentsOf:)来获取路径下的图片→

swift 复制代码
let data = try? Data(contentsOf: Public.fullFilePath(file_name: model.filePath)

2.创建为UIImage实例,这里使用if let值绑定,在闭包内处理两种情况(无图片和有图片时)

swift 复制代码
if let image = UIImage(data: data ?? Data()) {}

3.获取图片高度,在图片高度加上间距(36)

swift 复制代码
let size = ChatImageCell.getSize(with: image)
                return size.height + 36

4.处理图片崩溃的情况

swift 复制代码
if model.type == "image" {
            let data = try? Data(contentsOf: Public.fullFilePath(file_name: model.filePath))
            if let image = UIImage(data: data ?? Data()) {
                let size = ChatImageCell.getSize(with: image)
                return size.height + 36
            } else {
                return 30 + 36
            }

创建每个图片信息Cell

swift 复制代码
else {
            let cell: ChatImageCell = tableView.cellForRow(at: indexPath) as? ChatImageCell ?? ChatImageCell(style: .default, reuseIdentifier: "ChatImageCell")
            cell.reDraw(with: model)
            cell.selectionStyle = .none
            cell.backgroundColor = .clear
            return cell
        }

最终效果:

设置相机按钮功能

在系统处添加权限请求

Privacy - Camera Usage Description

检查相机权限,如无则请求相机权限openCamera

swift 复制代码
func openCamera(){
        let status = PHPhotoLibrary.authorizationStatus()
        if status == .authorized {
            self.camera_presentImagePickerController()
            
        } else if status == .denied {
            self.showPermissionAlert()
            
        } else {
            PHPhotoLibrary.requestAuthorization{ [weak self] status in
                if status == .authorized {
                    DispatchQueue.main.async {
                    self?.camera_presentImagePickerController()
                    }
        } else {
                DispatchQueue.main.async {
                    self?.camera_presentImagePickerController()
                }
              }
            }
        }
    }
有权限时,打开选择器获取照片,编写**camera_presentImagePickerController函数,设置获取源为相机**

从照相机获取图片

swift 复制代码
func camera_presentImagePickerController(){
        let camera_presentImagePickerController = UIImagePickerController()
        camera_presentImagePickerController.delegate = self
        camera_presentImagePickerController.sourceType = .camera //设置源为相机
        present(camera_presentImagePickerController, animated: true, completion: nil)
    }

由于通过相机选择的也是图片,所以这张图片同样会被didFinish...函数捕获,然后走和相册选择一样的流程。

func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {}

把该功能绑定到点击按钮MoreView的索引处,确保点击图标时功能被调用。

swift 复制代码
 lazy var moreView: MoreView = {
        let moreView = MoreView(frame: .init(x: 0, y: SCREENHEIGHT - 98 - bottomSafeArea, width: SCREENWIDTH, height: 98 + bottomSafeArea))
        moreView.backgroundColor = .lightGray
        moreView.btnClick = { [weak self] idx in
            print(idx)
            if idx == 0 {
                self?.openPhotoLib()
            } else if idx == 1 {
                self?.openCamera()
            } else {
                
            }
        }

效果图:

练习:点击发送的图片,放大

图片本身是按钮,点击时添加一个全屏的UIControl,并显示图片,再次点击时将其从父视图移除。

先新开一页设置一下这个自定义的UIControl,背景为纯黑,点击时从父视图移除。

objectivec 复制代码
import UIKit

class EnlargeImg: UIControl {
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupUI()
    }
    func setupUI(){
        self.backgroundColor = .black
        self.addTarget(self, action: #selector(removeEnlargeImg), for: .touchUpInside)
    }
    @objc func removeEnlargeImg(){
        self.removeFromSuperview()
    }
    
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

回到ChatViewController即为聊天主页面,使用参数为file_name的方法,执行放大图片与添加图片进UIControl的操作。

之所以把file_name设为参数,是因为使用cell中cilckblock进行回调的时候,也可以传递一个参数,而这个参数是用来读取文件名的,所以类型是String。

swift 复制代码
func enlargeImage(file_name: String) {
        
    }

通过图片地址获取图片,获取到图片之后,进行一些操作

swift 复制代码
func enlargeImage(file_name: String) {
        let data = try? Data(contentsOf: Public.fullFilePath(file_name: file_name))
        if let image = UIImage(data: data ?? Data()){
        //获取到图片之后,进行一些操作
        }
    }

现在获取到了图片,将其创建为UIImageView,图片设置为这张图

objectivec 复制代码
let imgView = UIImageView(frame: .init(origin: .zero, size: .zero))
imgView.image = image

观察需求,图片放大后的宽度等于屏幕宽度SCREENWIDTH,而长度随比例适应变化。

也就是说放大后的宽度乘以比例就可以得出图片放大后的高度。

先取原来图片的宽高计算比例。

swift 复制代码
let originalWidth = image.size.width
let originalHeight = image.size.height
let newHeight = (SCREENWIDTH / originalWidth) * originalHeight

再重新设置ImageView的size

swift 复制代码
imgView.frame = .init(origin: .zero, size: .init(width: SCREENWIDTH, height: newHeight))

创建自定义UIControl**EnlargeImg的实例,将设置好的imgView添加进去,并居中放置(中心相同)**

swift 复制代码
let bgControl = EnlargeImg(frame: .init(origin: .zero, size: .init(width: SCREENWIDTH, height: SCREENHEIGHT)))
bgControl.addSubview(imgView)
imgView.center = bgControl.center

最后将这个实例添加到主视图中

swift 复制代码
func enlargeImage(file_name: String) {
        let data = try? Data(contentsOf: Public.fullFilePath(file_name: file_name))
        if let image = UIImage(data: data ?? Data()){
            let imgView = UIImageView(frame: .init(origin: .zero, size: .zero))
            imgView.image = image
            let originalWidth = image.size.width
            let originalHeight = image.size.height
            let newHeight = (SCREENWIDTH / originalWidth) * originalHeight
            imgView.frame = .init(origin: .zero, size: .init(width: SCREENWIDTH, height: newHeight))
            
            let bgControl = EnlargeImg(frame: .init(origin: .zero, size: .init(width: SCREENWIDTH, height: SCREENHEIGHT)))
            bgControl.addSubview(imgView)
            imgView.center = bgControl.center
            self.view.addSubview(bgControl)
        }
    }

在cell中使用ImageCell里设置好的空闭包clickBlock(同时也作为imgBtn的点击事件)。

然后调用刚刚设置好的函数传进这个空闭包里,并利用model将filePath作为参数传进函数中。

swift 复制代码
else {
            let cell: ChatImageCell = tableView.cellForRow(at: indexPath) as? ChatImageCell ?? ChatImageCell(style: .default, reuseIdentifier: "ChatImageCell")
            cell.reDraw(with: model)
            cell.clickBlock = { [weak self] in
                self?.enlargeImage(file_name: model.filePath)
            //把图片放大到全屏,点击消失,无需动画和其它功能
            }
            cell.selectionStyle = .none
            cell.backgroundColor = .clear
            return cell
        }
相关推荐
_code_bear_1 小时前
OpenSpec CLI 与 OPSX 工作流说明
前端·后端·架构
parade岁月1 小时前
开源一个 Vue 3 Table:API 学 antdv、主题学 Nuxt UI
前端·vue.js
JiaWen技术圈2 小时前
Web 安全深入审计检查清单
前端·安全
江米小枣tonylua2 小时前
从红绿灯到方向盘:TDD 在 AI 时代的新角色
前端·设计模式·ai编程
祀爱2 小时前
Asp.net core+ Layui 项目中编辑按钮传递数据的方法
前端·c#·asp.net·layui
DanCheOo2 小时前
Prompt 工程化管理:从散落在代码里到版本化、可测试、可回滚
前端·ai编程
涛涛ing2 小时前
Vue 3.5 下一站:cached 提案,重新定义响应式缓存
前端
胖子不胖2 小时前
svg之viewBox
前端
吹牛不交税2 小时前
tree-transfer-vue3 前端插件安装问题解决(--legacy-peer-deps)(其他插件可考虑)适用
前端·javascript·vue.js