浴火重生:从挫败到成功,前端框架的疑难杂症解决之道

本文最后更新于:4 个月前

前言

Ant Design Pro

  • 什么是Ant Design Pro?按我的理解,这是一个前端框架,却又不仅仅是一个前端框架
  • Ant Design Pro的语言是React,它基于Ant Design设计体系,提供了丰富的页面模板、组件和功能,非常适合快速搭建前台页面
  • Ant Design 组件库:分页 Pagination - Ant Design
  • Ant Design Pro 脚手架:文档总览 - Ant Design Pro
  • 下面是比较官方的定义:
1
Ant Design Pro 是一个开箱即用的企业级中后台前端/设计解决方案,基于 Ant Design 设计体系,内置了丰富的页面模板、组件和功能,旨在帮助开发者快速搭建企业级中后台应用。
1
2
3
4
5
Ant Design Pro 提供了完整的前端开发工作流和最佳实践,具有以下特点和优势:
1.高度定制化:Ant Design Pro 提供了可灵活配置的模板,可以根据项目需求自定义布局和功能,非常适合快速定制企业级应用。
2.丰富的模板和组件:Ant Design Pro 内置了多个精心设计的页面模板和丰富的 UI 组件,包括表单、表格、图表、地图等,使开发者可 以快速构建复杂的中后台应用。
3.系统化的工作流程:Ant Design Pro 提供了完善的开发工作流程,包括开发环境、测试环境和生产环境的配置、代码检查、测试和构建 等,使开发者能够便捷地进行前端开发。
4.功能丰富的插件集成:Ant Design Pro 集成了常用的中后台应用所需的功能,包括菜单权限管理、数据请求、国际化、登录认证等,可 以快速构建完整的、稳定的企业级应用。

Ant Design Vue

Vite

写作目标

  • 脚手架的搭建固然简单,在脚手架的基础上,我们可以更方便地改造代码、对接后端代码、编写我们自己的业务逻辑
  • 但前提是我们要理清框架的目录结构,以及相关的逻辑代码
  • Ant Design Pro内置了独特的登录页面、欢迎页面、表格页面,我们要理清登录校验逻辑、发送请求到自己的后端接口、重定向到login页的逻辑、管理员校验、页面路由配置等等,这些都是必须熟练掌握的
  • 参考官网:启动项目 - Ant Design Pro
  • 将来开发个人网站,或者开发个人项目,都可以基于Ant Design Pro框架,更快捷、更高效

正文

Vite

开发思路

  • 使用 vite工具快速构建Vue开发框架来从零开发,要做什么?一定是做到以下几点:(2023/08/01午)
    • vite 快速构建框架,启动项目
    • 配置启动端口
    • 全局axios配置,开发全局请求拦截和相应拦截器,做好跨域请求处理

快速搭建

1
npm create vite@latest
  • cd 进项目目录,安装依赖:
1
npm install
  • 修改tsconfig.json文件,添加如下配置:
1
"moduleResolution": "Node",
  • 如果依赖安装失败,可能是npm镜像源的问题,两种解决方法:
    • 修改镜像源:淘宝镜像 -> 官方镜像
    1
    npm set registry https://registry.npmjs.org/
    • 使用yarn包管理工具:
    1
    yarn install
  • 启动项目:(2023/08/01早)
1
npm run dev
1
yarn dev

启动端口

1
2
3
4
5
6
7
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
server: {
port: 7073, // 修改为你想要的端口号
},
});

全局axios配置

  • 全局安装 axios:
1
npm install axios
1
yarn add axios
  • 在指定目录下封装全局请求/响应拦截器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import axios from "axios";

// 创建 Axios 实例
const myAxios = axios.create({
baseURL: "http://localhost:8083/api", // 设置请求的基础URL
withCredentials: true,
});

// 处理请求拦截器
myAxios.interceptors.request.use(
(config) => {
// 在请求发送之前做一些处理
return config;
},
(error) => {
// 发送请求错误时的处理
return Promise.reject(error);
}
);

// 处理响应拦截器
myAxios.interceptors.response.use(
(response) => {
// 对响应数据做处理
return response.data;
},
(error) => {
// 响应错误时的处理
return Promise.reject(error);
}
);

export default myAxios;

组件样式BUG

  • 在做Memory-伙伴匹配时,一直有一个问题困扰着我:组件样式有问题
  • 这次使用Vue从零开发壁纸网站时,终于彻查了这个不起眼的问题:
1
2
3
4
5
6
7
import { createApp } from "vue";
import "vant/lib/index.css";
import Vant from "vant";
import App from "./App.vue";

createApp(App).use(Vant).mount("#app");

  • 没错,只需在main.ts下,引入 “vant/lib/index.css” 样式即可(2023/08/02早)

请求头设置无效

  • 这个真恶心到我了,POST请求的请求头设置无效(2023/08/01晚)
  • 本来是打算做一个前端上传图片文件,后端接收的功能,但这涉及到前端如何发送,后端如何接收的问题
  • 做了大量的尝试,只有这种方法,才能在后端正常接收到图片文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const res = await myAxios
