【手把手带你搓组件库】从零开始实现Element Plus

从零开始实现Element Plus

  • 前言
  • 亮点
  • 项目搭建
    • 1、创建项目
      • 初始化
      • monorepo
      • 创建 .gitignore
      • 目录结构
      • 安装基础依赖
      • 配置文件
      • 创建各个分包入口
        • utils
        • components
        • core
        • play
        • theme
    • 2、创建VitePress文档
    • 3、部署到Github Actions
        • 生成 GH_TOKEN
        • GitHub Page 演示
    • 4、总结

前言

在本文中,将手把手带你从零开始实现一个类似于Element Plus 的组件库。Element Plus 是一个非常流行的Vue UI 组件库,我们将尝试实现一些常见的组件,如基础组件、反馈组件、表单组件等。让我们开始吧!

亮点

  • Vite+Vitest+Vitepress 工具链 (项目构建+测试+项目文档)
  • monorepo 分包管理
  • GitHub actions 实现 CI/CD 自动化部署
  • 大模型辅助:使用大模型辅助完成需求分析,设计思路,快速实现组件,提升开发效率
  • 发布开箱即用的npm包

项目搭建

1、创建项目

初始化

mkdir Wannaer-element
cd Wannaer-element
git init
pnpm init

在这里插入图片描述

monorepo

monorepo ,那就先创建一个 pnpm-workspace.yaml 文件。

mkdir packages
echo -e 'packages:\n  - "packages/*"' > pnpm-workspace.yaml
// 在Windows系统中,echo命令默认不支持像在Linux系统中那样使用"-e"参数来表示换行符
// 创建完成后,手动操作换行
echo 'packages:\n  - "packages/*"' > pnpm-workspace.yaml

// 如果出现 pnpm: null byte is not allowed in input (1:4) 可能是有隐藏字符问题
packages:
  - "packages/*"

在这里插入图片描述

创建 .gitignore

# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
coverage
dist
dist-ssr
*.local

/cyperss/videos/
/cypress/srceenshots/

.vitepress/dist
.vitepress/cache

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

目录结构

为了目录扁平,就只创建 packages 这么一个 pnpm 工作区,下面大概说一下这个项目计划的分包结构

- components # 组件目录
- core # npm 包入口
- docs # 文档目录
- hooks # 组合式API hooks 目录
- play # 组件开发实验室
- theme # 主题目录
- utils # 工具函数目录
// 创建这些目录
cd packages
// 分别初始化这些目录 在 packages 目录下创建 init.shell 内容如下
for i in components core docs hooks theme utils; do
  mkdir $i
  cd $i
  pnpm init
  cd ..
done
// 执行后删除 init.shell

在这里插入图片描述
这波 play 目录先留着,我们用 vite 来创建一个 vue 开发项目

pnpm create vite play --template vue-ts

创建完成后分别到 各个分包目录中修改 package.json 中的 name,防止重名

- core # npm 包入口
	"name": "Wannaer-element",
- components # 组件目录
	"name": "@Wannaer-element/components",
- docs # 文档目录
	"name": "@Wannaer-element/docs",
- hooks # 组合式API hooks 目录
	"name": "@Wannaer-element/hooks",
- play # 组件开发实验室
	"name": "@Wannaer-element/play",
- theme # 主题目录
	"name": "@Wannaer-element/theme",
- utils # 工具函数目录
	"name": "@Wannaer-element/utils",
- 根目录
	“name”: "@Wannaer-element/workspace"

安装基础依赖

在根目录 安装
-Dw表示在package.json文件中配置的scripts中运行特定的脚本命令,xxx为脚本命令的名称。
-w表示在指定的工作区目录中运行特定的脚本命令,xxx为脚本命令的名称。

