chore: 新版本开发

This commit is contained in:
pengzhanbo 2022-04-05 11:50:15 +08:00
parent f2924b15d2
commit d65964242c
229 changed files with 15046 additions and 3805 deletions

View File

@ -4,4 +4,3 @@ lib/
dist/
!.vuepress/
!.*.js
example/

View File

@ -14,7 +14,7 @@ module.exports = {
files: ['*.ts', '*.vue'],
extends: 'vuepress-typescript',
parserOptions: {
project: ['tsconfig.json'],
project: ['./tsconfig.json'],
},
rules: {
'@typescript-eslint/ban-ts-comment': 'off',

24
.gitignore vendored
View File

@ -1,25 +1,13 @@
# VuePress files
example/.vuepress/.temp/
example/.vuepress/.cache/
example/.vuepress/dist/
node_modules
# Dist files
lib/
docs/.vuepress/.cache
docs/.vuepress/.temp
docs/.vuepress/dist
# Test temp files
**/__fixtures__/.temp/
lib
# Test coverage files
coverage/
# Node modules
node_modules/
# MacOS Desktop Services Store
.DS_Store
# Log files
*.log
# Typescript build info
*.tsbuildinfo
.mind

View File

@ -1,8 +0,0 @@
/example
/node_modules
/src
.editorconfig
.eslint*
.git*
**/*.tsbuildinfo
vuepress.config.js

29
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,29 @@
{
"editor.insertSpaces": true,
"editor.tabSize": 2,
"files.encoding": "utf8",
"files.eol": "\n",
"files.trimFinalNewlines": true,
"files.trimTrailingWhitespace": true,
"[markdown]": {
"files.trimTrailingWhitespace": false
},
"typescript.tsdk": "node_modules/typescript/lib",
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue"
],
"cSpell.words": [
"caniuse",
"composables",
"Docsearch",
"nprogress",
"tsbuildinfo",
"vite",
"vuepress",
"vueuse"
]
}

91
docs/.vuepress/config.ts Normal file
View File

@ -0,0 +1,91 @@
import * as path from 'path'
import type { PlumeThemeOptions } from '@vuepress-plume/vuepress-theme-plume'
import { defineUserConfig } from '@vuepress/cli'
export default defineUserConfig<PlumeThemeOptions>({
lang: 'zh',
title: 'Plume Theme',
description: '',
public: path.resolve(__dirname, '../public'),
theme: '@vuepress-plume/vuepress-theme-plume',
themeConfig: {
logo: 'https://pengzhanbo.cn/g.gif',
avatar: {
url: 'https://via.placeholder.com/300?text=Profile+Photo',
name: 'Plume Theme',
description: 'The Theme for Vuepress 2.0',
},
social: {
email: 'volodymyr@foxmail.com',
github: 'pengzhanbo',
QQ: '942450674',
weiBo: 'https://weibo.com',
zhiHu: 'https://zhihu.com',
facebook: 'https://baidu.com',
twitter: 'https://baidu.com',
linkedin: 'https://baidu.com',
},
notes: {
notes: [
{
link: 'typescript',
dir: 'typescript',
text: 'Typescript',
sidebar: [],
},
],
},
darkMode: true,
navbar: [
{ text: '首页', link: '/' },
{
text: '分类',
link: '/category/',
},
{
text: '标签',
link: '/tag/',
},
{
text: '笔记',
children: [
// {
// text: '技术',
// children: [{ text: '《typescript学习笔记》', link: '/' }],
// },
// {
// text: '技术',
// children: [{ text: '《typescript学习笔记》', link: '/' }],
// },
{
text: 'typescript',
link: '/note/typescript/',
},
{
text: '标签',
link: '/tag/',
},
],
},
],
footer: {
copyright: 'Copyright © 2022-present pengzhanbo',
},
themePlugins: {
caniuse: {
mode: 'embed',
},
search: {
// hotKeys: ['s', '/'],
// maxSuggestions: 5,
// isSearchable: (page) => page.path !== '/',
// getExtraFields: () => [],
locales: {
'/': {
placeholder: '搜索',
},
},
},
},
},
})

View File

@ -0,0 +1,38 @@
---
title: BFC 块级格式化上下文
createTime: 2018/05/17 12:28:33
permalink: /article/o5g7ggvf
author: pengzhanbo
top: false
tags:
- html
type: null
---
## 概念
BFC, Block Formating Context。是 W3C CSS2.1规范中的一个概念。 是页面中的一块块级渲染区域,并且有一套渲染规则,它决定了其子元素将如何定位,以及和其他元素的关系和作用。
具有BFC特性的元素可以看做是一个被隔离了的独立容器容器内的元素不会在布局上影响到外面的元素并且BFC具有普通容器所没有的一些特性。
## 创建BFC的方式
1. 根元素html
2. 浮动元素,即 float值不为 none。
3. 绝对定位元素, 元素的 position 为 absolute 或者 fixed
4. 行内块元素, 元素的 display 为 inline-block
5. 表格单元格, 元素的 display 为 table-cell。 HTML表格单元格默认为该值
6. 表格标题, 元素的display为table-caption。 HTML表格标题默认为该值
7. 匿名表格单元格元素, 元素的display为 table、table-row、table-row-group、table-header-group、table-footer-group 。 (分别是 HTML table、row、tbody、thead、tfoot的默认属性或 inline-table。
8. overflow计算值不为visible的块元素
9. display值为 flow-root的元素
10. contain值为 layout、content、paint的元素
11. 弹性元素display为 flex、inline-flex元素的直接子元素
12. 网格元素, display为gird、inline-gird元素的直接子元素
13. 多列容器元素的column-count或column-width不为 auto 包括column-count不为1
14. colum-span为all的元素始终会创建一个新的BFC即使该元素没有包裹在一个多列容器中。
## 作用
1. 同一个BFC的外边距会发生折叠合并 通过将其放在不同的BFC中规避折叠。
2. BFC可以包含浮动元素即清除浮动。
3. BFC可以阻止元素被浮动元素覆盖。

View File

@ -0,0 +1,163 @@
---
title: CSS At-Rules
createTime: 2018/10/06 08:16:38
permalink: /article/btkqop1a
author: pengzhanbo
tags:
- css
top: false
type: null
---
## @charset
#### 概述
指定样式表中使用的字符编码。 它必须位于样式表中的第一个元素,且前面不得有任何字符。
不能在 `<style>` 元素内的样式属性内使用。
#### 示例:
``` css
@charset "UTF-8";
@charset "utf-8"; /*大小写不敏感*/
/* 设置css的编码格式为Unicode UTF-8 */
@charset 'UTF-8'; /* 无效的, 使用了错误的引号 */
@charset "UTF-8"; /* 无效的, 多于一个空格 */
@charset "UTF-8"; /* 无效的, 在at-rule之前多了一个空格 */
@charset UTF-8; /* Invalid, without ' or ", the charset is not a CSS <string> */
```
## @font-face
#### 概述
指定一个用于显示文本的自定义字体。
字体可以从远程服务器,也可以是用户本地安装的字体。
`@font-face` 可以解除对用户电脑字体的依赖。
#### 语法
``` css
@font-face {
[ font-family: <family-name>; ] ||
[ src: <src>; ] ||
[ unicode-range: <unicode-range>; ] ||
[ font-variant: <font-variant>; ] ||
[ font-feature-settings: <font-feature-settings>; ] ||
[ font-variation-settings: <font-variation-settings>; ] ||
[ font-stretch: <font-stretch>; ] ||
[ font-weight: <font-weight>; ] ||
[ font-style: <font-style>; ] ||
[ size-adjust: <size-adjust>; ] ||
[ ascent-override: <ascent-override>; ] ||
[ descent-override: <descent-override>; ] ||
[ line-gap-override: <line-gap-override>; ]
}
```
- `font-family`: 指定的 `<family-name>` 将会被用于 `font``font-family`的属性
- `src`: 远程字体文件的位置,或者通过`local`函数通过字体名字从本地加载字体。
#### 使用示例:
加载远程字体文件:
``` html
...
<style>
@font-face {
font-family: custom-font;
src: url("http://example.com/custom-font.ttf")
}
body {
font-family: custom-font;
}
</style>
...
```
加载字体文件,先尝试从用户本地加载,如果加载失败则从远程服务器下载:
``` html
...
<style>
@font-face {
font-family: MgOpenModernaBold;
src: local("Helvetica Neue Bold"),
url(MgOpenModernaBold.ttf);
}
body {
font-family: MgOpenModernaBold;
}
</style>
...
```
加载不同文件格式的字体,根据用户环境判断使用兼容的字体文件格式:
``` html
...
<style>
@font-face {
font-family: custom;
src: url("custom.ttf") format("tff"),
url("custom.woff") format("woff"),
url("custom.woff2") format("woff2");
}
body {
font-family: custom;
}
</style>
...
```
## @import
#### 概述
从其他样式表导入样式规则。
`@import` 必须优先于其他类型的规则,即需要在文件顶部声明。`@charset` 除外。
#### 语法
``` css
@import url;
@import url list-of-media-queries;
```
- `url` 样式规则文件资源位置
- `list-of-media-queries` [媒体查询](https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries/Using_media_queries),支持用逗号分隔多个查询条件。资源仅在满足媒体查询条件时才会被加载。
## @keyframes
#### 概述
通过在动画序列中定义关键帧的样式来控制CSS动画序列中的中间步骤。
#### 示例
使用 `from`,`to` 定义起始和结束关键帧的样式 实现动画
``` css
@keyframes slidein {
from {
transform: translateX(0%);
}
to {
transform: translateX(100%);
}
}
```
使用 百分比 定义触发关键帧的时间点
``` css
@keyframes slidein {
0% {
transform: translateX(0%);
}
50% {
transform: translateX(50%);
}
100% {
transform: translateX(100%);
}
}
```
## @media
媒体查询,详见 [CSS @media 媒体查询](/post/fe5ruia1/)

View File

@ -0,0 +1,192 @@
---
title: CSS 媒体查询
createTime: 2018/08/18 08:43:02
permalink: /article/fe5ruia1
author: pengzhanbo
tags:
- css
top: false
type: null
---
开发响应式网站时,常常需要使用到 media 媒体查询。这里总结下媒体查询的使用方法。
## 概述
媒体查询是通过判断当前媒体是否满足 媒体查询规则,从而使其包含的 CSS规则生效。
从 CSS level 2 开始,就已经支持 `media-queries`,到 CSS level 3 以及之后的版本,媒体查询变得更加的丰富和能够适应更多的场景。
## 使用
媒体查询可以通过以下三种方式进行使用:
#### 在 `<link>` 元素引入CSS资源时声明 `media` 属性
``` html
<link rel="stylesheet" type="text/css" href="media/custom.css" media="screen and (min-width: 400px)">
```
#### 在`<style>` 上 声明 `media` 属性
``` html
<style media="screen and (min-width: 400px)">
</style>
```
#### 在`@import` 后 声明 媒体查询条件
``` css
@import url('custom.css') screen and (min-width: 400px);
```
#### 在样式表中使用 At-Rule `@media` 使用媒体查询规则
``` css
@media screen and (min-width: 400px) {
.example {
color: red;
}
}
```
### 语法
``` html
<link rel="stylesheet" type="text/css" href="media/custom.css" media="[media-queries-list]">
<style media="[media-queries-list]">
</style>
<style>
@import url [media-queries-list];
@media [media-queries-list] {
<style-sheet-group>
}
</style>
```
## 媒体查询 [media-queries-list]
`media-queries-list` 可以由以下三种内容组成:
- `Media types` :媒体类型, 表示设备
- `Media features` :媒体特性, 表示设备的状态
- `Logical operators` 逻辑操作符, 连接多个 `media-query`
### Media types
`Media types` 描述设备的一般类型。可以使用以下值:
- `all`: 表示适用于所有设备。 默认值。
- `print`: 表示 适用于在屏幕上以打印预览的模式查看页面和文档。
- `screen`: 表示 适用于屏幕 。
> 在 *css2.1**Media Queries 3* 中还支持 `tty``tv``projection``handheld``braille``embossed``aural`,但这些值都已经在*Media Queries 4* 中被弃用。
### Media features
媒体特性,描述 用户代理、输出设备以及环境的特定特征。
媒体特性表达式是完全是可选的,并且负责测试这些特性是否存在,值为多少。 且每个媒体特性表达式都必须使用括号括起来。
*以下仅列出比较常用到的媒体特性*
- `width`: 视窗viewport的宽度包括纵向滚动条的宽度。
值的类型为 number单位可以是 `px``em` 等。
``` css
with: 400px
```
- `height`: 视窗viewport的高度。
值的类型为 number单位可以是 `px``em` 等。
``` css
height: 600px
```
- `aspect-ratio` 视窗viewport的宽高比。
值的类型为 number/number。
``` css
aspect-ratio: 3/2
```
- `orientation` 视窗viewport 的旋转方向。
- portrait 设备竖屏
- landscape 设备横屏
``` css
orientation: landscape
```
- `resolution`: 输出设备的分辨率
值的类型为 number单位为 `dpi`
``` css
resolution: 320dpi
```
- `scan`:输出设备的扫描过程(适用于电视机等)。
#### 媒体特性前缀
大部分的媒体特性均支持前缀,用于约束媒体特性的作用范围。
- `max-[media feature]` 小于指定的最大值时,返回*true*
- `min-[media feature]`: 大于指定的最小值时,返回*true*
*个人认为使用前缀时其表述稍显拗口,建议使用取值范围的方式声明表达式*
#### 媒体特性语法
- 以键值对的形式,表述取固定的值
````
([media-feature-name]: [media-feature-value])
````
- 直接书写name 表示值的结果为 boolean
```
([media-feature-name])
```
- 表述 特性的取值范围
*声明 range 为描述数学符号 : '<' | '>' | '<=' | '>='*
```
([media-feature-name] [range] [media-feature-value])
([media-feature-name] [range] [media-feature-value] [range] [media-feature-value])
```
### Logical operators
逻辑操作符用于组成复合的 media queries。
- `and`: 用于合并多条`media query`, 且 每条 `media query` 均返回 *true* 时,
媒体查询表达式的结果返回*true*。
- `not`: 取反操作,使用`not [media query]`,当`media query` 返回 *false* 时,
媒体查询表达式的结果返回*true*。
- `,`: or操作符组合多个 `media query`,任意一个`media query` 返回 *true*,
媒体查询表达式的结果返回*true*。
- `only`: 不支持更加高级的媒体类型的浏览器检测到only修饰的时候就会抛弃这个规则
## 使用示例详解
### 示例1
``` css
@media screen and (width > 414px) {}
```
当设备的屏幕视窗宽度大于414px时应用CSS块中的样式规则。
### 示例2
``` css
@media (width > 800px), screen and (orientation: landscape) {}
```
当前设备 视窗宽度大于 800px 或者设备方向为横向时应用css块中的样式规则。
### 示例3
``` css
@media screen and (414px < width < 800px) {}
```
当前设备屏幕视窗宽度 大于 414px 且 小于 800px 时, 应用css块中的样式规则。

View File

@ -0,0 +1,647 @@
---
title: CSS选择器
createTime: 2018/09/20 03:29:20
permalink: /article/8vev8ixl
author: pengzhanbo
tags:
- css
top: false
type: null
---
## Basic Selectors 基础选择器
### Element selector
根据 element type 匹配 一组元素
``` html
...
<style>
p { color: red; }
</style>
...
<p>content</p>
...
```
### Class selector
根据 element 声明的 class属性值 匹配一组元素
``` html
...
<style>
.red { color: red; }
</style>
...
<p class="red">content</p>
...
```
### ID selector
根据 element 声明的 ID属性值匹配一个元素一个页面中ID具有唯一性
``` html
...
<style>
#red { color: red; }
</style>
...
<p id="red">content</p>
...
```
### Universal selector
通配符,匹配所有 element
``` html
...
<style>
* { color: red; }
</style>
...
<p>content</p>
<span>span</span>
...
```
## Attribute Selectors
### \[attribute\] selector
匹配声明了该attribute的 一组 element
``` html
...
<style>
[href] { color: red; }
</style>
...
<a href="">content</a>
...
```
### \[attribute="x"\] selector
匹配声明了该attribute且值为 x 的一组 element
``` html
...
<style>
[title="a"] { color: red; }
</style>
...
<abbr title="a">abbr</abbr>
...
```
### \[attribute~="x"\] selector
匹配声明了该attribute且值包含了 单词 x 的一组 element
``` html
...
<style>
[title~="style"] { color: red; } /* 匹配包含了 独立单词 style 的 element */
</style>
...
<abbr title="sheet style">abbr</abbr>
<abbr title="sheetstyle"></abbr> <!-- no match -->
...
```
### \[attribute|="x"\] selector
匹配声明了该attribute且值包含了一个 `x-` 开头的连字符拼接的词 的一组 element
``` html
...
<style>
/* lang的值必须 包含 en 通过连接符 - 连接另一个单词的 词 */
[lang|="en"] { color: red; }
</style>
...
<abbr lang="en-US">abbr</abbr>
<!-- no match lang="en" lang="enUS" -->
...
```
### \[attribute^="x"\] selector
匹配声明了该attribute且值是以 x 作为开头的 一组 element
``` html
...
<style>
[href^="https://"] { color: red; }
</style>
...
<a href="https://example.com">content</a>
...
```
### \[attribute$="x"\] selector
匹配声明了该attribute且值是以 x 作为结尾的 一组 element
``` html
...
<style>
[href$=".pdf"] { color: red; }
</style>
...
<a href="https://example.com/a.pdf">content</a>
...
```
### \[attribute*="x"\] selector
匹配声明了该attribute且值包含了子串 x 的 一组 element
``` html
...
<style>
[href*="example"] { color: red; }
</style>
...
<a href="https://example.com">content</a>
...
```
## Combinators 关系选择器
关系选择器适用于 任意选择器 的组合
### selector1 selector2 后代关系选择器
匹配 selector1 的元素中,所有 selector2 的 元素
``` html
...
<style>
section span { color: red; }
</style>
...
<section>
<span></span> <!-- match -->
<p><span></span></p> <!-- match -->
</section>
...
```
### selector1 > selector2 子代关系选择器
匹配 selector1 的下一级满足 selector2 的 一组元素
``` html
...
<style>
section > span { color: red; }
</style>
...
<section>
<span></span> <!-- match -->
<p><span></span></p> <!-- no match -->
</section>
...
```
### selector1 + selector2 相邻兄弟选择器
匹配selector1后同级的紧跟的selector2的一个元素
``` html
...
<style>
h2 + p { color: red; }
</style>
...
<p></p> <!-- no match -->
<h2></h2>
<p></p> <!-- match -->
<p></p> <!-- no match -->
...
```
### selector ~ selector2 一般兄弟选择器
匹配selector1后同级的selector2的一组元素
``` html
...
<style>
h2 ~ p { color: red; }
</style>
...
<p></p> <!-- no match -->
<h2></h2>
<p></p> <!-- match -->
<p></p> <!-- match -->
<span></span>
<p></p> <!-- match -->
...
```
## Group Selectors 组合选择器
### selector1, selector2, ...
匹配用`,` 隔开的所有选择器
``` html
...
<style>
p, span { color: red; }
</style>
...
<section>
<span></span>
<p><span></span></p>
</section>
...
```
## Pseudo-elements 伪元素选择器
### ::first-letter
匹配 element中的首个字符字母、中文字、符号均可
``` html
...
<style>
p::first-letter { color: red; }
</style>
...
<p>One</p> <!-- match: O -->
...
```
### ::first-line
匹配 element中的首行文字
``` html
...
<style>
p::first-line { color: red; }
</style>
...
<p>
One Two <br> <!-- match -->
Three
</p>
...
```
### ::before
`content` 属性一起使用,在匹配的元素内容之前生成的内容
``` html
...
<style>
p::before { content: 'before ' }
</style>
...
<p>
One Two <!-- render: before One Two -->
</p>
...
```
### ::after
`content` 属性一起使用,在匹配的元素内容之后生成的内容
``` html
...
<style>
p::after { content: ' after' }
</style>
...
<p>
One Two <!-- render: One Two after -->
</p>
...
```
## Pseudo-classes 伪类选择器
### :link
匹配一个没有被访问过的链接
``` html
...
<style>
a:link { color: red }
</style>
...
<a href="">link</a>
...
```
### :visited
匹配一个已访问过的链接
``` html
...
<style>
a:visited { color: red }
</style>
...
<a href="">link</a>
...
```
### :active
匹配一个正在被激活的链接
``` html
...
<style>
a:active { color: red }
</style>
...
<a href="">link</a>
...
```
### :hover
匹配一个被光标悬停的链接
``` html
...
<style>
a:hover { color: red }
</style>
...
<a href="">link</a>
...
```
### :focus
匹配一个具有焦点的元素
``` html
...
<style>
input:focus { color: red }
</style>
...
<input type="text">
...
```
### :target
匹配一个已被链接到的元素。
例如通过`<a href="#heading"></a>`链接的head元素
``` html
...
<style>
h2:target { color: red }
</style>
...
<h2 id="heading">heading</h2>
...
```
### :first-child
匹配在同一个父元素内的的第一个子元素
``` html
...
<style>
p:first-child { color: red }
</style>
...
<p>first child</p> <!-- match -->
<p>second child</p>
...
```
### :last-child
匹配在同一个父元素内的的最后一个子元素
``` html
...
<style>
p:last-child { color: red }
</style>
...
<p>first child</p>
<p>last child</p> <!-- match -->
...
```
### :nth-child(n)
匹配在同一个父元素内的从上往下数的第N子个元素
``` html
...
<style>
p:nth-child(2) { color: red }
</style>
...
<p>first child</p>
<p>second child</p> <!-- match -->
...
```
### :nth-last-child(n)
匹配在同一个父元素内的从下往上数的第N个子元素
``` html
...
<style>
p:nth-last-child(2) { color: red }
</style>
...
<p>first child</p> <!-- match -->
<p>second child</p>
...
```
### :first-of-type
匹配在同一个父元素中的同类型的第一个元素
``` html
...
<style>
p:first-of-type { color: red }
</style>
...
<p>first child</p> <!-- match -->
<p>second child</p>
...
```
### :last-of-type
匹配在同一个父元素中的同类型的最后一个元素
``` html
...
<style>
p:last-of-type { color: red }
</style>
...
<p>first child</p> <!-- match -->
<p>second child</p>
...
```
### :nth-of-type(n)
匹配在同一个父元素中的同类型的从上往下数的第N个元素
``` html
...
<style>
p:nth-of-type(2) { color: red }
</style>
...
<p>first child</p>
<p>second child</p> <!-- match -->
...
```
### :nth-last-of-type(n)
匹配在同一个父元素中的同类型的从下往上数的第N个元素
``` html
...
<style>
p:nth-last-of-type(2) { color: red }
</style>
...
<p>first child</p> <!-- match -->
<p>second child</p>
...
```
### :only-child
如果元素是其父元素的唯一子元素,则匹配该元素
``` html
...
<style>
section p:only-child { color: red }
</style>
...
<section>
<p> only child </p>
</section>
...
```
### :only-type
如果元素是其父元素的唯一的同类型的子元素,则匹配该元素
``` html
...
<style>
section p:only-of-type { color: red }
</style>
...
<section>
<p> only </p> <!-- match -->
<span></span>
</section>
...
```
### :lang(lang)
匹配给定语言的元素
``` html
...
<style>
div:lang(fr) { color: red }
</style>
...
<section>
<div lang="fr"><q>This French quote has a <q>nested</q> quote inside.</q></div>
</section>
...
```
### :empty
匹配没有子元素或内容的元素
``` html
...
<style>
div:empty { background-color: red }
</style>
...
<section>
<div></div>
</section>
...
```
### :root
匹配文档的根元素, (即匹配的 `<html>`元素)
### :enabled
匹配未被禁用的表单控件元素
### :disabled
匹配被禁用的表单控件元素
### :checked
匹配选中的单选或复选框类型的输入元素。
### :not(selector)
协商伪类。匹配不匹配选择器的元素。
## 实验中的 Selectors
这些选择器在某些浏览器中尚处于开发中,功能对应的标准文档可能被修改,在未来的版本中可能发生变化,谨慎使用。
### :any-link
匹配有链接锚点的元素,而不管元素是否被访问过。
即会匹配每一个有 `href`属性的`<a>``<area>``<link>`的元素,匹配到所有的`:link``:visited`
``` html
...
<style>
a:any-link {
border: 1px solid blue;
color: orange;
}
</style>
...
<a href="https://example.com">External link</a><br>
<a href="#">Internal target link</a><br>
<a>Placeholder link (won't get styled)</a>
...
```
::: caniuse css-any-link
:::
### :dir(dir)
如果元素的内容的书写方向是 dir , 则匹配该元素
*dir* : ltr | rtl
``` html
...
<style>
:dir(ltr) {
background-color: yellow;
}
:dir(rtl) {
background-color: powderblue;
}
</style>
...
<div dir="rtl">
<span>test1</span>
<div dir="ltr">test2
<div dir="auto">עִבְרִית</div>
</div>
</div>
...
```
::: caniuse css-dir-pseudo
:::
### :has(selector)
如果一个元素A恰好满足包含了selector 匹配的元素则匹配元素A
``` html
...
<style>
a:has(> img) {
background-color: yellow;
}
</style>
...
<a><img src="example.jpg"></a> <!-- match -->
<a></a>
...
```
::: caniuse css-has
:::
### :is() / :any()
匹配一组选择器选中的元素。
优先级是由它的选择器列表中优先级最高的选择器决定。
``` html
...
<style>
:is(header, main, footer) p:hover {
color: red;
cursor: pointer;
}
</style>
...
<!-- 等价于 -->
<style>
header p:hover,
main p:hover,
footer p:hover {
color: red;
cursor: pointer;
}
</style>
...
```
::: caniuse css-matches-pseudo
:::
### :where()
匹配一组选择器选中的元素。
:where() 的优先级总是为 0。
``` html
...
<style>
:where(header, main, footer) p:hover {
color: red;
cursor: pointer;
}
</style>
...
<!-- 等价于, 但优先级不同 -->
<style>
header p:hover,
main p:hover,
footer p:hover {
color: red;
cursor: pointer;
}
</style>
...
```

View File

@ -0,0 +1,108 @@
---
title: <!DOCTYPE> 文档类型声明
createTime: 2018/03/14 01:06:52
permalink: /article/s8udp6vp
author: pengzhanbo
tags:
- html
top: false
type: null
---
Web世界中随着历史的发展技术的迭代发展出了许多不同的文档只有了解文档的类型浏览器才能正确的解析渲染文档。
<!-- more -->
HTML也有多个不同的版本只有完全明白页面使用的是哪个确切的HTML版本浏览器才能完全正确的显示出HTML页面。
## 定义
`<!DOCTYPE>` 标签是一种标准通用标记语言的文档类型声明目的是告诉标准通用标记语言解析器它应该使用什么样的文档类型定义DTD来解析文档。
## 作用
声明文档的解析类型 document.compatMode避免浏览器的怪异模式。
__document.compatMode:__
- `BackCompat`: 怪异模式,浏览器使用自己的怪异模式解析渲染页面。
- `CSS1Compat`: 标准模式浏览器使用W3C的标准解析渲染页面。
## 使用
在文档的首行进行声明。必须位于 html标签之前。
`<!DOCTYPE>` 声明不是HTML标签它是指示浏览器关于页面使用哪个HTML版本的指令。
> 如果页面没有 DOCTYPE 声明,那么默认是 怪异模式为了确保浏览器按预期渲染页面必须进行DOCTYPE声明。
### 常用的DOCTYPE声明
一般情况下,默认使用以下声明即可。
``` html
<!DOCTYPE html>
<html>
</html>
```
## 一般DOCTYPE声明列表
### html5
``` html
<!DOCTYPE html>
```
### HTML 4.01 Strict
该 DTD 包含所有 HTML 元素和属性,但不包括展示性的和弃用的元素(比如 font。不允许框架集Framesets
``` html
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
```
### HTML 4.01 Transitional
该 DTD 包含所有 HTML 元素和属性,包括展示性的和弃用的元素(比如 font。不允许框架集Framesets
``` html
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
```
### HTML 4.01 Frameset
该 DTD 等同于 HTML 4.01 Transitional但允许框架集内容。
``` html
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN"
"http://www.w3.org/TR/html4/frameset.dtd">
```
### XHTML 1.0 Strict
该 DTD 包含所有 HTML 元素和属性,但不包括展示性的和弃用的元素(比如 font。不允许框架集Framesets。必须以格式正确的 XML 来编写标记。
``` html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
```
### XHTML 1.0 Transitional
该 DTD 包含所有 HTML 元素和属性,包括展示性的和弃用的元素(比如 font。不允许框架集Framesets。必须以格式正确的 XML 来编写标记。
```html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "
http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
```
### XHTML 1.0 Frameset
该 DTD 等同于 XHTML 1.0 Transitional但允许框架集内容。
```html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">
```
### XHTML 1.1
该 DTD 等同于 XHTML 1.0 Strict但允许添加模型例如提供对东亚语系的 ruby 支持)。
``` html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
```

View File

@ -0,0 +1,275 @@
---
title: HTML5新特性
createTime: 2018/02/17 12:49:58
permalink: /article/8rv45yuy
author: pengzhanbo
tags:
- html
top: false
type: null
---
## 语义标签
`<header>` `<footer>` `<nav>` `<section>` `<article>` `<aside>` `<details>` `<summary>` `<dialog>` ` <figure>` `<main>` `<mark>` `<time>` `<hgroup>`
## 增强型表单
### 新增表单元素
`<detailist>` 数据列表为input提供输入建议列表
`<progress>`:进度条,展示连接/下载进度
`<meter>`:刻度尺/度量衡,描述数据所处的阶段,红色(危险)=>黄色(警告)=>绿色(优秀)
`<output>`:输出内容,语义上表示此处的数据是经过计算而输出得到的
其他
### 新增表单属性
placehoder 输入框默认提示文字
required 要求输入的内容是否可为空
pattern 描述一个正则表达式验证输入的值
min/max 设置元素最小/最大值
step 为输入域规定合法的数字间隔
height/wdith 用于image类型`<input>`标签图像高度/宽度
autofocus 规定在页面加载时,域自动获得焦点
multiple 规定`<input>`元素中可选择多个值
### 新增 input type 类型
color 颜色选取
date 日期选择
datetime 日期选择UTC时间
datetime-local 日期选择(无时区)
month 月份选择
week 周和年 选择
time 选择时间
email 包含 email的地址输入域
number: 数值选择
url url输入域
tel 电话号码和字段
search 搜索域
range 数字范围输入域
## 视频和音频
`<audio>` 音频元素
```html
<audio controls>
<source src="horse.ogg" type="audio/ogg">
<source src="horse.mp3" type="audio/mpeg">
您的浏览器不支持 audio 元素
</audio>
```
`<video>` 视频元素
```html
<video width="320" height="240" controls>
<source src="movie.mp4" type="video/mp4">
<source src="movie.ogg" type="video/ogg">
您的浏览器不支持Video标签。
</video>
```
## Canvas绘图
`<canvas>` 是 HTML5 新增的,一个可以使用脚本(通常为 JavaScript) 在其中绘制图像的 HTML 元素。它可以用来制作照片集或者制作简单(也不是那么简单)的动画,甚至可以进行实时视频处理和渲染。
[](https://www.runoob.com/w3cnote/html5-canvas-intro.html)
## 地理位置
使用getCurrentPosition()方法来获取用户的位置。以实现“LBS服务”
```jsx
window.navigator.geolocation : {
  watchPosition(){},
  clearWatch(){},
  getCurrentPosition(function(pos){
    // '定位成功'
    // 定位时间pos.timestamp
    // 维度pos.coords.latitude
    // 经度pos.coords.longitude
    // 海拔pos.coords.altitude
    // 速度pos.coods.speed
  }, function(err){
    // '定位失败'
  }){},
}
```
## 拖放API
### 拖动的源对象(source)可能触发的事件:
**dragstart**:拖动开始
**drag**:拖动中
**dragend**:拖动结束
### 拖动的目标对象(target)可能触发的事件:
**dragenter**:拖动进入
**dragover**:拖动悬停
**drop**:松手释放
**dragleave**:拖动离开
拖放API事件句柄中所有的事件对象都有一个dataTransfer属性数据运输对象用于在源对象和目标对象间传递数据。
**源对象**event.dataTransfer.setData(key, value)
**目标对象**var value = event.dataTransfer.getData(key)
## WebWorker
[使用 Web Workers - Web API 接口参考 | MDN](https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Workers_API/Using_web_workers)
### 背景
Chrome浏览器中发起资源请求的有6个线程但是只有1个线程负责渲染页面——称为UI主线程——浏览器中所有的代码只能由一个线程来执行。
### 问题
若浏览器加载了一个很耗时的JS文件(可能影响DOM树结构),浏览器必须等待该文件执行完成才会继续执行后续的代码(HTML/CSS/JS等)——如果一个JS文件要执行10s(可能有很深的循环/递归等科学计算/解密)会发生什么——执行耗时JS任务过程中会暂停页面中一切内容的渲染以及事件的处理。
### 作用
一个执行指定任务的独立线程且该线程可以与UI主线程进行消息数据传递。
使用方式:
```jsx
// 主线程
var worker = new Worker('xx.js')
worker.postMessage('message') // 发送消息到worker线程
worker.onmessage = function (e) {
console.log(e.data) // 来自worker线程的信息
}
// worker线程
onmessage = function (e) {
console.log(e.data) // 接收主线程的消息
postMessage('message') // 发送消息到主线程
}
```
### 共享 worker
一个共享worker可以被多个脚本使用——即使这些脚本正在被不同的window、iframe或者worker访问。
> 如果共享worker可以被多个浏览上下文调用所有这些浏览上下文必须属于同源相同的协议主机和端口号
>
```jsx
var myWorker = new SharedWorker('worker.js');
// 主线程中调用
myWorker.port.start()
myWorker.port.postMessage('message');
myWorker.port.onmessage = function(e) {
console.log('Message received from worker');
}
// worker 线程调用
port.start();
// worker 需要在 onconnect事件处理函数来执行代码
onconnect = function(e) {
var port = e.ports[0];
port.onmessage = function(e) {
var workerResult = 'Result: ' + (e.data);
port.postMessage(workerResult);
}
}
```
## WebStorage
### localStorage
本地跨会话级,持久化存储
### sessionStorage
会话级存储
## WebSocket
在用户的浏览器和服务器之间打开交互式通信会话。
```jsx
const ws = new WebSocket('wx://xx')
ws.onopen = function () {}
ws.onmessage = function (e) {
console.log(e.data)
}
```
## History API
对history栈中内容进行操作。
### pushState(stateObj, title, url)
```jsx
history.pushState({}, 'foo', 'foo.html')
```
添加历史记录条目
### replaceState(stateObj, title, url)
```jsx
history.replaceState({}, 'bar', 'bar.html')
```
修改历史记录条目,浏览器不会检查替换的路径是否存在。
### popState 事件
每当活动的历史记录项发生变化时, popstate 事件都会被传递给window对象。如果当前活动的历史记录项是被 pushState 创建的,或者是由 replaceState 改变的,那么 popstate 事件的状态属性 state 会包含一个当前历史记录状态对象的拷贝。
### 获取当前状态
页面加载时或许会有个非null的状态对象。这是有可能发生的举个例子假如页面通过pushState() 或 replaceState() 方法设置了状态对象而后用户重启了浏览器。那么当页面重新加载时页面会接收一个onload事件但没有 popstate 事件。然而假如你读取了history.state属性你将会得到如同popstate 被触发时能得到的状态对象。
```jsx
// 尝试通过 pushState 创建历史条目,然后再刷新页面查看state状态对象变化;
window.addEventListener('load',() => {
let currentState = history.state;
console.log('currentState',currentState);
})
```
[History API - Web API 接口参考 | MDN](https://developer.mozilla.org/zh-CN/docs/Web/API/History_API)

View File

@ -0,0 +1,133 @@
---
title: WebComponent——template
lang: zh-CN
tags:
- html
- javascript
createTime: 2018/8/2 11:15:27
permalink: /article/5fmy4kla
author: pengzhanbo
top: false
type: null
---
在web开发领域中模板并不少见。从服务器端的模板语言`Django``jsp`等,应用十分广泛,存在了很长时间。又如前端,早期例如`art(artTemplate)`以及近年来大多数的MV*框架涌现,绝大多数在展现层使用了同样的渲染机制:模板。
<!-- more -->
> __定义__
>模板,一个拥有预制格式的文档或者文件,可作为特定应用的出发点,这样就避免在每次使用格式的时候都重复创建。
从模板的定义中我们可以发现“避免在每次使用格式的时候重复创建”从这句话来看模板可以让我们避免重复的工作。那么web平台有没有提供原生支持呢
答案是,有,在 [WhatWG HTML 模板规范](https://html.spec.whatwg.org/multipage/scripting.html#the-template-element)中,它定义了一个新的`<template>` 元素用于描述一个标准的以DOM为基础的方案来实现客户端模板。该模板允许你定义一段可以被转为 HTML 的标记,在页面加载时不生效,但可以在后续进行动态实例化。
### 声明
跟普通的html标签一样`template`标签包含的内容,即是声明的模板内容。
``` html
<template>
<img src="" />
<p>content</p>
</template>
```
“模板内容”本质上,是 __一大块的惰性可复制DOM__。在这个例子中,标签内的元素并不会被渲染,图片资源也不会发出请求。模板可以理解为单个零件,在整个应用的生命周期中,你都可以使用、以及重用它。
### 特性
使用`<template>`标签包裹我们的内容,可以为我们提供一下几个重要的特性。
1. __它的内容在激活前都是惰性的。__ template标签默认是隐藏的它的内容也是不可见的同时也不会被渲染。
2. __处于模板中的内容不会产生副作用。__ 放在模板中的脚本、音频、视频、图片资源不会被加载,不会被播放,直到模板中的内容被使用。
3. __内容不在文档中。__ 在主页面使用`document.getElementById()`,不会返回模板子节点。
4. __模板能够放置在任何位置。__ 你可以把`<template>` 放置在`<head>``<body>``<frameset>`并且任何能够出现在以上元素的内容都可以放置在模板中。__“任何位置”__ 意味着`<template>`标签可以出现在HTML解析器不允许出现的位置 _(必须是在`<html>`标签内)_,几乎可以作为任何元素的子节点。它也可以作为`<table>``<select>`的子节点。当然,如果写在声明`type="text/javascript"``<script>`标签中,绝对报错,原因我就不说了。(同时实测发现,如果`<template>`标签放在`<head>``<body>`同级,放在`<body>`前面,都会被解析到`<head>`标签内,放在`<body>`后,会被解析到`<body>`内)。
``` html
<table>
<tr>
<templete>
<td>content</td>
</templete>
</tr>
</table>
```
### 使用模板
想要使用模板,首先需要激活模板,否则它的内容将无法被渲染。模板对象包含了一个`content`属性,该属性是只读属性,关联一个包含模板内容的`DocumentFragment`
我们可以使用`document.importNode()`对模板的`.content`进行深拷贝。
``` html
<template id="template1">
<img src="" />
<p>content</p>
</template>
```
``` javascript
var tmp = document.querySelector('#template1');
// 可以在获取模板的时候,对内容进行填充
tmp.content.querySelector('img').scr = 'logo.png';
var clone = document.importNode(tmp.content, true);
document.body.appendChild(clone);
```
模板中的资源,比如图片资源,只有被激活后,才会发出请求。
### 浏览器支持
想要检测浏览器是否支持该标签需要创建一个template元素并检查它是否拥有`.content`属性。
``` javascript
function supportTemplate() {
return 'content' in document.createElement('template');
}
if (supportsTemplate()) {
// 浏览器支持 template 元素
} else {
// 浏览器不支持template元素
}
```
从目前来看IE13+开始支持低于此版本的IE均无法使用如果有项目只需要考虑 webkit内核的浏览器template标签还是可以一用。
::: caniuse mdn-html__elements__template
:::
如果浏览器不支持template标签那么就会认为是一个普通的自定义元素内部的标签会被作为一般的标签被渲染。
### 模板标准之路
HTML 模板标准化进程耗时十分长久。从过去到现在,出现了很多各种各样的方法去创建可重用的模板。
__方法一使用隐藏的DOM元素将模板内容放在某个标签内使用`display:none`隐藏元素。__
``` html
<div style="display:none">
<img src="" />
</div>
```
使用这种方式,有利有弊:
1. √ 使用DOM浏览器能够很好的处理DOM结构我们可以方便的复制、使用DOM。
2. √ 没有内容渲染,`display: none` 阻止了内容渲染。
3. × 非惰性, 图片资源依然会发出请求。
4. x 难以设置样式和主题需要为所有CSS增加规则。
__方法二使用textarea 标签,并使用`display:none`隐藏元素。__
``` html
<textarea style="display:none">
<img src="" />
</textarea>
```
这种方法是对方法一的一种改进,但也有新的利弊:
1. √ 没有内容渲染,`display: none` 阻止了内容渲染。
2. √ 惰性,由于模板内容是字符串,图片资源不会发出请求。
3. x 模板内容是字符串需要进一步将其转为DOM。
__方法三 重载脚本__
``` html
<script type="text/x-handlebars-template">
<img src="" />
</script>
```
利弊:
1. √ 没有内容渲染script 标签默认`display:none`
2. √ 惰性,脚本类型不为 `text/javascript`浏览器不会认为是脚本不会将其作为JS解析。
3. x 安全问题,由于使用 `innerHTML`获取内容,对用户提供的字符串进行运行时解析,很容易倒是 XSS漏洞。
### 总结
模板标准化使得我们在做web开发整个过程更加健全更容易维护。

View File

@ -0,0 +1,233 @@
---
title: WebComponent——custom elements
tags:
- html
- javascript
createTime: 2018/08/01 11:15:27
permalink: /article/m63fd7lf
author: pengzhanbo
top: false
type: null
---
在我们的web应用开发中HTML标签为我们提供了基础的应用和交互我们使用HTML标签构建了各种各样丰富的web应用。
然而在我们开发web应用的过程中html标签提供的语义化并不能完全满足我们的场景。虽然在HTML5标准中也增加了不少包括`<header>``<section>``<article>``<nav>``<container>``<footer>`等语义化标签,但它们主要是为内容或布局添加的通用语义化标签,在实际的场景中,我们还需要使用 `class` 等一些属性或者辅助说明,声明该标签的具体语义。
<!-- more -->
``` html
<div class="login-wrapper"></div>
```
如果可以这么做呢:
``` html
<login></login>
```
使用更加语义化的标签,满足我们各种场景,甚至是扩展已有标签的特性。那么我们该怎么做呢?
接下来是我们的主角: __[自定义元素custom Elements](http://w3c.github.io/webcomponents/spec/custom/)__
### 自定义元素
> 自定义元素能够帮助web开发者创建拥有自身特性的自定义标签。
### 创建自定义元素
_创建自定义元素有两种方式这里只讨论 __DOM LEVEL 3__ 提供的 `customElements`,在 __DOM LEVEL 2__ 中的 `document.registerElement` 将作为补充内容在本文最后补充。_
[Custom Element API 规范](http://w3c.github.io/webcomponents/spec/custom/) 定义了`customElements`作为统一的对象管理自定义元素并对ES6 class提供了更完善的支持。
>规范还定义了 `CustomElementRegistry`, 并且 `customElements instanceof CustomElementRegistry`
我们可以通过 `customElements.define()` 方法来注册一个custom element该方法接受以下参数
`customElements.define(tarName, class[, option])`
- `tarName`: `DOMString`,用于表示所创建的元素名称。名称必须是小写字母开头,且必须包含至少一个`-`,任何不含`-`的自定义标签都会导致错误。例如`my-tag`,`my-list-item`为合法标签,`my_tag`,`myTag`都是非法的自定义标签名称;
- `class`: 类对象,用于定义元素行为.
- `option`: 包含 `extends` 属性的配置对象,可以指定所创建的元素继承自那个内置元素,可以继承任何内置元素;
`customElements`的类对象可以通过 ES 2015的类语法定义
``` javascript
class MyTag extends HTMLElement {
constructor() {
super();
}
}
customElements.define("my-tag", MyTag);
```
### 使用自定义元素的生命周期回调函数
`customElements`的构造函数中,我们可以指定多个不同的回调函数,他们会在不同的声明周期被触发。
- `connectedCallback`: 元素首次插入到文档DOM时回调
- `discannectedCallback`: 元素从文档DOM中删除时回调
- `attributeChangedCallback` 元素增加、删除、修改自身属性时回调;
- `adoptedCallback`:元素被移动到新的文档时回调;
``` javascript
class MyCustom extends HTMLElement {
// 自定义元素开始提升时调用
// 元素提升并不说明元素已插入到文档中
// 在此阶段尽量避免进行DOM操作
constructor() {
super();
}
// 元素插入到文档时回调
connectedCallback() {
// do something...
}
// 元素从文档中删除时回调
discannectedCallback() {
// do something...
}
/*
* 元素属性变化回调
* @param name {string} 变化的属性名
* @param oldValue {any} 变化前的值
* @param newVlalue {any} 变化后的值
*/
attributeChangedCallback(name, oldValue, newValue) {
// do something...
}
// 元素被移动到新的文档中时调用
// When it is adopted into a new document, its adoptedCallback is run.
// 具体场景示例通过document.adoptNode方法修改元素ownerDocument属性时可以触发
adoptedCallback() {
// do something...
}
}
```
如果需要在元素属性发生变化后触发 `attributeChangedCallback`,就必须监听这些属性。 我们可以通过定义静态属性`observedAttributed`的 get函数来添加需要监听的属性
``` javascript
static get observedAttributed() {
return ['name'];
}
```
### 使用自定义元素
我们可以在文档的任何地方使用`customElements.define`注册的自定义元素,即使是在自定义元素注册之前。
``` html
<my-tag></my-tag>
```
或者:
``` js
class MyTag extends HTMLElement {
constructor() {
super();
}
}
customElements.define("my-tag", MyTag);
// 方式一:
var tag = document.createElement('my-tag');
document.appendChild(tag);
// 方式二:
var tag = new MyTag();
document.appendChild(tag);
```
### 元素提升
浏览器是如何解析非标准的标签的?为什么对非标准的标签,浏览器不会报错?
> HTML规范
> 非规范定义的元素必须使用 _HTMLUnknownElement_ 接口。
我们在页面中声明一个 `<myTag>`标签,由于它是非标准标签,所以会继承 `HTMLUnknownElement`
对于自定义元素,情况有所不同。 拥有合法元素名称的自定义元素继承自`HTMLElement`
对于不支持自定义元素的浏览器,拥有合法元素名称的标签,仍然继承`HTMLUnknownElement`
### 扩展内置元素特性
在创建自定义元素时,置顶所需的扩展的元素,使用时,在内置元素上声明`is`属性指定自定义元素名称:
``` js
class CustomButton extends HTMLButtonElement {
constructor() {
super();
}
}
customElements.define("custom-button", CustomButton, {
extends: 'button'
});
```
``` html
<button is="custom-button"></button>
```
### 自定义元素样式
自定义元素和内置元素一样可以使用CSS各类选择器定义样式。
自定义元素规范还提出了一个新的CSS伪类`:unresolved`。在浏览器调用你的`createdCallback()` 之前,这个伪类可以匹配到未完成元素提升的自定义元素。
``` css
custom-button{
opacity: 1;
transition: opacity 300ms;
}
custom-button:unresolved{
opacity: 0
}
```
> :unresolved 不能用于继承自HTMLUnkownElement的元素。
### 浏览器支持
`Chrome``Opera`默认支持custom elements。`Firefox`计划在60/61的版本中默认支持自定义元素。`Safair`目前不支持自定义元素对内置元素的扩展。`Edge`在实现中。
### 补充内容:`document.registerElement`
使用`document.registerElement()` 创建自定义元素
``` javascript
var MyTag = document.registerElement('my-tag');
```
添加自定义元素特性:
``` javascript
var proto = Object.create(HTMLElement.prototype);
proto.hello = 'hello';
proto.sayHello = function () {
alert(this.hello);
};
var MyTag = document.registerElement('my-tag', {
prototype: proto
});
```
扩展原生元素特性
`document.registerElement()` 的第二个参数还允许我们为扩展原生素的特性。
``` javascript
var MyButton = document.registerElement('my-button', {
extend: 'button',
prototpye: Object.create(HTMLButtonElement.prototype)
});
```
``` html
<button is="my-button"><button>
```
生命周期以及回调方法
1. createdCallback(): 元素创建后回调。
2. attachCallback(): 元素附加到文档后调用。
3. detachCallback(): 元素从文档移除后调用。
4. attributeChangedCallback(): 元素任意属性变化后调用。
``` javascript
var myTagProto = Object.create(HTMLElement.prototype);
myTagProto.createdCallback = function() {
// 元素创建后回调。
this.textContent = '我被创建了';
};
var MyTag = document.registerElement('my-tag', {
prototype: myTagProto
});
```
### 结语
自定义元素作为 `webComponent` 规范中的一部分为web应用开发提供了更多的可能性配合`webComponent` 规范的其他内容可以为web开发者提供更强大的能力。

View File

@ -0,0 +1,219 @@
---
title: meta 标签说明
createTime: 2018/03/15 01:21:48
permalink: /article/bp1nxjs6
author: pengzhanbo
tags:
- html
top: false
type: null
---
<meta> 标签提供关于 HTML 文档的元数据。它不会显示在页面上,但是对于机器是可读的。可用于浏览器(如何显示内容或重新加载页面),搜索引擎(关键词),或其他 web 服务。
<!-- more -->
## 定义
提供有关页面的元信息meta-information比如针对搜索引擎和更新频度的描述和关键词。
## 用法
标签位于文档的头部,不包含任何内容。<meta> 标签的属性定义了与文档相关联的名称/值对。
## 属性
| 属性 | 是否可选 | 描述 |
| :----: | :----: | :---- |
| content | 必选 | 定义与 http-equiv 或 name 属性相关的元信息。 |
| http-equiv | 可选 | 把 content 属性关联到 HTTP 头部。 |
| name | 可选 | 把 content 属性关联到一个名称。 |
| charset | 可选 | 定义编码格式 |
## 常用meta标签说明
### charset
charset是声明文档使用的字符编码主要用于解决编码问题导致的乱码。 charset一定要写在第一行。
两种charset的写法
```html
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
```
## viewport
viewport主要是影响移动端页面布局
```html
<meta name="viewport" content="width=device-width, initial-scale=1.0">
```
**content 参数:**
1. **width** viewport 宽度(数值/device-width)
2. **height** viewport 高度(数值/device-height)
3. **initial-scale** 初始缩放比例
4. **maximum-scale** 最大缩放比例
5. **minimum-scale** 最小缩放比例
6. **user-scalable** 是否允许用户缩放(yes/no)
### SEO优化相关
```html
<!-- 页面标题<title>标签(head 头部必须) -->
<title>your title</title>
<!-- 页面关键词 keywords -->
<meta name="keywords" content="your keywords">
<!-- 页面描述内容 description -->
<meta name="description" content="your description">
<!-- 定义网页作者 author -->
<meta name="author" content="author,email address">
<!-- 定义网页搜索引擎索引方式robotterms 是一组使用英文逗号「,」分割的值,
通常有如下几种取值nonenoindexnofollowallindex和follow。 -->
<meta name="robots" content="index,follow">
```
**robots具体参数如下**
1. none : 搜索引擎将忽略此网页等价于noindexnofollow。
2. noindex : 搜索引擎不索引此网页。
3. nofollow: 搜索引擎不继续通过此网页的链接索引搜索其它的网页。
4. all : 搜索引擎将索引此网页与继续通过此网页的链接索引等价于indexfollow。
5. index : 搜索引擎索引此网页。
6. follow : 搜索引擎继续通过此网页的链接索引搜索其它的网页。
### 移动端常用的meta
```html
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<!-- 删除苹果默认的工具栏和菜单栏 -->
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<!-- 设置苹果工具栏颜色 -->
<meta name="format-detection" content="telphone=no, email=no" />
<!-- 忽略页面中的数字识别为电话忽略email识别 -->
<!-- 启用360浏览器的极速模式(webkit) -->
<meta name="renderer" content="webkit">
<!-- 避免IE使用兼容模式 -->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!-- 针对手持设备优化主要是针对一些老的不识别viewport的浏览器比如黑莓 -->
<meta name="HandheldFriendly" content="true">
<!-- 微软的老式浏览器 -->
<meta name="MobileOptimized" content="320">
<!-- uc强制竖屏 -->
<meta name="screen-orientation" content="portrait">
<!-- QQ强制竖屏 -->
<meta name="x5-orientation" content="portrait">
<!-- UC强制全屏 -->
<meta name="full-screen" content="yes">
<!-- QQ强制全屏 -->
<meta name="x5-fullscreen" content="true">
<!-- UC应用模式 -->
<meta name="browsermode" content="application">
<!-- QQ应用模式 -->
<meta name="x5-page-mode" content="app">
<!-- windows phone 点击无高光 -->
<meta name="msapplication-tap-highlight" content="no">
```
### 百度禁止转码
百度会自动对网页进行转码,这个标签是禁止百度的自动转码
```html
<meta http-equiv="Cache-Control" content="no-siteapp" />
```
### Microsoft Internet Explorer
```html
<!-- 优先使用最新的ie版本 -->
<meta http-equiv="x-ua-compatible" content="ie=edge">
<!-- 是否开启cleartype显示效果 -->
<meta http-equiv="cleartype" content="on">
<meta name="skype_toolbar" content="skype_toolbar_parser_compatible">
<!-- Pinned Site -->
<!-- IE 10 / Windows 8 -->
<meta name="msapplication-TileImage" content="pinned-tile-144.png">
<meta name="msapplication-TileColor" content="#009900">
<!-- IE 11 / Windows 9.1 -->
<meta name="msapplication-config" content="ieconfig.xml">
```
### Google Chrome
```html
<!-- 优先使用最新的chrome版本 -->
<meta http-equiv="X-UA-Compatible" content="chrome=1" />
<!-- 禁止自动翻译 -->
<meta name="google" value="notranslate">
```
### 360浏览器
```html
<!-- 选择使用的浏览器解析内核 -->
<meta name="renderer" content="webkit|ie-comp|ie-stand">
```
### UC手机浏览器
```html
<!-- 将屏幕锁定在特定的方向 -->
<meta name="screen-orientation" content="landscape/portrait">
<!-- 全屏显示页面 -->
<meta name="full-screen" content="yes">
<!-- 强制图片显示,即使是"text mode" -->
<meta name="imagemode" content="force">
<!-- 应用模式,默认将全屏,禁止长按菜单,禁止手势,标准排版,强制图片显示。 -->
<meta name="browsermode" content="application">
<!-- 禁止夜间模式显示 -->
<meta name="nightmode" content="disable">
<!-- 使用适屏模式显示 -->
<meta name="layoutmode" content="fitscreen">
<!-- 当页面有太多文字时禁止缩放 -->
<meta name="wap-font-scale" content="no">
```
### QQ手机浏览器
```html
<!-- 锁定屏幕在特定方向 -->
<meta name="x5-orientation" content="landscape/portrait">
<!-- 全屏显示 -->
<meta name="x5-fullscreen" content="true">
<!-- 页面将以应用模式显示 -->
<meta name="x5-page-mode" content="app">
```
### Apple iOS
```html
<!-- Smart App Banner -->
<meta name="apple-itunes-app"
content="app-id=APP_ID,affiliate-data=AFFILIATE_ID,app-argument=SOME_TEXT">
<!-- 禁止自动探测并格式化手机号码 -->
<meta name="format-detection" content="telephone=no">
<!-- Add to Home Screen添加到主屏 -->
<!-- 是否启用 WebApp 全屏模式 -->
<meta name="apple-mobile-web-app-capable" content="yes">
<!-- 设置状态栏的背景颜色,只有在 “apple-mobile-web-app-capable” content=”yes” 时生效 -->
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<!-- 添加到主屏后的标题 -->
<meta name="apple-mobile-web-app-title" content="App Title">
```
### Google Android
```html
<meta name="theme-color" content="#E64545">
<!-- 添加到主屏 -->
<meta name="mobile-web-app-capable" content="yes">
```

View File

@ -0,0 +1,22 @@
---
title: 继承与原型链
createTime: 2018/07/06 09:40:54
permalink: /article/extends-prototype
author: pengzhanbo
tags:
- javascript
top: false
type: null
---
当谈到继承时javascript只有一种结构对象。
每个实例对象object都有一个私有属性\_\_proto\_\_指向它的构造函数的原型对象(__prototype__)。
该原型对象也有自己的原型对象((\_\_proto\_\_层层向上直到有一个的原型对象为null。根据定义null没有原型并作为这个原型链的最后一个环节。
<!-- more -->
几乎所有javascript中的对象都是位于原型链顶端的`Object`的实例。
## 基于原型链的继承
### 继承属性

View File

@ -1,17 +1,16 @@
---
title: 正则表达式
createTime: 2022/03/08 06:32:01
permalink: /post/tjya08e9
lang: zh-CN
createTime: 2018/11/26 11:15:27
permalink: /article/e8qbp0dh
author: pengzhanbo
top: false
type: # original: 原创: reprint 转载 可为空不填
tags:
- javascript
top: false
type: null
---
本文正则表达式基于`javascript`,不同的计算机语言对正则表达式的支持情况以及实现,语法不尽相同,不一定适用于其他语言。
<!-- more -->
_本文正则表达式基于`javascript`不同的计算机语言对正则表达式的支持情况以及实现语法不尽相同不一定适用于其他语言。_
### 简介
`正则表达式`是一种文本模式Regular Expression是对字符串的一种匹配查找规则。可以方便的在某一文本字符串中查找、定位、替换符合某种规则的字符串。

View File

@ -0,0 +1,8 @@
---
title: Event Loop 浏览器端的事件循环
createTime: 2021/06/03 01:53:17
permalink: /article/browser-event-loop
author: pengzhanbo
top: false
type: null
---

View File

@ -0,0 +1,249 @@
---
title: 详解 Promise
createTime: 2020/11/22 12:58:28
permalink: /article/q40nq4hv
author: pengzhanbo
sticky: true
type: null
---
## 概述
`Promise` 是一个构造函数,用于创建一个新的 Promise 对象。该构造函数主要用于包装还没有添加 promise 支持的函数。
``` ts
Promise(resolver : (resolve, reject) => void)
```
`Promise` 接受一个函数`resolver`作为参数,包装需要执行的处理程序,当处理结果为成功时,将成功的返回值作为参数调用`resolve` 方法,
如果失败,则将失败原因作为参数调用`reject`方法。
### 示例
``` js
const promise = new Promise(function (resolve, reject) {
setTimeout(() => {
// do something
if (Math.random() * 10 > 5) {
resolve({ status: 'success', data: '' })
} else {
reject(new Error('error'))
}
}, 500)
})
```
### Promise状态
Promise 创建后,必然处于以下几种状态
- `pending` : 待定状态,既没有被兑现,也没有被拒绝
- `fulfilled` : 操作成功。
- `rejected` : 操作失败。
当状态从 `pending` 更新为`fulfilled``rejected` 后,就再也不能变更为其他状态。
### Promise 实例方法
#### `.then(onFulfilled, onRejected)`
*then()* 接收两个函数参数(也可以仅接收一个函数参数 onFulfilled
- onFulfilled 函数参数,表示当 promise的状态从 `pending` 更新为`fulfilled` 时触发,并将成功的结果 value 作为`onFulfilled`函数的参数。
- onRejected 函数参数表示当promise的状态从 `pending` 更新为`rejected` 时触发,并将失败的原因 reason 作为 `onRejected`函数的参数。
#### `.catch(onRejected)`
*catch()* 可以相当于 *.then(null, onRejected)*即仅处理当promise的状态从 `pending` 更新为`rejected` 时触发。
#### `.finally(onFinally)`
表示promise的状态无论是从`pengding`更新为`fulfilled``rejected`,当所有的 then() 和 catch() 执行完成后,最后会执行 finally() 的回调。
由于无法知道promise的最终状态`onFinally` 回调函数不接收任何参数,它仅用于无论最终结果如何都要执行的情况。
``` js
promise
.then(function (res) {
console.log(res) // { status: 'success', data: '' }
})
.catch(function (reason) {
console.log(reason) // error: error
})
.finally(() => {
// do something
})
```
### 链式调用
使用Promise的一个优势是可以链式调用的方式执行多个`then()`/`catch()`方法。
且回调函数允许我们返回任何值,返回的值将会被包装为一个 promise实例将值传给下一个`then()`/`catch()`方法。
``` js
promise
.then(res => {
res.data = { a: 2 }
return res
})
.then(res => {
console.log(res) // { status: 'success', data: { a: 2 } }
throw new Error('cath error')
})
.catch(reason => {
console.log(reason) // error: cath error
})
```
## `Promise` 静态方法
### Promise.resolve(value)
返回一个状态由给定value决定的Promise对象。如果该值是thenable(即带有then方法的对象)返回的Promise对象的最终状态由then方法执行决定否则的话(该value为空基本类型或者不带then方法的对象),返回的Promise对象状态为fulfilled并且将该value传递给对应的then方法。通常而言如果您不知道一个值是否是Promise对象使用Promise.resolve(value) 来返回一个Promise对象,这样就能将该value以Promise对象形式使用。
### Promise.reject(reason)
返回一个状态为失败的Promise对象并将给定的失败信息传递给对应的处理方法
### Promise.all(promises)
`all()` 允许传入一组promise实例并返回一个新的promise实例。
promises并发执行并且当这组promises的最终状态均更新为`fulfilled`才触发返回的promise实例的`onFulfilled`
并将这组promises的执行结果已promises的定义顺序以数组的形式传给`onFulfilled`
如果其中某个promise的最终状态更新为`rejected`则立即触发返回的promise实例的`onRejected`
#### 示例:
``` js
const promises = [
Promise.resolve({ a: 1}),
new Promise((resolve) => {
setTimeout(() => {
resolve({ b: 1 })
}, 0)
})
]
Promise.all(promises).then(res => {
console.log(res) // [ { a: 1}, { b: 1 } ]
})
```
#### 手写Promise.all 实现代码
``` js
function promiseAll(promises) {
promises = promises || []
let length = promises.length
if (length === 0) return Promise.resolve([])
let count = 0
const list = []
return new Promise((resolve, reject) => {
const resolveFn = (res, index) => {
list[index] = res
count ++
if (count >= length) {
resolve(list)
}
}
promises.forEach((item, i) => {
if (item instanceof Promise) {
item.then(res => resolveFn(res, i), reject)
} else {
resolveFn(item, i)
}
})
})
}
```
### Promise.allSettled
`allSettled(promises)` 允许传入一组promise实例并返回一个新的promise对象。
当这组promises的状态从`pending` 都更新到最终状态、无论最终状态是 `fulfilled``rejected`触发返回的promise的`onfulfilled`
`onfulfilled` 回调函数根据promises定义的顺序将执行结果以 `{ status: string, [value|reason]: any }[]` 的形式作为参数传入。
#### 示例
``` js
const promises = [
Promise.resolve({ a: 1}),
Promise.reject('reason')
]
Promise.allSettled(promises).then(res => {
console.log(res) // [ { status: 'fulfilled, value: { a: 1 } }, { status: 'rejected', reason: 'reason' } ]
})
```
#### 手写Promise.allSettled 实现代码
``` js
function promiseAllSettled(promises) {
promises = promises || []
let length = promises.length
if (length === 0) return Promise.resolve([])
let count = 0
const list = []
return new Promise((resolve) => {
const resolveFn = (res, index, status) => {
list[index] = { status }
if (status === 'fulfilled') {
list[index].value = res
} else {
list[index].reason = res
}
count ++
if (count >= length) {
resolve(list)
}
}
promises.forEach((item, i) => {
if (item instanceof Promise) {
item.then(
res => resolveFn(res, i, 'fulfilled'),
reason => resolveFn(reason, i, 'rejected')
)
} else {
resolveFn(item, i, 'fulfilled')
}
})
})
}
```
### Promise.race
`Promise.race(promises)` 接收一组promise实例作为参数并返回一个新的promise对象。
当这组promises中的任意一个promise的状态从`pending`更新为`fulfilled``rejected`返回的promise对象将会把该promise的成功返回值或者失败原因
作为参数调用返回的promise的`onFulfilled``onRejected`
#### 示例
``` js
const promises = [
new Promise((resolve) => {
setTimeout(() => {
resolve('timeout')
}, 500)
}),
Promise.resolve('resolve')
]
Promise.race(promises).then(res => {
console.log(res) // resolve
})
```
#### 手写Promise.race 实现代码
``` js
function promiseRace(array) {
array = array || []
return new Promise((resolve, reject) => {
array.forEach(item => {
if (item instanceof Promise) {
item.then(resolve, reject)
} else {
resolve(item)
}
})
})
}
```
## 参考资料
> [Promise A+ 规范](https://malcolmyu.github.io/2015/06/12/Promises-A-Plus/)
>
> [MDN Promise](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise)
>
> [MDN 使用Promise](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Using_promises)

View File

@ -0,0 +1,249 @@
---
title: 1px解决方案
createTime: 2019/05/15 10:41:32
permalink: /article/tz7ncicn
author: pengzhanbo
tags:
- html
- css
- develop
top: false
type: null
---
在日常移动端前端应用开发中,经常遇到一个问题就是 1px的线在移动端 Retina屏下的渲染并未达到预期。以下总几种不同场景下的 1px解决方案。
<!-- more -->
## 背景及原因
首先,需要明确的一个概念是, CSS的 `pixels` 并不完全等价于 设备的 `pixels`。当我们假定设备的 `pixels` 为标准的`pixels` 宽度。这些pixels决定了设备的分辨率。在默认情况下 PC设备上用户未进行缩放操作即zoom缩放为100%时), CSS的`pixels`与设备的`pixels`重叠当用户进行了缩放操作时假设用户缩放了200%,那么 124px的CSS`pixels`实际占用了248设备`pixels`
但我们开发时,通常设备的`pixels`对我们毫无用处前端只需要关注CSS的`pixels`浏览器会根据用户缩放自动处理CSS的pixels是被伸展还是收缩。
但在移动端设备中由于设备的宽度较小导致了可显示的内容要少得多。浏览器或者缩放变小导致内容无法阅读或者通过拖动来浏览未被显示的内容。这导致了原本适合于PC设备的CSS布局放到了移动端变得十分丑陋。
为了解决这个问题移动端设备的厂商的通常做法是让viewport更宽这里的viewport指的是设备的视窗它决定了HTML标签的宽度表现继而影响其他的元素
移动端的 viewport 被分为了 虚拟的 viewport 和 布局的 viewport
- `visual viewport` 虚拟viewport
- `layout viewport` 布局viewport
![](/images/viewport.jpg)
两者的概念, 可以想象 `layout viewport` 为一张不可改变大小和角度的图片,但它被一层蒙板挡住了, `visual viewport` 是一个蒙板上我们可以观察到 这张图片的窗口。我们可以通过这个窗口观察到 图片的部分内容。并且可以对这个窗口进行拖动或缩放,进而观察到图片的完整内容。
在这里,`visual viewport` 相当于 移动端设备的屏幕,用户的 缩放和拖动操作,反馈到 `layout viewport` ,则是相对的变成 `layout viewport` 被 拖动和缩放。
而通常我们关注的 CSS`pixels`,通常是按照 `layout viewport`来定义的,所以会比`visual viewport` 宽很多。而 `<html>`元素的宽度继承于`layout viewport`。这可以保证你的网站的UI可以在移动端设备和桌面设备表现一致。
但是 `layout viewport`的宽度有多宽,不同的设备,不同的浏览器各有不同。如 iPhone 的Safari 使用的是 980px。
但是在移动端的交互中,我们并不期望 网站的内容是被缩放的,需要让用户进行缩放和拖动。 所以通常我们会在 html文件的head中进行一个 meta声明。
``` html
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
```
即强制设置了`layout viewport` 等于 设备宽度, 设置了缩放为100%并且用户无法进行缩放操作。这样做的好处是我们可以以一种期望的方式进行设计UI和交互。
但在前面,我们介绍了, CSS的`pixels`并不等价于设备的`pixels`。通常在移动端设备,我们可以通过 `window.devicePixelRatio` 查看当前设备的CSS`pixels`和设备`pixels`的比例,如 `window.devicePixelRatio` 值为 2时 表示 1个CSS`pixels`的宽度占用2个设备`pixels`,即实际占用了 2x2 的设备`pixels`
这也是导致了 `1px`的线,在移动设备上的渲染,看起来会比实际上的 `1px`更粗的原因。
知道了问题的背景,和产生的原因,那么只需要让 `1px`的 CSS`pixels`的表现,接近于或者贴合 `1px`的设备`pixels`, 那么就可以解决这个问题了。
## 解决方案
如何让 `1px`的 CSS`pixels`的表现,接近于或者贴合 `1px`的设备`pixels`。这个问题需要具体场景具体分析。
### border-width: 0.5px
一种最简单的,且适合各种场景的方案,就是使用 `0.5px` 的值代替 `1px` 的值。 但这个方案有一个兼容问题,现代浏览器并不全都支持该值的。
可以先检查是否支持 `0.5px`,然后在 根元素上添加一个 类,进行使用。
``` js
if (window.devicePixelRatio && devicePixelRatio >= 2) {
var testElem = document.createElement('div');
testElem.style.border = '.5px solid transparent';
document.body.appendChild(testElem);
if (testElem.offsetHeight == 1)
{
document.querySelector('html').classList.add('hairlines');
}
document.body.removeChild(testElem);
}
```
``` css
div {
border: 1px solid #bbb;
}
.hairlines div {
border-width: 0.5px;
}
```
这种方案的好处是简单能够适配所有场景但是从兼容性上看iOS7及之前的版本、Android设备等均不支持`0.5px`的渲染。
### 伪类 + transform缩放
该方法是是利用 元素的伪类进行线的渲染。
比如 利用 `::before` 或者 `::after`, 画一条上边框的线
``` css
.hairlines {
position: relative;
}
.hairlines::before {
content: '';
position: absolute;
left: 0;
top: 0;
display: block;
width: 100%;
height: 1px;
background-color: #000;
transform: scaleY(0.5);
transform-origin: 0 0;
}
```
比如,利用 `::before` 或者 `::after`, 画一个线框:
``` css
.hairlines {
position: relative;
}
.hairlines::before {
content: '';
position: absolute;
left: 0;
top: 0;
box-sizing: border-box;
display: block;
width: 200%;
height: 200%;
border: 1px solid #000;
transform: scale(0.5);
transform-origin: 0 0;
}
```
该方案的好处同样能都适配多数场景,并且支持圆角的情况。
但缺点在于由于对元素本身设置了`position`,以及使用了伪类,但另一个交互需要使用到被占用的属性时,需要分情况处理问题。
### border-image 图片
使用 border-image-slice 对边框图片进行偏移。
该方案的方法,比如处理 x轴方向的线时 需要准备 一张2px高的图片根据显示是上边框还是下边框如上边框则该图片的 上一半1px为对应的颜色的先下一半为透明。
_line.png_ ![1px-lines.png](/images/1px-lines.png)
``` css
div {
border-top: 1px transparent;
border-image: url(line.png) 2 0 0 0 repeat;
}
```
同理,处理其他方向的边框类似方法。
该方法的缺点是 如果改变颜色,或者有不同颜色的线,需要准备多张图片。
优先是适合多数的场景,且不对元素本身做出影响文档流的改动。
### SVG
由于CSS也支持 SVG 作为 image 资源使用且SVG是矢量图片能够相比于使用jpg、png格式的图片获得更好的保真。
可以配合 CSS 的 `background-image` 或者 `border-image` 满足不同场景的需要。
建议此方案配合 CSS 预渲染,如`stylus/sass/less` 进行使用, 也可使用`postcss` 相关插件使用。
如在 stylus中
``` stylus
// 画一个元素的线框
borderXY(color = #eee, radius = 8px) {
$r = unit(radius/ 2, '');
border-radius radius /*px*/
background-image url(s("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' width='100%' height='100%' viewBox='0 0 200% 200%' preserveAspectRatio='xMidYMid meet'><rect fill='rgba(0,0,0,0)' width='100%' height='100%' stroke-width='1' stroke='%s' rx='%s' ry='%s'/></svg>", color, $r, $r))
background-repeat no-repeat
background-position 0 0
background-size 100% 100%
}
// 画一个元素的 上下边框
borderX(color = #eee) {
border 0
border-top: 1px solid color; /*no*/
border-bottom: 1px solid color; /*no*/
border-image: url(s("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' height='200' width='100'><line x1='0' y1='25' x2='100' y2='25' stroke='%s' style='stroke-width:50'/><line x1='0' y1='75' x2='100' y2='75' style='stroke:transparent;stroke-width:50'/><line x1='0' y1='125' x2='100' y2='125' style='stroke:transparent;stroke-width:50'/><line x1='0' y1='175' x2='100' y2='175' stroke='%s' style='stroke-width:50'/></svg>", color, color)) 100 0 100 0 stretch;
}
// 画一个元素的 左右边框
borderY(color = #eee) {
border 0
border-left: 1px solid color; /*no*/
border-right: 1px solid color; /*no*/
border-image: url(s("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' height='100' width='200'><line x1='25' y1='0' x2='25' y2='100' stroke='%s' style='stroke-width:50'/><line x1='75' y1='0' x2='75' y2='100' style='stroke:transparent;stroke-width:50'/><line x1='125' y1='0' x2='125' y2='100' style='stroke:transparent;stroke-width:50'/><line x1='175' y1='0' x2='175' y2='100' stroke='%s' style='stroke-width:50'/></svg>", color, color)) 0 100 0 100 stretch;
}
// 画一个元素的上边框
borderTop(color = #eee) {
border 0
border-top: 1px solid color; /*no*/
border-image: url(s("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' height='100' width='100'><line x1='0' y1='25' x2='100' y2='25' stroke='%s' style='stroke-width:50'/><line x1='0' y1='75' x2='100' y2='75' style='stroke:transparent;stroke-width:50'/></svg>", color)) 100 0 0 0 stretch;
}
// 画一个元素的下边框
borderBottom(color = #eee) {
border 0
border-bottom: 1px solid color; /*no*/ // 设置border 0后如果color设置为transparent则该边框会变成透明
border-image: url(s("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' height='100' width='100'><line x1='0' y1='25' x2='100' y2='25' style='stroke:transparent;stroke-width:50'/><line x1='0' y1='75' x2='100' y2='75' stroke='%s' style='stroke-width:50'/></svg>", color)) 0 0 100 0 stretch;
}
// 画一个元素的左边框
borderLeft(color = #eee) {
border 0
border-left: 1px solid color; /*no*/
border-image: url(s("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' height='100' width='100'><line x1='25' y1='0' x2='25' y2='100' stroke='%s' style='stroke-width:50'/><line x1='75' y1='0' x2='75' y2='100' style='stroke:transparent;stroke-width:50'/></svg>", color)) 0 0 0 100 stretch;
}
// 画一个元素的右边框
borderRight(color = #eee) {
border 0
border-right: 1px solid color; /*no*/
border-image: url(s("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' height='100' width='100'><line x1='25' y1='0' x2='25' y2='100' style='stroke:transparent;stroke-width:50'/><line x1='75' y1='0' x2='75' y2='100' stroke='%s' style='stroke-width:50'/></svg>", color)) 0 100 0 0 stretch;
}
div {
borderXY()
}
```
如果是使用 `postcss` ,可以使用安装插件 [postcss-write-svg](https://github.com/csstools/postcss-write-svg),配合使用
``` css
@svg square {
@rect {
fill: var(--color, black);
width: var(--size);
height: var(--size);
}
}
.example {
background: svg(square param(--color green) param(--size 100%)) center / cover;
}
```
使用SVG的优点是支持调整线的颜色支持设置圆角可以根据场景不同选择 `background-image` 或者 `border-image` 满足绝大多数的场景。
### background-image + jpg/png 图片
该做法是使用一张该元素的多倍的背景图,进行线的渲染。
该做法一般不推荐。
### 背景渐变
该方案不推荐
### box-shadow
该方案不推荐

View File

@ -0,0 +1,121 @@
---
title: lerna使用
createTime: 2021/11/26 06:28:37
permalink: /article/i1wc1uld
author: pengzhanbo
top: false
type: null
---
![lerna](https://user-images.githubusercontent.com/645641/79596653-38f81200-80e1-11ea-98cd-1c6a3bb5de51.png)
## 概述
`lerna` 是一个多包管理工具,针对使用 git 和 npm/yarn 等管理多软件包的代码仓库的工作流程进行优化。
在开发一个大型项目时,往往会将整个项目拆分为多个代码仓库,进行独立版本化的软件包管理,这对于代码共享非常有用。
比如开源项目 `babel`,整个项目被拆分为了`@babel/core`, `@babel/parser`, `@babel/traverse`等多个软件包。
但是这也会导致如果某些更改跨越了多个代码仓库的话,会变得麻烦且难以跟踪。
`lerna`可以帮助优化对多个代码仓库的依赖、版本管理、工作流等。
## 安装
lerna 可以全局安装,也可以在项目中安装(以下内容使用项目中安装的方式)
``` sh
# npm
npm install lerna
# yarn
yarn add lerna
```
## 简单入门
创建一个项目并使用lerna进行项目环境初始化
``` sh
mkdir lerna-demo && cd $_
yarn init -y
yarn add lerna
npx lerna init
```
你将会得到一个包含以下内容的项目文件夹:
``` sh
lerna-demo
packages/
lerna.json
package.json
```
其中,`packages/` 目录用于存放所有的软件包。`lerna.json`是lerna的配置文件。
## 配置说明
``` json
// lerna.json
{
"useWorkspaces": true,
"npmClient": "npm", // npm | yarn
"packages": ["packages/*"],
"version": "0.0.0",
"command": {
"bootstrap": {
// more...
},
// more
}
}
```
- `npmClient`:设置当前使用的包管理器, 默认是npm 可以设置为yarn
- `version`:软件包版本号,根据 semver版本号规范命名
- `packages`软件包所在的目录可以使用golb做模式匹配
- `useWorkspaces`使用工作空间这个选项可以更好的跟yarn配合使用
- `command`对lerna的各个command进行配置。
## 命令行说明
### lerna init
初始化一个lerna项目默认将会在目录中新建 packages/ 和 lerna.json。
`--independent`: 使用分包独立版本管理模式,各个软件包使用独立的版本号。
### lerna create pkgName [location]
在项目中新建一个子包, pkgName设置包名。 location制定包所在目录默认是 packages配置的第一个元素。
### lerna add \<package\>[@version] [--dev] [--exact] [--peer]
类似于 `yarn add``npm install`在一个lerna repo中往dependency中添加依赖包。
- `--dev`: 表示将包添加到 devDependencies
- `--exact`: 添加一个确定版本的包如1.0.1),而不是一个版本范围的包如(^1.0.1
- `--peer`: 添加一个前置依赖包。
### lerna bootstrap
为当前 lerna repo 中的所有包安装 依赖库,并 link所有 同域依赖。
### lerna run \<script\>
在当前 lerna repo 中的所有包中执行 script 命令。
``` sh
packages/
package1/
package2/
```
``` sh
lerna run build # 相当于在 package1、package2 中执行 npm run build
```
- --scope 过滤符合条件的包
``` sh
lerna run build --scope test component
```
- --stream 使用报名作为前缀,交叉输出所有包的控制台信息流。
``` sh
lerna run build --stream
```
- --parallel 类似于 stream。
``` sh
lerna run build --parallel
```
### lerna clean
删除所有包的node_modules

View File

@ -0,0 +1,111 @@
---
title: 移动端适配方案
createTime: 2020/08/14 01:54:29
permalink: /article/vhpmovsm
author: pengzhanbo
tags:
- develop
top: false
type: null
---
## 背景
移动端设备由于不同品牌、不同机型,不同设备中,使用的不同浏览器,带来的一系列适配问题。在这些设备中,如何实现展示效果、交互的一致性,是比较头疼的问题。
## 发展
早期的适配方案五花八门。
在2015年双十一左右 阿里前端团队 AmFe 分享了 `flexiable` 的移动端适配方案,在往后的几年中, `flexiable`成为了主流的移动端适配方案在各大移动端应用中使用。
随着技术的发展CSS3的`viewport`越来越得到了更多的设备支持, 逐渐的可以直接使用 viewport 来作为 移动端适配方案2017年左右开始步入开发者的视野。
后来 AmFe 宣布 推荐使用 `viewport` 方案代替 `flexible``viewport`方案逐渐成为主流适配方案。
## lib-flexible 方案介绍
### viewport
viewport 即浏览器窗口在移动端设备中viewport太窄为了更好的服务于CSS提供了 `visual viewport``layout viewport`
### 物理像素physical pixel
物理像素即设备像素,是显示设备中最微小的物理部件。
### 设备独立像素density-independent pixel
设备独立像素也称为 密度无关像素,可以认为是计算机坐标系统中的一个点,这个点代表一个可以由程序使用的虚拟像素,然后由系统转换为物理像素
### CSS像素CSS pixel
css像素是一个抽象单位主要用在浏览器上用来精确的度量Web页面上的内容。
一般情况下, CSS像素称为设备无关的像素简称 `DIPs`
### 屏幕密度
屏幕密度是指一个设备表面上存在的像素密度,它通常已每英寸有多少像素来计算(`PPI`
### 设备像素比device pixel ratio
简称 `DPR`,定义了物理像素和设备独立像素的对应关系
```html
设备像素比 = 物理像素 / 设备独立像素
```
### 简要说明
`flexiable` 通过hack手段根据设备的dpr值相应改变 `<meta>` 标签中viewport的值
```html
<!-- dpr = 1-->
<meta name="viewport" content="initial-scale=scale,maximum-scale=scale,minimum-scale=scale,user-scalable=no">
<!-- dpr = 2-->
<meta name="viewport" content="initial-scale=0.5,maximum-scale=0.5,minimum-scale=0.5,user-scalable=no">
<!-- dpr = 3-->
<meta name="viewport" content="initial-scale=0.3333333333,maximum-scale=0.3333333333,minimum-scale=0.3333333333,user-scalable=no">
```
从而让页面达到缩放的效果,变相的实现页面的适配功能。
主要的思想:
1. 根据 `dpr`的值来修改 `viewport` 实现1px的线
2. 根据 `dpr`的值来修改 `html``font-size`从而使用rem实现等比缩放
3. 使用 `hack` 手段用`rem`模拟 `vw`的特性
### 使用
> github: [https://github.com/amfe/lib-flexible](https://github.com/amfe/lib-flexible)
>
> px-to-rem: [https://www.npmjs.com/package/postcss-pxtorem](https://www.npmjs.com/package/postcss-pxtorem)
>
## px-to-viewport 适配方案
`Flexiable` 是通过javascript 模拟 `vw`的特性,到今天未知,`vw`已经得到了众多浏览器的支持,完全可以考虑直接将`vw`单位用于我们的适配布局中。
在css level3 中,定义了和 viewport相关的四个单位分别是 `vw``vh``vmin``vmax`
- `vw`: viewport width 简写1vw等于 `window.innerWidth``1%`
- `vh` viewport height简写1vh 等于 `window.innerHeight``1%`
- `vmin`vmin的值是当前 vw和vh中较小值
- `vmax` vmax的值是当前 vw和vh中较大值
![](/images/viewport.png)
在一张 750px的设计稿中 100vw=750px 1vw=7.5px通过公式即可转换px单位为vw单位实现适配。
可以通过 postcss-px-to-viewport 来帮助实现自动转换
> github: [https://github.com/evrone/postcss-px-to-viewport](https://github.com/evrone/postcss-px-to-viewport/blob/master/README_CN.md)
>
### 适用vw适配页面的场景
1. 容器适配
2. 文本适配
3. 大于1px的边框、圆角、阴影
4. 内边距和外边距

View File

@ -0,0 +1,139 @@
---
title: Jenkins 使用
lang: zh-CN
createTime: 2018/09/16 11:15:27
permalink: /article/bmtl5ah4
author: pengzhanbo
tags:
- 工具
top: false
type: null
---
[Jenkins](https://jenkins.io/) 是一款功能强大的应用程序,允许持续集成和持续交付项目。这里记录一些 Jenkins 使用的方法。
<!-- more -->
_以下基于 `CentOS` 系统。_
### 安装
安装详见 官网 [Jenkins 安装](https://jenkins.io/download/) 流程,各个系统如何安装均有说明。
环境依赖: `java`
CentOS 下安装:
``` bash
sudo wget -O /etc/yum.repos.d/jenkins.repo https://pkg.jenkins.io/redhat/jenkins.repo
sudo rpm --import https://pkg.jenkins.io/redhat/jenkins.io.key
yum install jenkins
```
- __默认安装目录__ : `/var/lib/jenkins`
- __默认日志目录__ `/var/log/jenkins`
- __默认缓存目录__ : `/var/cache/jenkins`
- __默认admin密码文件__ : `/var/lib/jenkins/secrets/initialAdminPassword`
- __配置文件__ : `/etc/sysconfig/jenkins`
### 运行
``` bash
# 启动 Jenkins
service jenkins start
# 重启 Jenkins
service jenkins restart
# 停止 Jenkins
service jenkins stop
```
默认运行在 `8080` 端口, 本机可通过 `localhost:8080` 访问。
### 卸载
``` bash
service jenkiins stop
yum clean all
yum remove jenkins
rm -rf /var/lib/jenkins
rm -rf /var/cache/jenkins
rm -rf /var/log/jenkins
```
### 修改端口
1. 打开`Jenkins` 配置文件
``` bash
vim /etc/sysconfig/jenkins
```
2. 修改 `$HTTP_PORT`
``` bash
$HTTP_PORT="8080"
```
### 获取root用户权限
1. 打开`Jenkins` 配置文件
``` bash
vim /etc/sysconfig/jenkins
```
2. 修改 `HTTP_PORT`
``` bash
$JENKINS_USER="root"
```
3. 修改`Jenkins` 相关目录权限
``` bash
chown -R root:root /var/lib/jenkins
chown -R root:root /var/log/jenkins
chown -R root:root /var/cache/jenkins
```
4. 重启`Jenkins`并验证
``` bash
service jenkins restart
ps -ef|grep jenkins
# 若显示为root用户则表示修改完成
```
### 开机自启
``` bash
chkconfig jenkins on
```
### 全局工具配置
全局工具配置可以 配置相关工具如`Maven``GIT`等工具的路径、或者安装新的不同版本的工具。
配置该设置需要获取 `admin`权限,进入`系统管理 > 全局工具配置`
如:配置全局 GIT
![](/images/jenkins_globalconfig.png)
### 用户管理以及用户权限
- 使用`admin`权限的账号,进入`系统管理 > 用户管理`, 可以添加/修改/删除 用户。
- 进入`系统管理 > 全局安全配置` 中,勾选 __启用安全__。访问控制选择 __Jenkins专有用户数据库__,使用 __项目矩阵授权策略__, 可以为每个用户分配全局权限。
- 进入项目配置中,权限 __启用项目安全__ 可以单独为该项目分配用户权限。 从而确保每个项目的安全性。
### Git Parameter
为项目添加 `git`分支/标签选择参数构建配置,从而方便通过不同分支构建项目。
项目配置:
![](https://wiki.jenkins-ci.org/download/attachments/58917601/image2018-9-20_22-0-7.png?version=1&modificationDate=1537473611000&api=v2)
参数化构建:
![](https://wiki.jenkins-ci.org/download/attachments/58917601/image2018-9-20_22-2-47.png?version=1&modificationDate=1537473769000&api=v2)
基础`pipeline`配置:
``` groovy
// Using git without checkout
pipeline {
agent any
parameters {
gitParameter branchFilter: 'origin/(.*)', defaultValue: 'master', name: 'BRANCH', type: 'PT_BRANCH'
}
stages {
stage('Example') {
steps {
git branch: "${params.BRANCH}", url: 'https://github.com/jenkinsci/git-parameter-plugin.git'
}
}
}
}
```
[阅读插件原文git-parameter](https://plugins.jenkins.io/git-parameter)
### 其他
相关工具以及项目配置,都只是小问题而已...

View File

@ -0,0 +1,13 @@
---
title: caniuse
createTime: 2021/02/07 06:41:12
permalink: /article/h4z91gyz
author: pengzhanbo
top: false
type: null
---
### 工具
将caniuse 的feature 结果以图片或者iframe的形式嵌入到站点。
[https://caniuse.bitsofco.de/](https://caniuse.bitsofco.de/)

View File

@ -0,0 +1,128 @@
---
title: VSCode 常用插件推荐
lang: zh-CN
createTime: 2018/12/29 11:15:27
permalink: /article/ofp08jd8
author: pengzhanbo
tags:
- VSCode
top: false
type: null
---
`VS Code` 作为我现在工作中最常用的编辑器,也是我十分喜欢的编辑器。它强大的功能和插件系统,对我的工作提供了很多帮助和支持。将我在工作中经常使用的插件,推荐给大家。
<!-- more -->
### Code
1. [Code Spell Checker](https://github.com/Jason-Rev/vscode-spell-checker)
单词拼写检查插件,帮助检查代码中单词是否拼写错误,包括驼峰形式的变量名称检查。可以在一定程度避免一些不必要的单词拼写错误导致的一些低级错误。
2. [ESLint](https://github.com/Microsoft/vscode-eslint)
javascript ES6 代码规范、语法检查工具,帮助规范团队代码规范。
3. [EditorConfig](https://github.com/editorconfig/editorconfig-vscode)
编辑器配置,代码格式规范相关,必备。
4. [Prettier](https://github.com/prettier/prettier-vscode)
帮助格式化`javascript``typescript``CSS`代码。 <br />
`Prettier` 会读取 `.editorconfig`,或根据提供相关配置,格式化代码为符合项目代码规范。
5. [Bracket Pair Colorizer](https://github.com/CoenraadS/BracketPair)
可以对每一个代码块或者每一层嵌套,以不同的颜色高亮,帮助阅读代码。
![Bracket Pair Colorizer](https://github.com/CoenraadS/BracketPair/raw/develop/images/example.png) <br/>
主要是针对 `()``[]``{}` 进行不同嵌套的颜色高亮
6. [Code Runner](https://github.com/formulahendry/vscode-code-runner)
`VSCode`中运行各种各样的语言。并将结果输出到输出控制台。
方便代码调试。
7. [Color Highlight](https://github.com/sergiirocks/vscode-ext-color-highlight)
颜色高亮插件,读取文件中的 十六进制、RGB、RGBA 等颜色,并以对应的颜色高亮显示。
### theme
1. [Atom One Dark Theme](https://github.com/akamud/vscode-theme-onedark)
一款 Atom 的 暗色系主题皮肤。 习惯了`Atom`编辑器,转而使用`VSCode`的小伙伴们可以使用这款皮肤。<br/>
颜色对比度适中,不会太过强烈。
![Atom One Dark Theme](https://raw.githubusercontent.com/akamud/vscode-theme-onedark/master/screenshots/preview.png)
### GIT
1. [Git Blame](https://marketplace.visualstudio.com/items?itemName=waderyan.gitblame)
可以帮助查看到文件的每一行的详细修改信息,包括 HASH串、作者、日期等。
2. [Git history](https://marketplace.visualstudio.com/items?itemName=donjayamanne.githistory)
以可视化的界面查看 git log 信息。支持:<br/>
查看所有分支或者某一个分支的log信息<br/>
查看某一个文件的log信息<br/>
查看某一个作者的log信息等。<br/>
3. [Git Project Manager](https://github.com/felipecaputo/git-project-manager)
该插件可以帮助你快速在VSCode新窗口打开本地git项目。<br />
命令:`ctrl+shift+p` / `cmd+shift+p` <br/>
或者按下 `F1`,输入 `GPM`
4. [Git Tags](https://github.com/leftstick/vscode-git-tags)
Git Tag 管理插件
5. [Git Lens](https://gitlens.amod.io/)
这款插件十分适合在多人协作项目中使用可以定位到当前文件每一行的最后提交作者、时间等git log也可以查看到当前文件的所有日志等。
如果有装这一款插件,`Git Blame`插件就没有必要装了。
6. [git ignore](https://github.com/CodeZombieCH/vscode-gitignore)
### Markdown
1. [Markdown Preview Enhanced](https://shd101wyy.github.io/markdown-preview-enhanced)
一款功能强大的 markdown 插件。让你在vscode 中拥有更好的 markdown 写作体验。
### Icon
1. [vscode-icons](https://github.com/vscode-icons/vscode-icons)
文件菜单 icons。 根据文件夹命名、文件后缀等,对文件夹、文件菜单栏添加 对应的`icon`
![vscode-icons](https://raw.githubusercontent.com/vscode-icons/vscode-icons/master/images/screenshot.gif)
2. [VSCode Great Icons](https://marketplace.visualstudio.com/items?itemName=emmanuelbeziat.vscode-great-icons)
另一款 文件菜单栏 icons支持 100+的文件类型。<br/>
![VSCode Great Icons](https://raw.githubusercontent.com/EmmanuelBeziat/vscode-great-icons/icons-test/icons.jpg)
### IDE support
1. [View In Browser](https://github.com/hellopao/view-in-browser)
快速打开html页面在浏览器中访问。
2. [ftp-simple](https://github.com/humy2833/FTP-Simple)
FTP 上传/下载插件。
3. [Rest Client](https://github.com/Huachao/vscode-restclient)
允许你在 `VSCode` 中发送HTTP请求并查看response的内容。
### Framework support
1. [Vetur](https://github.com/vuejs/vetur)
Vue 支持。
2. [minapp](https://github.com/wx-minapp/minapp-vscode)
微信小程序 支持。

View File

@ -0,0 +1,202 @@
---
title: Vue组件间通信
lang: zh-CN
tags:
- vue
createTime: 2018/07/20 11:15:27
permalink: /article/iezlvhvg
author: pengzhanbo
top: false
type: null
---
在我们在进行基于[Vue](https://cn.vuejs.org/)的项目开发时,组件间的数据通信,是我们必须考虑的。
<!-- more -->
> 注: 本文所实现的方式,是在不考虑`vuex`下所做的实现。
我把组件间的关系,大致分为三种:
1. 父子组件
``` html
<parent>
<child></child>
</parent>
```
拥有类似结构,`parent`组件包含`child`组件,则`child`组件是`parent`的子组件,`parent`组件是`child`组件的父组件。
2. 兄弟组件
``` html
<item></item>
<item></item>
```
两个`item`组件在结构上同级,我们称之互为兄弟组件。
3. 跨多级组件
``` html
<list>
<item>
<message><message>
</item>
</list>
<dialog>
<content></content>
</dialog>
```
在这个结构中,`<list>``<message>`并不是直接的父子组件,中间还跨了一个级,在实际场景中,还会有跨更多层级的组件关系。`<message>``<content>` 组件两个既不是兄弟组件,又不是父子组件,而是跨了兄弟,父子的多级关系,实际场景中也会有发生交互。
那么这三种关系的组件,我们应该如何进行组件通信?
### 父子组件通信
要讲父子组件的通信,首先,我们需要了解 `vue` 组件的 特性。
1. 单向数据流,数据自上而下。
> Prop 是单向绑定的:当父组件的属性变化时,将传导给子组件,但是反过来不会。这是为
>了防止子组件无意间修改了父组件的状态,来避免应用的数据流变得难以理解。
2. 事件自下而上。
组件内部状态的变化,通过事件往上冒泡,通知上一级组件,由上一级组件监听事件,并触发相应回调。
基于以上,父子组件通信推荐的方式是:
父组件通过`props`将状态传到子组件,子组件通过事件将状态冒泡到父组件,由父组件监听触发回调改变状态。
`parent.vue`
``` html
<template>
<div class="parent">
<child
:name="name"
@name-change="nameChange"
>
</child>
</div>
</template>
<script>
import Child from './child';
export default {
name: 'parent',
data () {
return {
name: 'Jack'
};
}
methods: {
nameChange(name) {
this.name = name;
}
},
components: {
Child
}
}
</script>
```
`child.vue`
``` html
<template>
<div class="child">
<span>{{name}}</span>
<button @click="onClick">change name</button>
</div>
</template>
<script>
export default {
name: 'child',
props: {
name: {
type: String,
defualt() {
return '';
}
}
},
methods: {
onClick() {
this.$emit('name-change', 'John');
}
}
}
</script>
```
在某些例子或个人项目中,经常有发现到在子组件中使用 `this.$parent` 直接改变父组件的状态,诚然这种方式能够简化两个深耦合的组件的数据通信,在一些简单的场景中也会比较方便,但其实并不推荐采用这种方式实现父子组件通信,这样做的后果就是导致了数据流的不明确性,牺牲了单项数据流的简洁性,数据的变化流动变得不易于理解。
### 兄弟组件通信、跨多级组件通信
这两种组件关系,并没有直接的联系。
如兄弟组件,我们会很自然的想到使用他们的父级组件作为中转,将 `子组件1` 的状态通信到父组件,再由父组件通过 `props` 流向 `子组件2` ,反之亦然,但是如果兄弟组件间的交互复杂,但又与父组件没有存在直接的交互关联,父组件在这个过程当中,承担了多余的职责。
又如跨多级组件,上述例子中,`<list>``<message>`之间间隔了多层,如果我们继续使用父子组件通信`prop`和事件冒泡,中间的层需要重复的定义`prop`和事件,这显然也导致了它们承担了多余的职责。 `<message>``<content>` 组件之间,更是在结构上没有关联,`prop`和事件冒泡显得十分乏力,无法直接完成通信。
那么这两种组件关系,该如何完成通信,又不对它们中间层级组件,或者父级组件造成多余的干扰?
由于两种组件关系没有直接的关联,所以我们需要有一个桥梁,能够直接连接它们,使它们变得有关联。即,我们需要一个`中间件`
官方给我们的解决方案是`vuex`,但我认为它更多是的作为全局状态的管理,使用它作为某两个组件的通信中间件,显得大材小用,所以我这里不做讨论。
我所采取的方案是使用 自定义事件 完成组件通信。
__实例化Vue__
`vue`已实现了一套事件系统,可以很方便的使用它来完成我们的组件通信。
``` javascript
let middleware = new Vue();
export defualt middleware;
```
`message.vue`
``` javascript
export default {
name: 'message',
data () {
return {
info: 'hello'
};
},
methods: {
sayHello() {
middleware.$emit('say-hello', this.info);
}
}
};
```
`content.vue`
``` javascript
export default {
name: 'content',
data() {
return {
info: '';
}
},
created() {
middleware.$on('say-hello', info => {
this.info = info;
});
}
}
```
我们通过 `middleware``content.vue`注册了`say-hello`事件,当`message.vue`触发该事件时,`content.vue`监听到事件触发回调,从而实现了状态传导。
组件数据传导不再是通过`props`传导,而是通过事件进行通信。
__如果不使用实例化Vue的方式去完成我们也可以自己实现一套自定义事件。__ 这可以做更加个性化的自定义事件,满足项目中的多样的使用场景。
``` javascript
class Event{
constructor() {
// some props
}
on() {
// do something
}
emit() {
// do something
}
off() {
// do somethig
}
}
```
### 总结
复杂结构的组件通信,实现它们的通信,关键是实现中间件作为桥梁连接它们,无论是使用自定义事件,还是其他的方案。

View File

@ -0,0 +1,6 @@
---
title: 面试2
createTime: 2022/04/04 01:48:00
author: pengzhanbo
permalink: /article/exavsmm1
---

View File

@ -0,0 +1,60 @@
---
title: 面试题以及个人答案 JS篇
tags:
- 面试
createTime: 2018/08/23 11:15:27
permalink: /article/4ml7z17g
author: pengzhanbo
top: false
type: null
---
### JS变量声明方式有哪些有什么区别
声明变量的方式有三种,分别是`var``let``const`。其中`let``const``es6`新增的变量声明方式。
`var`声明的变量的作用域是它当前执行的上下文中,并且存在变量提升
``` js
function fn() {
console.log(a);
var a = 1;
console.log(a);
}
fn();
```
相当于
``` js
function fn() {
var a;
console.log(a); // undefined
a = 1;
console.log(a); // 1
}
fn();
```
并且在可以重复声明同一变量在该声明的上下文中不会丢失其值。通过var声明的全局变量会作为窗口对象的属性。
`let` 声明的变量是块级作用域,不存在变量提升,并存在暂时死区,不允许在同一块级作用域重复声明。
``` js
function fn() {
console.log(a); // ReferenceError: a is not defined
let a = 1;
let a = 2; // TypeError thrown
console.loga(a);
}
fn();
```
`const` 声明一个常量,其作用域可以是全局或者是本地声明的块级作用域,与`var`变量不同,全局常量不会变为窗口对象的属性。常量必须在声明的同时初始化。同时声明创建的值是一个只读引用,但不意味着它所持有的值是不可变的,只是变量标识符不能重新分配。如果引用内容是对象的情况下,则可以改变对象内容,除非使用`Object.freeze()`方法冻结对象。`const`声明常量同样存在暂时死区。
____
### JS数据类型隐式转换
————
### JS模块化各自的优劣与不同点
____
### EVENT LOOP

View File

@ -0,0 +1,152 @@
---
title: 面试题以及个人答案 CSS篇
tags:
- 面试
createTime: 2018/08/22 11:15:27
permalink: /article/565o1wn0
author: pengzhanbo
top: false
type: null
---
### CSS什么是盒模型盒模型有哪些具体的表现和不同点是什么
盒模型是CSS规范定义的模块它规定了一个矩形盒子标准盒模型描述任意元素在文档树中占据的空间区域。每个盒子有四个边`外边距边margin edge or outer edge``边框边border edge``内填充边padding edge``内容边content edge or inner edge`,可以划分四个区域`外边距区域margin area``边框区域border area``内填充区域padding area``内容区域content area`
![css box model](https://drafts.csswg.org/css-box-3/images/box.png)
为什么会有盒模型类型严格来说多数浏览器都按照规范实现了标准盒模型而盒模型的类型主要是来自于不同浏览器对元素宽高的方式不同而导致IE浏览器认为元素的`width/height`应该是由元素的`内容+内填充+边框`组成而W3C规定的元素的`width/height`应该是元素的`内容`,从而衍生了不同的盒子模型。到`CSS3`,添加了`box-sizing`属性用于更改用于计算元素宽高的默认盒子模型并将IE浏览器和W3C规范纳入了实现中。可以使用此属性来模拟不正确支持CSS盒子模型规范的浏览器的行为。
_注`width/height`最终并不能完全决定元素的实际占用宽高。_
``` css
/* 关键字值 */
box-sizing: border-box; /* 默认值 */
box-sizing: content-box;
/* 全局值 */
box-sizing: inherit;
box-sizing: initial;
box-sizing: unset;
```
`border-box`规定了元素的`width``内容+内填充+边框`组成即IE浏览器的实现。 元素的实际占据宽度由 width属性+外边距。内容宽度为`width - padding - border`
`content-box`规定了元素的`width``内容宽度`, W3C规范的标准。元素的实际占据宽度由`widht + padding + border + margin`。内容宽度为`width`
`box-sizing`还有一个待废除的值`padding-box``width``height` 属性包括内容和内边距但是不包括边框和外边距。只有Firefox实现了这个值它在Firefox 50中被删除。
在高度计算上以上规则同样适用,但对非替换行内元素,尽管内容周围存在内边距与边框,但其占用空间受到`line-height`属性影响。
____
### CSS: 什么是外边距合并?什么情况下会发生外边距合并?
块元素的上外边距和下外边距有时候会发生合并,其大小取其中绝对值最大的值,这种行为叫做外边距合并。
__浮动元素__ 和 __绝对定位元素__ 的外边距不会发生合并。这是因为触发了 __块格式化上下文__
1. 相邻元素之间的外边距会发生合并(如果后一个元素需要清除前面的浮动,则不一定发生合并)。
2. 父元素与其第一个子元素之间不存在边框、内边距、行内内容、没有创建 __块格式化上下文__、没有清除浮动或者父元素与其最后一个子元素之间不存在边框、内边距、行内内容、heigh、min-height、max-height那么子元素的外边距会溢出到父元素外面。
3. 如果一个块级元素不包含任何内容并且在不存在边框、内边距、行内内容、heigh、min-height则该元素的上下外边距会发生合并。
三种情况的外边距合并是可以组合产生更加复杂的外边距合并情况的。
_如果外边距合并的值都是负值则合并的值为最小的外边距的值。_
_如果发生外边距合并的值包含负值则合并后的值为最大的正外边距与最小的负外边距之和。_
_____
### CSS垂直水平居中
_这是个老生常谈的问题了场景可以有很多答案也有很多答案而言其实本身不重要重要是明白为什么这个方法为什么可以实现垂直居中。_
__设立一个场景在一个宽高不固定的容器中实现一个宽高不固定的内容盒子并垂直水平居中。__
``` html
<!-- 假设 warpper、container 宽高不固定 实现container相对于wrapper垂直水平居中-->
<div class="wrapper">
<div class="container">
</div>
</div>
```
__方法一__ 使用 flex 布局
``` css
.wrapper{
display: flex;
}
.container{
margin: auto;
}
```
适用于支持 flex布局的浏览器IE11以上其他现代浏览器。这里是利用flex弹性布局的特性弹性容器改变了其子元素填充可用空间的方式子元素默认从容器左上角开始排列在不设置宽高时子元素填充空间由`flex`声明,默认值为`0 1 auto`,即
`flex-grow: 0;flex-shrink: 1;flex-basis: auto`; 其中 `flex-basis`定义了子元素的宽和高的尺寸大小,`auto`值表示自动尺寸,根据子元素内容计算宽高,在子元素上设置`margin: auto`,这是利用`auto`平均分配水平或垂直方向上的额外的空间,从而达到目的。(此方法实现的结果是“真正的”垂直水平居中)
或者
``` css
.wrapper{
display: flex;
justify-content: center;
align-content: center;
}
```
__方法二__ 使用 table 布局
``` css
.wrapper{
display: table-cell;
vertical-align: middle;
}
.container{
margin: auto;
}
```
利用的是table布局的特性不过该方法有个缺点就是`display: table-cell`元素的宽高设置百分比数值是“无效的”,原因是父元素非`table`元素或`display: table`元素,`display: table-cell`元素的宽高百分比数字是相对于`table`计算的。
__方法三__ `position` + `transform`
``` css
.wrapper{
position: relative;
}
.container{
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
```
该方法与前面两个方法的作用机理有很大的不同,首先第一点是`container`脱离了文档流,并且`container`自身的宽高发生了坍塌,在不设置宽高属性下,尺寸由内容撑开,`container`相对`wrapper`元素进行绝对定位,水平方向与垂直方向上,`container`的左上角顶点偏移到`wrapper`中点,`container``transform`是相对于自身的,` translate(-50%, -50%)`相对于自身,将左上角顶点做左上偏移自身的一半,从而实现了目的。
_有一些面试者给出了`container`元素上设置`margin-left: -50%; margin-top: -50%`的答案然而margin的百分比值是相对于其父元素计算的。_
__方法四__ 使用 行内块元素
``` css
.wrapper{
text-align: center;
}
.wrapper:after{
content: '';
display: inline-block;
vertical-align: middle;
height: 100%;
}
.container{
display: inline-block;
vertical-align: middle;
text-align: left;
}
```
该方法实现的垂直水平居中其实是一个近似垂直水平居中兼容IE7以上的浏览器。水平方向上`.wrapper`设置`text-align: center;`实现了水平居中;垂直方向上,给定`container`声明行内块元素,并`vertical-align: middle`,但由于`container`高度不确定无法声明具体的行高所以借助了父元素的伪类元素创建了一个宽度为0高度为100%的行内块元素,从而使`container`元素在垂直方向上实现了居中。但由于`vertical-align: middle`是元素的中线与字符X的中心点对齐大多数字体设计字体的中心点偏下也导致了实现的垂直居中并不是绝对的垂直居中。而要实现绝对的垂直居中需要添加一下属性
```css
.wrapper{
font-size: 0;
white-space: nowrap;
}
.container{
font-size: 14px; /* 重置回默认字体大小 */
white-space: normal;
}
```
实现方法有很多,这里暂时只列出其中的四种。
____
_想到还有其他问题继续补充..._

5
docs/README.md Normal file
View File

@ -0,0 +1,5 @@
---
home: true
# banner: https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fpic1.win4000.com%2Fwallpaper%2F2019-08-20%2F5d5bb3ec573e4.jpg&refer=http%3A%2F%2Fpic1.win4000.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1651329706&t=5dac9f133df18a9cdcef5003e33f0b03
# mobileBanner: https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg9.51tietu.net%2Fpic%2F2019-091215%2Fg1s4d0voiqog1s4d0voiqo.jpg&refer=http%3A%2F%2Fimg9.51tietu.net&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1651329706&t=b0bc39d9dc448a3e77c6f5f0b544f516
---

6
docs/notes/README.md Normal file
View File

@ -0,0 +1,6 @@
---
title: README
createTime: 2022/04/04 11:13:30
author: pengzhanbo
permalink: /note/
---

View File

@ -0,0 +1,6 @@
---
title: md
createTime: 2022/04/04 01:56:31
author: pengzhanbo
permalink: /note/typescript/5j2ggf0m
---

View File

@ -0,0 +1,6 @@
---
title: md
createTime: 2022/04/04 01:57:39
author: pengzhanbo
permalink: /note/typescript/casr1ibn
---

View File

@ -0,0 +1,6 @@
---
title: Typescript
createTime: 2022/04/04 09:45:31
author: pengzhanbo
permalink: /note/typescript/
---

View File

@ -1,14 +0,0 @@
---
title: 组件
createTime: 2022/03/06 09:41:52
permalink: /post/yiobrmp0
author: pengzhanbo
top: false
type: # original: 原创: reprint 转载 可为空不填
tags:
- react
- 组件
---
# React 组件4

View File

@ -1,14 +0,0 @@
---
title: 组件
createTime: 2022/03/06 09:41:26
permalink: /post/zrd6axrp
author: pengzhanbo
top: false
type: # original: 原创: reprint 转载 可为空不填
tag:
- Vue
---
# 组件
组件内容 组件 组件

View File

@ -1,12 +0,0 @@
---
title: typescript学习
createTime: 2022/03/06 09:47:02
permalink: /post/upiqrep3
author: pengzhanbo
top: false
type: # original: 原创: reprint 转载 可为空不填
tags:
- typescript
---
# typescript 学习

View File

@ -1,10 +0,0 @@
---
title: 呵呵
createTime: 2022/03/07 05:16:12
permalink: /post/zhervgal
author: pengzhanbo
top: false
type: # original: 原创: reprint 转载 可为空不填
tags:
- notes
---

View File

@ -1,12 +0,0 @@
---
title: 哈哈
createTime: 2022/03/07 05:14:23
permalink: /post/0ts9cam6
author: pengzhanbo
top: false
type: # original: 原创: reprint 转载 可为空不填
tags:
- Vue
- notes
---

View File

@ -1,17 +0,0 @@
---
title: 电影杂谈
author: pengzhanbo
tags:
- 杂谈
- 电影
permalink: /post/zozx35d9
createTime: 2022/03/06 09:47:13
top: false
type: # original: 原创: reprint 转载 可为空不填
---
呵呵呵呵
<!-- more -->
111

View File

@ -1,3 +0,0 @@
---
home: true
---

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

5
lerna.json Normal file
View File

@ -0,0 +1,5 @@
{
"useWorkspaces": true,
"npmClient": "yarn",
"version": "1.0.0-beta.0"
}

View File

@ -1,90 +1,42 @@
{
"name": "@pengzhanbo/vuepress-theme-plume",
"version": "1.0.0-beta.7",
"description": "blog theme by vuepress2.x",
"main": "lib/node/index.js",
"types": "lib/node/index.d.ts",
"files": [
"lib",
"templates",
"tailwind.config.js"
],
"keywords": [
"vuepress-next",
"vuepress",
"vuepress-theme"
"name": "vuepress-theme-plume",
"version": "1.0.0-beta.0",
"private": true,
"workspaces": [
"packages/*"
],
"scripts": {
"docs:vite-serve": "vuepress-vite dev docs",
"docs:vite-build": "vuepress-vite build docs",
"docs:webpack-serve": "vuepress-webpack dev docs",
"docs:webpack-build": "vuepress-webpack build docs",
"docs:clean": "rimraf docs/.vuepress/.cache docs/.vuepress/.temp docs/.vuepress/dist",
"docs": "yarn docs:vite-serve",
"dev:package": "lerna run dev --parallel",
"build:package": "lerna run build --stream",
"package:clean": "lerna run clean",
"dev": "concurrently \"yarn dev:package\" \"yarn docs:vite-serve\"",
"build": "yarn build:package",
"lerna": "lerna clean && lerna bootstrap"
},
"author": "pengzhanbo",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/pengzhanbo/vuepress-theme-plume.git"
},
"homepage": "https://github.com/pengzhanbo/vuepress-theme-plume",
"bugs": {
"url": "https://github.com/pengzhanbo/vuepress-theme-plume/issues"
},
"scripts": {
"dev-watch": "yarn clean && concurrently \"yarn copy-watch\" \"yarn tsc-watch\"",
"dev": "vuepress dev example --debug",
"tsc": "tsc -b tsconfig.build.json",
"tsc-watch": "tsc -b tsconfig.dev.json -w",
"clean": "rimraf lib *.tsbuildinfo",
"copy": "cpx \"src/**/*.{d.ts,vue,scss,css,html}\" lib",
"copy-watch": "cpx \"src/**/*.{d.ts,vue,scss,css,html}\" lib -w",
"build": "yarn clean & yarn tsc & yarn copy"
},
"prettier": "prettier-config-vuepress",
"dependencies": {
"@tailwindcss/typography": "^0.5.2",
"@types/webpack-env": "^1.16.3",
"@vuepress/client": "^2.0.0-beta.35",
"@vuepress/core": "^2.0.0-beta.36",
"@vuepress/plugin-active-header-links": "^2.0.0-beta.36",
"@vuepress/plugin-back-to-top": "^2.0.0-beta.36",
"@vuepress/plugin-container": "^2.0.0-beta.36",
"@vuepress/plugin-git": "^2.0.0-beta.36",
"@vuepress/plugin-medium-zoom": "^2.0.0-beta.36",
"@vuepress/plugin-nprogress": "^2.0.0-beta.36",
"@vuepress/plugin-palette": "^2.0.0-beta.36",
"@vuepress/plugin-prismjs": "^2.0.0-beta.36",
"@vuepress/plugin-theme-data": "^2.0.0-beta.36",
"@vuepress/shared": "^2.0.0-beta.35",
"@vuepress/theme-default": "^2.0.0-beta.36",
"@vuepress/utils": "^2.0.0-beta.35",
"@vueuse/core": "^7.7.1",
"autoprefixer": "^10.4.2",
"chokidar": "^3.5.3",
"devDependencies": {
"concurrently": "^7.0.0",
"cpx2": "^4.2.0",
"dayjs": "^1.10.8",
"eslint": "^8.10.0",
"cross-env": "^7.0.3",
"eslint": "^8.12.0",
"eslint-config-vuepress": "^3.5.0",
"eslint-config-vuepress-typescript": "^2.5.0",
"gray-matter": "^4.0.3",
"json2yaml": "^1.1.0",
"nanoid": "^3.3.1",
"postcss": "^8.4.8",
"postcss-at-rules-variables": "^0.3.0",
"postcss-each": "^1.1.0",
"postcss-import": "^14.0.2",
"postcss-nested": "^5.0.6",
"postcss-preset-env": "^7.4.2",
"postcss-simple-vars": "^6.0.3",
"prettier": "^2.5.1",
"lerna": "^4.0.0",
"prettier": "^2.6.1",
"prettier-config-vuepress": "^1.3.0",
"rimraf": "^3.0.2",
"sass": "^1.49.9",
"tailwindcss": "^3.0.23",
"typescript": "^4.6.2",
"vue": "^3.2.31",
"vue-router": "^4.0.13",
"vuepress": "^2.0.0-beta.36"
},
"publishConfig": {
"access": "public"
},
"peerDependencies": {
"vuepress": ">=2.0.0-beta.36"
"typescript": "^4.6.3",
"vuepress-vite": "^2.0.0-beta.37",
"vuepress-webpack": "^2.0.0-beta.37",
"webpack-env": "^0.8.0",
"@vuepress/cli": "^2.0.0-beta.37"
}
}

View File

@ -0,0 +1,11 @@
# `plugin-caniuse`
> TODO: description
## Usage
```
const pluginCaniuse = require('plugin-caniuse');
// TODO: DEMONSTRATE API
```

View File

@ -0,0 +1,28 @@
{
"name": "@vuepress-plume/vuepress-plugin-caniuse",
"version": "1.0.0-beta.0",
"description": "The Plugin for VuePres 2, Support Can-I-Use feature",
"author": "pengzhanbo <volodymyr@foxmail.com>",
"homepage": "",
"license": "ISC",
"main": "lib/node/index.js",
"files": [
"lib"
],
"scripts": {
"ts": "tsc -b tsconfig.build.json",
"ts:watch": "tsc -b tsconfig.build.json --watch",
"clean": "rimraf lib *.tsbuildinfo",
"dev": "yarn ts:watch",
"build": "yarn clean && yarn ts"
},
"dependencies": {
"@vuepress/client": "^2.0.0-beta.37",
"@vuepress/core": "^2.0.0-beta.37",
"@vuepress/utils": "^2.0.0-beta.37",
"markdown-it-container": "^3.0.0"
},
"publishConfig": {
"access": "public"
}
}

View File

@ -0,0 +1,49 @@
# vuepress-plugin-caniuse
VuePress 2 Plugin
VuePress 2 插件
在Markdown中添加 [can-i-use](https://caniuse.com/) 支持这对于你在写前端技术博客时说明某个feature的兼容性时特别有用。
## Install
``` sh
yarn add @vuepress-plume/vuepress-plugin-caniuse
```
## Usage
### 在VuePress 配置文件中添加插件
``` js
// .vuepress/config.js
export default {
// ...
plugins: [
['@vuepress-plume/vuepress-plugin-caniuse', { mode: 'image' }]
]
// ...
}
```
### 在markdown中编写
``` md
::: caniuse <feature>
:::
```
### Options
- `options.mode`: can-i-use插入文档的模式 支持 `embed``image`, 默认值是 `image`
- `image`: 插入图片
- `embed`: 使用iframe嵌入 can-i-use
### \<feature>
正确取值请参考 [https://caniuse.bitsofco.de/](https://caniuse.bitsofco.de/)
## Example
``` md
::: caniuse css-matches-pseudo
:::
```
效果:
![can-i-use css-matches-pseudo](https://caniuse.bitsofco.de/image/css-dir-pseudo.webp)

View File

@ -0,0 +1,17 @@
import { defineClientAppEnhance } from '@vuepress/client'
import type { CanIUseMode } from '../shared'
import { resolveCanIUse } from './resolveCanIUse'
declare const __CAN_I_USE_INJECT_MODE__: CanIUseMode
declare const __VUEPRESS_SSR__: boolean
const mode = __CAN_I_USE_INJECT_MODE__
export default defineClientAppEnhance(({ router }) => {
if (__VUEPRESS_SSR__) return
router.afterEach((to) => {
if (mode === 'embed') {
setTimeout(() => resolveCanIUse(), 1500)
}
})
})

View File

@ -0,0 +1,31 @@
export const resolveCanIUse = (): void => {
const canIUseEls = document.getElementsByClassName('ciu_embed')
for (let t = 0; t < canIUseEls.length; t++) {
const el = canIUseEls[t]
const feature = el.getAttribute('data-feature')
const periods = el.getAttribute('data-periods')
const accessible = el.getAttribute('data-accessible-colours') || 'false'
const image = el.getAttribute('data-image-base') || 'none'
if (feature) {
const url = 'https://caniuse.bitsofco.de/embed/index.html'
const d = `<iframe src="${url}?feat=${feature}&periods=${periods}&accessible-colours=${accessible}&image-base=${image}" frameborder="0" width="100%" height="400px"></iframe>`
el.innerHTML = d
} else
el.innerHTML =
"A feature was not included. Go to <a href='https://caniuse.bitsofco.de/#how-to-use'>https://caniuse.bitsofco.de/#how-to-use</a> to generate an embed."
}
window.addEventListener('message', (message) => {
const data = message.data
if (typeof data === 'string' && data.indexOf('ciu_embed') > -1) {
const [, feature, height] = data.split(':')
for (let i = 0; i < canIUseEls.length; i++) {
const el = canIUseEls[i]
if (el.getAttribute('data-feature') === feature) {
const h = parseInt(height) + 30
;(el.childNodes[0] as any).height = h + 'px'
break
}
}
}
})
}

View File

@ -0,0 +1,6 @@
import { plugin } from './plugin'
export * from './plugin'
export * from '../shared'
export default plugin

View File

@ -0,0 +1,51 @@
import type { Plugin, PluginObject } from '@vuepress/core'
import { path } from '@vuepress/utils'
import * as container from 'markdown-it-container'
import type * as Token from 'markdown-it/lib/token'
import type { CanIUseMode, CanIUsePluginOptions } from '../shared'
import { resolveCanIUse } from './resolveCanIUse'
const modeMap: CanIUseMode[] = ['image', 'embed']
const isMode = (mode: CanIUseMode): boolean => modeMap.includes(mode)
export const plugin: Plugin = ({ mode = modeMap[0] }: CanIUsePluginOptions) => {
mode = isMode(mode) ? mode : modeMap[0]
const type = 'caniuse'
const validateReg = new RegExp(`^${type}\\s+(.*)$`)
const pluginObj: PluginObject = {
name: '@vuepress-plume/vuepress-plugin-caniuse',
clientAppEnhanceFiles: path.resolve(
__dirname,
'../client/clientAppEnhance.js'
),
define: {
__CAN_I_USE_INJECT_MODE__: mode,
},
}
const validate = (info: string): boolean => {
return validateReg.test(info.trim())
}
const before = '<div class="caniuse-container">\n'
const after = '\n</div>'
const render = (tokens: Token[], index: number): string => {
const token = tokens[index]
if (token.nesting === 1) {
const feature = token.info.trim().slice(type.length).trim() || ''
if (feature) {
return before + resolveCanIUse(feature, mode)
}
return before
} else {
return after
}
}
pluginObj.extendsMarkdown = (md) => {
md.use(container, type, { validate, render })
}
return pluginObj
}

View File

@ -0,0 +1,17 @@
import type { CanIUseMode } from '../shared'
export const resolveCanIUse = (feature: string, mode: CanIUseMode): string => {
const before =
mode === 'embed'
? `<p class="ciu_embed" data-feature="${feature}" data-periods="future_2,future_1,current,past_1,past_2" data-accessible-colours="false">`
: ''
const after = mode === 'embed' ? '</p>' : ''
return `
${before}
<picture>
<source type="image/webp" srcset="https://caniuse.bitsofco.de/image/${feature}.webp">
<source type="image/png" srcset="https://caniuse.bitsofco.de/image/${feature}.png">
<img src="https://caniuse.bitsofco.de/image/${feature}.jpg" alt="Data on support for the ${feature} feature across the major browsers from caniuse.com">
</picture>
${after}
`
}

View File

@ -0,0 +1,5 @@
export type CanIUseMode = 'embed' | 'image'
export interface CanIUsePluginOptions {
mode: CanIUseMode
}

View File

@ -0,0 +1 @@
export * from './caniuse'

View File

@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.base.json",
"references": [
{
"path": "./tsconfig.esm.json"
},
{
"path": "./tsconfig.cjs.json"
}
],
"files": []
}

View File

@ -1,9 +1,12 @@
{
"extends": "./tsconfig.base.json",
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"module": "CommonJS",
"rootDir": "./src",
"outDir": "./lib"
},
"include": ["./src/node", "./src/shared"]
"include": [
"./src/node",
"./src/shared"
]
}

View File

@ -0,0 +1,15 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"module": "ES2020",
"rootDir": "./src",
"outDir": "./lib",
"types": [
"@vuepress/client/types"
]
},
"include": [
"./src/client",
"./src/shared"
]
}

11
packages/theme/README.md Normal file
View File

@ -0,0 +1,11 @@
# `theme`
> TODO: description
## Usage
```
const theme = require('theme');
// TODO: DEMONSTRATE API
```

View File

@ -0,0 +1,53 @@
{
"name": "@vuepress-plume/vuepress-theme-plume",
"version": "1.0.0-beta.0",
"description": "A Blog Theme for VuePress 2.0",
"author": "pengzhanbo <volodymyr@foxmail.com>",
"homepage": "",
"license": "MIT",
"main": "lib/node/index.js",
"files": [
"lib",
"template"
],
"scripts": {
"copy": "cpx \"src/**/*.{d.ts,vue,css,scss,jpg,png}\" lib",
"copy:watch": "yarn copy -w",
"ts": "tsc -b tsconfig.build.json",
"ts:watch": "tsc -b tsconfig.build.json --watch",
"clean": "rimraf lib *.tsbuildinfo",
"dev": "concurrently \"yarn copy:watch\" \"yarn ts:watch\"",
"build": "yarn clean && yarn copy && yarn ts"
},
"dependencies": {
"@types/lodash.merge": "^4.6.6",
"@vuepress-plume/vuepress-plugin-caniuse": "^1.0.0-beta.0",
"@vuepress/client": "^2.0.0-beta.37",
"@vuepress/core": "^2.0.0-beta.37",
"@vuepress/plugin-container": "^2.0.0-beta.37",
"@vuepress/plugin-docsearch": "^2.0.0-beta.37",
"@vuepress/plugin-external-link-icon": "^2.0.0-beta.37",
"@vuepress/plugin-medium-zoom": "^2.0.0-beta.37",
"@vuepress/plugin-nprogress": "^2.0.0-beta.37",
"@vuepress/plugin-prismjs": "^2.0.0-beta.37",
"@vuepress/plugin-search": "^2.0.0-beta.36",
"@vuepress/plugin-theme-data": "^2.0.0-beta.37",
"@vuepress/plugin-toc": "^2.0.0-beta.37",
"@vuepress/shared": "^2.0.0-beta.37",
"@vuepress/utils": "^2.0.0-beta.37",
"@vueuse/core": "^8.2.3",
"chokidar": "^3.5.3",
"date-fns": "^2.28.0",
"gray-matter": "^4.0.3",
"json2yaml": "^1.1.0",
"lodash.merge": "^4.6.2",
"nanoid": "^3.3.2",
"sass": "^1.49.9",
"sass-loader": "^12.6.0",
"vue": "^3.2.31",
"vue-router": "^4.0.14"
},
"publishConfig": {
"access": "public"
}
}

View File

@ -0,0 +1,22 @@
import { defineClientAppEnhance } from '@vuepress/client'
import { h } from 'vue'
import './styles/index.scss'
export default defineClientAppEnhance(({ app }) => {
app.component('NavbarSearch', () => {
const SearchComponent =
app.component('Docsearch') || app.component('SearchBox')
if (SearchComponent) {
return h(SearchComponent)
}
return null
})
app.component('Toc', () => {
const Toc = app.component('TocCom')
if (Toc) {
return h(Toc)
}
return null
})
})

View File

@ -1,6 +1,6 @@
import { defineClientAppSetup } from '@vuepress/client'
// import { setupDarkMode } from './composables'
import './styles/index.css'
import { setupDarkMode } from './composables'
export default defineClientAppSetup(() => {
// setupDarkMode()
setupDarkMode()
})

View File

@ -0,0 +1,3 @@
<template>
<div></div>
</template>

View File

@ -0,0 +1,105 @@
<script lang="ts">
/* eslint-disable import/first, import/no-duplicates, import/order */
import { defineComponent } from 'vue'
export default defineComponent({
inheritAttrs: false,
})
</script>
<script lang="ts" setup>
import { computed, toRefs } from 'vue'
import type { PropType } from 'vue'
import { useRoute } from 'vue-router'
import { useSiteData } from '@vuepress/client'
import type { NavLink } from '../../shared'
import { isLinkHttp, isLinkMailto, isLinkTel } from '@vuepress/shared'
const props = defineProps({
item: {
type: Object as PropType<NavLink>,
require: true,
default: () => ({ text: '' }),
},
})
const route = useRoute()
const site = useSiteData()
const { item } = toRefs(props)
const hasHttpProtocol = computed(() => isLinkHttp(item.value.link))
const hasNonHttpProtocol = computed(
() => isLinkMailto(item.value.link) || isLinkTel(item.value.link)
)
const linkTarget = computed(() => {
if (hasNonHttpProtocol.value) return undefined
if (item.value.target) return item.value.target
if (hasHttpProtocol.value) return '_blank'
return undefined
})
const isBlankTarget = computed(() => linkTarget.value === '_blank')
const isRouterLink = computed(
() =>
!hasHttpProtocol.value && !hasNonHttpProtocol.value && !isBlankTarget.value
)
const linkRel = computed(() => {
if (hasNonHttpProtocol.value) return undefined
if (item.value.rel) return item.value.rel
if (isBlankTarget.value) return 'noopener noreferrer'
return undefined
})
const linkAriaLabel = computed(() => item.value.ariaLabel || item.value.text)
const shouldBeActiveInSubpath = computed(() => {
const localeKeys = Object.keys(site.value.locales)
if (localeKeys.length) {
return !localeKeys.some((key) => key === item.value.link)
}
return item.value.link !== '/'
})
const isActiveInSubpath = computed(() => {
if (!shouldBeActiveInSubpath.value) return false
return route.path.startsWith(item.value.link)
})
const isActive = computed(() => {
if (isRouterLink.value) return false
if (item.value.activeMatch) {
return new RegExp(item.value.activeMatch).test(route.path)
}
return isActiveInSubpath.value
})
</script>
<template>
<RouterLink
v-if="isRouterLink"
:class="{ 'router-link-active': isActive }"
:to="item.link"
:aria-label="linkAriaLabel"
v-bind="$attrs"
>
<slot name="before" />
{{ item.text }}
<slot name="after" />
</RouterLink>
<a
v-else
class="external-link"
:href="item.link"
:rel="linkRel"
:target="linkTarget"
:aria-label="linkAriaLabel"
v-bind="$attrs"
>
<slot name="before" />
{{ item.text }}
<ExternalLinkIcon v-if="isBlankTarget" />
<slot name="after" />
</a>
</template>

View File

@ -0,0 +1,16 @@
<script lang="ts" setup>
import BloggerInfo from './BloggerInfo.vue'
</script>
<template>
<aside class="blog-info-wrapper">
<BloggerInfo />
</aside>
</template>
<style lang="scss">
.blog-info-wrapper {
width: 18.75rem;
margin-left: 1.25rem;
position: sticky;
top: calc(var(--navbar-height) + 1.25rem);
}
</style>

View File

@ -0,0 +1,135 @@
<script lang="ts" setup>
import DropdownTransition from '@theme-plume/DropdownTransition.vue'
import { isLinkHttp, isLinkMailto } from '@vuepress/shared'
import type { FunctionalComponent, Ref } from 'vue'
import { ref } from 'vue'
import { useThemeLocaleData } from '../composables'
import {
EmailIcon,
FacebookIcon,
GithubIcon,
LinkedinIcon,
QQIcon,
TwitterIcon,
WeiBoIcon,
ZhiHuIcon,
} from './icons'
interface SocialItem {
url: string
icon: FunctionalComponent
}
type SocialData = SocialItem[]
type SocialRef = Ref<SocialData>
const themeLocale = useThemeLocaleData()
const avatar = themeLocale.value.avatar || {}
const social = themeLocale.value.social || {}
const useSocialList = (): SocialRef => {
const list: SocialRef = ref([])
if (social.QQ) {
const url = isLinkHttp(social.QQ)
? social.QQ
: `https://wpa.qq.com/msgrd?v=3&uin=${social.QQ}&site=qq&menu=yes`
list.value.push({ url, icon: QQIcon })
}
if (social.email) {
const url = isLinkMailto(social.email)
? social.email
: `mailto:${social.email}`
list.value.push({ url, icon: EmailIcon })
}
if (social.github) {
const url = isLinkHttp(social.github)
? social.github
: `https://github.com/${social.github}`
list.value.push({ url, icon: GithubIcon })
}
if (social.linkedin) {
list.value.push({ url: social.linkedin, icon: LinkedinIcon })
}
if (social.weiBo) {
list.value.push({ url: social.weiBo, icon: WeiBoIcon })
}
if (social.zhiHu) {
list.value.push({ url: social.zhiHu, icon: ZhiHuIcon })
}
if (social.facebook) {
list.value.push({ url: social.facebook, icon: FacebookIcon })
}
if (social.twitter) {
list.value.push({ url: social.twitter, icon: TwitterIcon })
}
return list
}
const socialList = useSocialList()
</script>
<template>
<DropdownTransition>
<section class="blogger-info">
<p v-if="avatar.url" class="avatar-img">
<img :src="avatar.url" :alt="avatar.name" />
</p>
<h3>{{ avatar.name }}</h3>
<p>{{ avatar.description }}</p>
<p class="blogger-social">
<a
v-for="item in socialList"
:key="item.url"
target="_blank"
:href="item.url"
>
<Component :is="item.icon" />
</a>
</p>
</section>
</DropdownTransition>
</template>
<style lang="scss">
.blogger-info {
padding: 1.25rem;
border-radius: var(--p-around);
background-color: var(--c-bg-container);
box-shadow: var(--shadow);
.avatar-img {
padding-bottom: 0.8rem;
img {
width: 75%;
}
}
p,
h3 {
text-align: center;
margin: 0;
}
h3 {
padding-bottom: 0.5rem;
}
.blogger-social {
vertical-align: middle;
text-align: center;
a {
display: inline-block;
margin: 0.5rem 0.15rem 0;
vertical-align: middle;
}
.icon {
width: 28px;
height: 28px;
}
.email-icon,
.github-icon,
.weiBo-icon {
width: 24px;
height: 24px;
}
}
}
</style>

View File

@ -0,0 +1,43 @@
<script lang="ts" setup>
import BlogInfo from '@theme-plume/BlogInfo.vue'
import DropdownTransition from '@theme-plume/DropdownTransition.vue'
import { useCategoryList } from '../composables'
import CategoryGroup from './CategoryGroup.vue'
const categoryList = useCategoryList()
</script>
<template>
<div class="category-wrapper">
<div class="category-container">
<DropdownTransition>
<div class="category-content">
<CategoryGroup
v-for="(category, index) in categoryList"
:key="category.type + '_' + index"
:category="category"
:index="index"
/>
</div>
</DropdownTransition>
<BlogInfo></BlogInfo>
</div>
</div>
</template>
<style lang="scss">
@import '../styles/_mixins';
.category-wrapper {
@include wrapper;
.category-container {
@include container_wrapper;
display: flex;
align-items: flex-start;
padding: 1.25rem 0;
}
.category-content {
flex: 1;
}
}
</style>

View File

@ -0,0 +1,98 @@
<script lang="ts" setup>
import DropdownTransition from '@theme-plume/DropdownTransition.vue'
import type { FunctionalComponent, PropType } from 'vue'
import { h } from 'vue'
import type { CategoryItem } from '../composables'
const props = defineProps({
category: {
type: Object as PropType<CategoryItem>,
required: true,
},
head: {
type: Number,
default: 2,
},
index: {
type: Number,
required: true,
},
})
const Heading: FunctionalComponent = () => {
const head = props.head > 4 ? 4 : props.head
return h(
`h${head}`,
{ id: props.category.label.trim().replace(/\s+/g, '-') },
[props.category.label]
)
}
</script>
<template>
<DropdownTransition :delay="index * 0.04">
<section class="category-group-wrapper">
<Heading />
<ul class="category-list">
<li
v-for="post in category.postList"
:key="post.path"
class="category-item"
>
<span>[{{ post.createTime }}]</span>
<RouterLink :to="post.path">{{ post.title }}</RouterLink>
</li>
</ul>
<CategoryGroup
v-for="(cate, i) in category.children"
:key="cate.type + '__' + i"
:category="cate"
:head="head + 1"
:index="i"
/>
</section>
</DropdownTransition>
</template>
<style lang="scss">
.category-group-wrapper {
padding: 1.25rem 1.5rem;
background-color: var(--c-bg-container);
border-radius: var(--p-around);
margin-bottom: 1.25rem;
box-shadow: var(--shadow);
transition: box-shadow var(--t-color);
.category-group-wrapper {
box-shadow: none;
border-radius: 0;
padding: 0;
margin-left: 1.25rem;
border-bottom: solid 1px var(--c-border);
&:last-child {
border-bottom: none;
}
.category-group-wrapper {
margin-left: 0;
}
}
.category-list {
list-style: none;
padding-left: 1.25rem;
}
.category-item {
span {
color: var(--c-text-lighter);
margin-right: 1.25rem;
}
a {
color: var(--c-text);
&:hover {
color: var(--c-text-accent);
}
}
}
}
</style>

View File

@ -1,9 +1,7 @@
<script setup lang="ts">
import { useDarkMode, useThemeLocaleData } from '../composables'
const themeLocale = useThemeLocaleData()
const isDarkMode = useDarkMode()
const toggleDarkMode = (): void => {
isDarkMode.value = !isDarkMode.value
}
@ -55,3 +53,25 @@ const toggleDarkMode = (): void => {
</svg>
</button>
</template>
<style lang="scss">
.toggle-dark-button {
display: flex;
margin: auto;
margin-left: 1rem;
border: 0;
background: none;
color: var(--c-text);
opacity: 0.8;
cursor: pointer;
&:hover {
opacity: 1;
}
.icon {
width: 1.25rem;
height: 1.25rem;
}
}
</style>

View File

@ -0,0 +1,59 @@
<script lang="ts">
import { defineComponent, Transition, TransitionGroup } from 'vue'
import type { PropType } from 'vue'
export default defineComponent({
name: 'DropdownTransition',
components: {
Transition,
TransitionGroup,
},
props: {
type: { type: String as PropType<'single' | 'group'>, default: 'single' },
delay: { type: Number, default: 0 },
duration: { type: Number, default: 0.25 },
},
setup(props) {
const setStyle = (item: Element): void => {
;(
item as HTMLElement
).style.transition = `transform ${props.duration}s ease-in-out ${props.delay}s, opacity ${props.duration}s ease-in-out ${props.delay}s`
;(item as HTMLElement).style.transform = 'translateY(-20px)'
;(item as HTMLElement).style.opacity = '0'
}
const unsetStyle = (item: Element): void => {
;(item as HTMLElement).style.transform = 'translateY(0)'
;(item as HTMLElement).style.opacity = '1'
}
return {
setStyle,
unsetStyle,
}
},
})
</script>
<template>
<Transition
v-if="type === 'single'"
name="drop"
appear
@appear="setStyle"
@after-appear="unsetStyle"
@enter="setStyle"
@after-enter="unsetStyle"
@before-leave="setStyle"
>
<slot />
</Transition>
<TransitionGroup
v-if="type === 'group'"
name="drop"
appear
@appear="setStyle"
@after-appear="unsetStyle"
@enter="setStyle"
@after-enter="unsetStyle"
@before-leave="setStyle"
>
<slot />
</TransitionGroup>
</template>

View File

@ -0,0 +1,28 @@
<script lang="ts" setup>
import BlogInfo from '@theme-plume/BlogInfo.vue'
import HomeBigBanner from '@theme-plume/HomeBigBanner.vue'
import PostList from '@theme-plume/PostList.vue'
</script>
<template>
<div class="plume-theme-home">
<HomeBigBanner></HomeBigBanner>
<div class="plume-theme-container">
<PostList></PostList>
<BlogInfo></BlogInfo>
</div>
</div>
</template>
<style lang="scss">
@import '../styles/_mixins';
.plume-theme-home {
@include wrapper;
.plume-theme-container {
@include container_wrapper;
display: flex;
justify-content: flex-start;
align-items: flex-start;
padding: 1.25rem 0;
}
}
</style>

View File

@ -0,0 +1,56 @@
<script lang="ts" setup>
import { usePageFrontmatter, withBase } from '@vuepress/client'
import { isLinkHttp } from '@vuepress/shared'
import { computed, onMounted, ref } from 'vue'
import type { PlumeThemeHomeFrontmatter } from '../../shared'
const frontmatter = usePageFrontmatter<PlumeThemeHomeFrontmatter>()
const MOBILE_WIDTH = 716
const bannerImg = ref(frontmatter.value.banner || '')
const hasBanner = computed(
() => !!(frontmatter.value.banner || frontmatter.value.mobileBanner)
)
const bannerStyle = computed(() => {
if (!hasBanner.value) return ''
const url = isLinkHttp(bannerImg.value)
? bannerImg.value
: withBase(bannerImg.value)
return {
'background-image': `url(${url})`,
}
})
function handleResize(): void {
const width = document.documentElement.offsetWidth
if (!hasBanner.value) return
if (width < MOBILE_WIDTH) {
bannerImg.value =
frontmatter.value.mobileBanner || frontmatter.value.banner || ''
} else {
bannerImg.value = frontmatter.value.banner || ''
}
}
onMounted(() => {
handleResize()
window.addEventListener('resize', handleResize, false)
window.addEventListener('orientationchange', handleResize, false)
})
</script>
<template>
<div
v-if="hasBanner"
class="home-big-banner-wrapper"
:style="bannerStyle"
></div>
</template>
<style lang="scss">
.home-big-banner-wrapper {
width: 100%;
height: calc(100vh - var(--navbar-height));
background-color: transparent;
background-position: 0 0;
background-size: cover;
background-repeat: no-repeat;
}
</style>

View File

@ -0,0 +1,133 @@
<script lang="ts" setup>
import DarkModeButton from '@theme-plume/DarkModeButton.vue'
import NavbarBrand from '@theme-plume/NavbarBrand.vue'
import NavbarItems from '@theme-plume/NavbarItems.vue'
import { computed, onMounted, ref } from 'vue'
import { useThemeLocaleData } from '../composables'
import { getCssValue } from '../utils'
const themeLocale = useThemeLocaleData()
const navbar = ref<HTMLElement | null>(null)
const navbarBrand = ref<HTMLElement | null>(null)
const linksWrapperMaxWith = ref(0)
const linksWrapperStyle = computed(() => {
if (!linksWrapperMaxWith.value) {
return {}
}
return {
maxWidth: linksWrapperMaxWith.value + 'px',
}
})
const enableDarkMode = computed(() => themeLocale.value.darkMode)
onMounted(() => {
const MOBILE_DESKTOP_BREAKPOINT = 719
const navbarHorizontalPadding =
getCssValue(navbar.value, 'paddingLeft') +
getCssValue(navbar.value, 'paddingRight')
const handleLinkWrapWidth = (): void => {
if (window.innerWidth <= MOBILE_DESKTOP_BREAKPOINT) {
linksWrapperMaxWith.value = 0
} else {
linksWrapperMaxWith.value =
navbar.value!.offsetWidth -
navbarHorizontalPadding -
(navbarBrand.value?.offsetWidth || 0)
}
}
handleLinkWrapWidth()
window.addEventListener('resize', handleLinkWrapWidth, false)
window.addEventListener('orientationchange', handleLinkWrapWidth, false)
})
</script>
<template>
<header ref="navbar" class="navbar-wrapper">
<span ref="navbarBrand">
<NavbarBrand />
</span>
<div class="navbar-items-wrapper" :style="linksWrapperStyle">
<slot name="before" />
<NavbarItems class="can-hide" is-header />
<slot name="after" />
<DarkModeButton v-if="enableDarkMode" />
<NavbarSearch />
</div>
</header>
</template>
<style lang="scss">
@import '../styles/variables';
.navbar-wrapper {
--navbar-line-height: calc(
var(--navbar-height) - 2 * var(--navbar-padding-v)
);
position: fixed;
top: 0;
left: 0;
z-index: 10;
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
height: var(--navbar-height);
padding: var(--navbar-padding-v) var(--navbar-padding-h);
background-color: var(--c-bg-navbar);
box-shadow: var(--shadow);
line-height: var(--navbar-line-height);
transition: background-color 0.3s ease;
.logo {
height: var(--navbar-line-height);
margin-right: var(--navbar-padding-v);
vertical-align: top;
}
.site-name {
font-size: 1.3rem;
font-weight: 600;
color: var(--c-text);
position: relative;
transition: color 0.3s ease;
}
.navbar-items-wrapper {
display: flex;
white-space: nowrap;
font-size: 0.9rem;
height: var(--navbar-line-height);
.search-box {
flex: 0 0 auto;
vertical-align: top;
}
.navbar-items .navbar-item {
& > .router-link-active {
color: var(--c-text-accent);
}
}
}
}
.DocSearch {
transition: background-color var(--t-color);
}
@media (max-width: $MQMobile) {
.navbar-wrapper {
padding-left: 4rem;
.can-hide {
display: none;
}
.site-name {
width: calc(100vw - 9.4rem);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}
</style>

View File

@ -0,0 +1,52 @@
<script lang="ts" setup>
import {
ClientOnly,
useRouteLocale,
useSiteLocaleData,
withBase,
} from '@vuepress/client'
import { computed, h } from 'vue'
import type { FunctionalComponent } from 'vue'
import { useDarkMode, useThemeLocaleData } from '../composables'
const routeLocale = useRouteLocale()
const siteLocale = useSiteLocaleData()
const themeLocale = useThemeLocaleData()
const isDarkMode = useDarkMode()
const navbarBrandLink = computed(
() => themeLocale.value.home || routeLocale.value
)
const navbarBrandTitle = computed(() => siteLocale.value.title)
const navbarBrandLogo = computed(() => {
if (isDarkMode.value && themeLocale.value.logoDark !== undefined) {
return themeLocale.value.logoDark
}
return themeLocale.value.logo
})
const NavbarBrandLogo: FunctionalComponent = () => {
if (!navbarBrandLogo.value) return null
const img = h('img', {
class: 'logo',
src: withBase(navbarBrandLogo.value),
alt: navbarBrandTitle.value,
})
if (themeLocale.value.logoDark === undefined) {
return img
}
return h(ClientOnly, img)
}
</script>
<template>
<RouterLink :to="navbarBrandLink">
<NavbarBrandLogo />
<span
v-if="navbarBrandTitle"
class="site-name"
:class="{ 'can-hide': navbarBrandLogo }"
>
{{ navbarBrandTitle }}
</span>
</RouterLink>
</template>

View File

@ -0,0 +1,326 @@
<script lang="ts" setup>
import AutoLink from '@theme-plume/AutoLink.vue'
import DropdownTransition from '@theme-plume/DropdownTransition.vue'
import type { PropType } from 'vue'
import { computed, ref, toRefs, watch } from 'vue'
import { useRoute } from 'vue-router'
import type { NavbarItem, NavGroup, ResolveNavbarItem } from '../../shared'
const props = defineProps({
item: {
type: Object as PropType<Exclude<ResolveNavbarItem, NavbarItem>>,
required: true,
},
isHeader: {
type: Boolean,
required: true,
},
})
const { item } = toRefs(props)
const dropdownAriaLabel = computed(
() => item.value.ariaLabel || item.value.text
)
const open = ref(false)
const route = useRoute()
watch(
() => route.path,
() => {
open.value = false
}
)
const handleDropdown = (e): void => {
const isTriggerByTab = e.detail === 0
if (isTriggerByTab || props.isHeader) {
open.value = !open.value
} else {
open.value = false
}
}
const isLastItemOfArray = (item: unknown, arr: unknown[]): boolean =>
arr[arr.length - 1] === item
const onSubTitleFocusout = (child): void => {
if (
isLastItemOfArray(child, item.value.children) &&
child.children &&
child.children.length === 0
) {
open.value = false
}
}
const onGrandChildFocusout = (grandchild, child): void => {
if (
isLastItemOfArray(grandchild, child.children) &&
isLastItemOfArray(child, item.value.children)
) {
open.value = false
}
}
</script>
<template>
<div
class="navbar-dropdown-wrapper"
:class="{ open }"
@mouseleave="open = false"
>
<button
v-if="isHeader"
class="navbar-dropdown-title"
type="button"
:aria-label="dropdownAriaLabel"
@click="handleDropdown"
@mouseenter="open = true"
>
<span class="title">{{ item.text }}</span>
<span class="arrow down"></span>
</button>
<button
v-else
class="navbar-dropdown-title-mobile"
type="button"
:aria-label="dropdownAriaLabel"
@click="open = !open"
>
<span class="title">{{ item.text }}</span>
<span class="arrow" :class="open ? 'down' : 'right'"></span>
</button>
<DropdownTransition>
<ul v-show="open" class="navbar-dropdown">
<li
v-for="child in item.children"
:key="child.text"
class="navbar-dropdown-item"
>
<template v-if="(child as NavGroup<NavbarItem>).children">
<h4 class="navbar-dropdown-subtitle">
<AutoLink
v-if="(child as NavbarItem).link"
:item="(child as NavbarItem)"
@focusout="onSubTitleFocusout(child)"
/>
<span v-else>{{ child.text }}</span>
</h4>
<ul class="navbar-dropdown-subitem-wrapper">
<li
v-for="grandchild in (child as unknown as NavGroup<NavbarItem>).children"
:key="grandchild.link"
class="navbar-dropdown-subitem"
>
<AutoLink
:item="grandchild"
@focusout="onGrandChildFocusout(grandchild, child)"
/>
</li>
</ul>
</template>
<template v-else>
<AutoLink
:item="(child as NavbarItem)"
@focusout="
isLastItemOfArray(child, item.children) && (open = false)
"
/>
</template>
</li>
</ul>
</DropdownTransition>
</div>
</template>
<style lang="scss">
@import '../styles/_variables';
@import '../styles/_mixins';
.navbar-dropdown-wrapper {
cursor: pointer;
.navbar-dropdown-title {
display: block;
font-size: 0.9rem;
font-family: inherit;
cursor: inherit;
padding: inherit;
line-height: 1.4rem;
background: transparent;
border: none;
font-weight: 500;
color: var(--c-text);
&:hover {
border-color: transparent;
}
.arrow {
vertical-align: middle;
margin-top: -1px;
margin-left: 0.4rem;
}
}
.navbar-dropdown-title-mobile {
@extend .navbar-dropdown-title;
display: none;
font-weight: 600;
font-size: inherit;
&:hover {
color: var(--c-text-accent);
}
}
.navbar-dropdown {
list-style: none;
.navbar-dropdown-item {
color: inherit;
line-height: 1.7rem;
.navbar-dropdown-subtitle {
margin: 0.45rem 0 0;
border-top: 1px solid var(--c-border);
padding: 1rem 0 0.45rem 0;
font-size: 0.9rem;
& > span {
padding: 0 1.5rem;
}
& > a {
font-weight: inherit;
&.router-link-active {
&::after {
display: none;
}
}
}
}
.navbar-dropdown-subitem-wrapper {
padding: 0;
list-style: none;
.navbar-dropdown-subitem {
font-size: 0.9em;
}
}
a {
display: block;
line-height: 1.7rem;
position: relative;
border-bottom: none;
font-weight: 400;
margin-bottom: 0;
padding: 0 1.5rem 0 1.25rem;
&:hover {
color: var(--c-text-accent);
}
&.router-link-active {
color: var(--c-text-accent);
&::after {
content: '';
width: 0;
height: 0;
border-left: 5px solid var(--c-text-accent);
border-top: 3px solid transparent;
border-bottom: 3px solid transparent;
position: absolute;
top: calc(50% - 2px);
left: 9px;
}
}
}
&:first-child .navbar-dropdown-subtitle {
margin-top: 0;
padding-top: 0;
border-top: 0;
}
}
}
}
@media (max-width: $MQMobile) {
.navbar-dropdown-wrapper {
&.open .navbar-dropdown-title {
margin-bottom: 0.5rem;
}
.navbar-dropdown-title {
display: none;
}
.navbar-dropdown-title-mobile {
display: block;
}
.navbar-dropdown {
@include dropdown_wrapper;
.navbar-dropdown-item {
.navbar-dropdown-subtitle {
padding-top: 0;
margin-top: 0;
border-top: 0;
padding-bottom: 0;
}
.navbar-dropdown-subtitle,
& > a {
font-size: 15px;
line-height: 2rem;
}
.navbar-dropdown-subitem {
font-size: 14px;
padding-left: 1rem;
}
}
}
}
}
@media (min-width: ($MQMobile + 1)) {
.navbar-dropdown-wrapper {
height: 1.8rem;
&.open .navbar-dropdown {
opacity: 1;
transform: none;
}
.navbar-dropdown {
opacity: 0;
transform: translateY(-0.5rem);
transition: opacity 0.3s ease, transform 0.3s ease;
height: auto !important;
max-height: calc(100vh - 2.7rem);
overflow-y: auto;
position: absolute;
top: 100%;
right: 0;
box-sizing: border-box;
background-color: var(--c-bg-navbar);
padding: 0.6rem 0;
border: 1px solid var(--c-border);
border-bottom-color: var(--c-border-dark);
text-align: left;
border-radius: 0.25rem;
white-space: nowrap;
margin: 0;
}
}
}
</style>

View File

@ -0,0 +1,72 @@
<script lang="ts" setup>
import AutoLink from '@theme-plume/AutoLink.vue'
import NavbarDropdown from '@theme-plume/NavbarDropdown.vue'
import { computed } from 'vue'
import type { NavGroup, ResolveNavbarItem } from '../../shared'
import {
useNavbarConfig,
useNavbarRepo,
useNavbarSelectLanguage,
} from '../composables'
defineProps({
isHeader: {
type: Boolean,
required: false,
default: false,
},
})
const navbarConfig = useNavbarConfig()
const navbarSelectLanguage = useNavbarSelectLanguage()
const navbarRepo = useNavbarRepo()
const navbarLinks = computed(() => [
...navbarConfig.value,
...navbarSelectLanguage.value,
...navbarRepo.value,
])
</script>
<template>
<nav v-if="navbarLinks.length" class="navbar-items">
<div v-for="item in navbarLinks" :key="item.text" class="navbar-item">
<NavbarDropdown
v-if="(item as NavGroup<ResolveNavbarItem>).children"
:item="item"
:is-header="isHeader"
/>
<AutoLink v-else :item="item" />
</div>
</nav>
</template>
<style lang="scss">
.navbar-items {
--navbar-line-height: calc(
var(--navbar-height) - 2 * var(--navbar-padding-v)
);
display: inline-block;
a {
display: inline-block;
line-height: 1.4rem;
color: inherit;
&:hover,
&.router-lint-active {
color: var(--c-text-accent);
}
}
.navbar-item {
position: relative;
display: inline-block;
margin-left: 1.5rem;
line-height: var(--navbar-line-height);
&:first-child {
margin-left: 0;
}
}
}
</style>

View File

@ -0,0 +1,68 @@
<script lang="ts" setup>
import DropdownTransition from '@theme-plume/DropdownTransition.vue'
import PostMeta from '@theme-plume/PostMeta.vue'
import { usePageData } from '@vuepress/client'
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import type { PlumeThemePageData } from '../../shared'
import { useThemeLocaleData } from '../composables'
const page = usePageData<PlumeThemePageData>()
const route = useRoute()
const themeLocale = useThemeLocaleData()
const isNote = computed(() => {
return page.value.isNote || false
})
</script>
<template>
<DropdownTransition>
<main class="page-wrapper">
<slot name="top"></slot>
<div class="page-container">
<main class="plume-theme-content">
<div class="page-content">
<h1>{{ page.title }}</h1>
<PostMeta :post="page" type="post" :border="true" />
<Content />
</div>
<div v-if="page.headers?.length > 0" class="plume-theme-page-toc">
<Toc />
</div>
</main>
</div>
<slot name="bottom"></slot>
</main>
</DropdownTransition>
</template>
<style lang="scss">
@import '../styles/_mixins';
.page-wrapper {
@include wrapper;
.page-container {
display: flex;
padding-top: 1.25rem;
padding-bottom: 1.25rem;
.plume-theme-content {
@include container_wrapper;
@include content;
display: flex;
flex: 1;
}
.page-content {
flex: 1;
width: 100%;
max-width: var(--content-width);
padding: 0 2rem 1rem;
margin: auto;
}
img {
max-width: 100%;
}
}
}
</style>

View File

@ -0,0 +1,71 @@
<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue'
import { onBeforeRouteUpdate, useRouter } from 'vue-router'
import { useThemeLocaleData } from '../composables'
const themeLocale = useThemeLocaleData()
const router = useRouter()
const footer = computed(() => {
return themeLocale.value.footer
})
const style = ref({})
function setStyle(): void {
setTimeout(() => {
if (
document.documentElement.scrollHeight <=
document.documentElement.clientHeight
) {
style.value = {
position: 'fixed',
bottom: 0,
left: 0,
}
} else {
style.value = {}
}
}, 30)
}
router.beforeEach(() => {
setStyle()
})
onMounted(() => setStyle())
onBeforeRouteUpdate(() => setStyle())
</script>
<template>
<footer v-if="footer" class="theme-plume-footer" :style="style">
<!-- eslint-disable vue/no-v-html -->
<div
v-if="footer.content"
class="theme-plume-footer-content"
v-html="footer.content"
></div>
<div
v-if="footer.copyright"
class="theme-plume-footer-copyright"
v-html="footer.copyright"
></div>
</footer>
</template>
<style lang="scss">
.theme-plume-footer {
width: 100%;
padding: 1.25rem;
margin-top: 4rem;
display: flex;
justify-content: flex-start;
align-items: flex-start;
background-color: var(--c-bg-container);
box-shadow: var(--shadow-footer);
font-size: 14px;
text-align: center;
.theme-plume-footer-content {
flex: 1;
}
.theme-plume-footer-copyright {
margin: auto;
padding: 0 1.25rem;
}
}
</style>

View File

@ -0,0 +1,172 @@
<script lang="ts" setup>
import { useOffsetPagination } from '@vueuse/core'
import { computed, ref, toRefs } from 'vue'
const emit = defineEmits(['togglePage'])
const props = defineProps({
page: {
type: Number,
required: true,
},
total: {
type: Number,
required: true,
},
})
const { page, total } = toRefs(props)
function handlePage({ currentPage }): void {
emit('togglePage', currentPage)
}
const { currentPage, pageCount, isFirstPage, isLastPage, prev, next } =
useOffsetPagination({
total,
page: page.value,
pageSize: 10,
onPageChange: handlePage,
onPageCountChange: handlePage,
})
const pageList = computed(() => {
const list: (number | '')[] = []
const count = pageCount.value
const current = currentPage.value
if (count <= 3 || current <= 2) {
new Array(Math.min(3, count)).fill(0).forEach((_, i) => list.push(i + 1))
if (count > 3) {
list.push('')
list.push(count)
}
} else if (current > count - 2) {
list.push(1)
list.push('')
new Array(3).fill(count - 2).forEach((_, i) => list.push(_ + i))
} else {
list.push(1)
current > 3 && list.push('')
;[current - 1, current, current + 1].forEach((page: number) =>
list.push(page)
)
current < count - 2 && list.push('')
list.push(count)
}
return list
})
const inputPage = ref(1)
function handleJump(): void {
if (
inputPage.value &&
inputPage.value >= 1 &&
inputPage.value <= pageCount.value
) {
emit('togglePage', inputPage.value)
}
}
</script>
<template>
<div v-if="pageCount > 1" class="pagination-wrapper">
<div class="pagination-container">
<button
type="button"
class="btn-prev"
:disabled="isFirstPage"
@click="prev"
>
prev
</button>
<template v-for="count in pageList" :key="count">
<button
v-if="count"
type="button"
:disabled="count === currentPage"
@click="currentPage = count"
>
{{ count }}
</button>
<button v-else type="button" disabled>..</button>
</template>
<button
type="button"
class="btn-next"
:disabled="isLastPage"
@click="next"
>
next
</button>
</div>
<div class="pagination-form">
<span>跳转到</span>
<input v-model="inputPage" type="number" :min="1" :max="pageCount" />
<span>/{{ pageCount }}</span>
<button type="button" class="btn-jump" @click="handleJump">确认</button>
</div>
</div>
</template>
<style lang="scss">
.pagination-wrapper {
display: flex;
justify-content: flex-start;
align-items: center;
text-align: center;
font-size: 14px;
.pagination-container {
flex: 1;
button {
border-right: solid 1px var(--c-border);
&:last-of-type {
border-right: none;
}
}
}
button {
background-color: var(--c-bg-container);
cursor: pointer;
font-size: inherit;
padding: 0.5rem 0.8rem;
border: solid 1px transparent;
color: var(--c-text);
box-shadow: var(--shadow-sm);
&:disabled {
color: var(--c-text-accent);
cursor: unset;
}
&.btn-prev,
&.btn-next,
&.btn-jump {
color: var(--c-text-accent);
&:disabled {
color: var(--c-text);
}
}
}
.pagination-form {
input {
font-size: inherit;
padding: 0.5rem;
width: 3.25rem;
border: solid 1px transparent;
color: var(--c-text);
box-shadow: var(--shadow-sm);
outline: 0;
margin-right: 0.5rem;
&:focus {
border-color: var(--c-brand);
}
}
span {
margin-right: 0.5rem;
}
}
}
</style>

View File

@ -0,0 +1,35 @@
<script lang="ts" setup>
import DropdownTransition from '@theme-plume/DropdownTransition.vue'
import type { PropType } from 'vue'
import type { PostItem } from '../../shared'
import AutoLink from './AutoLink.vue'
import { TopIcon } from './icons'
import PostMeta from './PostMeta.vue'
defineProps({
post: {
type: Object as PropType<PostItem>,
required: true,
},
index: {
type: Number,
default: 0,
},
})
</script>
<template>
<DropdownTransition :delay="index * 0.04">
<section :key="post.path" class="post-list-item">
<TopIcon v-if="post.sticky" />
<h3>
<AutoLink :item="{ text: post.title, link: post.path }" />
</h3>
<PostMeta :post="post" />
<!--eslint-disable vue/no-v-html-->
<div v-if="post.excerpt" class="post-excerpt" v-html="post.excerpt"></div>
<div v-if="post.excerpt" class="post-more">
<AutoLink :item="{ text: '阅读全文 >>', link: post.path }" />
</div>
</section>
</DropdownTransition>
</template>

View File

@ -0,0 +1,132 @@
<script lang="ts" setup>
import DropdownTransition from '@theme-plume/DropdownTransition.vue'
import PostItem from '@theme-plume/PostItem.vue'
import { usePageFrontmatter } from '@vuepress/client'
import type { PropType } from 'vue'
import { onMounted, toRefs, watch } from 'vue'
import { onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router'
import type { PlumeThemeHomeFrontmatter } from '../../shared'
import type { PostListData } from '../composables'
import { usePostList } from '../composables'
import Pagination from './Pagination.vue'
const props = defineProps({
postList: {
type: Array as PropType<PostListData | undefined>,
required: false,
default: () => undefined,
},
})
const router = useRouter()
const frontmatter = usePageFrontmatter<PlumeThemeHomeFrontmatter>()
const propsRef = toRefs(props)
const { postList, total, page, setPostListPage, resetPostIndex } = usePostList()
watch(
[propsRef.postList],
([newPostList]) => {
newPostList && resetPostIndex(newPostList as unknown as PostListData)
},
{ immediate: true }
)
const route = useRoute()
onBeforeRouteUpdate((to) => {
setPostListPage((to.query.p as unknown as number) || 1)
const { home, banner, mobileBanner } = frontmatter.value
let top = 0
if (home && (banner || mobileBanner)) {
rect =
rect || document.querySelector('.navbar-wrapper')?.getBoundingClientRect()
top = document.documentElement.clientHeight - (rect?.height || 0)
}
document.documentElement.scrollTop = top
})
setPostListPage((route.query.p as unknown as number) || 1)
let rect: any
const togglePage = (currentPage: number): void => {
router.push({
path: route.path,
query: {
...route.query,
p: currentPage,
},
})
}
</script>
<template>
<div class="post-list-wrapper">
<DropdownTransition>
<div>
<PostItem
v-for="(post, index) in postList"
:key="post.path"
:post="post"
:index="index"
></PostItem>
</div>
</DropdownTransition>
<Pagination :page="page" :total="total" @toggle-page="togglePage" />
</div>
</template>
<style lang="scss">
@import '../styles/_variables';
.post-list-wrapper {
flex: 1;
.post-list-item {
position: relative;
padding: 1.25rem 1.5rem;
background-color: var(--c-bg-container);
border-radius: var(--p-around);
margin-bottom: 1.25rem;
box-shadow: var(--shadow);
.top-icon {
position: absolute;
top: 0;
left: 0;
width: 2.65rem;
height: 2.65rem;
color: var(--c-brand);
}
}
h3 {
width: 100%;
margin-top: 0;
overflow: hidden;
text-overflow: ellipsis;
a {
color: var(--c-text);
}
}
.post-excerpt {
padding-top: 1.25rem;
:first-child {
margin-top: 0;
}
:last-child {
margin-bottom: 1rem;
}
}
.post-more {
text-align: right;
}
}
@media (max-width: $MQMobile) {
.post-list-wrapper {
.post-list-item {
border-radius: 0;
}
}
}
</style>

View File

@ -0,0 +1,116 @@
<script lang="ts" setup>
import type { PropType } from 'vue'
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import type { PostItem } from '../../shared'
import { useThemeLocaleData } from '../composables'
import { getColor, normalizePath } from '../utils'
import { ClockIcon, FolderIcon, TagIcon, UserIcon } from './icons'
const props = defineProps({
post: {
type: Object as PropType<PostItem>,
required: true,
},
border: {
type: Boolean,
required: false,
default: false,
},
})
const route = useRoute()
const router = useRouter()
const themeLocale = useThemeLocaleData()
const tags = computed(() => {
return (props.post.tags || []).filter((_, i) => i < 4)
})
const handleTag = (tag: string): void => {
const tagConfig = themeLocale.value.tag
if (!tagConfig) return
const link = tagConfig.link.replace(/^\/|\/$/g, '')
router.replace({
path: `/${link}/`,
query: { tag: normalizePath(tag) },
})
}
</script>
<template>
<div class="post-meta" :class="{ border: post.excerpt || border }">
<div v-if="post.author" class="post-meta-author">
<UserIcon />
<span>{{ post.author }}</span>
</div>
<div v-if="post.category.length > 0" class="post-meta-category">
<FolderIcon />
<template v-for="(cate, i) in post.category" :key="cate.type">
<span>{{ cate.name }}</span>
<span v-if="i < post.category.length - 1"> / </span>
</template>
</div>
<div v-if="tags.length > 0">
<TagIcon />
<template v-for="tag in tags" :key="tag">
<span
class="post-meta-tag"
:style="{ 'background-color': getColor() }"
@click="handleTag(tag)"
>
{{ tag }}
</span>
</template>
</div>
<div class="post-meta-create-time">
<ClockIcon />
<span>{{ post.createTime }}</span>
</div>
</div>
</template>
<style lang="scss">
.post-meta {
color: var(--c-text-light);
overflow: hidden;
font-size: 14px;
&.border {
border-bottom: solid 1px var(--c-border);
}
& > div {
float: left;
display: flex;
justify-content: flex-start;
align-items: center;
margin-right: 1.25rem;
height: 2rem;
line-height: 1.5rem;
padding-bottom: 0.5rem;
}
.icon {
width: 0.875rem;
height: 0.875rem;
margin-right: 0.2rem;
color: var(--c-text-lighter);
}
.post-meta-tag {
display: inline-block;
height: 1.25rem;
line-height: 1.25rem;
padding: 0 0.4rem;
color: #fff;
border-radius: 0.75rem;
margin: 0 0.15rem;
cursor: pointer;
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
}
}
</style>

View File

@ -0,0 +1,99 @@
<script lang="ts" setup>
import BlogInfo from '@theme-plume/BlogInfo.vue'
import DropdownTransition from '@theme-plume/DropdownTransition.vue'
import PostList from '@theme-plume/PostList.vue'
import { computed, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import type { PostListRef } from '../composables'
import { usePostAllIndex, useTagList } from '../composables'
import { normalizePath } from '../utils'
const tagList = useTagList()
const route = useRoute()
const router = useRouter()
const postList: PostListRef = ref([])
const postAllList = usePostAllIndex()
const currentTag = computed(() => {
return route.query.tag || ''
})
watch(
[currentTag, route],
([nowTag]) => {
if (nowTag) {
postList.value = postAllList.value.filter((post) => {
return post.tags.some((tag) => normalizePath(tag) === nowTag)
})
} else {
postList.value = []
}
},
{ immediate: true }
)
const handleTag = (tag: string): void => {
router.replace({
path: route.path,
query: { tag: normalizePath(tag) },
})
}
</script>
<template>
<main class="tag-wrapper">
<div class="tag-container">
<div class="tag-content">
<DropdownTransition>
<section class="tag-list">
<span
v-for="{ tag, color } in tagList"
:key="tag"
class="tag"
:style="{ 'background-color': color }"
@click="handleTag(tag)"
>{{ tag }}</span
>
</section>
</DropdownTransition>
<PostList :post-list="postList"></PostList>
</div>
<BlogInfo></BlogInfo>
</div>
</main>
</template>
<style lang="scss">
@import '../styles/_mixins';
.tag-wrapper {
@include wrapper;
.tag-container {
@include container_wrapper;
display: flex;
align-items: flex-start;
padding: 1.25rem 0;
}
.tag-content {
flex: 1;
}
.tag-list {
padding: 0 1.25rem 0.75rem;
margin: 0 -0.25rem;
.tag {
height: 1.75rem;
font-size: 16px;
line-height: 1.75rem;
display: inline-block;
padding: 0 0.75rem;
border-radius: 0.85rem;
margin: 0 0.25rem 0.5rem;
background-color: var(--c-bg-lighter);
cursor: pointer;
color: #fff;
box-shadow: var(--shadow-sm);
}
}
}
</style>

View File

@ -0,0 +1,39 @@
import { defineComponent, h } from 'vue'
import type { VNode } from 'vue'
export const IconBase = defineComponent({
name: 'IconBase',
props: {
name: {
type: String,
required: false,
default: '',
},
color: {
type: String,
required: false,
default: 'currentColor',
},
viewBox: {
type: String,
required: false,
default: '0 0 20 20',
},
},
setup:
(props, { slots }) =>
(): VNode =>
h(
'svg',
{
xmlns: 'http://www.w3.org/2000/svg',
class: ['icon', `${props.name}-icon`],
viewBox: props.viewBox,
ariaLabelledby: props.name,
},
[
h('title', { id: props.name, lang: 'en' }, `${props.name}`),
h('g', { fill: props.color }, slots.default?.()),
]
),
})

View File

@ -0,0 +1,58 @@
import { h } from 'vue'
import type { FunctionalComponent } from 'vue'
import { IconBase } from './IconBase'
export const UserIcon: FunctionalComponent = () =>
h(IconBase, { name: 'user' }, () =>
h('path', {
'fill-rule': 'evenodd',
'clip-rule': 'evenodd',
'd': 'M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z',
})
)
UserIcon.displayName = 'UserIcon'
export const FolderIcon: FunctionalComponent = () =>
h(IconBase, { name: 'folder' }, () =>
h('path', {
d: 'M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z',
})
)
FolderIcon.displayName = 'FolderIcon'
export const ClockIcon: FunctionalComponent = () =>
h(IconBase, { name: 'clock' }, () =>
h('path', {
'fill-rule': 'evenodd',
'clip-rule': 'evenodd',
'd': 'M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z',
})
)
ClockIcon.displayName = 'ClockIcon'
export const TagIcon: FunctionalComponent = () =>
h(IconBase, { name: 'tag' }, () =>
h('path', {
'fill-rule': 'evenodd',
'clip-rule': 'evenodd',
'd': 'M17.707 9.293a1 1 0 010 1.414l-7 7a1 1 0 01-1.414 0l-7-7A.997.997 0 012 10V5a3 3 0 013-3h5c.256 0 .512.098.707.293l7 7zM5 6a1 1 0 100-2 1 1 0 000 2z',
})
)
TagIcon.displayName = 'TagIcon'
export const TopIcon: FunctionalComponent = () =>
h(IconBase, { name: 'top', viewBox: '0 0 1024 1024' }, () => [
h('path', {
d: 'M80.96 449.194667l37.696-37.717334 19.626667 19.605334-37.717334 37.717333zM197.205333 541.44l116.16-116.138667 13.568 13.568-116.16 116.16zM220.565333 565.162667l116.16-116.16 13.568 13.589333-116.16 116.138667zM173.845333 517.738667l116.16-116.16 13.568 13.589333-116.16 116.138667zM245.354667 587.477333l116.202666-116.096 13.568 13.589334-116.202666 116.096z',
// fill: '#FA8D14',
}),
h('path', {
d: 'M339.2 0L0 345.6V1024L1024 0H339.2z m-115.2 283.733333l46.933333 46.933334-14.933333 12.8-4.266667-4.266667-140.8 140.8 4.266667 4.266667-14.933333 14.933333-46.933334-46.933333 170.666667-168.533334z m2.133333 375.466667l-12.8-12.8 29.866667-29.866667L149.333333 520.533333l64-64-12.8-12.8L108.8 533.333333l-10.666667-10.666666 89.6-89.6-10.666666-10.666667 14.933333-14.933333 10.666667 10.666666 91.733333-91.733333 10.666667 10.666667-91.733334 91.733333 12.8 12.8 68.266667-68.266667 96 96 27.733333-27.733333 12.8 12.8-204.8 204.8z m232.533334-236.8l-17.066667 17.066667c-6.4-6.4-14.933333-10.666667-21.333333-14.933334 8.533333-4.266667 14.933333-10.666667 21.333333-17.066666 6.4-6.4 6.4-12.8 0-19.2l-136.533333-136.533334-34.133334 34.133334-14.933333-17.066667L332.8 192l14.933333 14.933333-25.6 27.733334 138.666667 138.666666c14.933333 14.933333 14.933333 32-2.133333 49.066667z m-81.066667-200.533333l38.4-38.4-21.333333-34.133334-46.933334 46.933334-14.933333-14.933334 123.733333-123.733333 12.8 17.066667-59.733333 59.733333 21.333333 34.133333 57.6-57.6 98.133334 98.133334-14.933334 14.933333-83.2-83.2-78.933333 78.933333 85.333333 85.333334-14.933333 14.933333-102.4-98.133333z m138.666667 162.133333c-6.4-2.133333-14.933333-4.266667-25.6-4.266667 19.2-34.133333 25.6-61.866667 23.466666-85.333333-2.133333-21.333333-17.066667-44.8-42.666666-70.4L448 200.533333l14.933333-14.933333 23.466667 23.466667c17.066667 17.066667 29.866667 34.133333 38.4 49.066666 38.4-8.533333 74.666667-14.933333 106.666667-19.2l2.133333 25.6c-34.133333 2.133333-68.266667 8.533333-100.266667 14.933334 2.133333 4.266667 2.133333 8.533333 2.133334 12.8 6.4 23.466667 0 55.466667-19.2 91.733333z',
// fill: '#FA8D14',
}),
h('path', {
d: 'M183.765333 346.965333l37.696-37.717333 19.626667 19.584-37.696 37.738667zM132.288 398.037333l37.76-37.674666 19.584 19.626666-37.738667 37.674667z',
// fill: '#FA8D14',
}),
])
TopIcon.displayName = 'TopIcon'

View File

@ -0,0 +1,3 @@
export * from './IconBase'
export * from './icon'
export * from './socialIcon'

View File

@ -0,0 +1,126 @@
import { h } from 'vue'
import type { FunctionalComponent } from 'vue'
import { IconBase } from './IconBase'
export const GithubIcon: FunctionalComponent = () =>
h(IconBase, { name: 'github', viewBox: '0 0 1024 1024' }, () =>
h('path', {
d: 'M512 0C229.283787 0 0.142041 234.942803 0.142041 524.867683c0 231.829001 146.647305 428.553077 350.068189 497.952484 25.592898 4.819996 34.976961-11.38884 34.976961-25.294314 0-12.45521-0.469203-45.470049-0.725133-89.276559-142.381822 31.735193-172.453477-70.380469-172.453477-70.380469-23.246882-60.569859-56.816233-76.693384-56.816234-76.693385-46.493765-32.58829 3.540351-31.948468 3.540351-31.948467 51.356415 3.71097 78.356923 54.086324 78.356923 54.086324 45.683323 80.19108 119.817417 57.072162 148.993321 43.593236 4.649376-33.91059 17.915029-57.029508 32.50298-70.167195-113.675122-13.222997-233.151301-58.223843-233.1513-259.341366 0-57.285437 19.919806-104.163095 52.678715-140.846248-5.246544-13.265652-22.820334-66.626844 4.990615-138.884127 0 0 42.996069-14.076094 140.760939 53.787741 40.863327-11.644769 84.627183-17.445825 128.177764-17.6591 43.465272 0.213274 87.271782 6.014331 128.135109 17.6591 97.679561-67.906489 140.59032-53.787741 140.59032-53.787741 27.938914 72.257282 10.407779 125.618474 5.118579 138.884127 32.844219 36.683154 52.593405 83.560812 52.593405 140.846248 0 201.586726-119.646798 245.990404-233.663158 258.957473 18.341577 16.208835 34.721032 48.199958 34.721032 97.210357 0 70.167195-0.639822 126.7275-0.639823 143.960051 0 14.033439 9.213443 30.370239 35.190235 25.209005 203.250265-69.527373 349.769606-266.123484 349.769605-497.867175C1023.857959 234.942803 794.673558 0 512 0',
fill: '#3E75C3',
})
)
GithubIcon.displayName = 'GithubIcon'
export const EmailIcon: FunctionalComponent = () =>
h(IconBase, { name: 'email', viewBox: '0 0 1024 1024' }, () => [
[
h('path', {
d: 'M848.76288 333.62432H164.99712C99.32288 333.62432 46.08 386.87232 46.08 452.54144v297.28768c0 65.67424 53.24288 118.92224 118.91712 118.92224h683.77088c65.66912 0 118.91712-53.24288 118.91712-118.92224V452.54144c-0.00512-65.66912-53.248-118.91712-118.92224-118.91712z',
fill: '#96383D',
}),
h('path', {
d: 'M639.8208 51.2h-474.8288a44.58496 44.58496 0 0 0-44.59008 44.59008v609.44896a44.57984 44.57984 0 0 0 44.59008 44.59008h683.776a44.58496 44.58496 0 0 0 44.59008-44.59008V304.73728L639.8208 51.2z',
fill: '#EBE2CE',
}),
h('path', {
d: 'M551.4752 229.57568H209.59232v44.59008h341.88288v-44.59008zM209.59232 794.42432h594.58048v-44.5952H209.59232v44.5952z m0-89.18528h594.58048v-44.5952H209.59232v44.5952z m0-178.37568h594.58048v-44.5952H209.59232v44.5952z m0 89.18528h594.58048v-44.59008H209.59232v44.59008z m0-222.96576v44.59008h594.58048v-44.59008H209.59232z',
fill: '#C9C1B1',
}),
h('path', {
d: 'M941.83936 393.31328L75.60704 955.02848c12.89216 10.93632 29.29664 17.77152 47.44192 17.77152H893.5936c40.91904 0 74.09152-33.4592 74.09152-74.74688V449.60768c-0.00512-22.58432-10.14784-42.58816-25.84576-56.2944z',
fill: '#D54D54',
}),
h('path', {
d: 'M71.99232 396.5696C56.25344 410.18368 46.08 430.08512 46.08 452.54144v445.93152C46.08 939.53024 79.34976 972.8 120.40192 972.8h772.95104c18.20672 0 34.65216-6.79424 47.56992-17.664L71.99232 396.5696z',
fill: '#EA5455',
}),
h('path', {
d: 'M655.52384 66.90816v236.8l237.82912 74.89024V304.73728z',
fill: '',
}),
h('path', {
d: 'M640.66048 52.0448v207.2576a44.58496 44.58496 0 0 0 44.5952 44.5952h207.2576l-251.8528-251.8528z',
fill: '#FFFBF2',
}),
],
])
EmailIcon.displayName = 'EmailIcon'
export const ZhiHuIcon: FunctionalComponent = () =>
h(IconBase, { name: 'zhiHu', viewBox: '0 0 1024 1024' }, () => [
h('path', {
d: 'M512 73.28A438.72 438.72 0 1 0 950.72 512 438.72 438.72 0 0 0 512 73.28z m-98.56 458.88l-16.8 66.88 23.68-20.8s53.92 61.28 64 76.48 1.44 68.96 1.44 68.96l-92.48-113.12s-29.12 101.12-68.48 124.16a97.6 97.6 0 0 1-80 6.56 342.08 342.08 0 0 0 85.44-89.76 382.88 382.88 0 0 0 39.52-119.36h-115.04s8.8-40.48 24.16-41.6 90.88 0 90.88 0l-1.76-124.8-43.2 2.24a96 96 0 0 1-32 48c-24.16 17.44-38.4 10.88-38.4 10.88s42.72-118.24 55.84-141.28 50.4-25.12 50.4-25.12l-23.04 66.72h147.84c17.6 0 18.56 40.64 18.56 40.64h-90.56v122.56s61.28-2.24 81.12 0 19.68 41.6 19.68 41.6z m329.44 160h-91.52l-65.12 46.24-13.6-46.24h-36.96v-368h208z',
fill: '#49C0FB',
}),
h('path', {
d: 'M602.88 691.68l54.88-41.44h43.04V364.64h-121.12v285.6h11.2l12 41.44z',
fill: '#49C0FB',
}),
])
ZhiHuIcon.displayName = 'ZhiHuIcon'
export const WeiBoIcon: FunctionalComponent = () =>
h(IconBase, { name: 'weiBo', viewBox: '0 0 1024 1024' }, () => [
h('path', {
d: 'M448.698182 482.210909c-96.814545 4.654545-175.010909 56.785455-175.010909 121.949091s78.196364 114.501818 175.010909 109.847273S623.709091 647.912727 623.709091 582.749091c-0.930909-64.232727-79.127273-105.192727-175.010909-100.538182z m65.163636 164.770909c-29.789091 39.098182-88.436364 57.716364-145.221818 26.065455-26.996364-14.894545-26.065455-43.752727-26.065455-43.752728s-11.170909-92.16 85.643637-103.330909c97.745455-12.101818 115.432727 81.92 85.643636 121.018182z',
fill: '#EA5D5C',
}),
h('path', {
d: 'M448.698182 584.610909c-6.516364 4.654545-7.447273 13.032727-3.723637 18.618182 2.792727 5.585455 11.170909 6.516364 16.756364 1.861818 5.585455-4.654545 8.378182-13.032727 4.654546-18.618182-2.792727-5.585455-10.24-6.516364-17.687273-1.861818zM403.083636 597.643636c-18.618182 1.861818-30.72 17.687273-30.72 33.512728 0 14.894545 14.894545 26.065455 32.581819 24.203636 17.687273-1.861818 32.581818-15.825455 32.581818-31.650909s-13.963636-27.927273-34.443637-26.065455z',
fill: '#EA5D5C',
}),
h('path', {
d: 'M512 0C229.003636 0 0 229.003636 0 512s229.003636 512 512 512 512-229.003636 512-512S794.996364 0 512 0z m197.352727 626.501818C669.323636 712.145455 538.065455 754.036364 441.250909 746.589091c-92.16-7.447273-211.316364-38.167273-223.418182-151.738182 0 0-6.516364-51.2 42.821818-117.294545 0 0 70.749091-99.607273 152.669091-128.465455 82.850909-27.927273 92.16 19.549091 92.16 48.407273-4.654545 24.203636-12.101818 38.167273 18.618182 28.858182 0 0 80.989091-38.167273 114.501818-4.654546 26.996364 26.996364 4.654545 65.163636 4.654546 65.163637s-11.170909 12.101818 12.101818 16.756363c21.410909 3.723636 94.021818 37.236364 53.992727 122.88z m-80.058182-236.450909c-8.378182 0-15.825455-7.447273-15.825454-15.825454 0-9.309091 7.447273-15.825455 15.825454-15.825455 0 0 99.607273-18.618182 87.505455 89.367273v1.861818c-0.930909 7.447273-7.447273 13.963636-15.825455 13.963636-9.309091 0-15.825455-7.447273-15.825454-15.825454 0-1.861818 15.825455-73.541818-55.854546-57.716364zM797.789091 493.381818c-2.792727 18.618182-12.101818 11.170909-22.341818 11.170909-13.032727 0-23.272727-16.756364-23.272728-29.789091 0-11.170909 4.654545-22.341818 4.654546-22.341818 0.930909-4.654545 12.101818-34.443636-7.447273-78.196363-35.374545-60.509091-106.123636-60.509091-114.501818-57.716364-8.378182 3.723636-21.410909 5.585455-21.410909 5.585454-13.032727 0-23.272727-10.24-23.272727-23.272727 0-11.170909 7.447273-19.549091 16.756363-22.341818 0 0 0 0.930909 0.930909 0.930909s1.861818 0.930909 1.861819 0.930909c10.24-1.861818 45.614545-4.654545 79.127272 3.723637 62.370909 14.894545 146.152727 83.781818 108.916364 211.316363z',
fill: '#EA5D5C',
}),
])
WeiBoIcon.displayName = 'WeiBoIcon'
// <svg t="1648887372594" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3602" width="200" height="200"><path d="" fill="#68A5E1" p-id="3603"></path></svg>
export const QQIcon: FunctionalComponent = () =>
h(IconBase, { name: 'qq', viewBox: '0 0 1024 1024' }, () =>
h('path', {
d: 'M512.268258 64.433103c-247.183323 0-447.569968 200.380501-447.569968 447.563825 0 247.189467 200.385621 447.570992 447.569968 447.570992s447.569968-200.380501 447.569968-447.569968c0-247.184347-200.386645-447.564849-447.569968-447.564849z m252.85872 584.692787c-18.997168 16.287968-43.668709-53.628042-47.2134-42.875198-8.642616 26.161294-12.695154 43.646184-38.148944 72.127602-1.35972 1.521494 29.43056 12.647032 38.148944 36.396051 8.346713 22.756875 24.596797 58.811973-81.725503 70.125906-62.389428 6.635801-107.471099-33.244533-111.964932-32.85648-8.325212 0.734126-4.618747 0-13.568528 0-7.321804 0-7.807126 0.534468-14.69685 0-1.899307-0.140272-22.632985 32.85648-115.364231 32.85648-71.878798 0-90.48177-45.243445-76.032701-70.125906 14.464428-24.877342 38.579999-32.122354 35.176604-36.06636-16.73643-19.39546-28.287904-40.1404-35.176604-58.882621-1.705793-4.666869-3.135137-9.209848-4.262434-13.574672-2.611931-10.008479-22.627866 58.76385-44.111028 42.875198-21.483162-15.883533-19.567472-56.309597-5.659014-95.003248 14.033372-39.006959 49.37687-76.562049 49.771065-84.854496 1.412962-30.849665-3.044011-35.975235 0-44.078263 6.780169-18.149391 15.034732-11.190043 15.034733-20.609788 0-118.64476 88.172909-214.829571 196.933079-214.829571 108.755051 0 196.928984 96.184811 196.928984 214.829571 0 4.554242 11.815637 0 17.474651 20.609788 1.165181 4.256291 1.968931 20.684531 0.58771 44.078263-0.658358 11.238165 29.954789 24.914202 45.777913 84.854496 15.845649 59.945414 0 88.215912-7.909514 95.003248z',
fill: '#68A5E1',
})
)
QQIcon.displayName = 'QQIcon'
export const TwitterIcon: FunctionalComponent = () =>
h(IconBase, { name: 'twitter', viewBox: '0 0 1024 1024' }, () => [
h('path', {
d: 'M512.274401 959.556658c247.17718 0 447.556658-200.366167 447.556658-447.556658 0-247.16387-200.379477-447.556658-447.556658-447.556658-247.188443 0-447.569968 200.392788-447.569968 447.556658 0 247.190491 200.382549 447.556658 447.569968 447.556658',
fill: '#78CBEF',
}),
h('path', {
d: 'M736.810405 394.754891c-16.48353 7.310541-34.227463 12.256931-52.82122 14.478763 19.004336-11.383557 33.588558-29.396772 40.435279-50.868671-17.780793 10.536804-37.440415 18.183179-58.42392 22.294079-16.741549-17.872942-40.666677-29.038412-67.134113-29.038412-50.766282 0-91.948998 41.192954-91.948998 91.972548 0 7.220439 0.784296 14.222791 2.366199 20.943574-76.439183-3.841618-144.191723-40.421969-189.587726-96.109044-7.915657 13.630985-12.452493 29.422369-12.452493 46.282688 0 31.877646 16.241893 60.042683 40.924696 76.552835-15.072616-0.460748-29.26981-4.637177-41.682371-11.485946v1.131393c0 44.585086 31.698466 81.757243 73.804725 90.185867-7.723167 2.160398-15.841554 3.239573-24.246628 3.239574a91.24866 91.24866 0 0 1-17.294447-1.63105c11.691747 36.527109 45.654023 63.139936 85.90705 63.845394-31.477307 24.682804-71.144672 39.382725-114.227718 39.382725-7.42624 0-14.762379-0.410578-21.946982-1.270642 40.706609 26.070168 89.057546 41.308653 140.992081 41.308653 169.209337 0 261.697922-140.132017 261.697922-261.695874 0-3.997248-0.078839-7.979138-0.244709-11.899595a186.466924 186.466924 0 0 0 45.883373-47.618859',
fill: '#FFFFFF',
}),
])
TwitterIcon.displayName = 'TwitterIcon'
export const FacebookIcon: FunctionalComponent = () =>
h(IconBase, { name: 'facebook', viewBox: '0 0 1024 1024' }, () => [
h('path', {
d: 'M512.262115 959.556658c247.175132 0 447.569968-200.366167 447.569968-447.556658 0-247.16387-200.394836-447.556658-447.569968-447.556658-247.17718 0-447.556658 200.392788-447.556658 447.556658-0.001024 247.190491 200.378454 447.556658 447.556658 447.556658',
fill: '#537BBC',
}),
h('path', {
d: 'M404.292383 436.216104h46.269378v-44.969044c0-19.828563 0.499656-50.408946 14.904699-69.347753 15.172957-20.05689 36.000832-33.690947 71.826579-33.690946 58.371702 0 82.952117 8.326235 82.952118 8.326235l-11.564785 68.550147s-19.285904-5.576079-37.275569-5.57608c-17.99888 0-34.111763 6.449454-34.111764 24.438095v52.269346h73.791416l-5.152191 66.958004h-68.639225v232.604221h-86.731278V503.174108h-46.269378v-66.958004z',
fill: '#FFFFFF',
}),
])
FacebookIcon.displayName = 'FacebookIcon'
export const LinkedinIcon: FunctionalComponent = () =>
h(IconBase, { name: 'linkedin', viewBox: '0 0 1024 1024' }, () => [
h('path', {
d: 'M512.267234 959.569968c247.223255 0 447.572016-200.400979 447.572016-447.582255 0-247.171037-200.347737-447.558705-447.572016-447.558705-247.194586 0-447.568944 200.387669-447.568944 447.558705 0 247.1823 200.373334 447.582255 447.568944 447.582255',
fill: '#1284C7',
}),
h('path', {
d: 'M387.013295 699.188763h-87.249365V419.999808h87.249365v279.188955z m-45.860848-314.114707h-0.628666c-31.57048 0-52.042043-21.341866-52.042043-48.378582 0-27.573232 21.086918-48.478922 53.286064-48.478922 32.175596 0 51.975491 20.852449 52.607228 48.402131 0.001024 27.046955-20.430608 48.455373-53.222583 48.455373z m394.899259 314.114707H637.125954V554.711376c0-37.815157-15.457597-63.618091-49.496664-63.61809-26.03126 0-40.488521 17.410146-47.233878 34.204937-2.518758 6.013279-2.133777 14.405043-2.133777 22.820356v151.06916h-98.001184s1.273713-255.921161 0-279.188955h98.001184v43.815125c5.794167-19.157918 37.097413-46.5018 87.093733-46.5018 61.986018 0 110.696338 40.168045 110.696338 126.630041v155.246613z',
fill: '#FFFFFF',
}),
])
LinkedinIcon.displayName = 'LinkedinIcon'

View File

@ -0,0 +1,89 @@
import { ref } from 'vue'
import type { Ref } from 'vue'
import type { PostItem } from '../../shared'
import { usePostIndex } from './postIndex'
export interface CategoryItem {
label: string
type: string | number
children: CategoryList
postList: PostItem[]
}
export type CategoryList = CategoryItem[]
export type CategoryListRef = Ref<CategoryList>
export const useCategoryList = (): CategoryListRef => {
let categoryListRaw: CategoryList = []
usePostIndex().value.forEach((post) => {
if (post.category.length === 0) return
const category = post.category.map((cate, index) => {
if (index > 0) {
return {
type: post.category
.slice(0, index + 1)
.map((c) => c.type)
.join('-'),
name: cate.name,
}
} else {
return cate
}
})
let index = 1
let cate = category[0]
let first = categoryListRaw.find((c) => c.type === cate.type)
if (!first) {
first = {
label: cate.name,
type: cate.type,
children: [],
postList: [],
}
categoryListRaw.push(first)
}
if (category.length === 1) {
first.postList.push(post)
}
let children = first.children
while ((cate = category[index])) {
let current = children.find((c) => c.type === cate.type)
if (!current) {
current = {
label: cate.name,
type: cate.type,
children: [],
postList: [],
}
children.push(current)
}
children = current.children
if (index === category.length - 1) {
current.postList.push(post)
}
index++
}
})
categoryListRaw = categorySort(categoryListRaw)
sortChildren(categoryListRaw, 1)
function sortChildren(list: CategoryList, deep: number): void {
list.forEach((category) => {
if (category.children.length > 0) {
category.children = categorySort(category.children, deep)
sortChildren(category.children, deep + 1)
}
})
}
return ref(categoryListRaw)
}
function categorySort(children: CategoryList, deep = 0): CategoryList {
return children.sort((left, right) => {
const leftType = Number((left.type + '').split('-')[deep])
const rightType = Number((right.type + '').split('-')[deep])
return leftType > rightType ? 1 : -1
})
}

View File

@ -1,25 +1,22 @@
import { usePreferredDark, useStorage } from '@vueuse/core'
import { computed, inject, onMounted, onUnmounted, provide, watch } from 'vue'
import type { InjectionKey, WritableComputedRef } from 'vue'
import { useThemeLocaleData } from '.'
import { useThemeLocaleData } from './themeData'
export type DarkModeRef = WritableComputedRef<boolean>
export const darkModeSymbol: InjectionKey<DarkModeRef> = Symbol('')
/**
* Inject dark mode global computed
*/
export const darkModeSymbol: InjectionKey<DarkModeRef> = Symbol(
__VUEPRESS_DEV__ ? 'darkMode' : ''
)
export const useDarkMode = (): DarkModeRef => {
const isDarkMode = inject(darkModeSymbol)
if (!isDarkMode) {
throw new Error('useDarkMode() is called without provider.')
throw new Error('useDarkMode() is called without provider')
}
return isDarkMode
}
/**
* Create dark mode ref and provide as global computed in setup
*/
export const setupDarkMode = (): void => {
const themeLocale = useThemeLocaleData()
const isDarkPreferred = usePreferredDark()
@ -27,15 +24,12 @@ export const setupDarkMode = (): void => {
const isDarkMode = computed<boolean>({
get() {
// disable dark mode
if (!themeLocale.value.darkMode) {
return false
}
// auto detected from prefers-color-scheme
if (darkStorage.value === 'auto') {
return isDarkPreferred.value
}
// storage value
return darkStorage.value === 'dark'
},
set(val) {
@ -47,20 +41,16 @@ export const setupDarkMode = (): void => {
},
})
provide(darkModeSymbol, isDarkMode)
updateHtmlDarkClass(isDarkMode)
}
export const updateHtmlDarkClass = (isDarkMode: DarkModeRef): void => {
const update = (value = isDarkMode.value): void => {
// set `class="dark"` on `<html>` element
const htmlEl = window?.document.querySelector('html')
htmlEl?.classList.toggle('dark', value)
}
onMounted(() => {
watch(isDarkMode, update, { immediate: true })
})
onUnmounted(() => update())
}

View File

@ -0,0 +1,12 @@
export * from './themeData'
export * from './darkMode'
export * from './navbar'
export * from './navLink'
export * from './resolveRouteWithRedirect'
export * from './postIndex'
export * from './postList'
export * from './scrollPromise'
export * from './tag'
export * from './category'

View File

@ -1,5 +1,5 @@
import type { NavLink } from '../../shared'
import { useResolveRouteWithRedirect } from './useResolveRouteWithRedirect'
import { useResolveRouteWithRedirect } from './resolveRouteWithRedirect'
declare module 'vue-router' {
interface RouteMeta {
@ -7,13 +7,6 @@ declare module 'vue-router' {
}
}
/**
* Resolve NavLink props from string
*
* @example
* - Input: '/README.md'
* - Output: { text: 'Home', link: '/' }
*/
export const useNavLink = (item: string): NavLink => {
const resolved = useResolveRouteWithRedirect(item)
return {

View File

@ -0,0 +1,115 @@
import { useRouteLocale, useSiteLocaleData } from '@vuepress/client'
import { isLinkHttp, isString } from '@vuepress/shared'
import type { ComputedRef } from 'vue'
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import type { NavbarGroup, NavbarItem, ResolveNavbarItem } from '../../shared'
import { resolveRepoType } from '../utils'
import { useNavLink } from './navLink'
import { useThemeLocaleData } from './themeData'
export const useNavbarSelectLanguage = (): ComputedRef<ResolveNavbarItem[]> => {
const router = useRouter()
const routeLocale = useRouteLocale()
const siteLocale = useSiteLocaleData()
const themeLocale = useThemeLocaleData()
return computed<ResolveNavbarItem[]>(() => {
const localePaths = Object.keys(siteLocale.value.locales)
if (localePaths.length < 2) {
return []
}
const currentPath = router.currentRoute.value.path
const currentFullPath = router.currentRoute.value.fullPath
const languageDropdown: ResolveNavbarItem = {
text: themeLocale.value.selectLanguageText ?? 'unknown language',
ariaLabel:
themeLocale.value.selectLanguageAriaLabel ?? 'unknown language',
children: localePaths.map((targetLocalPath) => {
const targetSiteLocale =
siteLocale.value.locales?.[targetLocalPath] ?? {}
const targetThemeLocale =
themeLocale.value.locales?.[targetLocalPath] ?? {}
const targetLang = `${targetSiteLocale.lang}`
const text = targetThemeLocale.selectLanguageName ?? targetLang
let link: string
if (targetLang === siteLocale.value.lang) {
link = currentFullPath
} else {
const targetLocalePage = currentPath.replace(
routeLocale.value,
targetLocalPath
)
if (
router.getRoutes().some((item) => item.path === targetLocalPath)
) {
link = targetLocalePage
} else {
link = targetThemeLocale.home ?? targetLocalPath
}
}
return { text, link }
}),
}
return [languageDropdown]
})
}
export const useNavbarRepo = (): ComputedRef<ResolveNavbarItem[]> => {
const themeLocale = useThemeLocaleData()
const repo = computed(() => themeLocale.value.repo)
const repoType = computed(() => {
return repo.value ? resolveRepoType(repo.value) : null
})
const repoLink = computed(() => {
if (repo.value && !isLinkHttp(repo.value)) {
return `https://github.com/${repo.value}`
}
return repo.value
})
const repoLabel = computed(() => {
if (!repoLink.value) return null
if (themeLocale.value.repoLabel) return themeLocale.value.repoLabel
if (repoType.value === null) return 'Source'
return repoType.value
})
return computed(() => {
if (!repoLink.value || !repoLabel.value) {
return []
}
return [
{
text: repoLabel.value,
link: repoLink.value,
},
]
})
}
const resolveNavbarItem = (
item: NavbarItem | NavbarGroup | string
): ResolveNavbarItem => {
if (isString(item)) {
return useNavLink(item)
}
if ((item as NavbarGroup).children) {
return {
...item,
children: (item as NavbarGroup).children.map(resolveNavbarItem),
}
}
return item as ResolveNavbarItem
}
export const useNavbarConfig = (): ComputedRef<ResolveNavbarItem[]> => {
const themeLocale = useThemeLocaleData()
return computed(() => (themeLocale.value.navbar || []).map(resolveNavbarItem))
}

View File

@ -0,0 +1,24 @@
import { postIndex as postIndexRaw } from '@internal/postIndex.js'
import { ref } from 'vue'
import type { Ref } from 'vue'
import type { PostIndex } from '../../shared'
export type PostIndexRef = Ref<PostIndex>
export const postIndex: PostIndexRef = ref(postIndexRaw)
export const usePostAllIndex = (): PostIndexRef => postIndex
// 在首页文章列表的,默认排除掉 note中的文章除非显示声明 article
export const usePostIndex = (): PostIndexRef => {
const postList = postIndex.value.filter((post) => {
return !post.isNote && post.article !== false
})
return ref(postList)
}
if (import.meta.hot) {
__VUE_HMR_RUNTIME__.updatePostIndex = (data: PostIndex) => {
postIndex.value = data
}
}

View File

@ -0,0 +1,45 @@
import type { Ref } from 'vue'
import { ref } from 'vue'
import type { PostItem } from '../../shared'
import { usePostIndex } from './postIndex'
export type PostListData = PostItem[]
export type PostListRef = Ref<PostListData>
interface UsePostList {
postList: PostListRef
total: Ref<number>
page: Ref<number>
setPostListPage: (page: number) => void
resetPostIndex: (postIndex: PostListData) => void
}
export const usePostList = (): UsePostList => {
const postIndex = ref(usePostIndex().value)
const pageNum = 10
const total = ref(postIndex.value.length)
let totalPage = Math.ceil(postIndex.value.length / pageNum)
const postList = ref<PostListData>([])
const page = ref(1)
const setPostListPage = (_page = 1): void => {
_page = _page - 1
if (_page < 0) _page = 0
if (_page > totalPage) _page = totalPage - 1
const start = _page * pageNum
const end = start + pageNum
postList.value = postIndex.value.filter((_: PostItem, index: number) => {
return start <= index && index < end
})
page.value = _page + 1
}
const resetPostIndex = (_postIndex: PostListData): void => {
postIndex.value = _postIndex
totalPage = Math.ceil(postIndex.value.length / pageNum)
total.value = postIndex.value.length
setPostListPage(1)
}
setPostListPage(1)
return { postList, setPostListPage, page, total, resetPostIndex }
}

View File

@ -1,10 +1,7 @@
import { isFunction, isString } from '@vuepress/shared'
import { useRouter } from 'vue-router'
import type { Router } from 'vue-router'
import { useRouter } from 'vue-router'
/**
* Resolve a route with redirection
*/
export const useResolveRouteWithRedirect = (
...args: Parameters<Router['resolve']>
): ReturnType<Router['resolve']> => {
@ -15,14 +12,15 @@ export const useResolveRouteWithRedirect = (
return route
}
const { redirect } = lastMatched
const resolvedRedirect = isFunction(redirect) ? redirect(route) : redirect
const resolvedRedirectObj = isString(resolvedRedirect)
? { path: resolvedRedirect }
: resolvedRedirect
const resolveRedirect = isFunction(redirect) ? redirect(route) : redirect
const resolveRedirectObj = isString(resolveRedirect)
? { path: resolveRedirect }
: resolveRedirect
return useResolveRouteWithRedirect({
hash: route.hash,
query: route.query,
params: route.params,
...resolvedRedirectObj,
...resolveRedirectObj,
})
}

View File

@ -3,7 +3,6 @@ export interface ScrollPromise {
pending: () => void
resolve: () => void
}
let promise: Promise<void> | null = null
let promiseResolve: (() => void) | null = null

View File

@ -0,0 +1,25 @@
import type { Ref } from 'vue'
import { ref } from 'vue'
import { getColor } from '../utils'
import { usePostAllIndex } from './postIndex'
export interface TagItem {
tag: string
color: string
}
export type TagRaw = TagItem[]
export type TagRef = Ref<TagRaw>
export const tagList: TagRef = ref([])
export const useTagList = (): TagRef => {
const postList = usePostAllIndex().value
let list: string[] = []
postList.forEach((post) => {
list.push(...post.tags)
})
list = Array.from(new Set(list))
tagList.value = list.map((tag) => ({ tag, color: getColor() }))
return tagList
}

View File

@ -10,5 +10,6 @@ import type { PlumeThemeData } from '../../shared'
export const useThemeData = (): ThemeDataRef<PlumeThemeData> =>
_useThemeData<PlumeThemeData>()
export const useThemeLocaleData = (): ThemeLocaleDataRef<PlumeThemeData> =>
_useThemeLocaleData<PlumeThemeData>()

View File

@ -0,0 +1,31 @@
<script lang="ts" setup>
import { useRouteLocale } from '@vuepress/client'
import { useThemeLocaleData } from '../composables'
const routeLocale = useRouteLocale()
const themeLocale = useThemeLocaleData()
const message = themeLocale.value.notFound ?? ['Not Found']
const getMsg = (): string => message[Math.floor(Math.random() * message.length)]
const homeLink = themeLocale.value.home ?? routeLocale.value
const homeText = themeLocale.value.backToHome ?? 'Back to home'
</script>
<template>
<div class="theme-plume not-found">
<div class="not-found-content">
<blockquote>{{ getMsg() }}</blockquote>
<RouterLink :to="homeLink">{{ homeText }}</RouterLink>
</div>
</div>
</template>
<style lang="scss">
.not-found {
width: 100%;
&-content {
padding: 1.25rem;
}
}
</style>

Some files were not shown because too many files have changed in this diff Show More