.post("/wallpaper/upload", file, {
headers: {
"Content-Type": "multipart/form-data",
},
})
.then(function (response) {
if (response.data) {
return response.data;
}
})
.catch(function (error) {
console.log(error);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@PostMapping("/upload")
public String uploadFile(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
return "上传的文件为空";
}

// 处理上传的文件
try {
byte[] bytes = file.getBytes();
// 可以选择保存文件到服务器的指定路径
// Files.write(Paths.get("文件保存路径"), bytes);

// 在这里可以根据需要进行相关的业务逻辑处理

return "文件上传成功";
} catch (IOException e) {
e.printStackTrace();
}
return "文件上传失败";
}

Ant Design Pro

快速搭建

  • 官网:开始使用 - Ant Design Pro
  • 我们可以按以下步骤,快速搭建Ant Design Pro脚手架:
  • 在正确安装Node.js环境下的前提下,依次执行以下命令:
  • 使用npm全局安装 pro-cli 构建工具(脚手架):
1
npm i @ant-design/pro-cli -g
  • 使用脚手架,在指定目录下快速搭建前端框架:
1
pro create memory-api 

image-20240130210310142

  • 项目下安装yarn包管理工具:
1
yarn -V
  • 至此,前端项目框架搭建完成,启动项目:(2023/07/21晚)
1
yarn run start

页面路由报错

  • 在 config/routes.ts 下设置好页面路由后,一定要在对应组件下添加index.tsx文件:
1
2
3
4
5
6
7
//接口全部详情页
{
name: '接口详情',
icon: 'user',
path: '/interfaceInfo/info',
component: './TableList/entireCommend',
},
  • 否则项目启动后会报错:(2023/08/05晚)

image-20230805233331422

页面路由跳转(1)

1

  • 命令式跳转:
1
2
3
4
5
import router from 'umi/router';

function goToListPage() {
router.push('/list');
}
  • 声明式跳转:(2023/08/05晚)
1
2
3
4
5
import Link from 'umi/link';

export default () => (
<Link to="/list">Go to list page</Link>
);

OpenAI生成接口

image-20230809132710999

  • 前端 config/config.ts下的配置:
1
2
3
4
5
6
7
openAPI: [
{
requestLibPath: "import { request } from '@umijs/max'",
schemaPath: 'http://localhost:8104/api/v2/api-docs?group=壁纸分享',
projectName: 'pic-memories',
},
],
  • 执行以下命令,一键生成接口:(2023/08/09午)
1
npm run openapi

最简页面样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import { listInterfaceInfoByPageUsingPOST } from '@/services/memory-api/interfaceInfoController';
import { ProList } from '@ant-design/pro-components';
import { Button, Space } from 'antd';
import React, { useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom';

const InterfaceInfoList: React.FC = (props) => {
const [interfaceInfo, setInterfaceInfo] = useState<API.InterfaceInfo[]>([]);

const fetchData = async (params: { pageSize?: number; current?: number }) => {
try {
const res = await listInterfaceInfoByPageUsingPOST({
...params,
});

console.log('res = ' + res.data?.records);

if (res.data) {
setInterfaceInfo(res.data.records);
}

// 对返回的数据进行处理
} catch (error) {
console.error(error);
}
};

const history = useHistory();
const handleButtonClick = () => {
// 在按钮点击事件中触发页面跳转
history.push('/TableList/entireCommend');
};

return (
// 页面渲染
useEffect(() => {
console.log('啊啊啊');
fetchData(1, 5);
}, []),
(
// 组件的其余部分
<ProList<any>
rowKey="name"
headerTitle="基础列表"
tooltip="基础列表的配置"
dataSource={interfaceInfo}
metas={{
title: {
dataIndex: 'name',
},
description: {
dataIndex: 'description',
},
actions: {
render: (text, row) => {
return (
<Space>
<div>
<Button onClick={handleButtonClick}>跳转到目标页面</Button>
</div>
</Space>
);
},
},
}}
/>
)
);
};

export default InterfaceInfoList;

改造框架

  • 今天我把API 接口开放平台的前端代码拉下来了,给PicMemories做一个管理员后台
  • 怎么快速改造呢?不要惊慌,做到三点即可:(2023/08/11晚)
    • 修改接口文档地址,重新生成接口
    • 改造获取用户登录态接口
    • 改造登录接口
    • 改造获取表单信息接口

路由配置错误

  • 如果项目启动报如下错误:
1
2
3
4
5
6
7
8
9
10
11
12
13
PS D:\Project\星球项目\memory-api\memory-api-frontend> yarn start:dev
yarn run v1.22.19
$ cross-env REACT_APP_ENV=dev MOCK=none UMI_ENV=dev max dev
at Proxy.getRoutes (D:\Project\星球项目\memory-api\memory-api-frontend\node_modules\@umijs\preset-umi\dist\features\tmpFiles\routes.js:61:46)
at Hook.fn (D:\Project\星球项目\memory-api\memory-api-frontend\node_modules\@umijs\preset-umi\dist\features\appData\appData.js:51:35)
at D:\Project\星球项目\memory-api\memory-api-frontend\node_modules\@umijs\core\dist\service\service.js:136:38 {
code: 'MODULE_NOT_FOUND'
}
fatal - A complete log of this run can be found in:
fatal - D:\Project\星球项目\memory-api\memory-api-frontend\node_modules\.cache\logger\umi.log
fatal - Consider reporting a GitHub issue on https://github.com/umijs/umi/issues
Done in 3.83s.

  • 肯定是路由配置错误了,检查检查吧

  • 好像上面的栏目提到过。。(2023/08/16午)

可视化数据图表

image-20230825204907577

最简页面2.0

  • 这里再补充一下React的最简页面:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import {PageContainer} from '@ant-design/pro-components';
import '@umijs/max';
import ReactECharts from 'echarts-for-react';
import * as echarts from 'echarts';

/**
* 接口分析
* @constructor
*/
const testAnalysis: React.FC = () => {
const option = {}

return (
<PageContainer>
<ReactECharts option={option}/>
</PageContainer>
);
};
export default testAnalysis;
  • 也就像我这样的前端小白,才会记录这些再简单不过的语法了😍

  • 呐,直接粘贴代码,看效果就完事了:

image-20230825205146265

  • 有时间就学学React框架语法了,挺有意思的,到时候总结点东西再记录下react的页面开发技巧(2023/08/25晚)

区分管理员页面和普通用户页面

  • 在管理员账号下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 管理员
{
path: '/',
access: 'canAdmin',
name: 'admin',
icon: 'smile',
routes: [
//用户信息页
{
name: '用户信息',
access: 'canAdmin',
icon: 'user',
path: '/user/list',
component: './TableList/Admin/User',
},
//接口信息页
{
name: '接口信息',
access: 'canAdmin',
icon: 'user',
path: '/interfaceInfo/list',
component: './TableList/Admin/InterfaceInfo',
},
// 接口分析页
{
name: '接口分析',
icon: 'user',
path: '/admin/interface_analysis',
component: './TableList/Admin/InterfaceAnalysis'
},

],
},
  • 如上,新增了一个二级菜单:管理员页面
  • 值得注意的是,这里管理员页面默认路由为 /,如果想实现访问 / 跳转至/welcome,则可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 //欢迎页
{
path: '/welcome',
name: 'welcome',
icon: 'smile',
component: './Welcome',
},

{
path: '/',
redirect: '/welcome',
},

// 管理员
{
path: '/',
access: 'canAdmin',
name: 'admin',
....................
},
  • 如上,这里的重定向一定要写在管理员页面路由的前面,这样才能正常跳转至/welcome(2023/09/27午)

Prettier 美化配置

  • 配置生效文件,以及快速格式化时,使用 Prettier 帮助快速美化代码 (2023/10/08早)

image-20231008092500820

一条龙完成 Ant Design Pro 初始化

  • 这是一个全新的、思路清晰的框架优化瘦身一条龙操作技巧,废话少说,开始打造自己的前端框架

  • 优化瘦身顺序:
    • 框架初始化:完成项目的首次构建,能够成功运行项目
    • 框架瘦身:删除不必要的组件 / 配置,减小框架体量,节省存储空间
    • 框架改造
      • 配置后端接口文档,快速生成后端请求地址
      • 接入后台已开发的登录 / 注册功能(用户登录、获取当前登录用户、管理员权限校验)
  • 优化前提:

    • 后端用户登录、获取当前登录用户、配置允许跨域请求、正确配置接口文档
    • 请确保以上接口开发 / 服务配置已经完成

框架初始化

  • 一切按照官方文档来,毕竟前端框架组件更新迭代太快了:

  • 官网:开始使用 - Ant Design Pro
  • 现阶段版本,只需依次执行以下命令即可:(2023/10/03晚)
  • 使用npm全局安装 pro-cli 构建工具(脚手架):
1
npm i @ant-design/pro-cli -g
  • 使用脚手架,在指定目录下快速搭建前端框架:
1
pro create memory-api 
  • 项目下安装yarn包管理工具:
1
yarn install
  • 至此,前端项目框架搭建完成,启动项目:(2023/10/03晚)
1
yarn run start

框架瘦身

  • 删除不需要的代码,减小空间占用

国际化移出
mock移出
swagger移出
logo修改 移出
tests types 移出
prettier美化配置
jest config测试框架 删除
ds 全局替换 Ant Design Pro / Ant Design

框架改造

登录注册
snap login.test 删除
删除lang
Subtitle 支持a标签 targetblank
initial Value 自动登录其他登录 删除
手机号登录 全部干掉 删除
忘记密码干掉 未登录?去注册
传参 userAccount userPassword
openApi 快速生成后端请求接口

登录请求 成功返回 失败捕获
获取当前登录用户fetch
删除未引用的

保存全局状态
只留current user 改类型
改调用方法
当前不是登录页面 返回 currenter
否则返回空

修改头像 水印
前端传递cokkie
修改请求拦截器 直接返回config
注册页面开发

  • Login 页面:
1
2
3
4
5
6
7
8
9
10
11
const fetchUserInfo = async () => {
const userInfo = await getLoginUserUsingGET();
if (userInfo) {
flushSync(() => {
setInitialState((s) => ({
...s,
currentUser: userInfo,
}));
});
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const handleSubmit = async (values: API.UserLoginRequest) => {
try {
// 登录
const res = await userLoginUsingPOST({ ...values, type });
if (res.code === 0) {
const defaultLoginSuccessMessage = intl.formatMessage({
id: 'pages.login.success',
defaultMessage: '登录成功!',
});
message.success(defaultLoginSuccessMessage);
await fetchUserInfo();
const urlParams = new URL(window.location.href).searchParams;
history.push(urlParams.get('redirect') || '/');
return;
}
} catch (error) {
const defaultLoginFailureMessage = '登录失败,请重试!'
console.log(error);
message.error(defaultLoginFailureMessage);
}
};
  • app.tsx 页面:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
export async function getInitialState(): Promise<{
currentUser?: API.LoginUserVO;
}> {
const fetchUserInfo = async () => {
try {
const res = await getLoginUserUsingGET({
skipErrorHandler: true,
});
return res.data;
} catch (error) {
history.push(loginPath);
}
return undefined;
};

// 如果不是登录页面,执行
const {location} = history;
if (location.pathname !== loginPath) {
const currentUser = await fetchUserInfo();
return {
currentUser,
};
}
return {};
}
  • access.tsx 页面:
1
2
3
4
5
6
export default function access(initialState: { currentUser?: API.LoginUserVO } | undefined) {
const { currentUser } = initialState ?? {};
return {
canAdmin: currentUser && currentUser.userRole === '0',
};
}
  • config.ts 页面:
1
2
3
4
5
6
7
8
9
openAPI: [
{
requestLibPath: "import { request } from '@umijs/max'",
// 或者使用在线的版本
schemaPath: "http://localhost:8101/api/v2/api-docs",
projectName: 'memory-bi',
mock: false,
},
],
  • 这里踩了个很恶心的 BUG,执行完成 run openapi 之后,有些方法调用的路径会发生紊乱:
1
import { outLogin } from '@//services/ant-design-pro/login';
  • 修改调整一番就好了(2023/10/03晚)

开发一个页面的步骤

  • 创建路由和页面(2023/10/11晚)
  • 获取需要的数据,定义 state 变量来存储数据,用于页面展示

  • 先把最简单最直观的数据展示给前端,再去调样式

  • 引入 Ant Design 的 List 组件,复制示例代码

  • 调整 List 组件中的内容为自己的(注意,获取用户头像可以从初始化状态中获取)

1
2
const { initialState} = useModel('@@initialState');
const { currentUser } = initialState ?? {};
  • 针对样式,对数据进行一些处理,比如统一隐藏自身的 title
  • 增加分页当前页码、每页展示数、总记录数)、增加展示列
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<List
grid={{
gutter: 16,
xs: 1,
sm: 1,
md: 1,
lg: 2,
xl: 2,
xxl: 2,
}}
itemLayout="vertical"
size="large"
pagination={{
onChange: (page) => {
setSearchParams({
...searchParams,
current: page,
})
},
current: searchParams.current,
pageSize: searchParams.pageSize,
total: total,
}}
dataSource={chartList}

..........................
)}
/>
  • 增加搜索框
1
2
3
4
5
6
7
8
9
10
11
12
<Search
className="margin-bottom-16"
placeholder="请输入图表名称"
enterButton
loading={loading}
onSearch={(value) => {
// 设置搜索条件
setSearchParams({
...initSearchParams,
name: value,
})
}}/>
  • 其他优化:比如添加 loading 效果
1
const [loading, setLoading] = useState<boolean>(false)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const loadData = async () => {
setLoading(true);
try {
const res = await listChartByPageUsingPOST(searchParams);
if (res.data) {
// 获取图表列表
setChartList(res.data?.records ?? []);
// 获取总数
setTotal(res.data?.total ?? 0);
// 搜索完成
setLoading(false);
} else {
message.error("获取我的图标失败")
}
} catch (e: any) {
message.error("获取我的图标失败" + e.message)
}
}
  • 这里添加了一个 loading 效果,在执行搜索过程中(即按下搜索后,到数据成功返回的时间段),搜索按钮的状态为转圈圈

模拟低网速运行状态

  • 明白上面提到的搜索按钮的 loading 效果后,就应该明白: (2023/11/11晚)

  • 只有执行搜索的时间越长,即响应的时间越长时,这个 loading 效果越明显
  • 那我们就要模拟服务在低网速状态运行的状态了,如下:

image-20231207235631152

  • 选中对应的网络状态,就可以模仿在该网络环境下发送请求(2023/12/07晚)

引入外部页面组件

  • 如下,编写页面组件 RandomPoem
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 接口分析
* @constructor
*/
import {PageContainer} from "@ant-design/pro-components";
import {CaretRightFilled} from "@ant-design/icons";
import {Button, Form, message, Input, Tag, Table} from 'antd';
import {ColumnsType} from "antd/es/table";
import React, {useState} from "react";
import initialState from "@@/plugin-initialState/@@initialState";
import {invokeInterfaceInfoUsingPOST} from "@/services/memory-api/interfaceInfoController";
import {useParams} from "@@/exports";

const RandomPoem: React.FC = () => {
.....................

return (
<PageContainer>
.....................
</PageContainer>
);
};

export default RandomPoem;

image-20231207235417783

  • 在需要引入该组件的页面导入该组件,并引入:
1
import RandomPoem from "@/pages/TableList/InterfaceInfo/RandomPoem";

image-20231207235457463

Ant Design Vue

  • 官网:Ant Design Vue (antdv.com)

  • 以下仅为开发过程中的个人经验总结,不具备教学价值,本人主Java后端,前端框架使用很不熟练~(2023/08/27早)

快速上手

🍖 推荐阅读:Home | Vue CLI (vuejs.org)(2024/01/15晚)

  • 安装脚手架工具(2023/08/05午)
1
2
3
npm install -g @vue/cli
OR
yarn global add @vue/cli
  • 使用命令行进行初始化:
1
vue create antd-demo  #项目名	

image-20230826202427695

image-20240115221613317

image-20230826202435390

image-20240115221624382

  • 按需选择,按我那样的设置就行,引入TS、Router等
  • 注意这里同意保存预设的话,之后构建项目就会按照这次的配置直接构建了,如果要修改项目构建配置,则需执行以下代码:

1
npm uninstall -g @vue/cli
  • 然后重新构建(2023/08/26晚)
1
npx @vue/cli create antd-demo	#在临时目录中执行 @vue/cli 命令,而不依赖于全局路径设置

引入Ant Design组件

  • 引入组件:
1
npm i --save ant-design-vue
  • 在main.ts中如此配置,引入Ant Design组件:
1
2
3
4
5
6
import {createApp} from 'vue'
import App from './App.vue'
import router from './router'
import Antd from 'ant-design-vue';

createApp(App).use(router).use(Antd).mount('#app')
  • 启动项目:(2023/08/05午)
1
npm run serve

配置路由

  • route.ts中如此初始化:
1
2
3
4
5
6
7
8
9
10
11
12
const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'index',
component: IndexPage
},
{
path: '/about',
name: 'about',
component: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue')
}
]