// 开发依赖
pnpm add -Dw typescript@^5.2.2 vite@^5.1.4 vitest@^1.4.0 vue-tsc@^1.8.27 postcss-color-mix@^1.1.0 postcss-each@^1.1.0 postcss-each-variables@^0.3.0 postcss-for@^2.1.1 postcss-nested@^6.0.1 @types/node@^20.11.20 @types/lodash-es@^4.17.12 @vitejs/plugin-vue@^5.0.4 @vitejs/plugin-vue-jsx@^3.1.0 @vue/tsconfig@^0.5.1

// 非开发依赖
pnpm add -w lodash-es@^4.17.21 vue@^3.4.19

在 根目录 package.json 中添加如下内容 添加一下子包的依赖

{
  "dependencies": {
    "Wannaer-element": "workspace:*",
    "@Wannaer-element/hooks": "workspace:*",
    "@Wannaer-element/utils": "workspace:*",
    "@Wannaer-element/theme": "workspace:*"
  }
}
  • components
pnpm add -D @vue/test-utils@^2.4.5 @vitest/coverage-v8@^1.4.0 jsdom@^24.0.0 --filter @Wannaer-element/components
pnpm add @popperjs/core@^2.11.8 async-validator@^4.2.5 --filter @Wannaer-element/components
  • core
// 在 core/package.json 中添加如下内容
{
  "dependencies": {
    "@Wannaer-element/components": "workspace:*"
  }
}
  • docs
pnpm add -D vitepress@1.0.0-rc.44 --filter @Wannaer-element/docs
  • play
    将 play/package.json 中冗余部分删除, 并且删除掉tsconfig.jsontsconfig.node.json
{
  "name": "@Wannaer-element/play",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vue-tsc && vite build",
    "preview": "vite preview"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^5.0.4"
  }
}

配置文件

在根目录创建一些必要额配置文件,比如刚才删除play中的ts配置,我们在根目录配置

  • tsconfig.json
{
  "extends": "@vue/tsconfig/tsconfig.dom.json",
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "module": "ESNext",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "preserve",
    "jsxImportSource": "vue",

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true
  },
  "include": ["packages/**/*.ts", "packages/**/*.tsx", "packages/**/*.vue"]
}
  • tsconfig.node.json
{
  "extends": "@tsconfig/node18/tsconfig.json",
  "include": ["packages/**/**.config.ts"],
  "compilerOptions": {
    "composite": true,
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "types": ["node"]
  }
}
  • postcss.config.cjs
/* eslint-env node */
module.exports = {
  plugins: [
    require("postcss-nested"),
    require("postcss-each-variables"),
    require("postcss-each")({
      plugins: {
        beforeEach: [require("postcss-for"), require("postcss-color-mix")],
      },
    }),
  ],
};

配置完成后,重新安装一下依赖 pnpm install 执行之前更新的部分操作

创建各个分包入口

utils

在utils文件夹 新建一个文件 install.ts 用于 vue plugin 安装的一系列操作

import type { App, Plugin } from "vue";
import { each } from "lodash-es";

type SFCWithInstall<T> = T & Plugin;

export function makeInstaller(components: Plugin[]) {
  const install = (app: App) =>
    each(components, (c) => {
      app.use(c);
    });

  return install;
}

export const withInstall = <T>(component: T) => {
  (component as SFCWithInstall<T>).install = (app: App) => {
    const name = (component as any)?.name || "UnnamedComponent";
    app.component(name, component as SFCWithInstall<T>);
  };
  return component as SFCWithInstall<T>;
};

创建一个utils入口 index.ts 文件 用于导出utils所有方法

export * from "./install";

在这里插入图片描述

components

创建 index.ts 以及第一个基础组件 Button 组件目录

// index.ts
export * from './Button'
//  Button 目录 Button.vue
<template>
  <button style="color: red">this is a button</button>
</template>

<script setup lang="ts">
defineOptions({
  name: "WButton",
});
</script>

<style scoped></style>
// Button 目录 index.ts
import Button from "./Button.vue";
import { withInstall } from "@Wannaer-element/utils";

export const WButton = withInstall(Button);

在这里插入图片描述

core

创建 index.ts 、components.ts

// components.ts

