mirror of
https://github.com/WeBankFinTech/fes.js.git
synced 2025-06-28 17:39:24 +08:00
Merge branch '2.0' of https://github.com/WeBankFinTech/fes.js into 2.0
This commit is contained in:
commit
9d7bc0471e
@ -1,54 +1,64 @@
|
||||
# @fesjs/plugin-layout
|
||||
|
||||
## 介绍
|
||||
为了进一步降低研发成本,我们尝试将布局通过 fes 插件的方式内置,只需通过简单的配置即可拥有布局,包括导航以及侧边栏。从而做到用户无需关心布局。
|
||||
- 侧边栏菜单数据根据路由中的配置自动生成。
|
||||
- 布局,提供 `side`、 `top`、`mixin` 三种布局。
|
||||
- 主题,提供 `light`、`dark` 两种主题。
|
||||
- 默认实现对路由的 404、403 处理。
|
||||
- 搭配 [@fesjs/plugin-access](./access.html) 插件使用,可以完成对路由的权限控制。
|
||||
- 搭配 [@fesjs/plugin-locale](./locale.html) 插件使用,提供切换语言的能力。
|
||||
- 支持自定义头部区域。
|
||||
- 菜单支持配置icon
|
||||
- 菜单标题支持国际化
|
||||
|
||||
- 可配置页面是否需要 layout。
|
||||
为了进一步降低研发成本,我们尝试将布局通过 fes 插件的方式内置,只需通过简单的配置即可拥有布局,包括导航以及侧边栏。从而做到用户无需关心布局。
|
||||
|
||||
- 侧边栏菜单数据根据路由中的配置自动生成。
|
||||
- 布局,提供 `side`、 `top`、`mixin` 三种布局。
|
||||
- 主题,提供 `light`、`dark` 两种主题。
|
||||
- 默认实现对路由的 404、403 处理。
|
||||
- 搭配 [@fesjs/plugin-access](./access.html) 插件使用,可以完成对路由的权限控制。
|
||||
- 搭配 [@fesjs/plugin-locale](./locale.html) 插件使用,提供切换语言的能力。
|
||||
- 支持自定义头部区域。
|
||||
- 菜单支持配置 icon
|
||||
- 菜单标题支持国际化
|
||||
- 可配置页面是否需要 layout。
|
||||
|
||||
## 启用方式
|
||||
|
||||
在 `package.json` 中引入依赖:
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"@fesjs/fes": "^2.0.0",
|
||||
"@fesjs/plugin-layout": "^4.0.0"
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 布局类型
|
||||
|
||||
配置参数是 `navigation`, 布局有三种类型 `side`、`mixin` 和 `top`, 默认是 `side`:
|
||||
|
||||
```js
|
||||
export default {
|
||||
layout: {
|
||||
navigation: 'side'
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### side
|
||||
|
||||
<!--  -->
|
||||
<img :src="$withBase('side.png')" alt="side">
|
||||
|
||||
### top
|
||||
|
||||
<!--  -->
|
||||
<img :src="$withBase('top.png')" alt="top">
|
||||
|
||||
### mixin
|
||||
|
||||
<!--  -->
|
||||
<img :src="$withBase('mixin.png')" alt="mixin">
|
||||
|
||||
### 页面禁用布局
|
||||
|
||||
布局是默认开启的,但是可能某些页面不需要展示布局样式,比如登录页面。我们只需要在页面的`.vue`中添加如下配置:
|
||||
|
||||
```vue
|
||||
<config lang="json">
|
||||
{
|
||||
@ -56,7 +66,9 @@ export default {
|
||||
}
|
||||
</config>
|
||||
```
|
||||
|
||||
如果只是不想展示`sidebar`,则:
|
||||
|
||||
```
|
||||
<config lang="json">
|
||||
{
|
||||
@ -66,17 +78,18 @@ export default {
|
||||
}
|
||||
</config>
|
||||
```
|
||||
|
||||
`layout`的可选配置有:
|
||||
|
||||
- **sidebar**: 左侧区域,从v4.0.0开始,之前名称叫`side`
|
||||
|
||||
- **header**: 头部区域,从v4.0.0开始,之前名称叫`top`
|
||||
|
||||
- **logo**:logo和标题区域。
|
||||
- **sidebar**: 左侧区域,从 v4.0.0 开始,之前名称叫`side`
|
||||
- **header**: 头部区域,从 v4.0.0 开始,之前名称叫`top`
|
||||
|
||||
- **logo**:logo 和标题区域。
|
||||
|
||||
## keep-alive
|
||||
|
||||
从 4.0.7 开始支持配置路由页面缓存:
|
||||
|
||||
```
|
||||
<config lang="json">
|
||||
{
|
||||
@ -86,7 +99,9 @@ export default {
|
||||
```
|
||||
|
||||
## 编译时配置
|
||||
|
||||
在 `.fes.js` 中配置:
|
||||
|
||||
```js
|
||||
export default {
|
||||
layout: {
|
||||
@ -124,74 +139,73 @@ export default {
|
||||
```
|
||||
|
||||
### footer
|
||||
- **类型**:`String`
|
||||
|
||||
- **默认值**:`null`
|
||||
- **类型**:`String`
|
||||
- **默认值**:`null`
|
||||
|
||||
- **详情**:页面底部的文字。
|
||||
- **详情**:页面底部的文字。
|
||||
|
||||
### theme
|
||||
- **类型**:`String`
|
||||
|
||||
- **默认值**:`dark`
|
||||
- **类型**:`String`
|
||||
- **默认值**:`dark`
|
||||
|
||||
- **详情**:主题,可选有 `dark`、`light`
|
||||
- **详情**:主题,可选有 `dark`、`light`
|
||||
|
||||
### navigation
|
||||
- **类型**:`String`
|
||||
|
||||
- **默认值**:`side`
|
||||
- **类型**:`String`
|
||||
- **默认值**:`side`
|
||||
|
||||
- **详情**:页面布局类型,可选有 `side`、 `top`、 `mixin`
|
||||
- **详情**:页面布局类型,可选有 `side`、 `top`、 `mixin`
|
||||
|
||||
### fixedHeader
|
||||
- **类型**:`Boolean`
|
||||
|
||||
- **默认值**:`false`
|
||||
- **类型**:`Boolean`
|
||||
- **默认值**:`false`
|
||||
|
||||
- **详情**:是否固定头部,不跟随页面滚动。
|
||||
- **详情**:是否固定头部,不跟随页面滚动。
|
||||
|
||||
### fixedSideBar
|
||||
- **类型**:`Boolean`
|
||||
|
||||
- **默认值**:`true`
|
||||
- **类型**:`Boolean`
|
||||
- **默认值**:`true`
|
||||
|
||||
- **详情**:是否固定sidebar,不跟随页面滚动。
|
||||
- **详情**:是否固定 sidebar,不跟随页面滚动。
|
||||
|
||||
### title
|
||||
- **类型**:`String`
|
||||
|
||||
- **默认值**:`name` in package.json
|
||||
- **类型**:`String`
|
||||
- **默认值**:`name` in package.json
|
||||
|
||||
- **详情**:产品名,会显示在 Logo 旁边。
|
||||
- **详情**:产品名,会显示在 Logo 旁边。
|
||||
|
||||
### logo
|
||||
- **类型**:`String`
|
||||
|
||||
- **默认值**:默认提供 fes.js 的 Logo
|
||||
|
||||
- **详情**:Logo的链接
|
||||
- **类型**:`String`
|
||||
- **默认值**:默认提供 fes.js 的 Logo
|
||||
|
||||
- **详情**:Logo 的链接
|
||||
|
||||
### multiTabs
|
||||
- **类型**:`boolean`
|
||||
|
||||
- **默认值**:`false`
|
||||
- **类型**:`boolean`
|
||||
- **默认值**:`false`
|
||||
|
||||
- **详情**:是否开启多页。
|
||||
- **详情**:是否开启多页。
|
||||
|
||||
### menus
|
||||
- **类型**:`Array`
|
||||
|
||||
- **默认值**:`[]`
|
||||
- **类型**:`Array`
|
||||
- **默认值**:`[]`
|
||||
|
||||
- **详情**:菜单配置,子项具体配置如下:
|
||||
- **详情**:菜单配置,子项具体配置如下:
|
||||
|
||||
- **name**:菜单的名称。通过匹配 `name` 和路由元信息 [meta](../../../guide/route.md#扩展路由元信息) 中的 `name`,把菜单和路由关联起来,然后使用路由元信息补充菜单配置,比如 `title`、`path` 等。
|
||||
- **name**:菜单的名称。通过匹配 `name` 和路由元信息 [meta](../../../guide/route.md#扩展路由元信息) 中的 `name`,把菜单和路由关联起来, 然后使用路由元信息补充菜单配置,比如 `title`、`path` 等。
|
||||
|
||||
- **path**:菜单的路径,可配置第三方地址。
|
||||
- **path**:菜单的路径,可配置第三方地址。
|
||||
|
||||
- **match**:额外匹配的路径,当前路由命中匹配规则时,此菜单高亮。 (v4.0.0+)
|
||||
- **match**:额外匹配的路径,当前路由命中匹配规则时,此菜单高亮。 (v4.0.0+)
|
||||
|
||||
```
|
||||
{
|
||||
@ -200,122 +214,139 @@ export default {
|
||||
}
|
||||
```
|
||||
|
||||
- **title**:菜单的标题,如果同时使用[国际化插件](./locale.md),而且`title`的值以`$`开头,则使用`$`后面的内容去匹配语言设置。
|
||||
- **title**:菜单的标题,如果同时使用[国际化插件](./locale.md),而且`title`的值以`$`开头,则使用`$`后面的内容去匹配语言设置。
|
||||
|
||||
- **icon**: 菜单的图标,只有一级标题展示图标。
|
||||
- 图标使用[fes-design icon](https://fes-design-4gvn317r3b6bfe17-1254145788.ap-shanghai.app.tcloudbase.com/zh/components/icon.html),在这里使用组件名称。
|
||||
|
||||
- **icon**: 菜单的图标,只有一级标题展示图标。
|
||||
- 图标使用[fes-design icon](https://fes-design-4gvn317r3b6bfe17-1254145788.ap-shanghai.app.tcloudbase.com/zh/components/icon.html),在这里使用组件名称。
|
||||
```js
|
||||
{
|
||||
icon: "AppstoreOutlined"
|
||||
icon: 'AppstoreOutlined';
|
||||
}
|
||||
```
|
||||
|
||||
- 图标使用本地或者远程svg图片。
|
||||
|
||||
```js
|
||||
{
|
||||
icon: "/wine-outline.svg"
|
||||
icon: '/wine-outline.svg';
|
||||
}
|
||||
```
|
||||
|
||||
- **children**:子菜单配置。
|
||||
- **children**:子菜单配置。
|
||||
|
||||
### menusConfig
|
||||
- **类型**:`Object`
|
||||
|
||||
- **默认值**:`{}`
|
||||
- **类型**:`Object`
|
||||
- **默认值**:`{}`
|
||||
|
||||
- **详情**:菜单的配置:
|
||||
- **详情**:菜单的配置:
|
||||
|
||||
- **defaultExpandAll**:是否默认展开全部菜单。
|
||||
- **defaultExpandAll**:是否默认展开全部菜单。
|
||||
|
||||
- **expandedKeys**:配置默认展开的菜单,需要传子项是菜单路径的数组。
|
||||
- **expandedKeys**:配置默认展开的菜单,需要传子项是菜单路径的数组。
|
||||
|
||||
- **accordion**:是否只保持一个子菜单的展开。
|
||||
- **accordion**:是否只保持一个子菜单的展开。
|
||||
|
||||
## 运行时配置
|
||||
|
||||
在 `app.js` 中配置:
|
||||
|
||||
```js
|
||||
import UserCenter from '@/components/UserCenter';
|
||||
export const layout = {
|
||||
customHeader: <UserCenter />
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### menus
|
||||
- **类型**:`(defaultMenus: [] )=> Ref | []`
|
||||
|
||||
- **详情**:运行时修改菜单,入参是默认菜单配置(.fes.js中的menu配置),需要返回一个`Ref`或者数组。
|
||||
- **类型**:`(defaultMenus: [] )=> Ref | []`
|
||||
- **详情**:运行时修改菜单,入参是默认菜单配置(.fes.js 中的 menu 配置),需要返回一个`Ref`或者数组。
|
||||
|
||||
```js
|
||||
import { ClusterOutlined } from '@fesjs/fes-design/icon'
|
||||
export const layout = layoutConfig => ({
|
||||
import { ClusterOutlined } from '@fesjs/fes-design/icon';
|
||||
export const layout = (layoutConfig) => ({
|
||||
...layoutConfig,
|
||||
customHeader: <UserCenter />,
|
||||
menus: (defaultMenuData) => {
|
||||
const menusRef = ref(defaultMenuData);
|
||||
watch(() => layoutConfig.initialState.userName, () => {
|
||||
menusRef.value = [{
|
||||
name: 'store',
|
||||
icon: <ClusterOutlined />
|
||||
}];
|
||||
});
|
||||
watch(
|
||||
() => layoutConfig.initialState.userName,
|
||||
() => {
|
||||
menusRef.value = [
|
||||
{
|
||||
name: 'store',
|
||||
icon: <ClusterOutlined />
|
||||
}
|
||||
];
|
||||
}
|
||||
);
|
||||
return menusRef;
|
||||
}
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
`layoutConfig.initialState` 是 `beforeRender.action`执行后创建的应用初始状态数据。
|
||||
|
||||
如果菜单需要根据某些状态动态改变,则返回`Ref`,否则只需要返回数组。
|
||||
|
||||
:::tip
|
||||
在运行时配置菜单中的icon,需要传组件本身,而不是组件的名称。
|
||||
在运行时配置菜单中的 icon,需要传组件本身,而不是组件的名称。
|
||||
:::
|
||||
|
||||
|
||||
### header
|
||||
- **类型**:`String`
|
||||
|
||||
- **默认值**:`true`
|
||||
- **类型**:`String`
|
||||
- **默认值**:`true`
|
||||
|
||||
- **详情**:是否显示 header 区域。
|
||||
- **详情**:是否显示 header 区域。
|
||||
|
||||
### sidebar
|
||||
- **类型**:`String`
|
||||
|
||||
- **默认值**:`true`
|
||||
- **类型**:`String`
|
||||
- **默认值**:`true`
|
||||
|
||||
- **详情**:是否显示 sidebar 区域。
|
||||
- **详情**:是否显示 sidebar 区域。
|
||||
|
||||
### logo
|
||||
- **类型**:`String`
|
||||
|
||||
- **默认值**:`true`
|
||||
- **类型**:`String`
|
||||
- **默认值**:`true`
|
||||
|
||||
- **详情**:是否显示 logo 区域。
|
||||
- **详情**:是否显示 logo 区域。
|
||||
|
||||
### onClickLogo
|
||||
|
||||
- **类型**:`Function`
|
||||
- **默认值**:`null`
|
||||
|
||||
- **详情**:logo 点击事件
|
||||
|
||||
### customHeader
|
||||
- **类型**:Vue Component
|
||||
|
||||
- **默认值**:`null`
|
||||
- **类型**:Vue Component
|
||||
- **默认值**:`null`
|
||||
|
||||
- **详情**:top的区域部分位置提供组件自定义功能。
|
||||
- **详情**:top 的区域部分位置提供组件自定义功能。
|
||||
|
||||
### unAccessHandler
|
||||
- **类型**:`Function`
|
||||
|
||||
- **默认值**:`null`
|
||||
- **类型**:`Function`
|
||||
- **默认值**:`null`
|
||||
|
||||
- **详情**:
|
||||
- **详情**:
|
||||
|
||||
当进入某个路由时,如果路由对应的页面不属于可见资源列表,则会暂停进入,调用 `unAccessHandler` 函数。
|
||||
- **参数**
|
||||
- router:createRouter 创建的路由实例
|
||||
- to: 准备进入的路由
|
||||
- from:离开的路由
|
||||
- next: [next函数](https://next.router.vuejs.org/zh/guide/advanced/navigation-guards.html#%E5%8F%AF%E9%80%89%E7%9A%84%E7%AC%AC%E4%B8%89%E4%B8%AA%E5%8F%82%E6%95%B0-next)
|
||||
当进入某个路由时,如果路由对应的页面不属于可见资源列表,则会暂停进入,调用 `unAccessHandler` 函数。
|
||||
|
||||
- **参数**
|
||||
- router:createRouter 创建的路由实例
|
||||
- to: 准备进入的路由
|
||||
- from:离开的路由
|
||||
- next: [next 函数](https://next.router.vuejs.org/zh/guide/advanced/navigation-guards.html#%E5%8F%AF%E9%80%89%E7%9A%84%E7%AC%AC%E4%B8%89%E4%B8%AA%E5%8F%82%E6%95%B0-next)
|
||||
|
||||
比如:
|
||||
|
||||
```js
|
||||
export const access = {
|
||||
unAccessHandler({ to, next }) {
|
||||
@ -330,24 +361,25 @@ export const access = {
|
||||
next('/403');
|
||||
}
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### noFoundHandler
|
||||
- **类型**:函数
|
||||
|
||||
- **默认值**:null
|
||||
- **类型**:函数
|
||||
- **默认值**:null
|
||||
|
||||
- **详情**:
|
||||
- **详情**:
|
||||
|
||||
当进入某个路由时,如果路由对应的页面不存在,则会调用 `noFoundHandler` 函数。
|
||||
- **参数**
|
||||
- router:createRouter 创建的路由实例
|
||||
- to: 准备进入的路由
|
||||
- from:离开的路由
|
||||
- next: [next函数](https://next.router.vuejs.org/zh/guide/advanced/navigation-guards.html#%E5%8F%AF%E9%80%89%E7%9A%84%E7%AC%AC%E4%B8%89%E4%B8%AA%E5%8F%82%E6%95%B0-next)
|
||||
当进入某个路由时,如果路由对应的页面不存在,则会调用 `noFoundHandler` 函数。
|
||||
|
||||
- **参数**
|
||||
- router:createRouter 创建的路由实例
|
||||
- to: 准备进入的路由
|
||||
- from:离开的路由
|
||||
- next: [next 函数](https://next.router.vuejs.org/zh/guide/advanced/navigation-guards.html#%E5%8F%AF%E9%80%89%E7%9A%84%E7%AC%AC%E4%B8%89%E4%B8%AA%E5%8F%82%E6%95%B0-next)
|
||||
|
||||
比如:
|
||||
|
||||
```js
|
||||
export const access = {
|
||||
noFoundHandler({ next }) {
|
||||
@ -358,16 +390,15 @@ export const access = {
|
||||
next('/404');
|
||||
}
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### logoUrl
|
||||
- **类型**:`String`
|
||||
|
||||
- **默认值**:默认提供 fes.js 的 Logo
|
||||
|
||||
- **详情**:Logo的链接。
|
||||
- **类型**:`String`
|
||||
- **默认值**:默认提供 fes.js 的 Logo
|
||||
|
||||
- **详情**:Logo 的链接。
|
||||
|
||||
### 其他运行时配置 (> 4.1.0)
|
||||
|
||||
编译时配置的内容同样支持在运行时配置,但是`logo`除外,用`logoUrl`替代。
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@fesjs/plugin-layout",
|
||||
"version": "4.2.4",
|
||||
"version": "4.2.5",
|
||||
"description": "@fesjs/plugin-layout",
|
||||
"main": "lib/index.js",
|
||||
"files": [
|
||||
|
@ -10,10 +10,18 @@
|
||||
collapsible
|
||||
:inverted="theme === 'dark'"
|
||||
>
|
||||
<div v-if="routeLayout.logo" class="layout-logo">
|
||||
<a
|
||||
v-if="routeLayout.logo"
|
||||
:class="[
|
||||
'layout-logo',
|
||||
routeLayout.onClickLogo && 'is-active'
|
||||
]"
|
||||
@click="routeLayout.onClickLogo"
|
||||
>
|
||||
<img :src="logo" class="logo-img" />
|
||||
<div class="logo-name">{{title}}</div>
|
||||
</div>
|
||||
<span class="logo-name">{{title}}</span>
|
||||
</a>
|
||||
|
||||
<Menu
|
||||
class="layout-menu"
|
||||
:menus="menus"
|
||||
@ -25,10 +33,7 @@
|
||||
:accordion="menuConfig?.accordion"
|
||||
/>
|
||||
</f-aside>
|
||||
<f-layout
|
||||
:fixed="fixedSideBar"
|
||||
:style="sideStyleRef"
|
||||
>
|
||||
<f-layout :fixed="fixedSideBar" :style="sideStyleRef">
|
||||
<f-header
|
||||
v-if="routeLayout.header"
|
||||
ref="headerRef"
|
||||
@ -64,10 +69,17 @@
|
||||
:inverted="theme === 'dark'"
|
||||
:fixed="currentFixedHeaderRef"
|
||||
>
|
||||
<div v-if="routeLayout.logo" class="layout-logo">
|
||||
<a
|
||||
v-if="routeLayout.logo"
|
||||
:class="[
|
||||
'layout-logo',
|
||||
routeLayout.onClickLogo && 'is-active'
|
||||
]"
|
||||
@click="routeLayout.onClickLogo"
|
||||
>
|
||||
<img :src="logo" class="logo-img" />
|
||||
<div class="logo-name">{{title}}</div>
|
||||
</div>
|
||||
<span class="logo-name">{{title}}</span>
|
||||
</a>
|
||||
<Menu
|
||||
class="layout-menu"
|
||||
:menus="menus"
|
||||
@ -105,10 +117,17 @@
|
||||
:fixed="currentFixedHeaderRef"
|
||||
:inverted="theme === 'dark'"
|
||||
>
|
||||
<div v-if="routeLayout.logo" class="layout-logo">
|
||||
<a
|
||||
v-if="routeLayout.logo"
|
||||
:class="[
|
||||
'layout-logo',
|
||||
routeLayout.onClickLogo && 'is-active'
|
||||
]"
|
||||
@click="routeLayout.onClickLogo"
|
||||
>
|
||||
<img :src="logo" class="logo-img" />
|
||||
<div class="logo-name">{{title}}</div>
|
||||
</div>
|
||||
<span class="logo-name">{{title}}</span>
|
||||
</a>
|
||||
<div class="layout-header-custom">
|
||||
<slot name="customHeader"></slot>
|
||||
</div>
|
||||
@ -160,8 +179,8 @@ import {
|
||||
FLayout, FAside, FMain, FFooter, FHeader
|
||||
} from '@fesjs/fes-design';
|
||||
import Menu from './Menu';
|
||||
import MultiTabProvider from './MultiTabProvider';
|
||||
import defaultLogo from '../assets/logo.png';
|
||||
import MultiTabProvider from './MultiTabProvider';
|
||||
import getRuntimeConfig from '../helpers/getRuntimeConfig';
|
||||
|
||||
export default {
|
||||
@ -264,7 +283,9 @@ export default {
|
||||
const currentFixedHeaderRef = computed(
|
||||
() => props.fixedHeader || props.navigation === 'mixin'
|
||||
);
|
||||
const headerStyleRef = computed(() => (currentFixedHeaderRef.value ? { top: `${headerHeightRef.value}px` } : null));
|
||||
const headerStyleRef = computed(() => (currentFixedHeaderRef.value
|
||||
? { top: `${headerHeightRef.value}px` }
|
||||
: null));
|
||||
const sideStyleRef = computed(() => (props.fixedSideBar
|
||||
? {
|
||||
left: collapsedRef.value ? '48px' : `${props.sideWidth}px`
|
||||
@ -286,6 +307,11 @@ export default {
|
||||
<style lang="less" scoped>
|
||||
.main-layout {
|
||||
height: 100vh;
|
||||
.layout-logo {
|
||||
&.is-active {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.layout-main {
|
||||
z-index: 0;
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@fesjs/plugin-watermark",
|
||||
"version": "2.1.1",
|
||||
"version": "2.1.2",
|
||||
"description": "@fesjs/plugin-watermark",
|
||||
"main": "lib/index.js",
|
||||
"files": [
|
||||
|
@ -30,40 +30,39 @@ function timeFormat(date, format = 'YYYY-MM-DD') {
|
||||
return format.replace(/Y+|M+|D+|H+|h+|m+|s+|S+|Q/g, str => String(map[str]));
|
||||
}
|
||||
|
||||
const defaultOption = {
|
||||
content: '请勿外传',
|
||||
container: document.body,
|
||||
width: 300,
|
||||
height: 300,
|
||||
textAlign: 'center',
|
||||
textBaseline: 'middle',
|
||||
fontSize: '14px',
|
||||
fontFamily: 'Microsoft Yahei',
|
||||
fillStyle: 'rgba(184, 184, 184, 0.3)',
|
||||
rotate: 25,
|
||||
zIndex: 99999,
|
||||
timestamp: 'YYYY-MM-DD HH:mm'
|
||||
};
|
||||
let wmContainerObx = null; // MutationObserver
|
||||
let wmObx = null; // MutationObserver
|
||||
let wmTimer = null; // timestamp
|
||||
let watermarkDiv = null;
|
||||
|
||||
let _wmMo = null; // MutationObserver
|
||||
let _wmTimer = null; // timestamp
|
||||
// 销毁水印
|
||||
export function destroyWatermark() {
|
||||
// 监听器关闭
|
||||
wmObx?.disconnect();
|
||||
wmObx = null;
|
||||
wmContainerObx?.disconnect();
|
||||
wmContainerObx = null;
|
||||
// 清除timer
|
||||
if (wmTimer) {
|
||||
window.clearTimeout(wmTimer);
|
||||
wmTimer = null;
|
||||
}
|
||||
// 删除水印元素
|
||||
watermarkDiv?.parentNode?.removeChild(watermarkDiv);
|
||||
watermarkDiv = null;
|
||||
}
|
||||
|
||||
function _createWatermark(param) {
|
||||
function innerCreateWatermark(param) {
|
||||
const {
|
||||
content,
|
||||
container,
|
||||
width,
|
||||
height,
|
||||
textAlign,
|
||||
textBaseline,
|
||||
fontSize,
|
||||
fontFamily,
|
||||
fillStyle,
|
||||
rotate,
|
||||
zIndex,
|
||||
timestamp
|
||||
content, container, width, height, textAlign, textBaseline, fontSize, fontFamily, fillStyle, rotate, zIndex, timestamp, watch
|
||||
} = param;
|
||||
|
||||
if (!container) {
|
||||
return console.warn('createWatermark配置的container不能为空');
|
||||
}
|
||||
|
||||
destroyWatermark();
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.setAttribute('width', `${width}px`);
|
||||
canvas.setAttribute('height', `${height}px`);
|
||||
@ -75,19 +74,14 @@ function _createWatermark(param) {
|
||||
ctx.fillStyle = fillStyle;
|
||||
ctx.translate(width / 2, height / 2);
|
||||
ctx.rotate(-(Math.PI / 180) * rotate);
|
||||
ctx.fillText(
|
||||
`${content}`,
|
||||
0,
|
||||
0
|
||||
);
|
||||
timestamp && ctx.fillText(
|
||||
`${timeFormat(new Date(), timestamp)}`,
|
||||
0,
|
||||
parseInt(fontSize) + 5
|
||||
);
|
||||
ctx.fillText(`${content}`, 0, 0);
|
||||
if (timestamp) {
|
||||
ctx.fillText(`${timeFormat(new Date(), timestamp)}`, 0, parseInt(fontSize) + 5);
|
||||
}
|
||||
|
||||
let __wm = document.querySelector('.__wm');
|
||||
const watermarkDiv = __wm || document.createElement('div');
|
||||
const CLASS_NAME = `wm_${Date.now()}`;
|
||||
|
||||
watermarkDiv = document.createElement('div');
|
||||
const styleStr = `
|
||||
position: ${container === document.body ? 'fixed' : 'absolute'};
|
||||
user-select: none;
|
||||
@ -99,31 +93,36 @@ function _createWatermark(param) {
|
||||
pointer-events: none !important;
|
||||
background-repeat: repeat;
|
||||
background-image: url('${canvas.toDataURL()}')`;
|
||||
|
||||
watermarkDiv.setAttribute('style', styleStr);
|
||||
watermarkDiv.classList.add('__wm');
|
||||
watermarkDiv.classList.add(CLASS_NAME);
|
||||
|
||||
if (!__wm) {
|
||||
if (container.firstChild) {
|
||||
container.insertBefore(watermarkDiv, container.firstChild);
|
||||
} else {
|
||||
container.appendChild(watermarkDiv);
|
||||
}
|
||||
|
||||
const MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
|
||||
if (MutationObserver) {
|
||||
_wmMo = new MutationObserver(() => {
|
||||
__wm = document.querySelector('.__wm');
|
||||
if ((__wm && __wm.getAttribute('style') !== styleStr) || !__wm) {
|
||||
// 避免一直触发
|
||||
_wmMo.disconnect();
|
||||
_wmMo = null;
|
||||
_createWatermark(param);
|
||||
const MutationObserver = window.MutationObserver;
|
||||
if (watch && MutationObserver) {
|
||||
wmContainerObx = new MutationObserver(() => {
|
||||
if (!container.querySelector(`.${CLASS_NAME}`)) {
|
||||
innerCreateWatermark(param);
|
||||
}
|
||||
});
|
||||
|
||||
_wmMo.observe(container, {
|
||||
attributes: true,
|
||||
subtree: true,
|
||||
wmContainerObx.observe(container, {
|
||||
childList: true
|
||||
});
|
||||
|
||||
wmObx = new MutationObserver(() => {
|
||||
if (watermarkDiv.getAttribute('style') !== styleStr) {
|
||||
innerCreateWatermark(param);
|
||||
}
|
||||
});
|
||||
|
||||
wmObx.observe(watermarkDiv, {
|
||||
attributes: true
|
||||
});
|
||||
}
|
||||
|
||||
if (timestamp) {
|
||||
@ -136,26 +135,12 @@ function _createWatermark(param) {
|
||||
timeout = 1000 * 60 * 60;
|
||||
}
|
||||
|
||||
_wmTimer = window.setTimeout(() => {
|
||||
// 触发 MutationObserver
|
||||
watermarkDiv.style.bottom = '0';
|
||||
wmTimer = window.setTimeout(() => {
|
||||
innerCreateWatermark(param);
|
||||
}, timeout);
|
||||
}
|
||||
}
|
||||
|
||||
// 销毁水印
|
||||
export function destroyWatermark() {
|
||||
// 监听器关闭
|
||||
_wmMo && _wmMo.disconnect();
|
||||
_wmMo = null;
|
||||
_wmTimer && window.clearTimeout(_wmTimer);
|
||||
_wmTimer = null;
|
||||
|
||||
// 删除水印元素
|
||||
const __wm = document.querySelector('.__wm');
|
||||
__wm && __wm.parentNode.removeChild(__wm);
|
||||
}
|
||||
|
||||
// canvas 实现 watermark
|
||||
export function createWatermark(option) {
|
||||
// eslint-disable-next-line no-undef
|
||||
@ -163,10 +148,23 @@ export function createWatermark(option) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 为避免多次调用 createWatermark 触发重复监听,这里先执行销毁水印操作
|
||||
destroyWatermark();
|
||||
const defaultOption = {
|
||||
content: '请勿外传',
|
||||
container: document.body,
|
||||
width: 300,
|
||||
height: 300,
|
||||
textAlign: 'center',
|
||||
textBaseline: 'middle',
|
||||
fontSize: '14px',
|
||||
fontFamily: 'Microsoft Yahei',
|
||||
fillStyle: 'rgba(184, 184, 184, 0.3)',
|
||||
rotate: 25,
|
||||
zIndex: 99999,
|
||||
timestamp: 'YYYY-MM-DD HH:mm',
|
||||
watch: true
|
||||
};
|
||||
|
||||
_createWatermark({
|
||||
innerCreateWatermark({
|
||||
...defaultOption,
|
||||
...option
|
||||
});
|
||||
|
@ -1,6 +1,8 @@
|
||||
|
||||
|
||||
import { access as accessApi, pinia, createWatermark } from '@fesjs/fes';
|
||||
import {
|
||||
access as accessApi, pinia, getRouter, createWatermark
|
||||
} from '@fesjs/fes';
|
||||
import PageLoading from '@/components/PageLoading';
|
||||
import UserCenter from '@/components/UserCenter';
|
||||
import { useStore } from '@/store/main';
|
||||
@ -29,6 +31,10 @@ export const beforeRender = {
|
||||
export const layout = layoutConfig => ({
|
||||
...layoutConfig,
|
||||
customHeader: <UserCenter />,
|
||||
onClickLogo() {
|
||||
const router = getRouter();
|
||||
router.push('/');
|
||||
},
|
||||
menus: (defaultMenuData) => {
|
||||
const menusRef = ref(defaultMenuData);
|
||||
// watch(() => initialValue.initialState.userName, () => {
|
||||
|
Loading…
x
Reference in New Issue
Block a user