封装全局axios

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import axios from "axios";

const instance = axios.create({
baseURL: "http://localhost:8102/api",
timeout: 10000,
headers: {},
});

// 添加响应拦截器
instance.interceptors.response.use(
function (response) {
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
const data = response.data;
if (data.code === 0) {
return data.data;
}
console.error("request error", data);
return response.data;
},
function (error) {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
return Promise.reject(error);
}
);

export default instance;

路由嵌套

  • app.vue中如此配置:(2023/08/27早)
1
2
3
<template>
<router-view/>
</template>
  • 这里解释一下:
1
2
3
4
5
在Ant Design Vue中,App.vue是项目的根组件,是所有页面组件的父容器。在这段代码中,`<router-view/>`是一个特殊的组件标签,它用于显示路由器(router)根据当前路由匹配的页面组件。

`<router-view/>`是Vue Router提供的一个占位符标签,它会根据当前的URL路径匹配到的路由,动态地渲染对应的组件内容。

具体来说,当用户访问不同的URL路径时,Vue Router会根据定义的路由规则来确定显示哪个组件。然后,这个组件会被动态地渲染到App.vue中的`<router-view/>`标签中。
  • 比如说当前URL中的path路径为:/about
  • 而我们配置的路由中:

1
2
3
4
5
{
path: '/about',
name: 'about',
component: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue')
}
  • 那么就会展示/about下的组件内容了
  • 我在主页面中引入了标签页组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!--标签页-->