import { ErButton } from "@toy-element/components";
import type { Plugin } from "vue";

export default [ErButton] as Plugin[];
import { makeInstaller } from "@toy-element/utils";
import components from "./components";

const installer = makeInstaller(components);

export * from "@toy-element/components";
export default installer;

在这里插入图片描述

play

在main.ts 中 引入了我们刚刚写好的"Wannaer-element"的自定义元素库,并在App.vue中使用。
通过createApp(App).use(WElement).mount(“#app”)这行代码,将"Wannaer-element"库应用到了Vue实例中,并挂载到了id为"app"的DOM元素上。
在这里插入图片描述
在根目录的package.json中配置

  "scripts": {
    "dev": "pnpm --filter @Wannaer-element/play dev",
    "test": "echo \"Error: no test specified\" && exit 1"
  }

它定义了一个名为"dev"的脚本命令。在这个命令中,使用了pnpm工具,并通过"–filter @Wannaer-element/play"参数指定了要过滤的包,然后执行"dev"命令。这段代码的作用是在开发过程中使用pnpm工具来过滤特定的包并执行相应的开发命令。

配置完成后运行 pnpm dev 可以查看到我们刚刚封装好的 Button 虽然很简陋 接下来我们进行样式的修改,让他变得更加美观
在这里插入图片描述

theme

创建 index.css 、reset.css 在 theme/index.css 中导入 reset.css

/** index.css */
@import "./reset.css";
/** reset.css */
body {
  font-family: var(--wan-font-family);
  font-weight: 400;
  font-size: var(--wan-font-size-base);
  line-height: calc(var(--wan-font-size-base) * 1.2);
  color: var(--wan-text-color-primary);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  -webkit-tap-highlight-color: transparent;
}

a {
  color: var(--wan-color-primary);
  text-decoration: none;

  &:hovwan,
  &:focus {
    color: var(--wan-color-primary-light-3);
  }

  &:active {
    color: var(--wan-color-primary-dark-2);
  }
}

h1,
h2,
h3,
h4,
h5,
h6 {
  color: var(--wan-text-color-regular);
  font-weight: inhwanit;

  &:first-child {
    margin-top: 0;
  }

  &:last-child {
    margin-bottom: 0;
  }
}

h1 {
  font-size: calc(var(--wan-font-size-base) + 6px);
}

h2 {
  font-size: calc(var(--wan-font-size-base) + 4px);
}

h3 {
  font-size: calc(var(--wan-font-size-base) + 2px);
}

h4,
h5,
h6,
p {
  font-size: inhwanit;
}

p {
  line-height: 1.8;

  &:first-child {
    margin-top: 0;
  }

  &:last-child {
    margin-bottom: 0;
  }
}

sup,
sub {
  font-size: calc(var(--wan-font-size-base) - 1px);
}

small {
  font-size: calc(var(--wan-font-size-base) - 2px);
}

hr {
  margin-top: 20px;
  margin-bottom: 20px;
  bordwan: 0;
  bordwan-top: 1px solid var(--wan-bordwan-color-lightwan);
}

最后改 package.json 中 入口为 index.css 在 core/index.ts 中导出我们的 theme
在这里插入图片描述

2、创建VitePress文档

可以直接参考官方文档

npx vitepress init

在这里插入图片描述

// 运行查看效果
pnpm docs:dev

在这里插入图片描述
我们改一下package.json指令 配置后统一可以从根目录运行

// docs目录 package.json
  "scripts": {
    "dev": "vitepress dev",
    "build": "vitepress build",
    "preview": "vitepress preview"
  },
// 根目录 package.js
  "scripts": {
    "dev": "pnpm --filter @Wannaer-element/play dev",
    "docs:dev": "pnpm --filter @Wannaer-element/docs dev",
    "docs:build": "pnpm --filter @Wannaer-element/docs build",
    "docs:preview": "pnpm --filter @Wannaer-element/docs preview",
    "test": "echo \"Error: no test specified\" && exit 1"
  },

接下来我们需要将 VitePress文档部署到 GitHub Actions
所以需要配置一下 docs目录下vitepress => config.mts 添加一个 base: “/wan-element”,解决部署后样式丢失问题

import { defineConfig } from "vitepress";

// https://vitepress.dev/reference/site-config
export default defineConfig({
  title: "Wan-Element",
  description: "高仿 ElementPlus 组件库",
  base: "/wan-element",
  themeConfig: {
    // https://vitepress.dev/reference/default-theme-config
    nav: [
      { text: "Home", link: "/" },
      { text: "Examples", link: "/markdown-examples" },
    ],

    sidebar: [
      {
        text: "Examples",
        items: [
          { text: "Markdown Examples", link: "/markdown-examples" },
          { text: "Runtime API Examples", link: "/api-examples" },
        ],
      },
    ],

    socialLinks: [
      { icon: "github", link: "https://github.com/vuejs/vitepress" },
    ],
  },
});

3、部署到Github Actions

创建一个 .github/workflows/deploy.yml 文件,内容如下

name: deploy

on:
  push:
    branches:
      - master

jobs:
  test:
    name: Run Lint and Test
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repo
        uses: actions/checkout@v3

      - name: Setup Node
        uses: actions/setup-node@v3

      - name: Install pnpm 
        run: npm install -g pnpm

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Run tests
        run: npm run test

  build:
    name: Build docs
    runs-on: ubuntu-latest
    needs: test

    steps:
      - name: Checkout repo
        uses: actions/checkout@v3

      - name: Setup Node
        uses: actions/setup-node@v3

      - name: Install pnpm
        run: npm install -g pnpm

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Build docs
        run: npm run docs:build

      - name: Upload docs
        uses: actions/upload-artifact@v3
        with:
          name: docs
          path: ./packages/docs/.vitepress/dist

  deploy:
    name: Deploy to GitHub Pages
    runs-on: ubuntu-latest
    needs: build
    steps:
      - name: Download docs
        uses: actions/download-artifact@v3
        with:
          name: docs

      - name: Deploy to GitHub Pages
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GH_TOKEN }}
          publish_dir: .

