preface
HowdzIs based onVue3
+ Typescript
Developed a fully customized browser navigation start page, which supports adding material components on demand, and can freely edit the location, size and function of components. It supports responsive design and can customize random wallpaper, dynamic wallpaper, etc. Project provisionWeb page online access. packingBrowser plug-in. packingDesktop application (electron)And other access methods.
This paper records the relevant technologies used in the project development.
Form encapsulation
Various material components can be added freely in the project, and each material component contains its own configuration item form, and some of them have the same configuration items, so a JS data-driven form encapsulation can be realized.
Currently usedElementPlus
Framework, which encapsulates a standardform component and passes it informData
AndformConf
Two attributes can generate a two-way bound form, which supportsJSX
Insert other custom components. Due to the space problem, the component packaging code can be referred to here:standard-form.vue
Then you can use a format similar to JSON to implement the configuration form of each material component, such asWeather
Componentsetting.tsx
As follows:
// @/materials/Weather/setting.tsx
import pick from '../ Base '// pick can freely select a public configuration
export default {
formData: {
weatherMode: 1,
cityName: '',
animationIcon: true,
duration: 15,
position: 5,
baseFontSize: 16,
textColor: '#262626',
textShadow: '0 0 1px #464646',
iconShadow: '0 0 1px #464646',
fontFamily: '',
padding: 10
},
Formconf (formdata: record < string, any >) {// pass in formdata to realize bidirectional binding
return {
weatherMode: {
Label: 'weather city',
type: 'radio-group',
radio: {
List: [{Name: 'automatic acquisition (IP)', value: 1}, {Name: 'manual input', value: 2}],
label: 'name',
value: 'value'
}
},
cityName: {
when: (formData: Record<string,any>) => formData. Weathermode = = = 2, // similar to V-IF
type: 'input',
Attrs: {placeholder: 'please enter city name (currently only Chinese city name is supported)', clear: true},
rules: [{
required: true,
validator: (rule: unknown, value: string, callback: (e?:Error) => void) => {
formData. weatherMode === 2 && ! value ? Callback (new error ('Please enter city name '): callback ()
}
}]// support El form native rules
},
animationIcon: {
Label: 'animation icon',
type: 'switch',
Tips: 'icon with animation is used by default. If you want to improve performance, you can turn off using static icon'
},
duration: {
Label: 'automatic refresh frequency',
type: 'input-number',
attrs: { 'controls-position': 'right', min: 5, max: 12 * 60 },
Tips: 'refresh rate, in minutes'
},
... Pick (formdata, [// select the public configuration
'position',
'baseFontSize',
'textColor',
'textShadow',
'iconShadow',
'fontFamily',
'padding'
])
}
}
}
Right click menu
After adding a material component, you can right-click the pop-up menu to change the configuration or delete it in the edit mode. The implementation source of the right-click menu is the same as the author’s open source@howdjs/mouse-menu
。 At the same time, in this project, in order to be compatible with the mobile terminal, the plug-in is encapsulated twice, and the function of long press pop-up menu is added. Secondary packaging codeRefer here。
Used in the project isVue instruction
The menu plug-in can receive any parameter for callback, so it can transfer the clicked material component data to the callback for various operations.
<template>
<div v-for="element in affix" :key="affix.id">
<div v-mouse-menu="{ disabled: () => isLock, params: element, menuList }">
<!--Material code-->
</div>
</div>
</template>
<script>
setup () {
const isLock = computed(() => store.state.isLock)
const menuList = ref([
{label: 'basic configuration', tips: 'edit base', FN: (params: componentoptions) = > emit ('edit ', params. I)},
{label: 'Delete', tips: 'Delete', FN: (params: componentoptions) = > store. Commit ('deletecomponent ', params)}
])
//Params in FN is component data
}
</script>
Material component layout
Currently, 2 layout methods are provided. One is grid layout based on class file stream, which will arrange components one by one, and the other is fixed layout, which can fix components at any position on the page.
Grid mode
Grid mode usagevue-grid-layoutThe plug-in vue3 version is in beta.
<template>
<grid-layout v-model:layout="list" :col-num="12" :row-height="rowHeight" :is-draggable="!isLock" :is-resizable="!isLock">
<grid-item v-for="item in list" :x="item.x" :y="item.y" :w="item.w" :h="item.h" :i="item.i">
<!--Material code-->
</grid-item>
</grid-layout>
</template>
<script>
setup () {
const isLock = computed(() => store.state.isLock)
const list = computed({
get: () => store.state.list,
set: (val) => { store.commit('updateList', val) }
})
}
</script>
usev-model:layout
Bi directional binding grid mode material component list data, because the material array exists in vuex, which is used herecomputed
Update the setter of.isLock
It is used to judge whether it is currently in editing mode. Dragging and size change are disabled in the locked state. The number of grids currently used is 12, that is, the screen width is divided into 12 copies.
Fixed mode
The fixed mode uses the author’s own open source@howdjs/to-controlAfter the plug-in is completed, you can fix the material components in any position on the page, and you can also drag and drop the lower right corner to change the size.
<template>
<div
v-for="element in affix"
v-to-control="{
positionMode: element.affixInfo.mode,
moveCursor: false,
disabled: () => isLock,
arrowOptions: { lineColor: '#9a98c3', size: 12, padding: 8 }
}"
:key="element.id"
@todragend="handleAffixDragend($event, element)"
@tocontrolend="handleAffixDragend($event, element)"
>
<!--Material code-->
</div>
</template>
<script>
setup () {
const isLock = computed(() => store.state.isLock)
const affix = computed(() => store.state.affix)
const handleAffixDragend = ($event: any, element: ComponentOptions) => {
const mode = element.affixInfo?.mode || 1
const { left, top, bottom, right, width, height } = $event
const _element = JSON.parse(JSON.stringify(element))
_element.affixInfo.x = [1, 3].includes(mode) ? left : right
_element.affixInfo.y = [1, 2].includes(mode) ? top : bottom
if (width && height) {
_element.w = width
_element.h = height
}
store.commit('editComponent', _element)
}
}
</script>
Different from the grid mode, the event callback function is used to update the vuex data of the component. Also useisLock
Determine whether the component is locked. The plug-in supports changing the positioning direction and recording it in the upper right corner, lower right corner, etc., which is very effective for responsive layout. For more usage, please refer to:@howdjs/to-control
Interactive pop-up Popover
The system provides a function of configuring interactive behavior, which can configure the pop-up window of another component when clicking one component, and configure the pop-up direction of the component. After investigation, it is found thatElement-plus
ofPopover
It is not suitable for this situation because the pop-up components are dynamic. So it supports not only one component, but also its own configurationPopover
In all directions, there is another extensionScreenCenter
Pop up, so that components can pop up in the middle of the screen (similar todialog
)。
By passing in the clicked element, the width and height of the target pop-up window and the pop-up window direction, return the information of the target pop-up windowx
andy
。 The core code is as follows:
/**
*Get Popover target information
*@ param element source DOM
*@ param popaverrect Popover info
*@ param direction Popover direction
* @returns [endX, endY, fromX, fromY]
*/
export function getPopoverActivePointByDirection(
element: HTMLElement,
popoverRect: PopoverOption,
direction = DirectionEnum.BOTTOM_CENTER
) {
const { width, height, top, left } = element.getBoundingClientRect()
const { width: popoverWidth, height: popoverHeight, offset = 10 } = popoverRect
const activePointMap = {
[DirectionEnum.SCREEN_CENTER]: [window.innerWidth / 2 - popoverWidth / 2, window.innerHeight / 2 - popoverHeight / 2],
[DirectionEnum.TOP_START]: [left, top - popoverHeight - offset],
[DirectionEnum.TOP_CENTER]: [left + width / 2 - popoverWidth / 2, top - popoverHeight - offset],
[DirectionEnum.TOP_END]: [left + width - popoverWidth, top - popoverHeight - offset],
[DirectionEnum.RIGHT_START]: [left + width + offset, top],
[DirectionEnum.RIGHT_CENTER]: [left + width + offset, top + height / 2 - popoverHeight / 2],
[DirectionEnum.RIGHT_END]: [left + width + offset, top + height - popoverHeight],
[DirectionEnum.BOTTOM_END]: [left + width - popoverWidth, top + height + offset],
[DirectionEnum.BOTTOM_CENTER]: [left + width / 2 - popoverWidth / 2, top + height + offset],
[DirectionEnum.BOTTOM_START]: [left, top + height + offset],
[DirectionEnum.LEFT_END]: [left - popoverWidth - offset, top + height - popoverHeight],
[DirectionEnum.LEFT_CENTER]: [left - popoverWidth - offset, top + height / 2 - popoverHeight / 2],
[DirectionEnum.LEFT_START]: [left - popoverWidth - offset, top]
}
const fromPoint = [left + width / 2, top + height / 2]
return [...activePointMap[direction], ...fromPoint] || [0, 0, ...fromPoint]
}
In addition, usetransform-origin
This attribute can realize the animation of pop-up window from clicking on elements. Finally, configure the direction of pop-up window and the type of pop-up components. Code reference:ActionPopover.vue
Get any website favicon
stayCollection
AndSearch
In the component, there is a function that the user can automatically obtain the favicon of the website after entering the website. In the first version, the website origin + / favicon is directly used ICO, but after a lot of attempts, it is found that the icons of many websites are not stored in this standard form. So later, I implemented a back-end interface to obtain.
Back end interface principle:
- Read the origin from the website entered by the user
- Try from
Redis
Read the cached icon path in, and return if it is read - If not in the cache, this is used
cheerio
Load website, use$('link[rel*="icon"]').attr('href')
Read icon path - If it is not read in the previous step, continue to try to read in the standard form, that is, the website origin + / favicon ico
- Write if reading is successful
Redis
Cache, otherwise, it will return the acquisition failure
Simultaneous interface receptiontype
Parameter, which can directly return the picture stream from the back end to solve the CORS restrictions on icon resources of some websites. Because inCollection
In the component, in order to reduce the number of initial access requests loaded, after reading the icon, the front end will convert the icon into Base64 format and store it in local storage. In this way, you need to use ajax to get the icon and let the interface directly return to the file stream, which can solve the cross domain problem.
In addition, when reading the icon, the front end will buckle the white part of the icon into transparency using the canvas channel method, and the code can beRefer here
summary
The project is still under continuous optimization and development. Various suggestions are welcome. Due to space problems, some of the technologies used will update the records from time to time. If you can continue to pay attention, star, thank you.