<a-tabs v-model:activeKey="activeKey">
<a-tab-pane key="文章" tab="文章">
<PostList :post-list="postList"/>
</a-tab-pane>

<a-tab-pane key="图片" tab="图片">
图片
</a-tab-pane>

<a-tab-pane key="用户" tab="用户">
用户
</a-tab-pane>
</a-tabs>
  • 可以看到在tab栏下,我引入了PostList组件,并传入了postList参数
  • 那么PostList组件就会展示在该标签栏下,效果如下:(2023/08/27早)

image-20230826215450284

子页面嵌套

  • 纠正以下上面说明的页面组件的嵌套方法,正确的嵌套方法应该是这样的:
  • 在index页面下引入子组件:
1
2
3
4
5
<script lang="ts" setup="ts">
import UserListPage from "@/components/userListPage.vue";

.......................
</script>
  • 子组件编写如下(注意导出参数name):
1
2
3
4
5
6
7
8
9
10
11
<template>
<div>
嘿嘿嘿
</div>
</template>

<script>
export default {
name: "UserListPage"
}
</script>
  • 直接如此引用子组件(还可传递参数,可参考上面 路由嵌套 的实现方法)
1
2
3
4
<a-tab-pane key="1" tab="用户列表">
在线用户列表
<UserListPage/>
</a-tab-pane>
  • 那么子页面如何获取传递的参数呢?