secrets.GH_TOKEN 需要到Github 上面去生成

接下来去 github 创建一个仓库
在这里插入图片描述
复制仓库地址

https://github.com/Manba0/wan-element.git
git remote add origin https://github.com/Manba0/wan-element.git

git add .

git commit -m ":data: first commit"
生成 GH_TOKEN

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

最后将刚刚提交的代码 push到Github仓库

git push origin master

如果 push 出现一下报错
fatal: unable to access ‘https://github.com/XXXX/XXXX.git/’: Failed to connect to github.com port 443 after 21067 ms: Couldn’t connect to server

有可能你的gitbub之前设置过代理,只需分别执行如下代码即可:

git config --global --unset http.proxy
git config --global --unset https.proxy

提交成功后 发现 Settings 中Page 没有找到访问的链接,我们查看 Actions 发现 Run tests 没有通过, 因为我们根目录下 package.json 中的 test 指令 "test": "echo \"Error: no test specified\" && exit 1",修改成 "test": "echo 'todo'"重新提交
在这里插入图片描述
在这里插入图片描述

这样就是成功了 我们直接去看Settings中的page https://manba0.github.io/wan-element/
在这里插入图片描述
在这里插入图片描述

GitHub Page 演示

在这里插入图片描述

4、总结

到此我们就已经全流程跑通了 接下来就是完善组件内容了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/644423.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

SVM原问题与对偶问题

目的&#xff1a;求出我们的f(X)&#xff0c;它代表着我们X映射到多维的情况&#xff0c;能够帮我们在多维中招到超平面进行分类。 1.优化问题&#xff1a; 1.1推荐好书&#xff1a; 1.2 优化理论中的原问题&#xff1a; 原问题和限制条件如下&#xff1a; 这是一个泛化性…

无人机飞手:ASFC无人机和航模爱好者证书详解

