├── .eslintrc.json
├── .github
└── workflows
│ └── main.deploy.yml
├── .gitignore
├── .husky
├── commit-msg
└── pre-commit
├── .vscode
└── settings.json
├── CHANGELOG.zh-CN.md
├── LICENSE
├── README.en.md
├── README.md
├── commitlint.config.cjs
├── dist
├── background.js
├── favicon-edge.png
├── favicon.ico
├── favicon.png
├── index.html
├── manifest.json
├── sw.js
└── workbox
│ ├── workbox-cacheable-response.prod.js
│ ├── workbox-core.prod.js
│ ├── workbox-expiration.prod.js
│ ├── workbox-range-requests.prod.js
│ ├── workbox-routing.prod.js
│ ├── workbox-strategies.prod.js
│ └── workbox-sw.js
├── index.html
├── package.json
├── public
├── background.js
├── favicon-edge.png
├── favicon.ico
├── favicon.png
├── img
│ └── icons
│ │ ├── baidu.svg
│ │ ├── bilibili.svg
│ │ ├── bing-en.svg
│ │ ├── bing.svg
│ │ ├── google.svg
│ │ ├── sougou.svg
│ │ ├── taobao.svg
│ │ └── youdao.svg
├── manifest.json
├── sw.js
└── workbox
│ ├── workbox-cacheable-response.prod.js
│ ├── workbox-core.prod.js
│ ├── workbox-expiration.prod.js
│ ├── workbox-range-requests.prod.js
│ ├── workbox-routing.prod.js
│ ├── workbox-strategies.prod.js
│ └── workbox-sw.js
├── scripts
├── build-crx.cjs
└── upload-cdn.cjs
├── src
├── App.vue
├── assets
│ ├── custom-animate.scss
│ ├── element-modules.scss
│ ├── element-variables.scss
│ ├── global.scss
│ ├── imgs
│ │ ├── capture-new
│ │ │ ├── Bookmark.png
│ │ │ ├── Clock.png
│ │ │ ├── Collection.png
│ │ │ ├── CountDown.png
│ │ │ ├── Day.png
│ │ │ ├── Editor.png
│ │ │ ├── Empty.png
│ │ │ ├── GithubTrending.png
│ │ │ ├── Iframe.png
│ │ │ ├── JuejinList.png
│ │ │ ├── MovieLines.png
│ │ │ ├── Search.png
│ │ │ ├── TodoList.png
│ │ │ ├── Verse.png
│ │ │ ├── Weather.png
│ │ │ ├── WeiboList.png
│ │ │ └── ZhihuList.png
│ │ ├── icons
│ │ │ ├── baidu.svg
│ │ │ ├── bilibili.svg
│ │ │ ├── bing-en.svg
│ │ │ ├── bing.svg
│ │ │ ├── google.svg
│ │ │ ├── sougou.svg
│ │ │ ├── taobao.svg
│ │ │ └── youdao.svg
│ │ ├── theme
│ │ │ ├── base.png
│ │ │ ├── mobile-pro.png
│ │ │ ├── mobile.png
│ │ │ ├── module.png
│ │ │ ├── movie-lines.png
│ │ │ ├── multi.png
│ │ │ ├── simple.png
│ │ │ └── tabs.gif
│ │ ├── video-thumb.png
│ │ ├── weather-animation-icon
│ │ │ ├── clear-day.svg
│ │ │ ├── cloudy.svg
│ │ │ ├── drizzle.svg
│ │ │ ├── fog.svg
│ │ │ ├── hurricane.svg
│ │ │ ├── mist.svg
│ │ │ ├── not-available.svg
│ │ │ ├── overcast-day.svg
│ │ │ ├── overcast.svg
│ │ │ ├── rain.svg
│ │ │ ├── sleet.svg
│ │ │ ├── snow.svg
│ │ │ ├── thermometer-colder.svg
│ │ │ ├── thermometer-warmer.svg
│ │ │ ├── thunderstorms-rain.svg
│ │ │ └── wind.svg
│ │ └── weather-static-icon
│ │ │ ├── clear-day.svg
│ │ │ ├── cloudy.svg
│ │ │ ├── drizzle.svg
│ │ │ ├── fog.svg
│ │ │ ├── hurricane.svg
│ │ │ ├── mist.svg
│ │ │ ├── not-available.svg
│ │ │ ├── overcast-day.svg
│ │ │ ├── overcast.svg
│ │ │ ├── rain.svg
│ │ │ ├── sleet.svg
│ │ │ ├── snow.svg
│ │ │ ├── thermometer-colder.svg
│ │ │ ├── thermometer-warmer.svg
│ │ │ ├── thunderstorms-rain.svg
│ │ │ └── wind.svg
│ └── variables.scss
├── components
│ ├── Action
│ │ └── ActionPopover.vue
│ ├── ActionConfig.vue
│ ├── AuxiliaryConfig.vue
│ ├── Axuiliary
│ │ ├── About.vue
│ │ ├── ChangeLog.vue
│ │ ├── CleanCache.vue
│ │ ├── EffectSelector.vue
│ │ ├── FAQ.md
│ │ ├── FAQ.vue
│ │ ├── ImportExport.vue
│ │ └── TabControl.vue
│ ├── BaseConfig.vue
│ ├── FormControl
│ │ ├── BackgroundFilterSelector.vue
│ │ ├── BackgroundSelector.vue
│ │ ├── BackgroundTool
│ │ │ ├── LocalImg.vue
│ │ │ ├── PersonalWallpaper.vue
│ │ │ ├── RecommendPicture.vue
│ │ │ └── RecommendVideo.vue
│ │ ├── FontSelector.vue
│ │ ├── MaterialSelector.vue
│ │ ├── StandardColorPicker.vue
│ │ └── WarnLock.vue
│ ├── Global
│ │ ├── BackgroundEffect.vue
│ │ ├── BackgroundEffect
│ │ │ ├── FireFlies.vue
│ │ │ ├── Focus.vue
│ │ │ ├── RainDrop.vue
│ │ │ ├── Snow.vue
│ │ │ ├── Star.vue
│ │ │ └── source
│ │ │ │ ├── ShaderProgram.js
│ │ │ │ ├── Star.ts
│ │ │ │ └── raindrop-fx.js
│ │ ├── BackgroundImage.vue
│ │ ├── DefaultTheme.vue
│ │ ├── DefaultThemeData
│ │ │ ├── Base.json
│ │ │ ├── Mobile.json
│ │ │ ├── MobilePro.json
│ │ │ ├── Module.json
│ │ │ ├── MovieLines.json
│ │ │ ├── Multiple.json
│ │ │ ├── Simple.json
│ │ │ └── Tabs.json
│ │ ├── EasyDialog.vue
│ │ ├── IframeOpener.vue
│ │ └── TabCarousel.vue
│ ├── GlobalConfig.vue
│ ├── GooeyMenu.vue
│ ├── Layout.vue
│ └── Tools
│ │ ├── Confirm.vue
│ │ ├── Icon.vue
│ │ ├── IconifyPicker.vue
│ │ ├── Loading.vue
│ │ ├── TextLoading.vue
│ │ ├── Tips.vue
│ │ └── Unset.vue
├── constanst
│ ├── icon.ts
│ └── index.ts
├── global.ts
├── hooks
│ └── useScreenMode.ts
├── lang
│ ├── index.ts
│ └── locales
│ │ ├── en.json
│ │ └── zh-cn.json
├── main.ts
├── materials
│ ├── Bookmark
│ │ ├── ConfigDialog.vue
│ │ ├── MoveDialog.vue
│ │ ├── index.vue
│ │ └── setting.tsx
│ ├── Clock
│ │ ├── index.vue
│ │ └── setting.tsx
│ ├── Collection
│ │ ├── index.vue
│ │ ├── setting.tsx
│ │ └── utils.ts
│ ├── CountDown
│ │ ├── index.vue
│ │ └── setting.tsx
│ ├── DailyHot
│ │ ├── index.vue
│ │ └── setting.tsx
│ ├── Day
│ │ ├── index.vue
│ │ └── setting.tsx
│ ├── Editor
│ │ ├── index.vue
│ │ ├── milkdown
│ │ │ ├── Milk.vue
│ │ │ ├── custom-nord.ts
│ │ │ ├── fonts
│ │ │ │ ├── howdz-editor.svg
│ │ │ │ ├── howdz-editor.ttf
│ │ │ │ └── howdz-editor.woff
│ │ │ └── icon-font.css
│ │ └── setting.tsx
│ ├── Empty
│ │ ├── index.vue
│ │ └── setting.tsx
│ ├── GithubTrending
│ │ ├── index.vue
│ │ └── setting.tsx
│ ├── Iframe
│ │ ├── index.vue
│ │ └── setting.tsx
│ ├── JuejinList
│ │ ├── index.vue
│ │ └── setting.tsx
│ ├── MovieLines
│ │ ├── index.vue
│ │ └── setting.tsx
│ ├── Search
│ │ ├── EngineConfig.vue
│ │ ├── index.vue
│ │ └── setting.tsx
│ ├── TodoList
│ │ ├── DatePicker.vue
│ │ ├── index.vue
│ │ └── setting.tsx
│ ├── Verse
│ │ ├── index.vue
│ │ └── setting.tsx
│ ├── Weather
│ │ ├── icon-map.ts
│ │ ├── index.vue
│ │ └── setting.tsx
│ ├── WeiboList
│ │ ├── index.vue
│ │ └── setting.tsx
│ ├── ZhihuList
│ │ ├── index.vue
│ │ └── setting.tsx
│ ├── base.tsx
│ └── setting.ts
├── plugins
│ ├── local-img.ts
│ ├── mouse-menu.ts
│ ├── position-selector
│ │ ├── index.ts
│ │ ├── mapPosition.ts
│ │ └── position-selector.vue
│ └── standard-form
│ │ ├── index.ts
│ │ └── src
│ │ ├── jsx-render.vue
│ │ └── standard-form.vue
├── shims-vue.d.ts
├── store
│ └── index.ts
├── types
│ ├── global.d.ts
│ └── index.d.ts
├── utils
│ ├── color.ts
│ ├── direction.ts
│ ├── font.ts
│ ├── gzip.ts
│ ├── images.ts
│ ├── index.ts
│ ├── preview-mode.ts
│ ├── request.ts
│ └── types.ts
└── vite-env.d.ts
├── tsconfig.json
└── vite.config.ts
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "extends": [
4 | "eslint:recommended",
5 | "plugin:oxlint/recommended",
6 | "plugin:vue/vue3-recommended",
7 | "plugin:@typescript-eslint/recommended"
8 | ],
9 | "parser": "vue-eslint-parser",
10 | "parserOptions": {
11 | "parser": "@typescript-eslint/parser",
12 | "sourceType": "module"
13 | },
14 | "overrides": [
15 | {
16 | "extends": ["plugin:@typescript-eslint/disable-type-checked"],
17 | "files": ["./**/*.js"]
18 | }
19 | ],
20 | "rules": {
21 | "@typescript-eslint/no-explicit-any": "off",
22 | "vue/multi-word-component-names": "off",
23 | "vue/max-attributes-per-line": ["warn", { "singleline": 4 }],
24 | "vue/require-default-prop": "off",
25 | "vue/no-reserved-component-names": "off",
26 | "vue/html-self-closing": [
27 | "warn",
28 | {
29 | "svg": "nerver"
30 | }
31 | ]
32 | }
33 | }
--------------------------------------------------------------------------------
/.github/workflows/main.deploy.yml:
--------------------------------------------------------------------------------
1 | name: Deploy Doc Website
2 | on:
3 | push:
4 | branches:
5 | - main
6 |
7 | jobs:
8 | main-deploy:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: Checkout
12 | uses: actions/checkout@v2
13 | with:
14 | persist-credentials: false
15 |
16 | - name: Setup node
17 | uses: actions/setup-node@v2
18 |
19 | - name: Install dependencies
20 | run: yarn
21 |
22 | - name: Build Demo
23 | run: yarn build
24 |
25 | - name: Build CRX file
26 | run: yarn build:crx
27 |
28 | - name: Move CRX file
29 | run: yarn move:crx
30 |
31 | #- name: Build Electorn file
32 | # run: yarn build:electron
33 |
34 | - name: Deploy
35 | uses: JamesIves/github-pages-deploy-action@3.7.1
36 | with:
37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
38 | BRANCH: gh-pages
39 | FOLDER: dist
40 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | dist-ssr
4 | *.local
5 | crx
6 | install
7 | electron
8 | yarn-error.log
9 | package-lock.json
10 | yarn.lock
11 | pnpm-lock.yaml
12 | stats.html
13 | dist/assets/*
14 | dist/img/*
15 | dist/howdz-dashboard.crx
16 | dist/howdz-dashboard.zip
17 |
18 | # Local ENV files
19 | .env.local
20 | .env.development.local
21 | .env.test.local
22 | .env.production.local
23 | .env
24 |
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npx --no-install commitlint --edit $1
5 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npm run lint
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | // "editor.formatOnSave": true,
3 | "editor.codeActionsOnSave": {
4 | "source.fixAll.eslint": "explicit"
5 | },
6 | "editor.tabSize": 2
7 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2025 Leon.D
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.en.md:
--------------------------------------------------------------------------------
1 |

2 | Howdz
3 |
4 | # Howdz Dashboard
5 |
6 | Custom your personal **browser start page** from some **configurable components**.
7 |
8 | Written in `Vue3`, `Vite`, `Typescript`.
9 |
10 | - 中文文档
11 |
12 | ## Website
13 |
14 | - [Online Website #1](https://howdz.xyz/)
15 | - [Online Website #2](https://s.kongfandong.cn/)
16 | - [Chrome Extension](https://chromewebstore.google.com/detail/howdz%E8%B5%B7%E5%A7%8B%E9%A1%B5/ggglfehkglgpenacfalffmiojghklamm)
17 | - [Edge Extension](https://microsoftedge.microsoft.com/addons/detail/howdz%E8%B5%B7%E5%A7%8B%E9%A1%B5/cgcggcdgjfmeoemmdpleinicgepijegd)
18 |
19 | ## Feature
20 |
21 | - ✨Build in responsive, Custom drag to layout.
22 | - 💫Two mode to layout, based on document flow or fixed position mode.
23 | - 🍭The component will be configurable, includes it's function or style.
24 | - 🍌Data export for random key or json file.
25 | - 🎉Pick up a default theme when first enter.
26 | - 🌟Dynamic wallpaper is ready, config a video url or pick a recommand picture in background setting.
27 | - 📋`Tab Pages Mode`, allow to config **multiple pages**.
28 | - 🍦`Component action`, config that click the component to toggle an another component.
29 | - 🚀`Service worker` is supported to cache the static source.
30 |
31 | ## Screenshot
32 |
33 | 
34 | 
35 | 
36 | 
37 |
38 | ## Materials
39 |
40 | - Empty
41 | - Clock
42 | - Verse
43 | - Search
44 | - Collection
45 | - Iframe
46 | - TodoList
47 | - Weather
48 | - CountDown
49 | - Juejin
50 | - Weibo
51 | - GithubTrending
52 | - Day
53 | - ZhihuList
54 | - Editor
55 | - MovieLines
56 | - Bookmark
57 | - DailyHot
58 |
59 | ## Chrome extension mode
60 |
61 |
62 |
63 | - **Chrome** - [Install from store](https://chrome.google.com/webstore/detail/howdz%E8%B5%B7%E5%A7%8B%E9%A1%B5/ggglfehkglgpenacfalffmiojghklamm/related)
64 | - **Microsoft Edge** - [Install from store](https://microsoftedge.microsoft.com/addons/detail/howdz%E8%B5%B7%E5%A7%8B%E9%A1%B5/cgcggcdgjfmeoemmdpleinicgepijegd)
65 |
66 | ## More
67 |
68 | - [CHANGELOG](./CHANGELOG.zh-CN.md)
69 | - [Introduce Video](https://www.bilibili.com/video/BV1Vu411Z7i1?share_source=copy_web)
70 | - [FAQ](https://github.com/leon-kfd/Dashboard/blob/main/src/components/Axuiliary/FAQ.md)
71 |
72 | ## License
73 |
74 | All for [MIT](https://github.com/leon-kfd/Dashboard/blob/main/LICENSE)
75 |
76 | Copyright (c) 2025 Leon.D
77 |
--------------------------------------------------------------------------------
/commitlint.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ['@commitlint/config-conventional']
3 | };
4 |
--------------------------------------------------------------------------------
/dist/background.js:
--------------------------------------------------------------------------------
1 |
2 | // chrome.browserAction.onClicked.addListener(function () {
3 | // chrome.management.getSelf(function (res) {
4 | // chrome.tabs.create({ url: 'chrome-extension://' + res.id + '/index.html' });
5 | // });
6 | // });
7 |
--------------------------------------------------------------------------------
/dist/favicon-edge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-kfd/Dashboard/38b221553ad241b65ed59ab44498af5092509357/dist/favicon-edge.png
--------------------------------------------------------------------------------
/dist/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-kfd/Dashboard/38b221553ad241b65ed59ab44498af5092509357/dist/favicon.ico
--------------------------------------------------------------------------------
/dist/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-kfd/Dashboard/38b221553ad241b65ed59ab44498af5092509357/dist/favicon.png
--------------------------------------------------------------------------------
/dist/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
11 |
15 |
19 |
20 |
24 |
25 |
26 |
27 | Howdz 起始页
28 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/dist/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Howdz起始页",
3 | "version": "1.5.5",
4 | "description": "提供大量组件用于定制化你的起始页,可适配响应式设计。",
5 | "icons": {
6 | "128": "favicon.png"
7 | },
8 | "permissions": [],
9 | "background": {
10 | "service_worker": "sw.js"
11 | },
12 | "offline_enabled": true,
13 | "manifest_version": 3,
14 | "chrome_url_overrides": {
15 | "newtab": "index.html"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/dist/sw.js:
--------------------------------------------------------------------------------
1 |
2 | if (location.href.includes('howdz.xyz')) {
3 | importScripts('https://cdn.staticfile.org/workbox-sw/7.0.0/workbox-sw.js')
4 | workbox.setConfig({
5 | debug: false,
6 | });
7 | console.log('sw.js is load by CDN!')
8 | } else {
9 | importScripts('./workbox/workbox-sw.js')
10 | workbox.setConfig({
11 | debug: false,
12 | modulePathPrefix: './workbox/'
13 | });
14 | console.log('sw.js is load by local!')
15 | }
16 |
17 | // Cache css/js/font.
18 | workbox.routing.registerRoute(
19 | ({ request }) => request.destination === 'style' || request.destination === 'script' || request.destination === 'font',
20 | new workbox.strategies.CacheFirst({
21 | cacheName: 'css-js-font',
22 | plugins: [
23 | new workbox.cacheableResponse.CacheableResponsePlugin({
24 | statuses: [200],
25 | }),
26 | new workbox.expiration.ExpirationPlugin({
27 | maxEntries: 50,
28 | maxAgeSeconds: 60 * 60 * 24 * 7, // 7 Days
29 | }),
30 | ]
31 | })
32 | );
33 |
34 | // Cache image.
35 | workbox.routing.registerRoute(
36 | ({ request }) => request.destination === 'image',
37 | new workbox.strategies.StaleWhileRevalidate({
38 | cacheName: 'image',
39 | plugins: [
40 | new workbox.cacheableResponse.CacheableResponsePlugin({
41 | statuses: [200],
42 | }),
43 | new workbox.expiration.ExpirationPlugin({
44 | maxEntries: 50,
45 | maxAgeSeconds: 60 * 60 * 24 * 7, // 7 Days
46 | })
47 | ]
48 | })
49 | )
50 |
51 | // Cache video
52 | workbox.routing.registerRoute(
53 | ({ request }) => request.destination === 'video',
54 | new workbox.strategies.CacheFirst({
55 | cacheName: 'video',
56 | plugins: [
57 | new workbox.cacheableResponse.CacheableResponsePlugin({
58 | statuses: [200],
59 | }),
60 | new workbox.expiration.ExpirationPlugin({
61 | maxEntries: 50,
62 | maxAgeSeconds: 60 * 60 * 24 * 7, // 7 Days
63 | }),
64 | new workbox.rangeRequests.RangeRequestsPlugin()
65 | ]
66 | })
67 | )
68 |
--------------------------------------------------------------------------------
/dist/workbox/workbox-cacheable-response.prod.js:
--------------------------------------------------------------------------------
1 | this.workbox=this.workbox||{},this.workbox.cacheableResponse=function(s){"use strict";try{self["workbox:cacheable-response:6.1.5"]&&_()}catch(s){}class t{constructor(s={}){this.j=s.statuses,this.O=s.headers}isResponseCacheable(s){let t=!0;return this.j&&(t=this.j.includes(s.status)),this.O&&t&&(t=Object.keys(this.O).some((t=>s.headers.get(t)===this.O[t]))),t}}return s.CacheableResponse=t,s.CacheableResponsePlugin=class{constructor(s){this.cacheWillUpdate=async({response:s})=>this.B.isResponseCacheable(s)?s:null,this.B=new t(s)}},s}({});
2 |
--------------------------------------------------------------------------------
/dist/workbox/workbox-expiration.prod.js:
--------------------------------------------------------------------------------
1 | this.workbox=this.workbox||{},this.workbox.expiration=function(t,s,e,i,a,n,h){"use strict";try{self["workbox:expiration:6.1.5"]&&_()}catch(t){}const r="cache-entries",c=t=>{const s=new URL(t,location.href);return s.hash="",s.href};class o{constructor(t){this.T=t,this.i=new e.DBWrapper("workbox-expiration",1,{onupgradeneeded:t=>this.M(t)})}M(t){const s=t.target.result.createObjectStore(r,{keyPath:"id"});s.createIndex("cacheName","cacheName",{unique:!1}),s.createIndex("timestamp","timestamp",{unique:!1}),i.deleteDatabase(this.T)}async setTimestamp(t,s){const e={url:t=c(t),timestamp:s,cacheName:this.T,id:this.L(t)};await this.i.put(r,e)}async getTimestamp(t){return(await this.i.get(r,this.L(t))).timestamp}async expireEntries(t,s){const e=await this.i.transaction(r,"readwrite",((e,i)=>{const a=e.objectStore(r).index("timestamp").openCursor(null,"prev"),n=[];let h=0;a.onsuccess=()=>{const e=a.result;if(e){const i=e.value;i.cacheName===this.T&&(t&&i.timestamp=s?n.push(e.value):h++),e.continue()}else i(n)}})),i=[];for(const t of e)await this.i.delete(r,t.id),i.push(t.url);return i}L(t){return this.T+"|"+c(t)}}class u{constructor(t,s={}){this.F=!1,this.H=!1,this.I=s.maxEntries,this.G=s.maxAgeSeconds,this.J=s.matchOptions,this.T=t,this.V=new o(t)}async expireEntries(){if(this.F)return void(this.H=!0);this.F=!0;const t=this.G?Date.now()-1e3*this.G:0,e=await this.V.expireEntries(t,this.I),i=await self.caches.open(this.T);for(const t of e)await i.delete(t,this.J);this.F=!1,this.H&&(this.H=!1,s.dontWaitFor(this.expireEntries()))}async updateTimestamp(t){await this.V.setTimestamp(t,Date.now())}async isURLExpired(t){if(this.G){return await this.V.getTimestamp(t){if(!a)return null;const n=this.W(a),h=this.X(i);s.dontWaitFor(h.expireEntries());const r=h.updateTimestamp(e.url);if(t)try{t.waitUntil(r)}catch(t){}return n?a:null},this.cacheDidUpdate=async({cacheName:t,request:s})=>{const e=this.X(t);await e.updateTimestamp(s.url),await e.expireEntries()},this.Y=t,this.G=t.maxAgeSeconds,this.Z=new Map,t.purgeOnQuotaError&&n.registerQuotaErrorCallback((()=>this.deleteCacheAndMetadata()))}X(t){if(t===a.cacheNames.getRuntimeName())throw new h.WorkboxError("expire-custom-caches-only");let s=this.Z.get(t);return s||(s=new u(t,this.Y),this.Z.set(t,s)),s}W(t){if(!this.G)return!0;const s=this.tt(t);if(null===s)return!0;return s>=Date.now()-1e3*this.G}tt(t){if(!t.headers.has("date"))return null;const s=t.headers.get("date"),e=new Date(s).getTime();return isNaN(e)?null:e}async deleteCacheAndMetadata(){for(const[t,s]of this.Z)await self.caches.delete(t),await s.delete();this.Z=new Map}},t}({},workbox.core._private,workbox.core._private,workbox.core._private,workbox.core._private,workbox.core,workbox.core._private);
2 |
--------------------------------------------------------------------------------
/dist/workbox/workbox-range-requests.prod.js:
--------------------------------------------------------------------------------
1 | this.workbox=this.workbox||{},this.workbox.rangeRequests=function(t,e,n){"use strict";try{self["workbox:range-requests:5.1.4"]&&_()}catch(t){}async function r(t,n){try{if(206===n.status)return n;const r=t.headers.get("range");if(!r)throw new e.WorkboxError("no-range-header");const s=function(t){const n=t.trim().toLowerCase();if(!n.startsWith("bytes="))throw new e.WorkboxError("unit-must-be-bytes",{normalizedRangeHeader:n});if(n.includes(","))throw new e.WorkboxError("single-range-only",{normalizedRangeHeader:n});const r=/(\d*)-(\d*)/.exec(n);if(!r||!r[1]&&!r[2])throw new e.WorkboxError("invalid-range-values",{normalizedRangeHeader:n});return{start:""===r[1]?void 0:Number(r[1]),end:""===r[2]?void 0:Number(r[2])}}(r),a=await n.blob(),o=function(t,n,r){const s=t.size;if(r&&r>s||n&&n<0)throw new e.WorkboxError("range-not-satisfiable",{size:s,end:r,start:n});let a,o;return void 0!==n&&void 0!==r?(a=n,o=r+1):void 0!==n&&void 0===r?(a=n,o=s):void 0!==r&&void 0===n&&(a=s-r,o=s),{start:a,end:o}}(a,s.start,s.end),i=a.slice(o.start,o.end),d=i.size,u=new Response(i,{status:206,statusText:"Partial Content",headers:n.headers});return u.headers.set("Content-Length",String(d)),u.headers.set("Content-Range",`bytes ${o.start}-${o.end-1}/`+a.size),u}catch(t){return new Response("",{status:416,statusText:"Range Not Satisfiable"})}}return t.RangeRequestsPlugin=class{constructor(){this.cachedResponseWillBeUsed=async({request:t,cachedResponse:e})=>e&&t.headers.has("range")?await r(t,e):e}},t.createPartialResponse=r,t}({},workbox.core._private,workbox.core._private);
2 |
3 |
--------------------------------------------------------------------------------
/dist/workbox/workbox-routing.prod.js:
--------------------------------------------------------------------------------
1 | this.workbox=this.workbox||{},this.workbox.routing=function(t,e){"use strict";try{self["workbox:routing:6.1.5"]&&_()}catch(t){}const s=t=>t&&"object"==typeof t?t:{handle:t};class r{constructor(t,e,r="GET"){this.handler=s(e),this.match=t,this.method=r}setCatchHandler(t){this.catchHandler=s(t)}}class n extends r{constructor(t,e,s){super((({url:e})=>{const s=t.exec(e.href);if(s&&(e.origin===location.origin||0===s.index))return s.slice(1)}),e,s)}}class i{constructor(){this.ft=new Map,this.dt=new Map}get routes(){return this.ft}addFetchListener(){self.addEventListener("fetch",(t=>{const{request:e}=t,s=this.handleRequest({request:e,event:t});s&&t.respondWith(s)}))}addCacheListener(){self.addEventListener("message",(t=>{if(t.data&&"CACHE_URLS"===t.data.type){const{payload:e}=t.data,s=Promise.all(e.urlsToCache.map((e=>{"string"==typeof e&&(e=[e]);const s=new Request(...e);return this.handleRequest({request:s,event:t})})));t.waitUntil(s),t.ports&&t.ports[0]&&s.then((()=>t.ports[0].postMessage(!0)))}}))}handleRequest({request:t,event:e}){const s=new URL(t.url,location.href);if(!s.protocol.startsWith("http"))return;const r=s.origin===location.origin,{params:n,route:i}=this.findMatchingRoute({event:e,request:t,sameOrigin:r,url:s});let o=i&&i.handler;const u=t.method;if(!o&&this.dt.has(u)&&(o=this.dt.get(u)),!o)return;let c;try{c=o.handle({url:s,request:t,event:e,params:n})}catch(t){c=Promise.reject(t)}const a=i&&i.catchHandler;return c instanceof Promise&&(this.wt||a)&&(c=c.catch((async r=>{if(a)try{return await a.handle({url:s,request:t,event:e,params:n})}catch(t){r=t}if(this.wt)return this.wt.handle({url:s,request:t,event:e});throw r}))),c}findMatchingRoute({url:t,sameOrigin:e,request:s,event:r}){const n=this.ft.get(s.method)||[];for(const i of n){let n;const o=i.match({url:t,sameOrigin:e,request:s,event:r});if(o)return n=o,(Array.isArray(o)&&0===o.length||o.constructor===Object&&0===Object.keys(o).length||"boolean"==typeof o)&&(n=void 0),{route:i,params:n}}return{}}setDefaultHandler(t,e="GET"){this.dt.set(e,s(t))}setCatchHandler(t){this.wt=s(t)}registerRoute(t){this.ft.has(t.method)||this.ft.set(t.method,[]),this.ft.get(t.method).push(t)}unregisterRoute(t){if(!this.ft.has(t.method))throw new e.WorkboxError("unregister-route-but-not-found-with-method",{method:t.method});const s=this.ft.get(t.method).indexOf(t);if(!(s>-1))throw new e.WorkboxError("unregister-route-route-not-registered");this.ft.get(t.method).splice(s,1)}}let o;const u=()=>(o||(o=new i,o.addFetchListener(),o.addCacheListener()),o);return t.NavigationRoute=class extends r{constructor(t,{allowlist:e=[/./],denylist:s=[]}={}){super((t=>this.gt(t)),t),this.qt=e,this.yt=s}gt({url:t,request:e}){if(e&&"navigate"!==e.mode)return!1;const s=t.pathname+t.search;for(const t of this.yt)if(t.test(s))return!1;return!!this.qt.some((t=>t.test(s)))}},t.RegExpRoute=n,t.Route=r,t.Router=i,t.registerRoute=function(t,s,i){let o;if("string"==typeof t){const e=new URL(t,location.href);o=new r((({url:t})=>t.href===e.href),s,i)}else if(t instanceof RegExp)o=new n(t,s,i);else if("function"==typeof t)o=new r(t,s,i);else{if(!(t instanceof r))throw new e.WorkboxError("unsupported-route-type",{moduleName:"workbox-routing",funcName:"registerRoute",paramName:"capture"});o=t}return u().registerRoute(o),o},t.setCatchHandler=function(t){u().setCatchHandler(t)},t.setDefaultHandler=function(t){u().setDefaultHandler(t)},t}({},workbox.core._private);
2 |
--------------------------------------------------------------------------------
/dist/workbox/workbox-sw.js:
--------------------------------------------------------------------------------
1 | !function(){"use strict";try{self["workbox:sw:6.1.5"]&&_()}catch(t){}const t={backgroundSync:"background-sync",broadcastUpdate:"broadcast-update",cacheableResponse:"cacheable-response",core:"core",expiration:"expiration",googleAnalytics:"offline-ga",navigationPreload:"navigation-preload",precaching:"precaching",rangeRequests:"range-requests",routing:"routing",strategies:"strategies",streams:"streams",recipes:"recipes"};self.workbox=new class{constructor(){return this.v={},this.Pt={debug:"localhost"===self.location.hostname,modulePathPrefix:null,modulePathCb:null},this.$t=this.Pt.debug?"dev":"prod",this.jt=!1,new Proxy(this,{get(e,s){if(e[s])return e[s];const o=t[s];return o&&e.loadModule(`workbox-${o}`),e[s]}})}setConfig(t={}){if(this.jt)throw new Error("Config must be set before accessing workbox.* modules");Object.assign(this.Pt,t),this.$t=this.Pt.debug?"dev":"prod"}loadModule(t){const e=this.St(t);try{importScripts(e),this.jt=!0}catch(s){throw console.error(`Unable to import module '${t}' from '${e}'.`),s}}St(t){if(this.Pt.modulePathCb)return this.Pt.modulePathCb(t,this.Pt.debug);let e=["https://storage.googleapis.com/workbox-cdn/releases/6.1.5"];const s=`${t}.${this.$t}.js`,o=this.Pt.modulePathPrefix;return o&&(e=o.split("/"),""===e[e.length-1]&&e.splice(e.length-1,1)),e.push(s),e.join("/")}}}();
2 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
11 |
15 |
19 |
20 |
24 |
25 |
26 |
27 | Howdz 起始页
28 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "howdz-dashboard",
3 | "version": "1.5.5",
4 | "description": "Custom your personal browser start page from some configurable components",
5 | "author": {
6 | "name": "leon-kfd",
7 | "email": "kfd_personal@163.com"
8 | },
9 | "keywords": [
10 | "javascript",
11 | "vue",
12 | "vite",
13 | "typescript",
14 | "dashboard",
15 | "drag",
16 | "custom"
17 | ],
18 | "type": "module",
19 | "licence": "MIT",
20 | "scripts": {
21 | "prepare": "husky install",
22 | "commit": "cz",
23 | "dev": "vite",
24 | "build": "cross-env VITE_APP_BUILD_MODE=cdn vite build",
25 | "upload": "node scripts/upload-cdn.cjs",
26 | "serve": "vite preview",
27 | "build:crx": "cross-env VITE_APP_BUILD_MODE=crx vite build",
28 | "move:crx": "node scripts/build-crx.cjs",
29 | "lint": "oxlint src"
30 | },
31 | "config": {
32 | "commitizen": {
33 | "path": "cz-conventional-changelog"
34 | }
35 | },
36 | "husky": {
37 | "hooks": {
38 | "commit-msg": "npx --no-install commitlint --edit '$1'",
39 | "pre-commit": "npm run lint"
40 | }
41 | },
42 | "dependencies": {
43 | "@emotion/css": "^11.7.1",
44 | "@howdyjs/mouse-menu": "^2.1.1",
45 | "@howdyjs/to-control": "^2.1.1",
46 | "@milkdown/core": "5.3.1",
47 | "@milkdown/plugin-clipboard": "5.3.1",
48 | "@milkdown/plugin-collaborative": "5.3.1",
49 | "@milkdown/plugin-history": "5.3.1",
50 | "@milkdown/plugin-listener": "5.3.1",
51 | "@milkdown/plugin-math": "5.3.1",
52 | "@milkdown/plugin-slash": "5.3.1",
53 | "@milkdown/plugin-tooltip": "5.3.1",
54 | "@milkdown/preset-commonmark": "5.3.1",
55 | "@milkdown/preset-gfm": "5.3.1",
56 | "@milkdown/theme-nord": "5.3.1",
57 | "dayjs": "^1.10.4",
58 | "element-plus": "2.4.1",
59 | "eslint-plugin-oxlint": "^0.2.7",
60 | "file-saver": "^2.0.5",
61 | "js-md5": "^0.7.3",
62 | "localforage": "^1.10.0",
63 | "pako": "1.0.11",
64 | "pinia": "^2.0.11",
65 | "pinia-plugin-persistedstate": "^1.5.0",
66 | "rough-notation": "^0.5.1",
67 | "vue": "^3.2.4",
68 | "vue-eslint-parser": "^9.4.2",
69 | "vue-grid-layout": "3.0.0-beta1",
70 | "vue-i18n": "9",
71 | "vuedraggable": "^4.0.1"
72 | },
73 | "devDependencies": {
74 | "@commitlint/config-conventional": "^16.2.4",
75 | "@intlify/unplugin-vue-i18n": "^1.4.0",
76 | "@types/file-saver": "^2.0.2",
77 | "@types/js-md5": "^0.4.2",
78 | "@types/node": "^14.14.37",
79 | "@types/pako": "^1.0.6",
80 | "@typescript-eslint/eslint-plugin": "^6.21.0",
81 | "@typescript-eslint/parser": "^6.21.0",
82 | "@vitejs/plugin-vue": "^4.4.0",
83 | "@vitejs/plugin-vue-jsx": "^3.0.2",
84 | "adm-zip": "^0.5.5",
85 | "commitizen": "^4.2.4",
86 | "commitlint": "^16.2.4",
87 | "cross-env": "^7.0.3",
88 | "cz-conventional-changelog": "^3.3.0",
89 | "dotenv": "^16.3.1",
90 | "eslint": "^8.57.0",
91 | "eslint-plugin-vue": "^9.24.0",
92 | "husky": "^7.0.4",
93 | "oxlint": "^0.2.16",
94 | "qiniu": "^7.9.0",
95 | "rimraf": "^3.0.2",
96 | "sass": "^1.68.0",
97 | "typescript": "5.2.2",
98 | "vite": "^5.2.8",
99 | "vite-plugin-md": "^0.21.5"
100 | },
101 | "repository": {
102 | "type": "git",
103 | "url": "https://github.com/leon-kfd/dashboard"
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/public/background.js:
--------------------------------------------------------------------------------
1 |
2 | // chrome.browserAction.onClicked.addListener(function () {
3 | // chrome.management.getSelf(function (res) {
4 | // chrome.tabs.create({ url: 'chrome-extension://' + res.id + '/index.html' });
5 | // });
6 | // });
7 |
--------------------------------------------------------------------------------
/public/favicon-edge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-kfd/Dashboard/38b221553ad241b65ed59ab44498af5092509357/public/favicon-edge.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-kfd/Dashboard/38b221553ad241b65ed59ab44498af5092509357/public/favicon.ico
--------------------------------------------------------------------------------
/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-kfd/Dashboard/38b221553ad241b65ed59ab44498af5092509357/public/favicon.png
--------------------------------------------------------------------------------
/public/img/icons/bing-en.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/img/icons/bing.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/img/icons/google.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/img/icons/sougou.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/img/icons/taobao.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Howdz起始页",
3 | "version": "1.5.5",
4 | "description": "提供大量组件用于定制化你的起始页,可适配响应式设计。",
5 | "icons": {
6 | "128": "favicon.png"
7 | },
8 | "permissions": [],
9 | "background": {
10 | "service_worker": "sw.js"
11 | },
12 | "offline_enabled": true,
13 | "manifest_version": 3,
14 | "chrome_url_overrides": {
15 | "newtab": "index.html"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/public/sw.js:
--------------------------------------------------------------------------------
1 |
2 | if (location.href.includes('howdz.xyz')) {
3 | importScripts('https://cdn.staticfile.org/workbox-sw/7.0.0/workbox-sw.js')
4 | workbox.setConfig({
5 | debug: false,
6 | });
7 | console.log('sw.js is load by CDN!')
8 | } else {
9 | importScripts('./workbox/workbox-sw.js')
10 | workbox.setConfig({
11 | debug: false,
12 | modulePathPrefix: './workbox/'
13 | });
14 | console.log('sw.js is load by local!')
15 | }
16 |
17 | // Cache css/js/font.
18 | workbox.routing.registerRoute(
19 | ({ request }) => request.destination === 'style' || request.destination === 'script' || request.destination === 'font',
20 | new workbox.strategies.CacheFirst({
21 | cacheName: 'css-js-font',
22 | plugins: [
23 | new workbox.cacheableResponse.CacheableResponsePlugin({
24 | statuses: [200],
25 | }),
26 | new workbox.expiration.ExpirationPlugin({
27 | maxEntries: 50,
28 | maxAgeSeconds: 60 * 60 * 24 * 7, // 7 Days
29 | }),
30 | ]
31 | })
32 | );
33 |
34 | // Cache image.
35 | workbox.routing.registerRoute(
36 | ({ request }) => request.destination === 'image',
37 | new workbox.strategies.StaleWhileRevalidate({
38 | cacheName: 'image',
39 | plugins: [
40 | new workbox.cacheableResponse.CacheableResponsePlugin({
41 | statuses: [200],
42 | }),
43 | new workbox.expiration.ExpirationPlugin({
44 | maxEntries: 50,
45 | maxAgeSeconds: 60 * 60 * 24 * 7, // 7 Days
46 | })
47 | ]
48 | })
49 | )
50 |
51 | // Cache video
52 | workbox.routing.registerRoute(
53 | ({ request }) => request.destination === 'video',
54 | new workbox.strategies.CacheFirst({
55 | cacheName: 'video',
56 | plugins: [
57 | new workbox.cacheableResponse.CacheableResponsePlugin({
58 | statuses: [200],
59 | }),
60 | new workbox.expiration.ExpirationPlugin({
61 | maxEntries: 50,
62 | maxAgeSeconds: 60 * 60 * 24 * 7, // 7 Days
63 | }),
64 | new workbox.rangeRequests.RangeRequestsPlugin()
65 | ]
66 | })
67 | )
68 |
--------------------------------------------------------------------------------
/public/workbox/workbox-cacheable-response.prod.js:
--------------------------------------------------------------------------------
1 | this.workbox=this.workbox||{},this.workbox.cacheableResponse=function(s){"use strict";try{self["workbox:cacheable-response:6.1.5"]&&_()}catch(s){}class t{constructor(s={}){this.j=s.statuses,this.O=s.headers}isResponseCacheable(s){let t=!0;return this.j&&(t=this.j.includes(s.status)),this.O&&t&&(t=Object.keys(this.O).some((t=>s.headers.get(t)===this.O[t]))),t}}return s.CacheableResponse=t,s.CacheableResponsePlugin=class{constructor(s){this.cacheWillUpdate=async({response:s})=>this.B.isResponseCacheable(s)?s:null,this.B=new t(s)}},s}({});
2 |
--------------------------------------------------------------------------------
/public/workbox/workbox-expiration.prod.js:
--------------------------------------------------------------------------------
1 | this.workbox=this.workbox||{},this.workbox.expiration=function(t,s,e,i,a,n,h){"use strict";try{self["workbox:expiration:6.1.5"]&&_()}catch(t){}const r="cache-entries",c=t=>{const s=new URL(t,location.href);return s.hash="",s.href};class o{constructor(t){this.T=t,this.i=new e.DBWrapper("workbox-expiration",1,{onupgradeneeded:t=>this.M(t)})}M(t){const s=t.target.result.createObjectStore(r,{keyPath:"id"});s.createIndex("cacheName","cacheName",{unique:!1}),s.createIndex("timestamp","timestamp",{unique:!1}),i.deleteDatabase(this.T)}async setTimestamp(t,s){const e={url:t=c(t),timestamp:s,cacheName:this.T,id:this.L(t)};await this.i.put(r,e)}async getTimestamp(t){return(await this.i.get(r,this.L(t))).timestamp}async expireEntries(t,s){const e=await this.i.transaction(r,"readwrite",((e,i)=>{const a=e.objectStore(r).index("timestamp").openCursor(null,"prev"),n=[];let h=0;a.onsuccess=()=>{const e=a.result;if(e){const i=e.value;i.cacheName===this.T&&(t&&i.timestamp=s?n.push(e.value):h++),e.continue()}else i(n)}})),i=[];for(const t of e)await this.i.delete(r,t.id),i.push(t.url);return i}L(t){return this.T+"|"+c(t)}}class u{constructor(t,s={}){this.F=!1,this.H=!1,this.I=s.maxEntries,this.G=s.maxAgeSeconds,this.J=s.matchOptions,this.T=t,this.V=new o(t)}async expireEntries(){if(this.F)return void(this.H=!0);this.F=!0;const t=this.G?Date.now()-1e3*this.G:0,e=await this.V.expireEntries(t,this.I),i=await self.caches.open(this.T);for(const t of e)await i.delete(t,this.J);this.F=!1,this.H&&(this.H=!1,s.dontWaitFor(this.expireEntries()))}async updateTimestamp(t){await this.V.setTimestamp(t,Date.now())}async isURLExpired(t){if(this.G){return await this.V.getTimestamp(t){if(!a)return null;const n=this.W(a),h=this.X(i);s.dontWaitFor(h.expireEntries());const r=h.updateTimestamp(e.url);if(t)try{t.waitUntil(r)}catch(t){}return n?a:null},this.cacheDidUpdate=async({cacheName:t,request:s})=>{const e=this.X(t);await e.updateTimestamp(s.url),await e.expireEntries()},this.Y=t,this.G=t.maxAgeSeconds,this.Z=new Map,t.purgeOnQuotaError&&n.registerQuotaErrorCallback((()=>this.deleteCacheAndMetadata()))}X(t){if(t===a.cacheNames.getRuntimeName())throw new h.WorkboxError("expire-custom-caches-only");let s=this.Z.get(t);return s||(s=new u(t,this.Y),this.Z.set(t,s)),s}W(t){if(!this.G)return!0;const s=this.tt(t);if(null===s)return!0;return s>=Date.now()-1e3*this.G}tt(t){if(!t.headers.has("date"))return null;const s=t.headers.get("date"),e=new Date(s).getTime();return isNaN(e)?null:e}async deleteCacheAndMetadata(){for(const[t,s]of this.Z)await self.caches.delete(t),await s.delete();this.Z=new Map}},t}({},workbox.core._private,workbox.core._private,workbox.core._private,workbox.core._private,workbox.core,workbox.core._private);
2 |
--------------------------------------------------------------------------------
/public/workbox/workbox-range-requests.prod.js:
--------------------------------------------------------------------------------
1 | this.workbox=this.workbox||{},this.workbox.rangeRequests=function(t,e,n){"use strict";try{self["workbox:range-requests:5.1.4"]&&_()}catch(t){}async function r(t,n){try{if(206===n.status)return n;const r=t.headers.get("range");if(!r)throw new e.WorkboxError("no-range-header");const s=function(t){const n=t.trim().toLowerCase();if(!n.startsWith("bytes="))throw new e.WorkboxError("unit-must-be-bytes",{normalizedRangeHeader:n});if(n.includes(","))throw new e.WorkboxError("single-range-only",{normalizedRangeHeader:n});const r=/(\d*)-(\d*)/.exec(n);if(!r||!r[1]&&!r[2])throw new e.WorkboxError("invalid-range-values",{normalizedRangeHeader:n});return{start:""===r[1]?void 0:Number(r[1]),end:""===r[2]?void 0:Number(r[2])}}(r),a=await n.blob(),o=function(t,n,r){const s=t.size;if(r&&r>s||n&&n<0)throw new e.WorkboxError("range-not-satisfiable",{size:s,end:r,start:n});let a,o;return void 0!==n&&void 0!==r?(a=n,o=r+1):void 0!==n&&void 0===r?(a=n,o=s):void 0!==r&&void 0===n&&(a=s-r,o=s),{start:a,end:o}}(a,s.start,s.end),i=a.slice(o.start,o.end),d=i.size,u=new Response(i,{status:206,statusText:"Partial Content",headers:n.headers});return u.headers.set("Content-Length",String(d)),u.headers.set("Content-Range",`bytes ${o.start}-${o.end-1}/`+a.size),u}catch(t){return new Response("",{status:416,statusText:"Range Not Satisfiable"})}}return t.RangeRequestsPlugin=class{constructor(){this.cachedResponseWillBeUsed=async({request:t,cachedResponse:e})=>e&&t.headers.has("range")?await r(t,e):e}},t.createPartialResponse=r,t}({},workbox.core._private,workbox.core._private);
2 |
3 |
--------------------------------------------------------------------------------
/public/workbox/workbox-routing.prod.js:
--------------------------------------------------------------------------------
1 | this.workbox=this.workbox||{},this.workbox.routing=function(t,e){"use strict";try{self["workbox:routing:6.1.5"]&&_()}catch(t){}const s=t=>t&&"object"==typeof t?t:{handle:t};class r{constructor(t,e,r="GET"){this.handler=s(e),this.match=t,this.method=r}setCatchHandler(t){this.catchHandler=s(t)}}class n extends r{constructor(t,e,s){super((({url:e})=>{const s=t.exec(e.href);if(s&&(e.origin===location.origin||0===s.index))return s.slice(1)}),e,s)}}class i{constructor(){this.ft=new Map,this.dt=new Map}get routes(){return this.ft}addFetchListener(){self.addEventListener("fetch",(t=>{const{request:e}=t,s=this.handleRequest({request:e,event:t});s&&t.respondWith(s)}))}addCacheListener(){self.addEventListener("message",(t=>{if(t.data&&"CACHE_URLS"===t.data.type){const{payload:e}=t.data,s=Promise.all(e.urlsToCache.map((e=>{"string"==typeof e&&(e=[e]);const s=new Request(...e);return this.handleRequest({request:s,event:t})})));t.waitUntil(s),t.ports&&t.ports[0]&&s.then((()=>t.ports[0].postMessage(!0)))}}))}handleRequest({request:t,event:e}){const s=new URL(t.url,location.href);if(!s.protocol.startsWith("http"))return;const r=s.origin===location.origin,{params:n,route:i}=this.findMatchingRoute({event:e,request:t,sameOrigin:r,url:s});let o=i&&i.handler;const u=t.method;if(!o&&this.dt.has(u)&&(o=this.dt.get(u)),!o)return;let c;try{c=o.handle({url:s,request:t,event:e,params:n})}catch(t){c=Promise.reject(t)}const a=i&&i.catchHandler;return c instanceof Promise&&(this.wt||a)&&(c=c.catch((async r=>{if(a)try{return await a.handle({url:s,request:t,event:e,params:n})}catch(t){r=t}if(this.wt)return this.wt.handle({url:s,request:t,event:e});throw r}))),c}findMatchingRoute({url:t,sameOrigin:e,request:s,event:r}){const n=this.ft.get(s.method)||[];for(const i of n){let n;const o=i.match({url:t,sameOrigin:e,request:s,event:r});if(o)return n=o,(Array.isArray(o)&&0===o.length||o.constructor===Object&&0===Object.keys(o).length||"boolean"==typeof o)&&(n=void 0),{route:i,params:n}}return{}}setDefaultHandler(t,e="GET"){this.dt.set(e,s(t))}setCatchHandler(t){this.wt=s(t)}registerRoute(t){this.ft.has(t.method)||this.ft.set(t.method,[]),this.ft.get(t.method).push(t)}unregisterRoute(t){if(!this.ft.has(t.method))throw new e.WorkboxError("unregister-route-but-not-found-with-method",{method:t.method});const s=this.ft.get(t.method).indexOf(t);if(!(s>-1))throw new e.WorkboxError("unregister-route-route-not-registered");this.ft.get(t.method).splice(s,1)}}let o;const u=()=>(o||(o=new i,o.addFetchListener(),o.addCacheListener()),o);return t.NavigationRoute=class extends r{constructor(t,{allowlist:e=[/./],denylist:s=[]}={}){super((t=>this.gt(t)),t),this.qt=e,this.yt=s}gt({url:t,request:e}){if(e&&"navigate"!==e.mode)return!1;const s=t.pathname+t.search;for(const t of this.yt)if(t.test(s))return!1;return!!this.qt.some((t=>t.test(s)))}},t.RegExpRoute=n,t.Route=r,t.Router=i,t.registerRoute=function(t,s,i){let o;if("string"==typeof t){const e=new URL(t,location.href);o=new r((({url:t})=>t.href===e.href),s,i)}else if(t instanceof RegExp)o=new n(t,s,i);else if("function"==typeof t)o=new r(t,s,i);else{if(!(t instanceof r))throw new e.WorkboxError("unsupported-route-type",{moduleName:"workbox-routing",funcName:"registerRoute",paramName:"capture"});o=t}return u().registerRoute(o),o},t.setCatchHandler=function(t){u().setCatchHandler(t)},t.setDefaultHandler=function(t){u().setDefaultHandler(t)},t}({},workbox.core._private);
2 |
--------------------------------------------------------------------------------
/public/workbox/workbox-sw.js:
--------------------------------------------------------------------------------
1 | !function(){"use strict";try{self["workbox:sw:6.1.5"]&&_()}catch(t){}const t={backgroundSync:"background-sync",broadcastUpdate:"broadcast-update",cacheableResponse:"cacheable-response",core:"core",expiration:"expiration",googleAnalytics:"offline-ga",navigationPreload:"navigation-preload",precaching:"precaching",rangeRequests:"range-requests",routing:"routing",strategies:"strategies",streams:"streams",recipes:"recipes"};self.workbox=new class{constructor(){return this.v={},this.Pt={debug:"localhost"===self.location.hostname,modulePathPrefix:null,modulePathCb:null},this.$t=this.Pt.debug?"dev":"prod",this.jt=!1,new Proxy(this,{get(e,s){if(e[s])return e[s];const o=t[s];return o&&e.loadModule(`workbox-${o}`),e[s]}})}setConfig(t={}){if(this.jt)throw new Error("Config must be set before accessing workbox.* modules");Object.assign(this.Pt,t),this.$t=this.Pt.debug?"dev":"prod"}loadModule(t){const e=this.St(t);try{importScripts(e),this.jt=!0}catch(s){throw console.error(`Unable to import module '${t}' from '${e}'.`),s}}St(t){if(this.Pt.modulePathCb)return this.Pt.modulePathCb(t,this.Pt.debug);let e=["https://storage.googleapis.com/workbox-cdn/releases/6.1.5"];const s=`${t}.${this.$t}.js`,o=this.Pt.modulePathPrefix;return o&&(e=o.split("/"),""===e[e.length-1]&&e.splice(e.length-1,1)),e.push(s),e.join("/")}}}();
2 |
--------------------------------------------------------------------------------
/scripts/build-crx.cjs:
--------------------------------------------------------------------------------
1 | const AdmZip = require('adm-zip');
2 | const zip = new AdmZip();
3 | zip.addLocalFolder('crx');
4 | zip.writeZip('dist/howdz-dashboard.crx');
5 | zip.writeZip('dist/howdz-dashboard.zip');
6 | console.log('build crx file success!');
7 |
--------------------------------------------------------------------------------
/scripts/upload-cdn.cjs:
--------------------------------------------------------------------------------
1 | require('dotenv').config();
2 | const fs = require('fs')
3 | const qiniu = require('qiniu');
4 | const QiniuAccessKey = process.env.QINIU_ACCESS_KEY
5 | const QinuiSecretKey = process.env.QINIU_SECRET_KEY
6 | const mac = new qiniu.auth.digest.Mac(QiniuAccessKey, QinuiSecretKey);
7 | const bucket = 'howdy'
8 | const bucketManager = new qiniu.rs.BucketManager(mac)
9 |
10 | const clearAssets = async () => {
11 | try {
12 | const assetsFileList = await new Promise((resolve, reject) => {
13 | bucketManager.listPrefix(bucket, {
14 | limit: 100,
15 | prefix: 'howdz/dist/assets/',
16 | }, function(err, respBody, respInfo) {
17 | if (err) {
18 | reject(err)
19 | }
20 | if (respInfo.statusCode === 200) {
21 | const list = respBody.items.map(item => item.key)
22 | resolve(list)
23 | } else {
24 | reject(new Error('Server error'))
25 | }
26 | });
27 | })
28 | const deleteOperations = assetsFileList.map(item => {
29 | return qiniu.rs.deleteOp(bucket, item)
30 | })
31 | deleteOperations.push(qiniu.rs.deleteOp(bucket, 'howdz/dist/index.html'))
32 | await new Promise((resolve, reject) => {
33 | bucketManager.batch(deleteOperations, function(err, respBody, respInfo) {
34 | if (err) {
35 | reject(err)
36 | } else {
37 | if (parseInt(respInfo.statusCode / 100) === 2) {
38 | resolve(1)
39 | } else {
40 | reject(new Error('Batch delete error'))
41 | }
42 | }
43 | })
44 | })
45 | } catch (e) {
46 | console.error(e)
47 | }
48 | }
49 |
50 | const uploadFile = async (fileString, fileName, isAssets = true) => {
51 | if (!fileName) {
52 | throw new Error('Lose filename')
53 | }
54 | const key = `howdz/dist/${isAssets ? 'assets/' : ''}${fileName}`
55 | const putPolicy = new qiniu.rs.PutPolicy({
56 | scope: bucket
57 | });
58 | const uploadToken = putPolicy.uploadToken(mac);
59 | const formUploader = new qiniu.form_up.FormUploader();
60 | const putExtra = new qiniu.form_up.PutExtra();
61 | return await new Promise((resolve, reject) => {
62 | formUploader.putFile(uploadToken, key, fileString, putExtra, function(err, ret) {
63 | if (!err) {
64 | resolve(ret)
65 | } else {
66 | reject(err)
67 | }
68 | })
69 | })
70 | }
71 |
72 | const uploadAssets = () => {
73 | const assetsFileList = fs.readdirSync('dist/assets')
74 | const task = assetsFileList.map(item => {
75 | return uploadFile(`dist/assets/${item}`, item, true)
76 | })
77 | return Promise.all(task)
78 | }
79 |
80 | const task = async () => {
81 | try {
82 | await clearAssets()
83 | await uploadAssets()
84 | await uploadFile('dist/index.html', 'index.html', false)
85 | if (fs.existsSync('dist/howdz-dashboard.zip')) {
86 | await uploadFile('dist/howdz-dashboard.zip', 'howdz-dashboard.zip', false)
87 | }
88 | if (fs.existsSync('dist/howdz-dashboard.crx')) {
89 | await uploadFile('dist/howdz-dashboard.crx', 'howdz-dashboard.crx', false)
90 | }
91 | console.log('upload success')
92 | } catch (e) {
93 | console.error(e)
94 | }
95 | }
96 |
97 | task()
98 |
--------------------------------------------------------------------------------
/src/assets/custom-animate.scss:
--------------------------------------------------------------------------------
1 | /* animate.css flipInY*/
2 | @keyframes flipInY {
3 | from {
4 | transform: perspective(400px) scale(0.5) rotate3d(0, 1, 0, 90deg);
5 | animation-timing-function: ease-in;
6 | opacity: 0;
7 | }
8 |
9 | 40% {
10 | transform: perspective(400px) scale(1) rotate3d(0, 1, 0, -20deg);
11 | animation-timing-function: ease-in;
12 | }
13 |
14 | 60% {
15 | transform: perspective(400px) rotate3d(0, 1, 0, 10deg);
16 | opacity: 1;
17 | }
18 |
19 | 80% {
20 | transform: perspective(400px) rotate3d(0, 1, 0, -5deg);
21 | }
22 |
23 | to {
24 | transform: perspective(400px);
25 | }
26 | }
27 | .animate__flipInY {
28 | backface-visibility: visible !important;
29 | animation-name: flipInY;
30 | }
31 |
32 | /* vue transition fadeIn */
33 | .fadeIn-enter-active {
34 | animation: fadeInCustom 0.5s;
35 | }
36 | .fadeIn-leave-active {
37 | animation: fadeInCustom 0.5s reverse;
38 | }
39 | .fadeOut-leave-active {
40 | animation: fadeInCustom 0.5s reverse;
41 | }
42 | @keyframes fadeInCustom {
43 | from {
44 | opacity: 0;
45 | }
46 | to {
47 | opacity: 1;
48 | }
49 | }
50 |
51 | /* vue transition fadeInUp */
52 | .fadeInUp-enter-active {
53 | animation: fadeInUpCustom 0.5s;
54 | }
55 | .fadeInUp-leave-active {
56 | animation: fadeInUpCustom 0.5s reverse;
57 | }
58 | @keyframes fadeInUpCustom {
59 | from {
60 | opacity: 0;
61 | transform: translateY(10px);
62 | }
63 | to {
64 | opacity: 1;
65 | transform: translateY(0);
66 | }
67 | }
68 |
69 | @keyframes bgmove {
70 | 0% {
71 | background-position: 0 0;
72 | }
73 | 50% {
74 | background-position: 100% 0;
75 | }
76 | 100% {
77 | background-position: 0 0;
78 | }
79 | }
80 |
81 | .zoomIn-enter-active {
82 | animation: zoomInCustom 0.4s;
83 | }
84 | .zoomIn-leave-active {
85 | animation: zoomInCustom 0.4s reverse;
86 | }
87 | @keyframes zoomInCustom {
88 | from {
89 | opacity: 0;
90 | transform: scale(0);
91 | }
92 | to {
93 | opacity: 1;
94 | transform: scale(1);
95 | }
96 | }
97 |
98 |
99 | .infinty-loading {
100 | animation: infintyLoading infinite 2s;
101 | }
102 | @keyframes infintyLoading {
103 | from {
104 | transform: rotate(0);
105 | }
106 | to {
107 | transform: rotate(360deg);
108 | }
109 | }
--------------------------------------------------------------------------------
/src/assets/element-modules.scss:
--------------------------------------------------------------------------------
1 | @use "element-plus/theme-chalk/src/base.scss";
2 | @use "element-plus/theme-chalk/src/dialog.scss";
3 | @use "element-plus/theme-chalk/src/input.scss";
4 | @use "element-plus/theme-chalk/src/input-number.scss";
5 | @use "element-plus/theme-chalk/src/radio.scss";
6 | @use "element-plus/theme-chalk/src/radio-group.scss";
7 | @use "element-plus/theme-chalk/src/radio-button.scss";
8 | @use "element-plus/theme-chalk/src/checkbox.scss";
9 | @use "element-plus/theme-chalk/src/checkbox-button.scss";
10 | @use "element-plus/theme-chalk/src/checkbox-group.scss";
11 | @use "element-plus/theme-chalk/src/switch.scss";
12 | @use "element-plus/theme-chalk/src/select.scss";
13 | @use "element-plus/theme-chalk/src/option.scss";
14 | @use "element-plus/theme-chalk/src/button.scss";
15 | @use "element-plus/theme-chalk/src/date-picker.scss";
16 | @use "element-plus/theme-chalk/src/tooltip.scss";
17 | @use "element-plus/theme-chalk/src/popper.scss";
18 | @use "element-plus/theme-chalk/src/form.scss";
19 | @use "element-plus/theme-chalk/src/form-item.scss";
20 | @use "element-plus/theme-chalk/src/tabs.scss";
21 | @use "element-plus/theme-chalk/src/tab-pane.scss";
22 | @use "element-plus/theme-chalk/src/notification.scss";
23 | @use "element-plus/theme-chalk/src/message.scss";
24 | @use "element-plus/theme-chalk/src/color-picker.scss";
25 | @use "element-plus/theme-chalk/src/image.scss";
26 | @use "element-plus/theme-chalk/src/alert.scss";
27 | @use "element-plus/theme-chalk/src/select-dropdown.scss";
28 | @use "element-plus/theme-chalk/src/scrollbar.scss";
29 |
--------------------------------------------------------------------------------
/src/assets/element-variables.scss:
--------------------------------------------------------------------------------
1 | @forward "element-plus/theme-chalk/src/common/var.scss" with (
2 | $colors: (
3 | "primary": ("base": rgb(46, 90, 219)),
4 | "success": ("base": rgb(100, 201, 53)),
5 | "warning": ("base": rgb(233, 174, 49)),
6 | "danger": ("base": rgb(216, 94, 94)),
7 | ),
8 | $border-radius: (
9 | 'base': 0,
10 | 'small': 0
11 | ),
12 | $input: (
13 | 'text-color': '#464649',
14 | 'bg-color': '#F7F7F9',
15 | 'placeholder-color': '#8D8DA5'
16 | ),
17 | $popover: (
18 | 'border-radius': 0
19 | ),
20 | $popper: (
21 | 'border-radius': 0
22 | )
23 | );
24 | @import "@/assets/variables.scss";
--------------------------------------------------------------------------------
/src/assets/imgs/capture-new/Bookmark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-kfd/Dashboard/38b221553ad241b65ed59ab44498af5092509357/src/assets/imgs/capture-new/Bookmark.png
--------------------------------------------------------------------------------
/src/assets/imgs/capture-new/Clock.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-kfd/Dashboard/38b221553ad241b65ed59ab44498af5092509357/src/assets/imgs/capture-new/Clock.png
--------------------------------------------------------------------------------
/src/assets/imgs/capture-new/Collection.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-kfd/Dashboard/38b221553ad241b65ed59ab44498af5092509357/src/assets/imgs/capture-new/Collection.png
--------------------------------------------------------------------------------
/src/assets/imgs/capture-new/CountDown.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-kfd/Dashboard/38b221553ad241b65ed59ab44498af5092509357/src/assets/imgs/capture-new/CountDown.png
--------------------------------------------------------------------------------
/src/assets/imgs/capture-new/Day.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-kfd/Dashboard/38b221553ad241b65ed59ab44498af5092509357/src/assets/imgs/capture-new/Day.png
--------------------------------------------------------------------------------
/src/assets/imgs/capture-new/Editor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-kfd/Dashboard/38b221553ad241b65ed59ab44498af5092509357/src/assets/imgs/capture-new/Editor.png
--------------------------------------------------------------------------------
/src/assets/imgs/capture-new/Empty.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-kfd/Dashboard/38b221553ad241b65ed59ab44498af5092509357/src/assets/imgs/capture-new/Empty.png
--------------------------------------------------------------------------------
/src/assets/imgs/capture-new/GithubTrending.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-kfd/Dashboard/38b221553ad241b65ed59ab44498af5092509357/src/assets/imgs/capture-new/GithubTrending.png
--------------------------------------------------------------------------------
/src/assets/imgs/capture-new/Iframe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-kfd/Dashboard/38b221553ad241b65ed59ab44498af5092509357/src/assets/imgs/capture-new/Iframe.png
--------------------------------------------------------------------------------
/src/assets/imgs/capture-new/JuejinList.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-kfd/Dashboard/38b221553ad241b65ed59ab44498af5092509357/src/assets/imgs/capture-new/JuejinList.png
--------------------------------------------------------------------------------
/src/assets/imgs/capture-new/MovieLines.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-kfd/Dashboard/38b221553ad241b65ed59ab44498af5092509357/src/assets/imgs/capture-new/MovieLines.png
--------------------------------------------------------------------------------
/src/assets/imgs/capture-new/Search.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-kfd/Dashboard/38b221553ad241b65ed59ab44498af5092509357/src/assets/imgs/capture-new/Search.png
--------------------------------------------------------------------------------
/src/assets/imgs/capture-new/TodoList.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-kfd/Dashboard/38b221553ad241b65ed59ab44498af5092509357/src/assets/imgs/capture-new/TodoList.png
--------------------------------------------------------------------------------
/src/assets/imgs/capture-new/Verse.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-kfd/Dashboard/38b221553ad241b65ed59ab44498af5092509357/src/assets/imgs/capture-new/Verse.png
--------------------------------------------------------------------------------
/src/assets/imgs/capture-new/Weather.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-kfd/Dashboard/38b221553ad241b65ed59ab44498af5092509357/src/assets/imgs/capture-new/Weather.png
--------------------------------------------------------------------------------
/src/assets/imgs/capture-new/WeiboList.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-kfd/Dashboard/38b221553ad241b65ed59ab44498af5092509357/src/assets/imgs/capture-new/WeiboList.png
--------------------------------------------------------------------------------
/src/assets/imgs/capture-new/ZhihuList.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-kfd/Dashboard/38b221553ad241b65ed59ab44498af5092509357/src/assets/imgs/capture-new/ZhihuList.png
--------------------------------------------------------------------------------
/src/assets/imgs/icons/bing-en.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/imgs/icons/bing.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/imgs/icons/google.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/imgs/icons/sougou.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/imgs/icons/taobao.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/imgs/theme/base.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-kfd/Dashboard/38b221553ad241b65ed59ab44498af5092509357/src/assets/imgs/theme/base.png
--------------------------------------------------------------------------------
/src/assets/imgs/theme/mobile-pro.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-kfd/Dashboard/38b221553ad241b65ed59ab44498af5092509357/src/assets/imgs/theme/mobile-pro.png
--------------------------------------------------------------------------------
/src/assets/imgs/theme/mobile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-kfd/Dashboard/38b221553ad241b65ed59ab44498af5092509357/src/assets/imgs/theme/mobile.png
--------------------------------------------------------------------------------
/src/assets/imgs/theme/module.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-kfd/Dashboard/38b221553ad241b65ed59ab44498af5092509357/src/assets/imgs/theme/module.png
--------------------------------------------------------------------------------
/src/assets/imgs/theme/movie-lines.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-kfd/Dashboard/38b221553ad241b65ed59ab44498af5092509357/src/assets/imgs/theme/movie-lines.png
--------------------------------------------------------------------------------
/src/assets/imgs/theme/multi.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-kfd/Dashboard/38b221553ad241b65ed59ab44498af5092509357/src/assets/imgs/theme/multi.png
--------------------------------------------------------------------------------
/src/assets/imgs/theme/simple.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-kfd/Dashboard/38b221553ad241b65ed59ab44498af5092509357/src/assets/imgs/theme/simple.png
--------------------------------------------------------------------------------
/src/assets/imgs/theme/tabs.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-kfd/Dashboard/38b221553ad241b65ed59ab44498af5092509357/src/assets/imgs/theme/tabs.gif
--------------------------------------------------------------------------------
/src/assets/imgs/video-thumb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-kfd/Dashboard/38b221553ad241b65ed59ab44498af5092509357/src/assets/imgs/video-thumb.png
--------------------------------------------------------------------------------
/src/assets/imgs/weather-animation-icon/clear-day.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/imgs/weather-animation-icon/cloudy.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/imgs/weather-animation-icon/drizzle.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/imgs/weather-animation-icon/fog.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/imgs/weather-animation-icon/hurricane.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/imgs/weather-animation-icon/mist.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/imgs/weather-animation-icon/not-available.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/imgs/weather-animation-icon/overcast-day.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/imgs/weather-animation-icon/overcast.svg:
--------------------------------------------------------------------------------
1 |
43 |
--------------------------------------------------------------------------------
/src/assets/imgs/weather-animation-icon/rain.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/imgs/weather-animation-icon/sleet.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/imgs/weather-animation-icon/snow.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/imgs/weather-animation-icon/thermometer-colder.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/imgs/weather-animation-icon/thermometer-warmer.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/imgs/weather-animation-icon/thunderstorms-rain.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/imgs/weather-animation-icon/wind.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/imgs/weather-static-icon/clear-day.svg:
--------------------------------------------------------------------------------
1 |
13 |
--------------------------------------------------------------------------------
/src/assets/imgs/weather-static-icon/cloudy.svg:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/src/assets/imgs/weather-static-icon/drizzle.svg:
--------------------------------------------------------------------------------
1 |
41 |
--------------------------------------------------------------------------------
/src/assets/imgs/weather-static-icon/fog.svg:
--------------------------------------------------------------------------------
1 |
30 |
--------------------------------------------------------------------------------
/src/assets/imgs/weather-static-icon/hurricane.svg:
--------------------------------------------------------------------------------
1 |
13 |
--------------------------------------------------------------------------------
/src/assets/imgs/weather-static-icon/mist.svg:
--------------------------------------------------------------------------------
1 |
33 |
--------------------------------------------------------------------------------
/src/assets/imgs/weather-static-icon/not-available.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/src/assets/imgs/weather-static-icon/overcast-day.svg:
--------------------------------------------------------------------------------
1 |
49 |
--------------------------------------------------------------------------------
/src/assets/imgs/weather-static-icon/overcast.svg:
--------------------------------------------------------------------------------
1 |
29 |
--------------------------------------------------------------------------------
/src/assets/imgs/weather-static-icon/rain.svg:
--------------------------------------------------------------------------------
1 |
41 |
--------------------------------------------------------------------------------
/src/assets/imgs/weather-static-icon/snow.svg:
--------------------------------------------------------------------------------
1 |
62 |
--------------------------------------------------------------------------------
/src/assets/imgs/weather-static-icon/thermometer-colder.svg:
--------------------------------------------------------------------------------
1 |
31 |
--------------------------------------------------------------------------------
/src/assets/imgs/weather-static-icon/thermometer-warmer.svg:
--------------------------------------------------------------------------------
1 |
31 |
--------------------------------------------------------------------------------
/src/assets/imgs/weather-static-icon/thunderstorms-rain.svg:
--------------------------------------------------------------------------------
1 |
44 |
--------------------------------------------------------------------------------
/src/assets/imgs/weather-static-icon/wind.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/variables.scss:
--------------------------------------------------------------------------------
1 | $color-primary: rgb(46, 90, 219);
2 | $color-success: rgb(100, 201, 53);
3 | $color-warning: rgb(233, 174, 49);
4 | $color-danger: rgb(216, 94, 94);
5 | $color-dark: #363636;
6 | $color-grey1: #43434b;
7 | $color-grey2: #56565c;
8 | $color-grey3: #898992;
9 | $color-grey4: #aaa2b3;
10 | $color-grey5: #d4d4e0;
11 | $color-white: #f9f9fa;
12 |
13 | @mixin flex-center {
14 | display: flex;
15 | justify-content: center;
16 | align-items: center;
17 | }
18 |
19 | @mixin flex-center-x {
20 | display: flex;
21 | justify-content: center;
22 | }
23 |
24 | @mixin flex-center-y {
25 | display: flex;
26 | align-items: center;
27 | }
--------------------------------------------------------------------------------
/src/components/Axuiliary/CleanCache.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ $t('clearDataTips1') }}{{ $t('clearDataTips2') }}
5 |
6 |
7 |
10 |
11 |
12 |
13 |
14 |
15 |
41 |
61 |
--------------------------------------------------------------------------------
/src/components/Axuiliary/FAQ.md:
--------------------------------------------------------------------------------
1 | ### 如何切换背景图?
2 |
3 | 点击右下角菜单进入全局设置,可选择设置背景图,当前支持`本地图片`、`网络图片`、`随机图片`等方式。当选择随机图片源后可以设置**自动刷新**,并且在主页面右键菜单会加入刷新壁纸菜单。随机图片源还允许你点击喜欢然后添加到**个人收藏库**,当收藏足够多壁纸后可以以自己的个人收藏库作为**随机图片源**。
4 |
5 | ### 如何更改组件大小与位置?
6 |
7 | 点击右下角菜单锁定按钮解除锁定进入`编辑模式`,**右键相应组件打开菜单**(移动端下可使用长按操作唤起菜单)选择基础配置可更改组件大小。当前组件有 2 种定位模式,按**栅格布局**与 **Fix 固定布局**,2 种模式都支持直接拖拽更改位置和拖拽右下角更改组件大小。
8 |
9 | ### 如何在另一设备同步我的配置?
10 |
11 | 当前提供 2 种数据同步方案,使用**密钥同步**(推荐)与**JSON 文件**同步,打开辅助功能弹出,选择导入导出,在导出模块生成密钥后即可复制密钥到另一设备进行导入。
12 |
13 | ### 组件被另外的组件阻挡?
14 |
15 | 可以在组件基础配置中,找到**ZIndex**配置项,尝试加大该值可将组件层级顺序加大。
16 |
17 | ### 如何实现自定义字体?
18 |
19 | 目前物料组件的字体库是可以选择亦可手动输入的。要实现自定义字体,可在全局设置的**CSS 注入**里面使用`@import`加载线上字体,然后字体库中手动输入字体名称即可。可在[Google Fonts](https://fonts.google.com/)、[Google Fonts 中文版](https://www.googlefonts.cn/)等网站寻找你喜欢的字体。
20 |
21 | ### 如何添加自定义搜索引擎?
22 |
23 | 在`Search`物料中,在组件配置中找到添加按钮即可添加自定义搜索引擎。其中搜索地址中关键词使用`[0]`进行占位。另外,这种方式也可以实现屏蔽某些网站搜索结果,例如**百度搜索屏蔽 CSDN**,其引擎地址为`https://baidu.com/s?wd=[0] -csdn`
24 |
25 | ### 如何导入浏览器书签?
26 |
27 | 在`Bookmark`物料中,可以添加自己收藏的网站,同时允许从 Chrome 或 Edge 浏览器导出的 HTML 书签文件进行导入。可以在`chrome://bookmarks/`右上角选择导出书签文件。书签的解析是使用**纯前端解析**,你不需担心隐私问题。另外书签文件记录的 ICON 是 16 x 16,若过于模糊可以编辑重新获取。
28 |
29 | ### 什么是预览模式?
30 |
31 | 用户数据默认是记录在`LocalStorage`中进行持久性存储,预览模式允许在当前地址后拼接`?preview={previewKey}`进入预览模式。预览模式下的数据存储在`SessionStroge`中,不会对原站的数据进行改写。
32 |
33 | `previewKey`可以是从页面导出生成的exportKey,也可以是一个远程JSON地址。该功能能快速将你自定义的站点分享给好友。以下是使用示例:
34 | + [https://howdz.xyz?preview=7ZMSA](https://howdz.xyz?preview=7ZMSA)
35 | + [https://howdz.xyz?preview=https://raw.gitmirror.com/leon-kfd/Dashboard/main/src/components/Global/DefaultThemeData/Simple.json](https://howdz.xyz?preview=https://raw.gitmirror.com/leon-kfd/Dashboard/main/src/components/Global/DefaultThemeData/Simple.json)
36 |
37 | ### 为什么 IFrame 组件无法正常加载?
38 |
39 | 因为当前系统部署在**HTTPS**站点下,新版 Chrome 等浏览器已不支持 https 与 http 协议混用,所以 Iframe 组件只支持 https 的外部网站。同时目标网站需要允许跨域加载,允许Iframe嵌入。
40 |
41 | ### 如何重新选择预设主题?
42 |
43 | **预设主题**只会第一次进入弹出,若想体验其他预设主题,你可以在辅助功能找到**清除数据**,清空数据后可以重新选择预设主题。清空数据会清空所有配置,请谨慎操作。
44 |
45 | ### 浏览器插件起始页突然空白?
46 |
47 | 出现这个情况一般是因为浏览器插件有了新版本,并且浏览器后台已自动更新完毕,这时**重启浏览器**即可。
48 |
--------------------------------------------------------------------------------
/src/components/Axuiliary/FAQ.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
57 |
122 |
--------------------------------------------------------------------------------
/src/components/FormControl/FontSelector.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
18 |
19 |
20 |
21 | - The quick brown fox jumps over a lazy dog.
22 | -
23 | The quick brown fox jumps over a lazy dog.
24 |
25 | - 落霞与孤鹜齐飞,秋水共长天一色。
26 | -
27 | 落霞与孤鹜齐飞,秋水共长天一色。
28 |
29 | - 0 1 2 3 4 5 6 7 8 9
30 | -
31 | 0 1 2 3 4 5 6 7 8 9
32 |
33 |
34 |
35 |
36 |
{{ item.cn }}
37 |
38 | {{ item.en }}
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
95 |
122 |
--------------------------------------------------------------------------------
/src/components/FormControl/StandardColorPicker.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
23 |
34 |
--------------------------------------------------------------------------------
/src/components/FormControl/WarnLock.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
30 |
32 |
--------------------------------------------------------------------------------
/src/components/Global/BackgroundEffect.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
25 |
26 |
35 |
--------------------------------------------------------------------------------
/src/components/Global/BackgroundEffect/FireFlies.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/src/components/Global/BackgroundEffect/Focus.vue:
--------------------------------------------------------------------------------
1 |
2 |
15 |
16 |
17 |
67 |
68 |
74 |
--------------------------------------------------------------------------------
/src/components/Global/BackgroundEffect/RainDrop.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
107 |
108 |
115 |
--------------------------------------------------------------------------------
/src/components/Global/BackgroundEffect/Star.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
20 |
21 |
28 |
--------------------------------------------------------------------------------
/src/components/Global/IframeOpener.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
36 |
37 |
44 |
--------------------------------------------------------------------------------
/src/components/Tools/Confirm.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 | {{ message }}
11 |
12 |
13 |
21 |
22 |
23 |
24 |
25 |
65 |
--------------------------------------------------------------------------------
/src/components/Tools/Icon.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
38 |
--------------------------------------------------------------------------------
/src/components/Tools/Loading.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
17 |
--------------------------------------------------------------------------------
/src/components/Tools/TextLoading.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Loading...
4 |
5 |
6 |
15 |
--------------------------------------------------------------------------------
/src/components/Tools/Tips.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 | {{ $attrs.content }}
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
25 |
34 |
--------------------------------------------------------------------------------
/src/components/Tools/Unset.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ tips || '🔨Wait for setting' }}
4 |
5 |
6 |
7 |
18 |
34 |
--------------------------------------------------------------------------------
/src/global.ts:
--------------------------------------------------------------------------------
1 | export const apiURL = import.meta.env.PROD ? 'https://kongfandong.cn' : '/api';
2 | export const publicPath = import.meta.env.VITE_APP_BUILD_MODE === 'crx'
3 | ? './'
4 | : import.meta.env.VITE_APP_BUILD_MODE === 'cdn'
5 | ? 'https://cdn.kongfandong.cn/howdz/dist/'
6 | : '/'
7 |
--------------------------------------------------------------------------------
/src/hooks/useScreenMode.ts:
--------------------------------------------------------------------------------
1 | import { onMounted, onUnmounted, ref } from 'vue'
2 | export default function () {
3 | const windowWidth = ref(0)
4 | const windowHeight = ref(0)
5 | const screenMode = ref(0)
6 | const fr = ref(0)
7 | const getScreenMode = () => {
8 | const w = window.innerWidth
9 | return w <= 721 ? 0 : w <= 1981 ? 1 : 2
10 | }
11 | const setValue = () => {
12 | screenMode.value = getScreenMode()
13 | const sw = ~~document.body.offsetWidth - 5
14 | windowWidth.value = sw
15 | windowHeight.value = window.innerHeight
16 | fr.value = screenMode.value === 0 ? sw / 12 : screenMode.value === 1 ? sw / 24 : sw / 36
17 | }
18 | let timer: number
19 | const onWindowSizeChange = () => {
20 | if (timer) window.clearTimeout(timer)
21 | timer = window.setTimeout(() => {
22 | setValue()
23 | }, 200)
24 | }
25 | // setValue()
26 | onMounted(() => window.addEventListener('resize', onWindowSizeChange))
27 | onUnmounted(() => window.removeEventListener('resize', onWindowSizeChange))
28 | onWindowSizeChange()
29 | return { windowWidth, windowHeight, screenMode, fr }
30 | }
31 |
--------------------------------------------------------------------------------
/src/lang/index.ts:
--------------------------------------------------------------------------------
1 | import { createI18n } from 'vue-i18n'
2 | import en from './locales/en.json'
3 | import zhCN from './locales/zh-cn.json'
4 |
5 | export const i18n = createI18n({
6 | legacy: false,
7 | locale: 'zh-cn',
8 | fallbackLocale: 'zh-cn',
9 | globalInjection: true,
10 | missingWarn: false,
11 | fallbackWarn: false,
12 | messages: {
13 | en,
14 | 'zh-cn': zhCN
15 | }
16 | })
17 |
18 | export const langList = [
19 | {
20 | label: '简体中文',
21 | value: 'zh-cn'
22 | },
23 | {
24 | label: 'English',
25 | value: 'en'
26 | }
27 | ]
28 |
29 | export default {}
--------------------------------------------------------------------------------
/src/lang/locales/zh-cn.json:
--------------------------------------------------------------------------------
1 | {
2 | "themeWarningText": "若对选择后的预设主题不满意可在辅助功能中清除数据后即可重新选择。另若在使用中遇到问题现在辅助功能的常见问题尝试寻找解决方案或在Github Issue中留言。",
3 | "editStatueWarningText": "编辑状态会出现提示边框,同时可以进行组件拖拽、右键菜单配置等",
4 | "siteTitleTips": "自定义网站的标题,刷新页面仍生效",
5 | "siteIconTips": "自定义网站标签页Icon, 需从图标库选择svg图标",
6 | "disabledDialogAnimationTips": "在一些低性能的机器上可禁用弹窗动画以优化性能",
7 | "showMenuBtnTips": "是否展示右下角菜单按钮组",
8 | "injectCSSPlaceholder": "请输入合法的CSS代码,此处写入CSS代码会插入到网页中,以覆盖默认样式",
9 | "injectJSPlaceholder": "如非必要,并不建议往网页注入JavaScript代码,另外自定义注入JS无法在浏览器插件模式下使用",
10 |
11 | "bgImgTips": "支持输入Video视频网络路径会自动识别成动态壁纸,需要原生浏览器Video支持播放的视频格式",
12 | "refreshDurationTips": "可配置定时刷新随机壁纸,单位为秒,设置为0为不启用定时刷新,最低频率为30s",
13 | "refreshBtnTips": "是否在左下角展示刷新、收藏等操作按钮,即使关闭你仍可使用右键菜单进行操作",
14 | "brightnessTips": "基于CSS3:filter属性的brightness",
15 | "blurTips": "基于CSS3:filter属性的blur",
16 |
17 | "positionTips1": "栅格模式采用布局栅格化,组件大小响应式",
18 | "positionTips2": "Fixed模式使用会让组件固定在屏幕相应位置",
19 | "fixedTips1": "Fixed定位方向属性,例如想固定到右下角请选右下",
20 | "fixedTips2": "在编辑模式下直接拖拽组件也可更改偏移参数",
21 | "sizeUnitTips1": "PX为固定宽高模式",
22 | "sizeUnitTips2": "FR为响应式单位,屏幕满屏宽为12份",
23 | "sizeUnitTips3": "在编辑模式下拖拽组件右下角箭头也可更改组件尺寸",
24 | "shadowPlaceholder": "合法box-shadow, e.g(0 4px #89909c)",
25 | "shadowTips": "基于CSS3的box-shadow属性,应输入合法的CSS盒子阴影代码片段",
26 | "zIndexTips": "物料组件的层级Zindex, 若出现组件被阻挡可尝试更改此值",
27 | "customIdTips": "组件的自定义ID,一般用于注入CSS或JS代码操作",
28 |
29 | "colorPlaceholder": "请输入合法的颜色值",
30 | "warnLockTips": "编辑完成后请点击锁定防止误操作!",
31 | "recommendVideoTips": "以下提供部分推荐动态壁纸选择,来源于网络CDN,有可能出现无法访问问题,你也可以手动输入自定义的网络Https视频路径。视频会被缓存,若缓存过期或加载异常请重新选择重置。",
32 | "actionMaterialTips": "更换物料会重置为默认配置,请谨慎操作",
33 |
34 | "clearDataTips": "数据删除后不可恢复,是否继续?",
35 | "clearDataTips1": "所有配置后的数据会保存到浏览器中,你可以点击下方按钮清除所有数据并恢复为初始状态,但注意数据删除后是",
36 | "clearDataTips2": "不可恢复的,请谨慎操作!",
37 | "tabsTips": "允许用户配置出多个独立的标签页面,双击标题可以重命名。",
38 | "tabsSwitchBtnTips": "在页面底部展示切换按钮",
39 | "tabsKeyboardSwitchTips": "使用DOWN或RIGHT切换下一个,设置刷新页面后才会生效",
40 |
41 | "baseFontSizeTips": "组件的字体大小基于em单位,可以更改该参数设置组件字体显示",
42 | "textShadowTips": "基于CSS3的text-shadow属性,应输入合法的CSS字体阴影代码片段",
43 | "iconShadowTips": "基于CSS3的filter:drop-shadow()属性,应输入合法的drop-shadow阴影代码片段",
44 | "clickActionTypeTips": "配置Logo的点击事件",
45 | "boxSizeTips": "图标外层容器的大小,只能为正方形",
46 | "closeClickOutsideTips": "文件夹弹窗是否可以点击外层进行关闭",
47 | "durationTips": "定时器刷新频率, 单位为ms",
48 | "durationSecondTips": "定时器刷新频率, 单位为秒",
49 | "durationMinuteTips": "定时器刷新频率, 单位为分钟",
50 | "keyboardEventTips": "开启后按下相应按键则会跳转到绑定的网页",
51 | "dayjsFormatterTips": "自定义Dayjs格式请点击下方链接前往查看",
52 | "chineseWeekDayTips": "只是强制将dddd格式转为中文的星期展示,但并不会转换其他格式为中文,因为没有引入中文语言包",
53 | "posterTypeTips": "剧照壁纸较适合在横屏的组件中,且图片较高清",
54 | "clickMovieLinesActionTypeTips": "配置电影台词区域的点击事件",
55 | "spotlightTips": "为电影壁纸图片添加椭圆聚光灯滤镜特效(Beta)",
56 | "clickVerseActionTypeTips": "配置古诗文本的点击事件",
57 | "searchConfigTips": "拖拽下方图标可更换引擎顺序,也可添加自定义引擎,对于自定义引擎双击图标重新编辑",
58 | "engineConfigTips1": "默认搜索内容会被拼接到引擎地址末尾, 也可以使用",
59 | "engineConfigTips2": "对原地址搜索关键词进行占位",
60 | "hiddenAddBtnTips": "隐藏添加按钮,一般在所有添加完成后可关闭,但你仍可以使用右键菜单进行添加",
61 | "textStrokeTips": "开启镂空字效果,基于-webkit-text-stroke和-webkit-text-fill-color,部分浏览器不支持",
62 |
63 | "bgEffectSelectorTips": "基于WebGL/CSS3实现, 可能在部分低端浏览器或低端机型失效, 设置更改后需刷新页面后才起效",
64 | "exportExpireTips": "密钥有效期为7天,请在有效期内尽快导入",
65 | "glassTips": "仅当背景含有透明度时才生效, 基于backdrop-filter属性",
66 |
67 | "iframeRefreshTips": "Iframe刷新时间,设置为0时不刷新,单位为分钟",
68 | "iframeCacheTips": "开启后在交互行为弹窗关闭时不会摧毁节点,再次打开复用节点,防止重复加载Iframe"
69 | }
70 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue'
2 | import VueGridLayout from 'vue-grid-layout'
3 | import { i18n } from '@/lang'
4 | import App from './App.vue'
5 | import store from './store'
6 | import '@/assets/element-modules.scss'
7 | import '@/assets/global.scss'
8 | import {
9 | ElRadioGroup,
10 | ElRadio,
11 | ElInput,
12 | ElOption,
13 | ElSelect,
14 | ElForm,
15 | ElFormItem,
16 | ElInputNumber,
17 | ElIcon,
18 | ElColorPicker,
19 | ElSwitch,
20 | ElTooltip,
21 | ElAlert,
22 | ElCheckbox,
23 | ElImage,
24 | ElTabs,
25 | ElTabPane,
26 | ElDatePicker
27 | } from 'element-plus'
28 | import EasyDialog from '@/components/Global/EasyDialog.vue'
29 | import Icon from '@/components/Tools/Icon.vue'
30 | import { setPreviewModeData } from '@/utils/preview-mode'
31 |
32 | if (import.meta.env.PROD) {
33 | // 强制重定向到https
34 | if (window?.location?.protocol === 'http:') {
35 | window.location.href = window.location.href.replace('http', 'https')
36 | }
37 | // 开启ServiceWorker
38 | if ('serviceWorker' in navigator) {
39 | window.addEventListener('load', () => {
40 | navigator.serviceWorker.register('./sw.js')
41 | });
42 | }
43 | // 判断当前是否为Edge扩展, 更换Favicon
44 | const isEdgeExtension = window.location.href.includes('chrome-extension') && window.navigator.userAgent.includes('Edg/')
45 | if (isEdgeExtension) {
46 | const iconRel = document.querySelector('link[rel="icon"]')
47 | iconRel?.setAttribute('href', 'favicon-edge.png')
48 | }
49 | }
50 |
51 | // 非Edge滚动条重置
52 | if (!window.navigator.userAgent.includes('Edg/')) {
53 | document.body.classList.add('scrollbar1')
54 | }
55 |
56 | const init = async () => {
57 | // Init app
58 | const app = createApp(App)
59 | app.use(store)
60 | app.use(i18n)
61 |
62 | // Get storage data if it is preview mode
63 | const isPreviewMode = window.location.href.includes('preview=')
64 | if (isPreviewMode) {
65 | const previewKey = (new URLSearchParams(window.location.search)).get('preview')
66 | if (previewKey) {
67 | try {
68 | await setPreviewModeData(previewKey)
69 | } catch (e) {
70 | console.error(e)
71 | if (confirm('获取预览数据失败, 即将返回主站')) {
72 | window.location.href = 'https://howdz.xyz'
73 | }
74 | }
75 | }
76 | }
77 |
78 | // Mount app
79 | const components = [
80 | ElRadioGroup,
81 | ElRadio,
82 | ElInput,
83 | ElOption,
84 | ElSelect,
85 | ElForm,
86 | ElFormItem,
87 | ElInputNumber,
88 | ElIcon,
89 | ElColorPicker,
90 | ElSwitch,
91 | ElTooltip,
92 | ElAlert,
93 | ElCheckbox,
94 | ElImage,
95 | ElTabs,
96 | ElTabPane,
97 | ElDatePicker,
98 | ]
99 | components.map(component => {
100 | app.use(component)
101 | })
102 | app.use(VueGridLayout)
103 | app.component('EasyDialog', EasyDialog)
104 | app.component('Icon', Icon)
105 | const globalLoading = document.querySelector('#globalLoading')
106 | if (globalLoading) {
107 | globalLoading.parentNode?.removeChild(globalLoading)
108 | }
109 | app.mount('#app')
110 | }
111 |
112 | init()
113 |
114 | // document.documentElement.style.setProperty('--el-border-radius-base', '8px')
115 | // document.documentElement.style.setProperty('--el-border-radius-small', '4px')
116 |
117 | // 移动端禁用右键菜单与任何长按选中
118 | if ('ontouchstart' in window) {
119 | document.documentElement.style.setProperty('--user-select', 'none')
120 | document.addEventListener('contextmenu', (e) => e.preventDefault())
121 | }
--------------------------------------------------------------------------------
/src/materials/Bookmark/setting.tsx:
--------------------------------------------------------------------------------
1 | import pick from '../base'
2 | export default {
3 | formData: {
4 | boxSize: 36,
5 | boxRadius: 4,
6 | iconSize: 20,
7 | textFontSize: 12,
8 | textColor: '#e9e9e9',
9 | padding: 10,
10 | fontFamily: '',
11 | maxWidth: 1024,
12 | tileTitleLines: 1,
13 | jumpType: 1,
14 | closeClickOutside: false,
15 | bookmark: [],
16 | hiddenAddBtn: false,
17 | folderBg: 'rgba(36, 36, 40, 0.9)'
18 | },
19 | formConf (formData: any) {
20 | return {
21 | boxSize: {
22 | label: '图标容器尺寸',
23 | type: 'input-number',
24 | attrs: {
25 | 'controls-position': 'right',
26 | min: 20,
27 | max: 120,
28 | style: 'width: 100px'
29 | },
30 | unit: 'px',
31 | tips: 'boxSizeTips'
32 | },
33 | iconSize: {
34 | label: '图标尺寸',
35 | type: 'input-number',
36 | attrs: {
37 | 'controls-position': 'right',
38 | min: 16,
39 | max: 120,
40 | style: 'width: 100px'
41 | },
42 | unit: 'px'
43 | },
44 | boxRadius: {
45 | label: '容器圆角',
46 | type: 'input-number',
47 | attrs: {
48 | 'controls-position': 'right',
49 | min: 0,
50 | max: 120,
51 | style: 'width: 100px'
52 | },
53 | unit: 'px'
54 | },
55 | ...pick(formData, [
56 | 'textFontSize',
57 | 'textColor',
58 | 'padding',
59 | 'fontFamily',
60 | ]),
61 | boxShadow: {
62 | label: '容器阴影',
63 | type: 'input',
64 | attrs: {
65 | placeholder: '请输入合法的box-shadow值'
66 | }
67 | },
68 | maxWidth: {
69 | label: '最大宽度',
70 | type: 'input-number',
71 | attrs: {
72 | 'controls-position': 'right',
73 | min: 200,
74 | max: 2048,
75 | style: 'width: 100px'
76 | },
77 | unit: 'px'
78 | },
79 | jumpType: {
80 | label: '网页跳转方式',
81 | type: 'radio-group',
82 | radio: {
83 | list: [
84 | {
85 | name: '新窗口打开',
86 | value: 1
87 | },
88 | {
89 | name: '当前页跳转',
90 | value: 2
91 | },
92 | {
93 | name: '页面内Iframe打开',
94 | value: 3
95 | }
96 | ],
97 | label: 'name',
98 | value: 'value'
99 | }
100 | },
101 | tileTitleLines: {
102 | label: '文本最大行数',
103 | type: 'input-number',
104 | attrs: {
105 | 'controls-position': 'right',
106 | min: 1,
107 | max: 4,
108 | style: 'width: 100px'
109 | },
110 | },
111 | closeClickOutside: {
112 | label: '弹窗快速关闭',
113 | type: 'switch',
114 | tips: 'closeClickOutsideTips'
115 | },
116 | folderBg: {
117 | label: '弹窗背景色',
118 | slot: () =>
119 | },
120 | hiddenAddBtn: {
121 | label: '隐藏添加按钮',
122 | type: 'switch',
123 | tips: 'hiddenAddBtnTips'
124 | }
125 | }
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/src/materials/Clock/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 | {{ now }}
15 |
16 |
17 |
18 |
70 |
78 |
--------------------------------------------------------------------------------
/src/materials/Clock/setting.tsx:
--------------------------------------------------------------------------------
1 | import pick from '../base'
2 | export default {
3 | formData: {
4 | duration: 5000,
5 | position: 5,
6 | textFontSize: 64,
7 | textColor: '#d8d8d8',
8 | textShadow: '0 0 1px #464646',
9 | fontFamily: '',
10 | padding: 10,
11 | textHollow: false,
12 | textHollowBorder: 1,
13 | textHollowBg: 'rgba(0,0,0,0)'
14 | },
15 | formConf (formData: any) {
16 | return {
17 | duration: {
18 | label: '刷新频率',
19 | type: 'input-number',
20 | attrs: {
21 | 'controls-position': 'right',
22 | min: 1000,
23 | max: 60000,
24 | step: 1000,
25 | style: 'width: 100px'
26 | },
27 | unit: 'ms',
28 | tips: 'durationTips'
29 | },
30 | ...pick(formData, [
31 | 'position',
32 | 'textFontSize',
33 | 'textColor',
34 | 'textShadow',
35 | 'fontFamily',
36 | 'padding'
37 | ]),
38 | textHollow: {
39 | label: '镂空字效果',
40 | type: 'switch',
41 | tips: 'textStrokeTips'
42 | },
43 | textHollowBorder: {
44 | when: (formData: any) => formData.textHollow,
45 | label: '镂空边框',
46 | type: 'input-number',
47 | attrs: {
48 | 'controls-position': 'right',
49 | min: 1,
50 | max: 10,
51 | style: 'width: 100px'
52 | },
53 | unit: 'px'
54 | },
55 | textHollowBg: {
56 | when: (formData: any) => formData.textHollow,
57 | label: '镂空背景色',
58 | slot: () =>
59 | }
60 | }
61 | },
62 | }
63 |
--------------------------------------------------------------------------------
/src/materials/Collection/setting.tsx:
--------------------------------------------------------------------------------
1 | import pick from '../base'
2 |
3 | type KeySetting = {
4 | key: string;
5 | url: string;
6 | remark?: string;
7 | iconType?: string;
8 | iconLink?: string;
9 | }
10 |
11 | export default {
12 | formData: {
13 | userSettingKeyMap: {} as Record,
14 | position: 5,
15 | useKeyboardEvent: true,
16 | jumpType: 1,
17 | keyboardMaxWidth: 920,
18 | keyGutter: 8,
19 | keyBorderRadius: 4,
20 | keyBackground: 'rgba(255,255,255,0.9)',
21 | padding: 10,
22 | fontFamily: '',
23 | },
24 | formConf (formData: any) {
25 | return {
26 | position: {
27 | label: '对齐方式',
28 | slot: () =>
29 | },
30 | keyboardMaxWidth: {
31 | label: '键盘最大宽度',
32 | type: 'input-number',
33 | attrs: {
34 | 'controls-position': 'right',
35 | min: 720,
36 | max: 1280,
37 | style: 'width: 100px'
38 | },
39 | unit: 'px'
40 | },
41 | useKeyboardEvent: {
42 | label: '快捷按键',
43 | type: 'switch',
44 | tips: 'keyboardEventTips'
45 | },
46 | jumpType: {
47 | label: '网页跳转方式',
48 | type: 'radio-group',
49 | radio: {
50 | list: [
51 | {
52 | name: '新窗口打开',
53 | value: 1
54 | },
55 | {
56 | name: '当前页跳转',
57 | value: 2
58 | },
59 | {
60 | name: '页面内Iframe打开',
61 | value: 3
62 | }
63 | ],
64 | label: 'name',
65 | value: 'value'
66 | }
67 | },
68 | keyGutter: {
69 | label: '按键间隔',
70 | type: 'input-number',
71 | attrs: {
72 | 'controls-position': 'right',
73 | min: 2,
74 | max: 32,
75 | style: 'width: 100px'
76 | },
77 | unit: 'px'
78 | },
79 | keyBorderRadius: {
80 | label: '按键圆角',
81 | type: 'input-number',
82 | attrs: {
83 | 'controls-position': 'right',
84 | min: 0,
85 | max: 16,
86 | style: 'width: 100px'
87 | },
88 | unit: 'px'
89 | },
90 | keyBackground: {
91 | label: '按键背景色',
92 | slot: () =>
93 | },
94 | ...pick(formData, [
95 | 'padding',
96 | 'fontFamily',
97 | ])
98 | }
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/src/materials/Collection/utils.ts:
--------------------------------------------------------------------------------
1 | type KeyMapSetting = {
2 | span?: number;
3 | keyCode?: number;
4 | }
5 | export const keyboardMap: Record = {
6 | tab: {
7 | span: 1
8 | },
9 | Q: {
10 | keyCode: 81
11 | },
12 | W: {
13 | keyCode: 87
14 | },
15 | E: {
16 | keyCode: 69
17 | },
18 | R: {
19 | keyCode: 82
20 | },
21 | T: {
22 | keyCode: 84
23 | },
24 | Y: {
25 | keyCode: 89
26 | },
27 | U: {
28 | keyCode: 85
29 | },
30 | I: {
31 | keyCode: 73
32 | },
33 | O: {
34 | keyCode: 79
35 | },
36 | P: {
37 | keyCode: 80
38 | },
39 | brackets: {
40 | span: 1
41 | },
42 | caps: {
43 | span: 2
44 | },
45 | A: {
46 | keyCode: 65
47 | },
48 | S: {
49 | keyCode: 83
50 | },
51 | D: {
52 | keyCode: 68
53 | },
54 | F: {
55 | keyCode: 70
56 | },
57 | G: {
58 | keyCode: 71
59 | },
60 | H: {
61 | keyCode: 72
62 | },
63 | J: {
64 | keyCode: 74
65 | },
66 | K: {
67 | keyCode: 75
68 | },
69 | L: {
70 | keyCode: 76
71 | },
72 | semi: {
73 | span: 2
74 | },
75 | shift: {
76 | span: 3
77 | },
78 | Z: {
79 | keyCode: 90
80 | },
81 | X: {
82 | keyCode: 88
83 | },
84 | C: {
85 | keyCode: 67
86 | },
87 | V: {
88 | keyCode: 86
89 | },
90 | B: {
91 | keyCode: 66
92 | },
93 | N: {
94 | keyCode: 78
95 | },
96 | M: {
97 | keyCode: 77
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/src/materials/CountDown/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 | {{ componentSetting.eventName }}
16 |
17 |
18 |
19 | {{ num }}
20 |
21 |
22 | {{ $t(componentSetting.unit) }}
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
92 |
120 |
--------------------------------------------------------------------------------
/src/materials/CountDown/setting.tsx:
--------------------------------------------------------------------------------
1 | import pick from '../base'
2 | export default {
3 | formData: {
4 | eventName: '',
5 | eventTime: '',
6 | unit: '小时',
7 | position: 5,
8 | textFontSize: 16,
9 | textColor: '#d8d8d8',
10 | textShadow: '0 0 1px #464646',
11 | fontFamily: '',
12 | padding: 10
13 | },
14 | formConf (formData: any) {
15 | return {
16 | eventName: {
17 | label: '目标事件名称',
18 | type: 'input',
19 | attrs: {
20 | placeholder: '请输入事件名称'
21 | }
22 | },
23 | eventTime: {
24 | label: '目标事件时间',
25 | type: 'date-picker',
26 | attrs: {
27 | type: 'datetime',
28 | placeholder: '选择日期时间',
29 | format: 'YYYY-MM-DD HH:mm',
30 | disabledDate(time: Date) {
31 | return time.getTime() < Date.now();
32 | }
33 | }
34 | },
35 | unit: {
36 | label: '时间单位',
37 | type: 'select',
38 | option: {
39 | list: ['天', '小时', '分钟']
40 | }
41 | },
42 | ...pick(formData, [
43 | 'position',
44 | 'textFontSize',
45 | 'textColor',
46 | 'textShadow',
47 | 'fontFamily',
48 | 'padding'
49 | ])
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/materials/DailyHot/setting.tsx:
--------------------------------------------------------------------------------
1 | import { DAILY_HOT_CLASSIFY } from '@/constanst'
2 | import pick from '../base'
3 | export default {
4 | formData: {
5 | enableList: ['weibo', 'zhihu', 'sspai', 'bilibili'],
6 | limit: 10,
7 | position: 5,
8 | textFontSize: 12,
9 | textColor: '#d8d8d8',
10 | textShadow: '0 0 1px #464646',
11 | fontFamily: '',
12 | padding: 10
13 | },
14 | formConf (formData: any) {
15 | return {
16 | enableList: {
17 | label: '热榜站点',
18 | type: 'checkbox-group',
19 | checkbox: {
20 | list: DAILY_HOT_CLASSIFY,
21 | label: 'label',
22 | value: 'value',
23 | attrs: {
24 | style: 'display: block; height: 24px;'
25 | }
26 | },
27 | attrs: {
28 | style: 'padding-top: 8px;'
29 | }
30 | },
31 | limit: {
32 | label: '列表条目数',
33 | type: 'input-number',
34 | attrs: {
35 | 'controls-position': 'right',
36 | min: 5,
37 | max: 20,
38 | style: 'width: 100px'
39 | },
40 | },
41 | ...pick(formData, [
42 | 'position',
43 | 'textFontSize',
44 | 'textColor',
45 | 'textShadow',
46 | 'fontFamily',
47 | 'padding'
48 | ])
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/materials/Day/setting.tsx:
--------------------------------------------------------------------------------
1 | import pick from '../base'
2 | const DAYJS_FORMAT_LINK = 'https://day.js.org/docs/zh-CN/display/format'
3 | export default {
4 | formData: {
5 | formatter: 'M月D日 dddd',
6 | custom: '',
7 | chineseWeekDay: true,
8 | duration: 5,
9 | position: 5,
10 | textFontSize: 16,
11 | textColor: '#d8d8d8',
12 | textShadow: '0 0 1px #464646',
13 | showTTS1: false,
14 | showTTS2: false,
15 | ttsFontSize: 16,
16 | fontFamily: '',
17 | padding: 10
18 | },
19 | formConf (formData: any) {
20 | return {
21 | formatter: {
22 | label: '格式化',
23 | type: 'radio-group',
24 | attrs: {
25 | class: 'block-radio-group'
26 | },
27 | radio: {
28 | list: [
29 | 'M月D日 dddd',
30 | 'YYYY-MM-DD HH:mm:ss',
31 | 'YYYY-MM-DD HH:mm dddd',
32 | '自定义'
33 | ]
34 | },
35 | tips: 'dayjsFormatterTips'
36 | },
37 | custom: {
38 | when: (formData: any) => formData.formatter === '自定义',
39 | formItemStyle: {
40 | marginTop: '-15px'
41 | },
42 | type: 'input',
43 | attrs: {
44 | placeholder: '请自定义的Dayjs格式',
45 | clearable: true
46 | },
47 | rules: [{
48 | required: true,
49 | validator: (rule: any, value: any, callback: any) => {
50 | if (formData.formatter === '自定义' && !value) {
51 | callback(new Error('请输入自定义的Dayjs格式'))
52 | }
53 | callback();
54 | }
55 | }],
56 | tips: 'dayjsFormatterTips'
57 | },
58 | dayLinkTips: {
59 | formItemStyle: {
60 | marginTop: '-12px'
61 | },
62 | slot: () => Dayjs格式化参考此处
63 | },
64 | chineseWeekDay: {
65 | label: '展示中文星期',
66 | type: 'switch',
67 | tips: 'chineseWeekDayTips'
68 | },
69 | duration: {
70 | label: '刷新频率',
71 | type: 'input-number',
72 | attrs: {
73 | 'controls-position': 'right',
74 | min: 1,
75 | max: 24 * 3600,
76 | step: 1,
77 | style: 'width: 100%'
78 | },
79 | tips: 'durationSecondTips'
80 | },
81 | ...pick(formData, [
82 | 'position',
83 | 'textFontSize',
84 | 'textColor',
85 | 'textShadow',
86 | 'fontFamily',
87 | 'padding'
88 | ]),
89 | showTTS1: {
90 | label: 'TTS文本①',
91 | type: 'switch',
92 | tips: '展示`回答明天放假吗`TTS文本,API由http://timor.tech/api/holiday/tts/tomorrow提供'
93 | },
94 | showTTS2: {
95 | label: 'TTS文本②',
96 | type: 'switch',
97 | tips: '展示`最近的一个节假日安排`TTS文本,API由http://timor.tech/api/holiday/tts/next提供'
98 | },
99 | ttsFontSize: {
100 | label: 'TTS字体大小',
101 | type: 'input-number',
102 | attrs: {
103 | 'controls-position': 'right',
104 | min: 12,
105 | max: 256,
106 | style: 'width: 100px'
107 | },
108 | unit: 'px'
109 | }
110 | }
111 | },
112 | }
113 |
--------------------------------------------------------------------------------
/src/materials/Editor/milkdown/custom-nord.ts:
--------------------------------------------------------------------------------
1 |
2 | import { themeFactory } from '@milkdown/core';
3 | import { font, size, color, mixin, override, view } from '@milkdown/theme-nord';
4 | import { injectGlobal } from '@emotion/css';
5 |
6 | const iconMapping: Record = {
7 | h1: 'h1',
8 | h2: 'h2',
9 | h3: 'h3',
10 | loading: 'loading',
11 | quote: 'quote',
12 | code: 'code',
13 | table: 'table',
14 | divider: 'line',
15 | image: 'image',
16 | brokenImage: 'image',
17 | bulletList: 'unorder-list',
18 | orderedList: 'order-list',
19 | taskList: 'checklist',
20 | bold: 'bold',
21 | italic: 'italic',
22 | inlineCode: 'code',
23 | strikeThrough: 'text-delete',
24 | link: 'link',
25 | leftArrow: 'arrow-left',
26 | rightArrow: 'arrow-right',
27 | upArrow: 'arrow-up',
28 | downArrow: 'arrow-down',
29 | alignLeft: 'align-left',
30 | alignRight: 'align-right',
31 | alignCenter: 'align-center',
32 | delete: 'delete',
33 | select: 'select-all',
34 | unchecked: 'unchecked',
35 | checked: 'checked',
36 | };
37 |
38 | export default themeFactory({
39 | font,
40 | size,
41 | color: color.lightColor,
42 | mixin,
43 | slots: () => ({
44 | icon: (id) => {
45 | const span = document.createElement('span');
46 | span.className = `icon ed-icon icon-${iconMapping[id]}`;
47 | return span;
48 | },
49 | }),
50 | global: (themeTool) => {
51 | const css = injectGlobal;
52 | css`
53 | ${view};
54 | ${override(themeTool)}
55 | `;
56 | }
57 | });
58 |
--------------------------------------------------------------------------------
/src/materials/Editor/milkdown/fonts/howdz-editor.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-kfd/Dashboard/38b221553ad241b65ed59ab44498af5092509357/src/materials/Editor/milkdown/fonts/howdz-editor.ttf
--------------------------------------------------------------------------------
/src/materials/Editor/milkdown/fonts/howdz-editor.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-kfd/Dashboard/38b221553ad241b65ed59ab44498af5092509357/src/materials/Editor/milkdown/fonts/howdz-editor.woff
--------------------------------------------------------------------------------
/src/materials/Editor/milkdown/icon-font.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: "howdz-editor";
3 | src: url("./fonts/howdz-editor.ttf?kukghb") format("truetype"),
4 | url("./fonts/howdz-editor.woff?kukghb") format("woff"),
5 | url("./fonts/howdz-editor.svg?kukghb#howdz-editor") format("svg");
6 | font-weight: normal;
7 | font-style: normal;
8 | font-display: block;
9 | }
10 |
11 | .ed-icon {
12 | /* use !important to prevent issues with browser extensions that change fonts */
13 | font-family: "howdz-editor" !important;
14 | speak: never;
15 | font-style: normal;
16 | font-weight: normal;
17 | font-variant: normal;
18 | text-transform: none;
19 | line-height: 1;
20 | font-size: 24px;
21 |
22 | /* Better Font Rendering =========== */
23 | -webkit-font-smoothing: antialiased;
24 | -moz-osx-font-smoothing: grayscale;
25 | }
26 |
27 | .icon-delete:before {
28 | content: "\e900";
29 | }
30 | .icon-table:before {
31 | content: "\e901";
32 | }
33 | .icon-image:before {
34 | content: "\e902";
35 | }
36 | .icon-checklist:before {
37 | content: "\e903";
38 | }
39 | .icon-quote:before {
40 | content: "\e904";
41 | }
42 | .icon-order-list:before {
43 | content: "\e905";
44 | }
45 | .icon-italic:before {
46 | content: "\e906";
47 | }
48 | .icon-text-underline:before {
49 | content: "\e907";
50 | }
51 | .icon-unorder-list:before {
52 | content: "\e908";
53 | }
54 | .icon-arrow-up:before {
55 | content: "\e909";
56 | }
57 | .icon-text-delete:before {
58 | content: "\e90a";
59 | }
60 | .icon-select-all:before {
61 | content: "\e90b";
62 | }
63 | .icon-arrow-right:before {
64 | content: "\e90c";
65 | }
66 | .icon-line:before {
67 | content: "\e90d";
68 | }
69 | .icon-link:before {
70 | content: "\e90e";
71 | }
72 | .icon-arrow-left:before {
73 | content: "\e90f";
74 | }
75 | .icon-align-left:before {
76 | content: "\e910";
77 | }
78 | .icon-align-center:before {
79 | content: "\e911";
80 | }
81 | .icon-align-right:before {
82 | content: "\e912";
83 | }
84 | .icon-bold:before {
85 | content: "\e913";
86 | }
87 | .icon-h3:before {
88 | content: "\e914";
89 | }
90 | .icon-h1:before {
91 | content: "\e915";
92 | }
93 | .icon-h2:before {
94 | content: "\e916";
95 | }
96 | .icon-arrow-down:before {
97 | content: "\e917";
98 | }
99 | .icon-code:before {
100 | content: "\e918";
101 | }
102 | .icon-unchecked:before {
103 | content: "\e919";
104 | }
105 | .icon-checked:before {
106 | content: "\e91a";
107 | }
108 |
--------------------------------------------------------------------------------
/src/materials/Editor/setting.tsx:
--------------------------------------------------------------------------------
1 | import Tips from '@/components/Tools/Tips.vue'
2 | import pick from '../base'
3 | export default {
4 | formData: {
5 | textFontSize: 16,
6 | textColor: '#494e59',
7 | padding: 10,
8 | enableTooltip: true,
9 | enableSlash: true,
10 | enableHistory: false,
11 | enableClipboard: false,
12 | // enablePrism: false,
13 | showTitle: true,
14 | markdown: '',
15 | fontFamily: ''
16 | },
17 | formConf (formData: any) {
18 | return {
19 | milkdownPlugins: {
20 | label: 'Milkdown插件',
21 | slot: () =>
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | {/*
41 |
42 |
43 |
*/}
44 |
45 | },
46 | showTitle: {
47 | label: '展示Logo',
48 | type: 'switch'
49 | },
50 | ...pick(formData, [
51 | 'textFontSize',
52 | 'textColor',
53 | 'fontFamily',
54 | 'padding'
55 | ])
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/materials/Empty/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 | {{ componentSetting.customText }}
15 |
16 |
17 |
18 |
37 |
45 |
--------------------------------------------------------------------------------
/src/materials/Empty/setting.tsx:
--------------------------------------------------------------------------------
1 | import pick from '../base'
2 | export default {
3 | formData: {
4 | customText: '',
5 | position: 5,
6 | textFontSize: 16,
7 | textColor: '#d8d8d8',
8 | textShadow: '0 0 1px #464646',
9 | padding: 10,
10 | fontFamily: ''
11 | },
12 | formConf (formData: any) {
13 | return {
14 | customText: {
15 | label: '自定义文本',
16 | type: 'input',
17 | attrs: {
18 | placeholder: '可配置显示自定义文本'
19 | }
20 | },
21 | ...pick(formData, [
22 | 'position',
23 | 'textFontSize',
24 | 'textColor',
25 | 'textShadow',
26 | 'fontFamily',
27 | 'padding'
28 | ])
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/materials/GithubTrending/setting.tsx:
--------------------------------------------------------------------------------
1 | import pick from '../base'
2 | export default {
3 | formData: {
4 | limit: 10,
5 | duration: 5,
6 | jumpType: 1,
7 | showTitle: true,
8 | clickActionType: 0,
9 | position: 5,
10 | textFontSize: 16,
11 | textColor: '#d8d8d8',
12 | textShadow: '0 0 1px #464646',
13 | iconShadow: '0 0 1px #464646',
14 | fontFamily: '',
15 | padding: 10
16 | },
17 | formConf (formData: any) {
18 | return {
19 | limit: {
20 | label: '列表条目数',
21 | type: 'input-number',
22 | attrs: {
23 | 'controls-position': 'right',
24 | min: 5,
25 | max: 30,
26 | style: 'width: 100px'
27 | },
28 | },
29 | duration: {
30 | label: '刷新频率',
31 | type: 'input-number',
32 | attrs: {
33 | 'controls-position': 'right',
34 | min: 5,
35 | max: 120,
36 | style: 'width: 100px'
37 | },
38 | tips: 'durationMinuteTips',
39 | unit: 'min'
40 | },
41 | ...pick(formData, [
42 | 'jumpType',
43 | 'showTitle',
44 | 'clickActionType',
45 | 'position',
46 | 'textFontSize',
47 | 'textColor',
48 | 'textShadow',
49 | 'iconShadow',
50 | 'fontFamily',
51 | 'padding'
52 | ])
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/materials/Iframe/setting.tsx:
--------------------------------------------------------------------------------
1 | export default {
2 | formData: {
3 | url: '',
4 | duration: 0,
5 | },
6 | formConf () {
7 | return {
8 | url: {
9 | label: 'URL',
10 | type: 'input',
11 | attrs: {
12 | placeholder: '请输入Iframe目标URL'
13 | },
14 | rules: [
15 | { pattern: /[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]/, message: '请输入正确URL', trigger: 'blur' }
16 | ]
17 | },
18 | duration: {
19 | label: '刷新频率',
20 | type: 'input-number',
21 | attrs: {
22 | 'controls-position': 'right',
23 | min: 0,
24 | max: 12 * 60,
25 | style: 'width: 100px'
26 | },
27 | unit: 'min',
28 | tips: 'iframeRefreshTips'
29 | },
30 | useCache: {
31 | label: '缓存节点',
32 | type: 'switch',
33 | tips: 'iframeCacheTips'
34 | }
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/materials/JuejinList/setting.tsx:
--------------------------------------------------------------------------------
1 | import pick from '../base'
2 | export default {
3 | formData: {
4 | limit: 10,
5 | duration: 5,
6 | jumpType: 1,
7 | showTitle: true,
8 | clickActionType: 0,
9 | position: 5,
10 | textFontSize: 16,
11 | textColor: '#d8d8d8',
12 | textShadow: '0 0 1px #464646',
13 | iconShadow: '0 0 1px #464646',
14 | fontFamily: '',
15 | padding: 10
16 | },
17 | formConf (formData: any) {
18 | return {
19 | limit: {
20 | label: '列表条目数',
21 | type: 'input-number',
22 | attrs: {
23 | 'controls-position': 'right',
24 | min: 5,
25 | max: 20,
26 | style: 'width: 100px'
27 | },
28 | },
29 | duration: {
30 | label: '刷新频率',
31 | type: 'input-number',
32 | attrs: {
33 | 'controls-position': 'right',
34 | min: 5,
35 | max: 120,
36 | style: 'width: 100px'
37 | },
38 | unit: 'min',
39 | tips: 'durationMinuteTips'
40 | },
41 | ...pick(formData, [
42 | 'jumpType',
43 | 'showTitle',
44 | 'clickActionType',
45 | 'position',
46 | 'textFontSize',
47 | 'textColor',
48 | 'textShadow',
49 | 'iconShadow',
50 | 'fontFamily',
51 | 'padding'
52 | ])
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/materials/TodoList/setting.tsx:
--------------------------------------------------------------------------------
1 | import pick from '../base'
2 | export default {
3 | formData: {
4 | todo: {},
5 | themeColor: '#643a7a',
6 | baseFontSize: 16,
7 | padding: 10
8 | },
9 | formConf (formData: any) {
10 | return {
11 | themeColor: {
12 | label: '主题颜色',
13 | slot: () =>
14 | },
15 | ...pick(formData, [
16 | 'baseFontSize',
17 | 'padding'
18 | ])
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/materials/Verse/setting.tsx:
--------------------------------------------------------------------------------
1 | import pick from '../base'
2 | export default {
3 | formData: {
4 | source: 1,
5 | duration: 5,
6 | clickActionType: 0,
7 | position: 5,
8 | textFontSize: 16,
9 | textColor: '#d8d8d8',
10 | textShadow: '0 0 1px #464646',
11 | fontFamily: '',
12 | padding: 10
13 | },
14 | formConf (formData: any) {
15 | return {
16 | source: {
17 | label: '随机源',
18 | type: 'radio-group',
19 | radio: {
20 | list: [
21 | {
22 | name: '古诗',
23 | value: 1
24 | },
25 | {
26 | name: '名言',
27 | value: 2
28 | }
29 | ],
30 | label: 'name',
31 | value: 'value'
32 | }
33 | },
34 | duration: {
35 | label: '刷新频率',
36 | type: 'input-number',
37 | attrs: {
38 | 'controls-position': 'right',
39 | min: 1,
40 | max: 12 * 60,
41 | style: 'width: 100px'
42 | },
43 | unit: 'min',
44 | tips: 'durationMinuteTips'
45 | },
46 | clickActionType: {
47 | label: '点击文本行为',
48 | type: 'select',
49 | option: {
50 | list: [
51 | { label: '无', value: 0 },
52 | { label: '切换下一个', value: 1 },
53 | { label: '跳转查看出处', value: 2 },
54 | { label: '复制文本', value: 3 }
55 | ],
56 | label: 'label',
57 | value: 'value'
58 | },
59 | tips: 'clickVerseActionTypeTips'
60 | },
61 | ...pick(formData, [
62 | 'position',
63 | 'textFontSize',
64 | 'textColor',
65 | 'textShadow',
66 | 'fontFamily',
67 | 'padding'
68 | ])
69 | }
70 | },
71 | }
72 |
--------------------------------------------------------------------------------
/src/materials/Weather/icon-map.ts:
--------------------------------------------------------------------------------
1 | export function weatherFormatter(name: string) {
2 | if (name.includes('风')) {
3 | if (['龙卷风', '狂爆风', '飓风', '热带风暴'].includes(name)) {
4 | return '龙卷风'
5 | } else {
6 | return '风'
7 | }
8 | }
9 | if (name.includes('霾') || name.includes('雾')) {
10 | if (['重', '大', '浓', '沙', '尘'].some(i => name.includes(i))) {
11 | return '大雾'
12 | } else {
13 | return '小雾'
14 | }
15 | }
16 | if (name.includes('雨')) {
17 | if (name.includes('雷')) {
18 | return '雷阵雨'
19 | } else if (name.includes('雪')) {
20 | return '雨夹雪'
21 | } else if (name.includes('大') || name.includes('暴')) {
22 | return '大雨'
23 | } else {
24 | return '小雨'
25 | }
26 | }
27 | if (name.includes('雪')) {
28 | return '雪'
29 | }
30 | return name
31 | }
32 |
33 | export const weatherMap: Record = {
34 | 晴: 'clear-day',
35 | 少云: 'cloudy',
36 | 晴间多云: 'overcast-day',
37 | 多云: 'overcast',
38 | 阴: 'overcast',
39 | 平静: 'overcast',
40 | 风: 'wind',
41 | 龙卷风: 'hurricane',
42 | 大雾: 'fog',
43 | 小雾: 'mist',
44 | 雷阵雨: 'thunderstorms-rain',
45 | 大雨: 'rain',
46 | 小雨: 'drizzle',
47 | 雨夹雪: 'sleet',
48 | 雪: 'snow',
49 | 热: 'thermometer-warmer',
50 | 冷: 'thermometer-colder',
51 | 未知: 'not-available'
52 | }
53 |
54 | function getAnimationIcon(name: string) {
55 | return new URL(`../../assets/imgs/weather-animation-icon/${name}.svg`, import.meta.url).href
56 | }
57 |
58 | function getStaticIcon(name: string) {
59 | return new URL(`../../assets/imgs/weather-static-icon/${name}.svg`, import.meta.url).href
60 | }
61 |
62 | export function getWeatherIconURL(name: string, isAnimationIcon = true) {
63 | const _name = weatherFormatter(name) || '未知'
64 | const word = weatherMap[_name] || 'not-available'
65 | return isAnimationIcon === false ? getStaticIcon(word) : getAnimationIcon(word)
66 | // return `https://cdn.jsdelivr.net/gh/leon-kfd/weather-icons/production/line/all/${word}.svg`
67 | }
68 |
--------------------------------------------------------------------------------
/src/materials/Weather/setting.tsx:
--------------------------------------------------------------------------------
1 | import pick from '../base'
2 | export default {
3 | formData: {
4 | weatherMode: 1,
5 | cityName: '',
6 | animationIcon: true,
7 | duration: 120,
8 | position: 5,
9 | baseFontSize: 16,
10 | textColor: '#d8d8d8',
11 | textShadow: '0 0 1px #464646',
12 | iconShadow: '0 0 1px #464646',
13 | fontFamily: '',
14 | padding: 10
15 | },
16 | formConf (formData: any) {
17 | return {
18 | weatherMode: {
19 | label: '天气城市',
20 | type: 'radio-group',
21 | radio: {
22 | list: [
23 | {
24 | name: '自动获取(IP)',
25 | value: 1
26 | },
27 | {
28 | name: '手动输入',
29 | value: 2
30 | }
31 | ],
32 | label: 'name',
33 | value: 'value'
34 | }
35 | },
36 | cityName: {
37 | when: (formData: any) => formData.weatherMode === 2,
38 | type: 'input',
39 | attrs: {
40 | placeholder: '请输入城市名(目前仅支持中国城市名)',
41 | clearable: true
42 | },
43 | rules: [{
44 | required: true,
45 | validator: (rule: any, value: any, callback: any) => {
46 | if (formData.weatherMode === 2 && !value) {
47 | callback(new Error('请输入城市名'))
48 | }
49 | callback();
50 | }
51 | }]
52 | },
53 | animationIcon: {
54 | label: '动画图标',
55 | type: 'switch',
56 | tips: '默认使用含动画的ICON,若想提高性能可关闭使用静态ICON'
57 | },
58 | duration: {
59 | label: '刷新频率',
60 | type: 'input-number',
61 | attrs: {
62 | 'controls-position': 'right',
63 | min: 60,
64 | max: 12 * 60,
65 | step: 10,
66 | style: 'width: 100px'
67 | },
68 | unit: 'min',
69 | tips: 'durationMinuteTips'
70 | },
71 | ...pick(formData, [
72 | 'position',
73 | 'baseFontSize',
74 | 'textColor',
75 | 'textShadow',
76 | 'iconShadow',
77 | 'fontFamily',
78 | 'padding'
79 | ])
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/materials/WeiboList/setting.tsx:
--------------------------------------------------------------------------------
1 | import pick from '../base'
2 | export default {
3 | formData: {
4 | limit: 10,
5 | duration: 5,
6 | jumpType: 1,
7 | showTitle: true,
8 | clickActionType: 0,
9 | position: 5,
10 | textFontSize: 16,
11 | textColor: '#d8d8d8',
12 | textShadow: '0 0 1px #464646',
13 | iconShadow: '0 0 1px #464646',
14 | fontFamily: '',
15 | padding: 10
16 | },
17 | formConf (formData: any) {
18 | return {
19 | limit: {
20 | label: '列表条目数',
21 | type: 'input-number',
22 | attrs: {
23 | 'controls-position': 'right',
24 | min: 5,
25 | max: 30,
26 | style: 'width: 100px'
27 | },
28 | },
29 | duration: {
30 | label: '刷新频率',
31 | type: 'input-number',
32 | attrs: {
33 | 'controls-position': 'right',
34 | min: 5,
35 | max: 120,
36 | style: 'width: 100px'
37 | },
38 | unit: 'min',
39 | tips: 'durationMinuteTips'
40 | },
41 | ...pick(formData, [
42 | 'jumpType',
43 | 'showTitle',
44 | 'clickActionType',
45 | 'position',
46 | 'textFontSize',
47 | 'textColor',
48 | 'textShadow',
49 | 'iconShadow',
50 | 'fontFamily',
51 | 'padding'
52 | ])
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/materials/ZhihuList/setting.tsx:
--------------------------------------------------------------------------------
1 | import pick from '../base'
2 | export default {
3 | formData: {
4 | limit: 10,
5 | duration: 5,
6 | jumpType: 1,
7 | showTitle: true,
8 | clickActionType: 0,
9 | position: 5,
10 | textFontSize: 16,
11 | textColor: '#d8d8d8',
12 | textShadow: '0 0 1px #464646',
13 | iconShadow: '0 0 1px #464646',
14 | fontFamily: '',
15 | padding: 10
16 | },
17 | formConf (formData: any) {
18 | return {
19 | limit: {
20 | label: '列表条目数',
21 | type: 'input-number',
22 | attrs: {
23 | 'controls-position': 'right',
24 | min: 5,
25 | max: 20,
26 | style: 'width: 100px'
27 | },
28 | },
29 | duration: {
30 | label: '刷新频率',
31 | type: 'input-number',
32 | attrs: {
33 | 'controls-position': 'right',
34 | min: 5,
35 | max: 120,
36 | style: 'width: 100px'
37 | },
38 | unit: 'min',
39 | tips: 'durationMinuteTips'
40 | },
41 | ...pick(formData, [
42 | 'jumpType',
43 | 'showTitle',
44 | 'clickActionType',
45 | 'position',
46 | 'textFontSize',
47 | 'textColor',
48 | 'textShadow',
49 | 'iconShadow',
50 | 'fontFamily',
51 | 'padding'
52 | ])
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/materials/base.tsx:
--------------------------------------------------------------------------------
1 | export default function (formData: any, fields: string[] | string) {
2 | const baseTemplate: Record = {
3 | position: {
4 | label: '文本对齐',
5 | slot: () =>
6 | },
7 | textFontSize: {
8 | label: '字体大小',
9 | type: 'input-number',
10 | attrs: {
11 | 'controls-position': 'right',
12 | min: 12,
13 | max: 256,
14 | style: 'width: 100px'
15 | },
16 | unit: 'px'
17 | },
18 | baseFontSize: {
19 | label: '基础字体大小',
20 | type: 'input-number',
21 | attrs: {
22 | 'controls-position': 'right',
23 | min: 12,
24 | max: 256,
25 | style: 'width: 100px'
26 | },
27 | unit: 'px',
28 | tips: 'baseFontSizeTips'
29 | },
30 | textColor: {
31 | label: '字体颜色',
32 | slot: () =>
33 | },
34 | textShadow: {
35 | label: '字体阴影',
36 | type: 'input',
37 | attrs: {
38 | placeholder: 'e.g. "0 0 1px #464646"'
39 | },
40 | tips: 'textShadowTips'
41 | },
42 | iconShadow: {
43 | label: '图标阴影',
44 | type: 'input',
45 | tips: 'iconShadowTips'
46 | },
47 | fontFamily: {
48 | label: '字体库',
49 | slot: () =>
50 | },
51 | padding: {
52 | label: '盒子内边距',
53 | type: 'input-number',
54 | attrs: {
55 | 'controls-position': 'right',
56 | min: 0,
57 | max: 256,
58 | style: 'width: 100px'
59 | },
60 | unit: 'px'
61 | },
62 | showTitle: {
63 | label: '标题LOGO',
64 | type: 'switch',
65 | attrs: {
66 | 'active-text': '展示顶部标题LOGO'
67 | }
68 | },
69 | clickActionType: {
70 | when: (formData: any) => formData.showTitle,
71 | label: '点击LOGO',
72 | type: 'select',
73 | option: {
74 | list: [
75 | { label: '无', value: 0 },
76 | { label: '刷新列表', value: 1 },
77 | { label: '跳转主页', value: 2 }
78 | ],
79 | label: 'label',
80 | value: 'value'
81 | },
82 | tips: '配置LOGO的点击事件'
83 | },
84 | jumpType: {
85 | label: '网页跳转方式',
86 | type: 'radio-group',
87 | radio: {
88 | list: [
89 | {
90 | name: '新窗口打开',
91 | value: 1
92 | },
93 | {
94 | name: '当前页跳转',
95 | value: 2
96 | }
97 | ],
98 | label: 'name',
99 | value: 'value'
100 | }
101 | }
102 | }
103 | const result: Record = {}
104 | if (typeof fields === 'string') {
105 | result[fields] = baseTemplate[fields]
106 | } else {
107 | fields.map(key => {
108 | result[key] = baseTemplate[key]
109 | })
110 | }
111 | return result
112 | }
113 |
--------------------------------------------------------------------------------
/src/materials/setting.ts:
--------------------------------------------------------------------------------
1 | import Empty from './Empty/setting'
2 | import Clock from './Clock/setting'
3 | import Verse from './Verse/setting'
4 | import Search from './Search/setting'
5 | import Collection from './Collection/setting'
6 | import Iframe from './Iframe/setting'
7 | import TodoList from './TodoList/setting'
8 | import Weather from './Weather/setting'
9 | import CountDown from './CountDown/setting'
10 | import JuejinList from './JuejinList/setting'
11 | import WeiboList from './WeiboList/setting'
12 | import GithubTrending from './GithubTrending/setting'
13 | import Day from './Day/setting'
14 | import ZhihuList from './ZhihuList/setting'
15 | import Editor from './Editor/setting'
16 | import MovieLines from './MovieLines/setting'
17 | import Bookmark from './Bookmark/setting'
18 | import DailyHot from './DailyHot/setting'
19 |
20 | const Setting: Record = {
21 | Empty,
22 | Clock,
23 | Verse,
24 | Search,
25 | Collection,
26 | Iframe,
27 | TodoList,
28 | Weather,
29 | CountDown,
30 | JuejinList,
31 | WeiboList,
32 | GithubTrending,
33 | Day,
34 | ZhihuList,
35 | Editor,
36 | MovieLines,
37 | Bookmark,
38 | DailyHot
39 | }
40 |
41 | export default Setting;
42 |
--------------------------------------------------------------------------------
/src/plugins/local-img.ts:
--------------------------------------------------------------------------------
1 | import localforage from 'localforage';
2 |
3 | export const isSupportIndexDB = localforage.supports(localforage.INDEXEDDB);
4 |
5 | export const localImg = localforage.createInstance({ name: 'LocalImg', driver: localforage.INDEXEDDB })
6 |
7 | export const localThumbImg = localforage.createInstance({ name: 'localThumbImg', driver: localforage.INDEXEDDB })
8 |
9 | export const cacheBackgroundImg = localforage.createInstance({ name: 'cacheBackgroundImg', driver: localforage.INDEXEDDB })
10 |
11 | export async function setCacheBgImg(imgDom: HTMLImageElement) {
12 | try {
13 | const canvas = document.createElement('canvas')
14 | const ctx = canvas.getContext('2d') as CanvasRenderingContext2D
15 | const { width, height, naturalWidth, naturalHeight } = imgDom
16 | canvas.width = width
17 | canvas.height = height
18 | const canvasRatio = width / height
19 | const imgRatio = naturalWidth / naturalHeight
20 | // img object-fit
21 | let sw, sh, sx, sy
22 | if (imgRatio <= canvasRatio) {
23 | sw = naturalWidth
24 | sh = sw / canvasRatio
25 | sx = 0
26 | sy = (naturalHeight - sh) / 2
27 | } else {
28 | sh = naturalHeight
29 | sw = sh * canvasRatio
30 | sx = (naturalWidth - sw) / 2
31 | sy = 0
32 | }
33 | ctx.drawImage(imgDom, sx, sy, sw, sh, 0, 0, canvas.width, canvas.height)
34 | const base64 = canvas.toDataURL('image/png')
35 | await cacheBackgroundImg.setItem('img', base64)
36 | } catch (e) {
37 | console.warn('Set cache background error.')
38 | console.error(e)
39 | }
40 | }
41 |
42 | export async function removeCacheBgImg() {
43 | if (isSupportIndexDB) {
44 | await cacheBackgroundImg.removeItem('img')
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/plugins/position-selector/index.ts:
--------------------------------------------------------------------------------
1 | import { App } from 'vue';
2 | import PositionSelector from './position-selector.vue';
3 | import type { SFCWithInstall } from '@/utils/types'
4 | import mapPosition from './mapPosition'
5 |
6 | PositionSelector.install = (app: App): void => {
7 | app.component(PositionSelector.name, PositionSelector);
8 | };
9 |
10 | const _PositionSelector = PositionSelector as SFCWithInstall;
11 |
12 | export default _PositionSelector;
13 |
14 | export {
15 | mapPosition
16 | }
17 |
--------------------------------------------------------------------------------
/src/plugins/position-selector/mapPosition.ts:
--------------------------------------------------------------------------------
1 | export default function (position: number) {
2 | const result = {
3 | justifyContent: '',
4 | alignItems: '',
5 | display: 'flex'
6 | }
7 | switch (position) {
8 | case 1:
9 | result.justifyContent = 'flex-start'
10 | result.alignItems = 'flex-start'
11 | break;
12 | case 2:
13 | result.justifyContent = 'center'
14 | result.alignItems = 'flex-start'
15 | break;
16 | case 3:
17 | result.justifyContent = 'flex-end'
18 | result.alignItems = 'flex-start'
19 | break;
20 | case 4:
21 | result.justifyContent = 'flex-start'
22 | result.alignItems = 'center'
23 | break;
24 | case 5:
25 | result.justifyContent = 'center'
26 | result.alignItems = 'center'
27 | break;
28 | case 6:
29 | result.justifyContent = 'flex-end'
30 | result.alignItems = 'center'
31 | break;
32 | case 7:
33 | result.justifyContent = 'flex-start'
34 | result.alignItems = 'flex-end'
35 | break;
36 | case 8:
37 | result.justifyContent = 'center'
38 | result.alignItems = 'flex-end'
39 | break;
40 | case 9:
41 | result.justifyContent = 'flex-end'
42 | result.alignItems = 'flex-end'
43 | break;
44 | }
45 | return result
46 | }
47 |
--------------------------------------------------------------------------------
/src/plugins/standard-form/index.ts:
--------------------------------------------------------------------------------
1 | import { App } from 'vue';
2 | import StandardForm from './src/standard-form.vue';
3 | import type { SFCWithInstall } from '@/utils/types'
4 |
5 | StandardForm.install = (app: App): void => {
6 | app.component(StandardForm.name, StandardForm);
7 | };
8 |
9 | const _StandardForm = StandardForm as SFCWithInstall;
10 |
11 | export default _StandardForm;
12 |
--------------------------------------------------------------------------------
/src/plugins/standard-form/src/jsx-render.vue:
--------------------------------------------------------------------------------
1 |
21 |
--------------------------------------------------------------------------------
/src/shims-vue.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.vue' {
2 | import { DefineComponent } from 'vue'
3 | const component: DefineComponent
4 | export default component
5 | }
6 |
7 | declare module '*.md' {
8 | import { DefineComponent } from 'vue'
9 | const component: DefineComponent
10 | export default component
11 | }
12 |
13 | declare module 'vue-grid-layout'
14 |
--------------------------------------------------------------------------------
/src/types/global.d.ts:
--------------------------------------------------------------------------------
1 | export { }
2 |
3 | declare global {
4 | interface Window {
5 | iframeCache: Map,
6 | queryLocalFonts?: () => Array<{
7 | family: string,
8 | fullName: string,
9 | postscriptName: string,
10 | style: string
11 | }>
12 | }
13 | }
--------------------------------------------------------------------------------
/src/types/index.d.ts:
--------------------------------------------------------------------------------
1 | interface AffixInfo {
2 | mode: number;
3 | x: number;
4 | y: number;
5 | }
6 |
7 | interface GlobalOptions {
8 | background: string;
9 | backgroundFilter?: string;
10 | lang?: string;
11 | gutter?: number;
12 | css?: string;
13 | js?: string;
14 | globalFontFamily?: string;
15 | loadHarmonyOSFont?: boolean;
16 | siteTitle?: string;
17 | siteIcon?: string;
18 | disabledDialogAnimation?: boolean;
19 | showMenuBtn?: boolean;
20 | }
21 |
22 | interface ActionClickValueOption {
23 | url: string;
24 | material: string;
25 | w: number;
26 | h: number;
27 | background: string;
28 | backgroundFilter: string;
29 | backdropFilter?: string;
30 | direction: number;
31 | boxShadow: string;
32 | borderRadius: number;
33 | componentSetting: Record;
34 | }
35 |
36 | interface ActionSetting {
37 | actionType: 0 | 1;
38 | actionClickType: 1 | 2 | 3;
39 | actionClickValue: ActionClickValueOption;
40 | }
41 |
42 | interface ComponentOptions {
43 | i?: string;
44 | position: number;
45 | affixInfo?: AffixInfo;
46 | w: number;
47 | h: number;
48 | x?: number;
49 | y?: number;
50 | material: string;
51 | background: string;
52 | backgroundFilter: string;
53 | backdropFilter?: string;
54 | boxShadow?: string;
55 | borderRadius?: number;
56 | componentSetting?: Record;
57 | actionSetting?: ActionSetting | null;
58 | zIndex?: number;
59 | customId?: string;
60 | refresh?: boolean;
61 | }
62 |
63 | interface ComponentSetting {
64 | formData: Record,
65 | formConf: Record | ((formData: any) => Record),
66 | minWidth?: number;
67 | }
68 |
69 | interface MaterialConstanst {
70 | label: string,
71 | text: string,
72 | img?: string
73 | }
74 |
75 | interface Bookmark {
76 | id: string,
77 | type: 'icon' | 'folder' | 'file',
78 | title?: string,
79 | url?: string,
80 | iconType?: 'api' | 'text' | 'network',
81 | iconPath?: string,
82 | iconText?: string,
83 | bgColor?: string,
84 | children?: Bookmark[]
85 | }
86 |
87 | type IFontData = {
88 | cn: string,
89 | en: string
90 | }
--------------------------------------------------------------------------------
/src/utils/color.ts:
--------------------------------------------------------------------------------
1 | export function lightenDarkenColor(col: string, amt: number) {
2 | let usePound = false;
3 | if (col[0] === '#') {
4 | col = col.slice(1);
5 | usePound = true;
6 | }
7 | const num = parseInt(col, 16);
8 | let r = (num >> 16) + amt;
9 | if (r > 255) r = 255;
10 | else if (r < 0) r = 0;
11 | let b = ((num >> 8) & 0x00FF) + amt;
12 | if (b > 255) b = 255;
13 | else if (b < 0) b = 0;
14 | let g = (num & 0x0000FF) + amt;
15 | if (g > 255) g = 255;
16 | else if (g < 0) g = 0;
17 | return (usePound ? '#' : '') + (g | (b << 8) | (r << 16)).toString(16);
18 | }
19 |
20 | export function getColorBrightness(col: string) {
21 | if (col[0] === '#') {
22 | col = col.slice(1);
23 | }
24 | const num = parseInt(col, 16);
25 | const r = num >> 16
26 | const g = num & 0x0000FF
27 | const b = (num >> 8) & 0x00FF
28 | const brightness = 0.3 * r + 0.6 * g + 0.1 * b
29 | // const brightness = ((r * 299) + (g * 587) + (b * 114)) / 1000
30 | return brightness
31 | }
32 |
--------------------------------------------------------------------------------
/src/utils/direction.ts:
--------------------------------------------------------------------------------
1 | enum DirectionEnum {
2 | SCREEN_CENTER = 0,
3 | TOP_START = 1,
4 | TOP_CENTER,
5 | TOP_END,
6 | RIGHT_START,
7 | RIGHT_CENTER,
8 | RIGHT_END,
9 | BOTTOM_END,
10 | BOTTOM_CENTER,
11 | BOTTOM_START,
12 | LEFT_END,
13 | LEFT_CENTER,
14 | LEFT_START
15 | }
16 |
17 | export const directionList: Record[] = []
18 | for (const key in DirectionEnum) {
19 | if (isNaN(key as any)) {
20 | const wordArr = key.split('_').map(word => {
21 | const _word = word.toLocaleLowerCase()
22 | return _word.charAt(0).toUpperCase() + _word.substring(1)
23 | })
24 | directionList.push({
25 | label: wordArr.join(' '),
26 | value: DirectionEnum[key]
27 | })
28 | }
29 | }
30 |
31 | export interface PopoverOption {
32 | width: number,
33 | height: number,
34 | offset?: number
35 | }
36 |
37 | /**
38 | * 获取Popover目标信息
39 | * @param element 来源DOM
40 | * @param popoverRect popover信息
41 | * @param direction popover方向
42 | * @returns [endX, endY, fromX, fromY]
43 | */
44 | export function getPopoverActivePointByDirection(
45 | element: HTMLElement | null | undefined,
46 | popoverRect: PopoverOption,
47 | direction = DirectionEnum.BOTTOM_CENTER
48 | ) {
49 | const { width, height, top, left } = element ? element.getBoundingClientRect() : {
50 | width: 0,
51 | height: 0,
52 | top: window.innerHeight / 2,
53 | left: window.innerWidth / 2
54 | }
55 | const { width: popoverWidth, height: popoverHeight, offset = 10 } = popoverRect
56 | const activePointMap = {
57 | [DirectionEnum.SCREEN_CENTER]: [window.innerWidth / 2 - popoverWidth / 2, window.innerHeight / 2 - popoverHeight / 2],
58 | [DirectionEnum.TOP_START]: [left, top - popoverHeight - offset],
59 | [DirectionEnum.TOP_CENTER]: [left + width / 2 - popoverWidth / 2, top - popoverHeight - offset],
60 | [DirectionEnum.TOP_END]: [left + width - popoverWidth, top - popoverHeight - offset],
61 | [DirectionEnum.RIGHT_START]: [left + width + offset, top],
62 | [DirectionEnum.RIGHT_CENTER]: [left + width + offset, top + height / 2 - popoverHeight / 2],
63 | [DirectionEnum.RIGHT_END]: [left + width + offset, top + height - popoverHeight],
64 | [DirectionEnum.BOTTOM_END]: [left + width - popoverWidth, top + height + offset],
65 | [DirectionEnum.BOTTOM_CENTER]: [left + width / 2 - popoverWidth / 2, top + height + offset],
66 | [DirectionEnum.BOTTOM_START]: [left, top + height + offset],
67 | [DirectionEnum.LEFT_END]: [left - popoverWidth - offset, top + height - popoverHeight],
68 | [DirectionEnum.LEFT_CENTER]: [left - popoverWidth - offset, top + height / 2 - popoverHeight / 2],
69 | [DirectionEnum.LEFT_START]: [left - popoverWidth - offset, top]
70 | }
71 | const fromPoint = [left + width / 2, top + height / 2]
72 | try {
73 | return [...activePointMap[direction], ...fromPoint]
74 | } catch {
75 | return [0, 0, ...fromPoint]
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/utils/gzip.ts:
--------------------------------------------------------------------------------
1 | import { gzip, inflate } from 'pako'
2 |
3 | export function zip(data: string) {
4 | return window.btoa(gzip(data, { to: 'string' }));
5 | }
6 |
7 | export function unzip(data: string) {
8 | const strData = window.atob(data);
9 | const charData = strData.split('').map((x) => x.charCodeAt(0))
10 | const binData = new Uint8Array(charData);
11 | const result = inflate(binData, { to: 'string' });
12 | return result
13 | }
--------------------------------------------------------------------------------
/src/utils/preview-mode.ts:
--------------------------------------------------------------------------------
1 | import request from '@/utils/request'
2 | import { unzip } from '@/utils/gzip'
3 | import { useStore } from '@/store'
4 |
5 | export async function getPreviewModeDataFromKey(previewKey: string) {
6 | const { errCode, data } = await request({
7 | url: '/getImport',
8 | method: 'post',
9 | data: { importKey: previewKey }
10 | })
11 | if (errCode === 200) {
12 | const result = unzip(data)
13 | return JSON.parse(result)
14 | } else {
15 | throw new Error('Resolve preview key error')
16 | }
17 | }
18 |
19 | export async function getPreviewModeDataFromRemote(url: string) {
20 | const result = await fetch(url)
21 | const json = await result.json()
22 | return json
23 | }
24 |
25 | export async function setPreviewModeData(previewKey: string) {
26 | let data: any
27 | if (/^[0-9A-Z]{5}$/.test(previewKey)) {
28 | data = await getPreviewModeDataFromKey(previewKey)
29 | } else {
30 | data = await getPreviewModeDataFromRemote(decodeURIComponent(previewKey))
31 | }
32 | if (!data.list || !data.global || !data.affix) {
33 | throw new Error('数据非法')
34 | }
35 | const store = useStore()
36 | const {
37 | list,
38 | affix,
39 | global,
40 | showBackgroundEffect,
41 | showRefreshBtn,
42 | tabList,
43 | showTabSwitchBtn,
44 | enableKeydownSwitchTab,
45 | backgroundEffectActive
46 | } = data
47 | store.updateStates([
48 | { key: 'tabList', value: tabList },
49 | { key: 'list', value: list },
50 | { key: 'affix', value: affix },
51 | { key: 'showBackgroundEffect', value: showBackgroundEffect },
52 | { key: 'showRefreshBtn', value: showRefreshBtn },
53 | { key: 'showTabSwitchBtn', value: showTabSwitchBtn },
54 | { key: 'enableKeydownSwitchTab', value: enableKeydownSwitchTab },
55 | { key: 'backgroundEffectActive', value: backgroundEffectActive }
56 | ])
57 | store.updateGlobal(global)
58 | }
--------------------------------------------------------------------------------
/src/utils/request.ts:
--------------------------------------------------------------------------------
1 | import { apiURL } from "@/global"
2 |
3 | export default async (options: {
4 | url: string,
5 | method?: 'get' | 'post' | 'head',
6 | params?: Record,
7 | data?: Record | FormData,
8 | headers?: Record,
9 | timeout?: number,
10 | return?: 'response' | 'json' | 'text'
11 | }) => {
12 | const method = options.method || 'get'
13 |
14 | const requestInit: RequestInit = { method }
15 | let target = options.url.indexOf('http') === 0 ? options.url : `${apiURL}${options.url}`
16 | if (method === 'get' && options.params) {
17 | const params = new URLSearchParams(options.params)
18 | target += `?${params}`
19 | }
20 | if (method === 'post' && options.data) {
21 | if (options.data instanceof FormData) {
22 | requestInit.body = options.data
23 | } else {
24 | requestInit.body = JSON.stringify(options.data)
25 | options.headers = {
26 | 'Content-Type': 'application/json',
27 | ...options.headers
28 | }
29 | }
30 | }
31 | requestInit.headers = options.headers
32 |
33 | const controller = new AbortController()
34 | requestInit.signal = controller.signal
35 | setTimeout(() => {
36 | controller.abort()
37 | }, options.timeout || 10000)
38 |
39 | try {
40 | const result = await fetch(target, requestInit)
41 | if (options.return === 'response') {
42 | return Promise.resolve(result)
43 | } else if (options.return === 'text'){
44 | const resultText = await result.text()
45 | return Promise.resolve(resultText)
46 | } else {
47 | const resultJSON = await result.json()
48 | return Promise.resolve(resultJSON)
49 | }
50 | } catch (e) {
51 | return Promise.reject(e)
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/utils/types.ts:
--------------------------------------------------------------------------------
1 | import type { App } from 'vue'
2 |
3 | export type SFCWithInstall = T & { install(app: App): void; }
4 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "module": "esnext",
5 | "moduleResolution": "node",
6 | "strict": true,
7 | "jsx": "preserve",
8 | "sourceMap": true,
9 | "resolveJsonModule": true,
10 | "esModuleInterop": true,
11 | "lib": ["esnext", "dom"],
12 | "baseUrl": "./",
13 | "paths": {
14 | "@/*": ["src/*"]
15 | },
16 | "typeRoots": [ "./node_modules/@types", "src/types", "element-plus/global"],
17 | "allowJs": true,
18 | "noImplicitAny": false,
19 | "outDir": "./dist",
20 | "isolatedModules": true,
21 | "checkJs": false
22 | },
23 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
24 | }
25 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import { resolve } from 'path'
3 | import Vue from '@vitejs/plugin-vue'
4 | import VueJSX from '@vitejs/plugin-vue-jsx'
5 | import VueMarkdown from 'vite-plugin-md'
6 | import VueI18n from '@intlify/unplugin-vue-i18n/vite'
7 | // import { visualizer } from 'rollup-plugin-visualizer'
8 |
9 | let base = '/'
10 | if (process.env.VITE_APP_BUILD_MODE === 'crx') base = './'
11 | if (process.env.VITE_APP_BUILD_MODE === 'cdn') base = 'https://cdn.kongfandong.cn/howdz/dist/'
12 |
13 | // https://vitejs.dev/config/
14 | export default defineConfig({
15 | base,
16 | resolve: {
17 | alias: {
18 | '@': resolve(__dirname, 'src'),
19 | 'vue-i18n': 'vue-i18n/dist/vue-i18n.runtime.esm-bundler.js'
20 | }
21 | },
22 | optimizeDeps: {
23 | include: [
24 | 'vue',
25 | 'vue-i18n',
26 | 'pinia',
27 | 'pinia-plugin-persistedstate',
28 | 'vue-grid-layout',
29 | 'element-plus',
30 | 'vuedraggable',
31 | '@howdyjs/to-control',
32 | '@howdyjs/mouse-menu',
33 | 'dayjs'
34 | ]
35 | },
36 | css: {
37 | preprocessorOptions: {
38 | scss: {
39 | charset: false,
40 | additionalData: '@use "@/assets/element-variables" as *;'
41 | // additionalData: "@import '@/assets/variables.scss';"
42 | }
43 | }
44 | },
45 | plugins: [
46 | Vue({ include: [/\.vue$/, /\.md$/] }),
47 | VueJSX(),
48 | VueMarkdown(),
49 | VueI18n({ include: resolve(__dirname, 'src/lang/locales/**') })
50 | // visualizer()
51 | ],
52 | build: {
53 | outDir: process.env.VITE_APP_BUILD_MODE === 'crx' ? 'crx' : 'dist'
54 | },
55 | server: {
56 | proxy: {
57 | '/api': {
58 | target: 'http://kongfandong.cn',
59 | changeOrigin: true,
60 | rewrite: (path: string) => path.replace(/^\/api/, '')
61 | }
62 | }
63 | }
64 | })
65 |
--------------------------------------------------------------------------------