1
2
3
4
5
<a-list
item-layout="horizontal"
:grid="{ gutter: 16, xs: 1, sm: 2, md: 4, lg: 4, xl: 6, xxl: 6 }"
:data-source="props.userInfoList"
>
1
2
3
4
5
6
7
8
9
10
11
12
<script setup lang="ts">
import {withDefaults, defineProps} from "vue";

interface Props {
userInfoList: any[];
}

const props = withDefaults(defineProps<Props>(), {
userInfoList: () => [],
name: "UserListPage"
})
</script>
  • 完成,页面显示如下,子组件已成功显示在index页面中:

image-20230909175611698

配置启动端口

  • 在 vue.config.js 下,进行如下配置:(2023/09/09午)
1
2
3
4
5
6
7
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
devServer: {
port: 7071
}
})

双向绑定

  • 做登录页面时,一个简单的表单和数据双向绑定搞了半天,这里直接给出代码示例了:(2023/09/10午)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<a-form @submit="handleSubmit">
<a-form-item label="用户名" name="username"
:rules="[{ required: true, message: '请输入用户名' }]">
<a-input :placeholder="'请输入用户名'" v-model:value="userAccount"/>
</a-form-item>

<a-form-item label="密码" name="userPassword"
:rules="[{ required: true, message: '请输入密码' }]">
<a-input :type="'password'" :placeholder="'请输入密码'" v-model:value="userPassword"
/>
</a-form-item>

<a-form-item>
<a-button type="primary" html-type="submit">登录</a-button>
</a-form-item>
</a-form>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 用户名 密码
const userAccount = ref("");
const userPassword = ref("");

// 登录请求
const handleSubmit = () => {
MyAxios.post("/user/login", {
userAccount: userAccount.value,
userPassword: userPassword.value,
})
.then((values) => {
// 处理表单提交逻辑
console.log('表单数据:', values);
})
.catch((error) => {
console.error('表单验证失败:', error);
});
};

日常犯傻

  • 使用 Vue 的 ref() 语法时,容易忘记取.value(2023/09/13晚)
1
const currentUserId = currentUser.value.id;
  • 访问后端服务器路径,容易忘记写/api
1
const socketUrl = `ws://localhost:8081/api/websocket/${currentUserId}`;

处理JSON字符串

  • 前台获取服务器转发的消息后,需要解析出接收者,再判断接收者是否为当前用户,是则为该用户展示具体消息(2023/09/14晚)
1
2
3
4
5
6
7
8
9
10
11
12
13
//获得消息事件(获得服务端转发的消息)
socket.onmessage = function (msg) {
// 封装返回消息
receiveMsg.value = msg.data;
// 解构
console.log("这条消息是发给: " + receiveMsg.value + " 的")
console.log("这条消息是发给: " + receiveMsg.value.receiverId + " 的")
const content = receiveMsg.value.content;
// 是否属于我的消息
if (receiverId === currentUserId) {
setMessage("服务端回应: " + content + "发给: " + receiverId);
}
}
  • 前端返回的 mes 是 JSON字符串,我看见控制台输出的内容是,以为是个对象,结果取到的属性值是undefined:
1
{"senderId":"1657284783190523906","receiverId":"1657284893320364034","content":"阿发","sendTime":"2023-09-14T13:29:11.309Z"}
  • 调试了半天,检查了下 mes 的类型,才发现是个JSON字符串,奶奶的
1
console.log("type of " + typeof (receiveMsg.value))
  • 所以要将msg JSON字符串解析为对象后,才可以正常拿到属性值
1
receiveMsg.value = JSON.parse(msg.data);
  • 但是连接服务器socket时,也会执行这里的代码,而服务器首次响应连接请求并传回的的参数类型不是JSON字符串

  • 解析失败,报以下错误:

image-20230914214515079

  • 最后优化代码,同时处理两种情况:服务器首次连接成功后的响应(普通字符串) / 服务器转发的消息(JSON字符串
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//获得消息事件(获得服务端转发的消息)
socket.onmessage = function (msg) {
// 封装返回消息
if (typeof (msg) === String) {
receiveMsg.value = JSON.parse(msg.data);
}
receiveMsg.value = msg.data;
// 解构
console.log("这条消息是发给: " + receiveMsg.value.receiverId + " 的")
const content = receiveMsg.value.content;
// 是否属于我的消息
if (receiverId === currentUserId) {
setMessage("服务端回应: " + content + "发给: " + receiverId);
}
};
  • 各种坑都能踩到,不过好在花点时间排错,都能解决掉 👏👏👏(2023/09/14晚)

页面路由跳转(2)

1
2
3
4
5
{
path: '/chat',
name: 'chat',
component: () => import(/* webpackChunkName: "about" */ '../pages/chatPage.vue')
},
  • 包含path(页面路由)、name(页面别名)、component(页面展示组件)
  • 那么我们可以如此实现精准的页面跳转和参数传递:

1
2
3
4
5
6
7
8
9
10
11
12
import router from "@/router";

router.push({
name:"chat",
path:"/chat",
query: {
chatUserId:12345
},
params:{
id:"12345"
}
})
  • 也可以简单的改变当前页面的路由参数,而不进行页面跳转:(适用于 Tab 页之间切换时,标记 tab页
1
2
3
4
5
6
7
import router from "@/router";

router.push({
query: {
name: "abab"
}
})
  • 那么跳转过去的页面该如何获取当前页面的路由地址携带的参数等信息呢?引入Vue Router组件:(2023/09/16早)
1
2
3
4
5
6
7
8
import {useRoute} from 'vue-router';

const route = useRoute();
onMounted(() => {
console.log('携带参数:', route.query)
console.log('路由地址:', route.path)
console.log('传递参数:', route.params)
})

动态新增tab标签页

  • 这是一个很有意思的实现,浅浅记录一下:
1
2
3
4
5
6
<a-tabs v-model:activeKey="activeKey" tab-position="left" @change="handleTabChange">
<a-tab-pane v-for="tab in tabs" :key="tab.key" :tab="tab.tab.chatTabName">
<!-- 在这里放置标签页内容 -->
{{ tab.content }}
</a-tab-pane>
</a-tabs>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 钩子函数
onMounted(() => {
addTab();
})
// 动态添加tab标签页
const addTab = () => {
chatTabName.value = route.query
const newTab = {
key: Date.now().toString(), // 每个标签页都需要一个唯一的 key
tab: chatTabName.value,
content: '新标签页内容',
};

tabs.value.push(newTab);
activeKey.value = newTab.key; // 新增的标签页设为激活状态
}
  • 这样就轻松实现了(MemoryChat开放过程中的废弃想法 )(2023/09/16午)

icon图标的使用

  • 看官网,首先下载安装相关依赖:(2023/09/17晚)
1
npm install --save @ant-design/icons-vue
  • 引入相关 icon图标 组件(这步操作,,WebStorm是不会自动给你引入这个组件的,怪不得我 tab标签页 的页面图标不显示,一个星期都没解决掉,这编译器害人不浅)
1
import { StarOutlined, StarFilled, StarTwoTone } from '@ant-design/icons-vue';
  • 直接在页面中用就行了,效果如下:
1
2
3
4
5
<div>
<StarOutlined />
<StarFilled />
<StarTwoTone twoToneColor="#eb2f96" />
</div>

image-20230918190908215

监听选中的Tab标签页

  • 我们选中Tab标签页后,如果绑定了activeKey,就会将该Tab页的Key值,给到activeKey
1
2
3
4
<a-tab-pane v-model:activeKey="activeKey" v-for="tab in friendList" :key="tab.id" :tab="tab.username"
@click="handleTabChange">
.........................
</a-tab-pane>
1
2
// tab页 key
const activeKey = ref(currentUserId);
  • 即,通过选中标签页的动作,就能拿到该Tab标签页的key值
  • 那么当页面刷新后,仍然会执行这个代码,初始化activeKey的值:

1
const activeKey = ref(currentUserId);
  • 我们现在要实现:刷新页面后,仍然选中刚才选中的Tab标签页,即activeKey值需要更新,更新为刚才的key值
  • 但是,当页面刷新后activeKey的值就丢失了,所以我们要对active进行监听,随时保存它的值,如下:
1
2
3
4
// 监听 activeKey 的变化,更新存储中的值
watch(activeKey, (value) => {
localStorage.setItem('activeKey', value);
});
  • 这里踩了个,监听的值一定是activeKey,而不是activeKey.value,即监听的变量是响应式变量 (2023/09/20晚)

  • 那么在页面刷新之后,我们就可以拿到localStorage中存储的key值,保持选中刚才的Tab页面

1
2
3
4
5
6
7
// 钩子函数
onMounted(() => {
........................

activeKey.value = localStorage.getItem('activeKey')
.....................
})

点击跳转不同的页面

  • 我要讲什么呢,好像还没说清楚,但我举个例子就好理解了
  • 我最近要实现这样的功能:点击博文,跳转到该博文的详情页

    • 这样的需求很常见,掘金、CSDN等博客网站都是这样,甚至说所有的文档网站都是这样吧
    • 要点就这几个:跳转前拿到该博文id,跳转页面时携带id,对应页面解析拿到该id从数据库中查询数据
    • 这就是最简单的实现方式了
  • 跳转前拿到该博文id
1
2
3
4
<!--博文-->
<a-card hoverable @click="goToRead(item.id)">
......................
</a-card>
  • 跳转页面时携带id
1
2
3
4
5
6
7
8
9
10
11
// 跳转博文详情页
const goToRead = (id: never) => {
// router.push(`/blog/read/${id}`)
router.push({
name: "blogRead",
path: "/blog/read",
query: {
articleId: id
}
})
}
  • 对应页面解析拿到该id从数据库中查询数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<div class="blog">
{{articleId}}
</div>
</template>

<script lang="ts" setup="ts">
import {useRoute} from 'vue-router';
import {ref} from "vue";

const route = useRoute();
const articleId = ref();
// 获取id值
articleId.value = route.query.articleId;
</script>
  • 就这样简单地记录下思路吧(2023/09/24午)

网页中支持Markdown语法写博客

  • 要在博客网站中支持Markdown语法写博客并展示,你可以使用第三方的Markdown解析库来解析Markdown文本,并将解析后的内容展示在网页上(2023/09/24晚)
  • 首先,你需要引入一个适用于Vue的Markdown解析库,例如markdown-it。可以通过NPM安装该库:

1
npm install markdown-it
  • 然后,在你的组件中,你可以导入并实例化markdown-it,将Markdown文本作为输入,使用.render()方法将其转换为HTML并展示在网页上
1
2
3
4
5
6
7
import MarkdownIt from 'markdown-it';

// Markdown语法
const parsedContent = ref()
const md = new MarkdownIt();
// 使用Markdown语法接收文章内容
parsedContent.value = md.render(articleInfo.value.content);
1
2
3
<div v-html="parsedContent"
style="position: absolute; margin-left: 10px; margin-right: 10px; margin-top: 20px;">
</div>
  • 在上面的示例代码中,我们导入了markdown-it

    • 然后在mounted钩子中实例化了MarkdownIt对象
    • 并将Markdown文本this.articleInfo.content传递给其.render()方法来解析为HTML并赋值给parsedContent
    • 然后我们使用v-html指令将解析后的内容展示在网页上。
  • 这样,无论用户使用Markdown语法还是普通的HTML编写博客内容,页面都会正确展示
  • 最终效果:

image-20230924235124466

子组件绑定自己独有的属性

  • 鬼知道这个标题是什么意思,我简单介绍下:

    • 最近在实现点击队伍,编辑并发布公告的功能,要求点击队伍后,只有该队伍对应的窗口会弹窗
    • 这就需要每个队伍中,都封装一个控制弹窗出现的变量
    • 我们当然不能在后端封装这个字段了,所以需要在前端做处理
  • 完善昨晚的队伍发布公告,遇到了些许小问题:
    • 由于编辑公告的弹窗是写在list列表中的,即每个team都有自己的公告弹窗,所以不应该使用一个变量来控制弹窗的出现/消失:
    1
    2
    3
    4
    5
    6
    // 弹窗
    const visible = ref<boolean>(false);
    // 展示弹窗
    const showModal = (team: any) => {
    visible.value = true;
    };
    • 而应该为每个team,都分配独有的控制弹窗出现的变量(showEditWindow):
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    myAxios.get("/team/created", {
    params: {
    "loginUserId": userId
    }
    }).then((res) => {
    createdTeamList.value = res.data.records.map((team: any) => {
    return {
    ...team,
    showEditWindow: false
    };
    });
    })
    • 为每一个team绑定:
    1
    2
    3
    4
    5
    6
    7
    <a-button size="large" danger @click="showModal(item)">发布公告</a-button>
    <div style="position: relative">
    <a-modal v-model:visible="item.showEditWindow" title="队伍公告" @ok="updateAnnouncement(item)">
    <p>请编辑队伍公告</p>
    <a-textarea v-model:value="announcement" style="height: 100px"/>
    </a-modal>
    </div>
    1
    2
    3
    4
    // 公告弹窗
    const showModal = (team: any) => {
    team.showEditWindow = true;
    };
  • 这个思路是普适的,在获取后台传回的数组信息时,为数组内每一个元素都封装自己的独有的元素

  • 实现每篇文章的点赞、收藏,以及申请加入队伍中,都是这样的思路(2023/09/28午)

Eslint 优化导致git代码推送失败

  • 这是我在今晚推送 Memory API接口开放平台时遇到的报错。详情看下图: (2023/10/18晚)

image-20231018214512698

  • 如上图所示,使用 git 推送代码竟然报错了,这是什么原因呢?
  • 我们简单罗列出以上的报错信息:

这些错误和警告是由静态代码分析工具 ESLint 提供的。根据报错信息,您遇到了一些问题:

  1. File ignored because of a matching ignore pattern. Use "--no-ignore" to override:这是一个警告,意味着某些文件被忽略了,因为它们匹配了 .eslintignore 文件中设定的忽略模式。您可以使用 --no-ignore 参数来覆盖这个忽略,让这些文件被检查。
  2. 'loading' is assigned a value but never used @typescript-eslint/no-unused-vars:这是一个错误,提示您在 InterfaceInfo/index.tsx 文件中定义了 loading 变量,但是从未使用过。您可以删除没被使用的变量,或者使用变量来解决您需要的逻辑。
  3. Missing "key" prop for element in array react/jsx-key:这是两个错误,它们指出在您的组件 InterfaceInforenderDataSource 方法中,两个元素没有提供 key 属性。React 要求在渲染组件数组时,每个元素都要提供一个唯一的 key 属性,以便在更新列表时进行准确的 diff 操作。您需要为这两个元素添加一个适当的 key 属性。
  • 所以,这样的报错信息是由静态代码分析工具 ESLint 导致的(2023/10/18晚)

    • 我们使用该工具暴露出编码过程中的代码不规范的问题
    • 但是,在推送代码时,这个工具却仍在校验代码的完整性和规范性,
  • 在这些代码报错不影响项目运行的前提下,影响了正常的代码提交流程
  • 解决方法很简单,只需要在 git commit 命令后添加 –no-version ,绕过 ESLint 工具的代码校验功能即可(如下):

1
git commit -m "接口调用页面优化 接口参数信息 接口文档预开发" --no-verify

image-20231018221051988

  • 这里其实还有很多有关 ESLint 的配置管理,我就暂时不深究了,日后有机会再展开聊聊

安装依赖

  • 导入外部插件时,要选择这个选项,嗖嗖嗖就安装好了(可能这个安装默认是 yarn 的缘故)(2023/11/11晚)
  • 但是点第一个或者执行以下命令就得卡住好一会儿,还下载安装失败:
1
npm install moment

image-20231111170920323

快速入门 Vue 指南

🍖 推荐阅读:写给后端大忙人看的vue入门指南 - 掘金 (juejin.cn)

还是非常推荐阅读的,我第一次学 Vue 就是跟着黑马的教程学的,这些操作也都过了一遍的

经常使用脚手架vitevue/cli 等)直接构建 Vue 项目,很方便很快捷

有时间再实操一遍吧,后端开发人员多了解一些 Vue 还是很有必要的(2023/11/11晚)

栅格化布局

  • 简单学习了解了下:(2023/12/05早)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<div style="margin-top: 30px">
<a-row :gutter="16">
<a-col :push="2" :span="4">
<div style="background-color: #4801">
dd
</div>
</a-col>

<a-col :push="2" :span="15">
<div style="background-color: #4801">
dd
</div>

</a-col>
</a-row>
</div>

image-20231205120654539

引入外部组件页面

  • 编写 ComprehensiveArticle.vue 页面:
1
2
3
4
5
6
7
8
9
10
<template>
<div>
综合文章
</div>
</template>

<script setup lang="ts">

</script>

  • ArticleList.vue 中导入该页面组件:
1
2
3
4
5
6
7
8
9
<div style="width: 500px">
<a-tabs v-model:activeKey="activeKey" tab-position="left">
<a-tab-pane key="1" tab="Tab 1">
<CompreArticleList/>
</a-tab-pane>
<a-tab-pane key="2" tab="Tab 2">Content of Tab 2</a-tab-pane>
<a-tab-pane key="3" tab="Tab 3">Content of Tab 3</a-tab-pane>
</a-tabs>
</div>
1
2
3
4
<script setup lang="ts">
import CompreArticleList from "@/components/CompreArticleList.vue";
.......................
</script>
  • 导入成功!效果如下:(2023/12/20晚)

image-20231220233126380

  • 页面优化完成:(2023/12/21晚)

image-20231222000308888

引入外部组件页面并传参

  • 外部页面组件 ArticleList 携带参数,参数名为 compreList,参数值为 articleList
1
2
3
4
5
6
7
<a-tabs v-model:activeKey="activeKey" tab-position="left">
<a-tab-pane key="1" tab="综合">
<CompreArticleList :compre-list="articleList"/>
</a-tab-pane>
<a-tab-pane key="2" tab="后端">
............................
</a-tabs>
  • 编写内部组件 CompreArticleList,接收参数 compreList,使用 a-list组件展示
1
2
3
4
5
6
7
8
 <a-list
class="demo-loadmore-list"
item-layout="horizontal"
:data-source="props.compreList"
style="width: 400%"
>
................................
</a-list>
  • CompreArticleList 下指明接收参数名(2023/12/22早)
1
2
3
4
5
6
7
8
9
10
11
12
13
<script setup lang="ts">
import {defineProps, withDefaults} from "vue/dist/vue";

..........................................
interface Props {
compreList: any[];
}

const props = withDefaults(defineProps<Props>(), {
compreList: () => [],
});

</script>

自定义 icon 图标

  • 复制 SVG 代码:

image-20240113231259009

  • 引入组件:
1
npm install --save @ant-design/icons-vue
1
import Icon from '@ant-design/icons-vue';
1
2
3
4
5
6
7
<Icon :style="{ color: 'hotpink'}">
<template #component>
<svg t="1705158531565" class="icon" viewBox="0 0 1024 1024" version="1.1"
....................................
</svg>
</template>
</Icon>
  • 完成!效果如下:

image-20240113231553890

组件 textarea 高度动态变化

  • 后台封装返回高度:
1
2
3
4
5
6
7
8
9
@Data
public class Result {
/**
* 文本框高度
*/
private int height;

..................................
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public Result getResultCommon(double result, double discount, String resultStr, int height) {
// 根据打着折扣计算最终结果
double afterDis = result * discount;

Result calResult = new Result();
calResult.setBeforeDis(0.0D);
calResult.setAfterDis(0.0D);
calResult.setBeforeDis(result);
calResult.setAfterDis(afterDis);
calResult.setProcedure(resultStr);
calResult.setHeight(height);

return calResult;
}
  • 使用 a-textarea 文本框组件,高度随接收参数值 height 动态变化:(2024/01/14晚)
1
2
3
4
5
6
<a-form-item label="计算过程">
<a-textarea v-model:value="result.procedure"
placeholder="Basic usage" :rows="result.height"
style="font-size: medium;font-weight: bold">
</a-textarea>
</a-form-item>
1
2
3
4
5
6
const result = ref<ResultState>({
beforeDis: 0,
afterDis: 0,
procedure: "",
height: 5
});

CSS 选择器巩固

1
2
3
4
5
6
7
8
9
// 捕获 id = "leftZhankai"
Element leftZhankai = doc.getElementById("leftZhankai");
Elements heads = leftZhankai.select(".sons .cont div:nth-of-type(2)");
for (Element head : heads) {
Elements title = head.select(">p:nth-of-type(2)");
// Elements author = head.select("p:nth-of-type(2)");
// System.out.println(title.text() + " " + author.text());
System.out.println("hhh" + title.text());
}

image-20231112200526767

  • 熟悉 CSS 选择器之后,解析 HTML 文档获取标题、诗人和内容就很轻松了:

image-20231112202249521

  • 存在多个标签,随机选择五个:(2024/01/16晚)
1
2
// 随机选择五个标签
List<Element> selectedItems = galleryItem.subList(0, 5);
  • 获取 img 标签的属性:
1
2
3
4
5
6
7
8
9
10
11
12
13
// 打印选中的标签
for (Element item : selectedItems) {
Elements img = item.select("> a > img");
String src = "https:" + img.attr("data-src");
String title = img.attr("title");

Picture picture = new Picture();
picture.setCategory(category);
picture.setTitle(title);
picture.setUrl(src);

pictureList.add(picture);
}

MarkDown 编辑器

官方文档:bytedance/bytemd: ByteMD v1 repository (github.com)

1
npm i @bytemd/vue-next
1
npm i @bytemd/plugin-highlight @bytemd/plugin-gfm

新建 Markdown 编辑器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<template>
<Editor :value="value" :plugins="plugins" @change="handleChange" />
</template>

<script setup lang="ts">
import gfm from "@bytemd/plugin-gfm";
import highlight from "@bytemd/plugin-highlight";
import { Editor } from "@bytemd/vue-next";
import { withDefaults, defineProps, ref } from "vue";

/**
* 定义组件属性类型
*/
interface Props {
value: string;
mode?: string;
handleChange: (v: string) => void;
}

const plugins = [
gfm(),
highlight(),
// Add more plugins here
];

/**
* 给组件指定初始值
*/
// const props = withDefaults(defineProps<Props>(), {
// value: () => "",
// mode: () => "split",
// handleChange: (v: string) => {
// console.log(v);
// },
// });

const value = ref("");

const handleChange = (v: string) => {
value.value = v;
};
</script>

<style></style>

在 main.ts 下,引入样式:

1
import "bytemd/dist/index.css";

image-20240201131855728

代码编辑器

总结


浴火重生:从挫败到成功,前端框架的疑难杂症解决之道
http://example.com/2023/07/21/浴火重生:从挫败到成功,前端框架的疑难杂症解决之道/
作者
Memory
发布于
2023年7月21日
更新于
2024年1月16日
许可协议