ASFC无人机和航模爱好者证书是由中国航空运动协会&#xff08;ASFC&#xff09;颁发的一种无人机操作资格认证。这种证书在无人机和航模爱好者群体中享有广泛的认可度&#xff0c;并被视为操作无人机的一种重要资质。 ASFC证书的定义和用途十分明确。它是民航局颁发的民用无人驾…

《最新出炉》系列入门篇-Python+Playwright自动化测试-40-录制生成脚本

宏哥微信粉丝群&#xff1a;https://bbs.csdn.net/topics/618423372 有兴趣的可以扫码加入 1.简介 各种自动化框架都会有脚本录制功能&#xff0c; playwright这么牛叉当然也不例外。很早之前的selenium、Jmeter工具&#xff0c;发展到每种浏览器都有对应的录制插件。今天我们…

基于 vuestic-ui 实战教程

1. 前言简介 Vuestic UI是一个基于开源Vue 3的UI框架。它是一个MIT许可的UI框架&#xff0c;提供了易于配置的现成前端组件&#xff0c;并加快了响应式和快速加载Web界面的开发。它最初于2021年5月由EpicMax发布&#xff0c;这就是今天的Vuestic UI。 官网地址请点击访问 体验…

【DASBOOK】Mark loves cat

文章目录 一、工具下载二、Mark loves cat解题感悟 一、工具下载 克隆dirsearch仓库&#xff1a; git clone https://github.com/maurosoria/dirsearch.git下载 githack工具 git clone https://github.com/lijiejie/GitHack.git二、Mark loves cat 用dirsearch扫描目录&…

urllib_post请求_百度翻译之详细翻译

百度翻译有一个详细翻译的接口&#xff1a; post请求&#xff1a; 请求参数&#xff08;较多&#xff09;&#xff1a; 打印之后&#xff0c;发现有问题&#xff1a; 改一下请求头&#xff1a; 将Accept-Encoding注释掉&#xff0c;因为我们使用的是utf-8编码&#xff1a; 加上…

STM32F1之OV7725摄像头·像素数据输出时序、FIFO 读写时序以及摄像头的驱动原理详解

STM32F1之OV7725摄像头-CSDN博客 STM32F1之I2C通信-CSDN博客 目录 1. 像素数据输出时序 2. FIFO 读写时序 2.1 写时序 2.2 读时序 3. 摄像头的驱动原理 1. 像素数据输出时序 主控器控制 OV7725 时采用 SCCB 协议读写其寄存器&#xff0c;而它输出图像时则使用 VGA 或…

无线技术整合到主动噪声控制(ANC)增强噪声降低性能

主动噪声控制&#xff08;ANC&#xff09;已成为一种广泛使用的降噪技术。基本原理是通过产生与外界噪音相等的反向声波&#xff0c;将噪音中和&#xff0c;从而达到降噪的效果。ANC系统通常包括以下几个部分&#xff1a;参考麦克风、处理芯片、扬声器和误差麦克风。参考麦克风…

设计循环队列(C语言)怎会如此简单!!!

目录 题目题目分析 解答结构体初始化判空判满插入删除去队头数据取队尾数据队列的销毁 题目 链接: 题目 设计你的循环队列实现。 循环队列是一种线性数据结构&#xff0c;其操作表现基于 FIFO&#xff08;先进先出&#xff09;原则并且队尾被连接在队首之后以形成一个循环。它…

对于高速信号完整性,一块聊聊啊(12)

常见的无源电子器件 电子系统中的无源器件可以按照所担当的电路功能分为电路类器件、连接类器件。 A、电路类器件&#xff1a; &#xff08;1&#xff09;二极管&#xff08;diode&#xff09; &#xff08;2&#xff09;电阻器&#xff08;resistor&#xff09; &#xf…

CAD二次开发(4)-编辑图形

工具类&#xff1a;EditEntityTool.cs using Autodesk.AutoCAD.ApplicationServices; using Autodesk.AutoCAD.DatabaseServices; using Autodesk.AutoCAD.Geometry; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Th…

✨✨使用jq+layui-tab+echarts+swiper实现选项卡轮播联动图表展示功能✨✨

使用jqlayui-tabechartsswiper实现选项卡轮播联动图表展示功能 ✨一、实现功能 &#x1f31f;技术框架 使用layui-tab实现tabs切换使用swiper.js实现轮播效果使用echarts.js实现图表展示 &#x1f31f;功能详情 布局为上中下&#xff1a;tab选项上&#xff0c;内容区为中&…

大作业爬取手机数据,实现手机推荐系统以及朋友圈手机论坛

1、功能简介 &#xff08;1&#xff09;用户注册与用户登录 &#xff08;2&#xff09;手机搜索、手机比拼、手机个性化推荐 &#xff08;3&#xff09;点击搜索的手机图片会就用户行为&#xff0c;轮播展示用户行为&#xff0c;推荐点击次数靠前的手机 &#xff08;4&#xf…

网络域名是什么意思

网络域名&#xff0c;顾名思义&#xff0c;就是网络上的名字&#xff0c;类似于现实中的地址或姓名一样&#xff0c;用来标识网络上的一个或一组计算机或服务器的位置&#xff0c;以及它们的相应服务资源。网络域名是互联网上最基础的基础设施之一&#xff0c;是网络通信的“标…

有一个3x4的矩阵,要求用函数编写程序求出其中值最大的那个元素,以及其所在的行号和列号

常量和变量可以用作函数实参&#xff0c;同样数组元素也可以作函数实参&#xff0c;其用法与变量相同。数组名也可以作实参和形参&#xff0c;传递的是数组的起始地址。 用数组元素作函数实参&#xff1a; 由于实参可以是表达式&#xff0c;而数组元素可以是表达式的组…

技术面试,项目实战,求职利器

之前找工作一直想找一个能真正系统性学开发的地方&#xff0c;之前毕业找工作的时候无意间碰到下面这个网站&#xff0c;感觉还挺不错的&#xff0c;用上面的技术实战内容应对技术面试&#xff0c;也算是求职利器了。有需要的可以自取&#xff1a; https://how2j.cn?p156336 实…

【运维】Linux 端口管理实用指南,扫描端口占用

在 Linux 系统中&#xff0c;你可以使用以下几种方法来查看当前被占用的端口&#xff0c;并检查 7860 到 7870 之间的端口&#xff1a; 推荐命令&#xff1a; sudo lsof -i :7860-7870方法一&#xff1a;使用 netstat 命令 sudo netstat -tuln | grep :78[6-7][0-9]这个命令…

从0开始学统计-卡方检验

1.什么是卡方检验&#xff1f; 卡方检验是一种用于检验观察频数与期望频数之间差异的统计方法。它通常用于分析分类变量之间的关联性或独立性。在卡方检验中&#xff0c;我们将观察到的频数与期望频数进行比较&#xff0c;从而确定它们之间的差异是否显著。 卡方检验的基本思…

深入理解与防御跨站脚本攻击(XSS):从搭建实验环境到实战演练的全面教程

跨站脚本攻击&#xff08;XSS&#xff09;是一种常见的网络攻击手段&#xff0c;它允许攻击者在受害者的浏览器中执行恶意脚本。以下是一个XSS攻击的实操教程&#xff0c;包括搭建实验环境、编写测试程序代码、挖掘和攻击XSS漏洞的步骤。 搭建实验环境 1. 安装DVWA&#xff…

手机相册的照片彻底删除了怎么恢复?删除照片恢复的5种方法

在数字化时代&#xff0c;手机相册里装满了我们的生活点滴和珍贵回忆。然而&#xff0c;一不小心就可能误删那些意义非凡的照片。别担心&#xff0c;今天小编就给大家介绍5种恢复误删照片的方法&#xff0c;让你的回忆不再丢失&#xff01; 方法一&#xff1a;相册App的“最近删…