├── .browserslistrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github └── workflows │ └── gh-pages.yml ├── .gitignore ├── .npmrc ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── index.html ├── locales ├── en.yml └── zh-CN.yml ├── package.json ├── public ├── CNAME ├── audio │ ├── fan.wav │ └── switch.mp3 ├── favicon.ico ├── favicon.png ├── img │ └── icons │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── pwa-192x192.png │ │ └── pwa-512x512.png ├── logo.png └── robots.txt ├── src ├── App.vue ├── components │ ├── BaseFooter.vue │ ├── BaseHeader.vue │ ├── Fan.vue │ └── FanSwitch.vue ├── composables │ ├── dark.ts │ └── index.ts ├── layouts │ └── default.vue ├── main.ts ├── modules │ ├── gtm.ts │ ├── i18n.ts │ ├── nprogress.ts │ └── pwa.ts ├── pages │ └── index.vue ├── shim.d.ts ├── stores │ └── index.ts ├── styles │ ├── fan.scss │ ├── index.scss │ ├── main.scss │ └── vars.scss └── types │ └── index.ts ├── tsconfig.json ├── vite.config.ts └── windi.config.ts /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not ie <= 8 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | public 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@antfu" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Pages 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | deploy: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | with: 12 | submodules: true 13 | 14 | - name: Setup Node 15 | uses: actions/setup-node@v2 16 | with: 17 | node-version: "14.x" 18 | 19 | - run: yarn 20 | - run: yarn build 21 | 22 | - name: Deploy 23 | uses: peaceiris/actions-gh-pages@v3 24 | with: 25 | github_token: ${{ secrets.GITHUB_TOKEN }} 26 | publish_dir: ./dist 27 | force_orphan: true 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vitesse 2 | .vite-ssg-dist 3 | .vite-ssg-temp 4 | components.d.ts 5 | auto-imports.d.ts 6 | 7 | yarn.lock 8 | package-lock.json 9 | pnpm-lock.yaml 10 | 11 | .DS_Store 12 | node_modules 13 | /dist 14 | 15 | # local env files 16 | .env.local 17 | .env.*.local 18 | 19 | # Log files 20 | *.log 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # Editor directories and files 26 | .idea 27 | *.suo 28 | *.ntvs* 29 | *.njsproj 30 | *.sln 31 | *.sw* 32 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "i18n-ally.keystyle": "nested", 3 | "i18n-ally.localesPaths": "locales", 4 | "i18n-ally.sortKeys": true, 5 | "prettier.enable": false, 6 | "typescript.tsdk": "node_modules/typescript/lib", 7 | "editor.codeActionsOnSave": { 8 | "source.fixAll.eslint": true, 9 | }, 10 | "files.associations": { 11 | "*.css": "postcss", 12 | }, 13 | "editor.formatOnSave": false, 14 | } 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 2019-02-19 4 | 5 | - transfer to [ElpsyCN](https://github.com/elpsycn) 6 | 7 | ## 2019-01-05 8 | 9 | - remove miniprogram 10 | - add fan sound effect 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 云游君 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.md: -------------------------------------------------------------------------------- 1 | # electric-fan 2 | 3 | > [Electric Fan](https://fan.elpsy.cn) 4 | 5 | [未来道具研究所](https://elpsy.cn) 6 | 7 | 一个智障小工具 8 | 9 | 电风扇! 为你的夏日带去清凉! 10 | 11 | - [Web Demo](https://fan.elpsy.cn) 12 | 13 | ## Feature 14 | 15 | ### 优势 16 | 17 | - 随时随地打开风扇 18 | - 便携 19 | - 低功耗(使用 HTML CSS 而非 Canvas 绘制) 20 | - 操作简单 21 | - 安装便捷 22 | 23 | ### 劣势 24 | 25 | - 没有风 26 | 27 | ## Intend 28 | 29 | ……这是我在小空调之前写的,因为写的太丑甚至没好意思宣传,没想到小空调意外的火了。 30 | 31 | - [x] [空调](https://github.com/YunYouJun/air-conditioner) 32 | - [ ] 温度计 🌡️ 33 | 34 | ## About 35 | 36 | ![云游君](https://cdn.jsdelivr.net/gh/YunYouJun/cdn/img/about/white-qrcode-and-search.jpg) 37 | 38 | ## [Change Log](CHANGELOG.md) 39 | 40 | ## Dev 开发 41 | 42 | ```sh 43 | # install dependencies 44 | yarn 45 | ``` 46 | 47 | ```sh 48 | # 启动 49 | # http://localhost:3000/ 50 | yarn dev 51 | ``` 52 | 53 | ```sh 54 | # 构建 55 | yarn build 56 | ``` 57 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 夏日清凉小风扇~ 10 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /locales/en.yml: -------------------------------------------------------------------------------- 1 | button: 2 | toggle_dark: Toggle dark mode 3 | -------------------------------------------------------------------------------- /locales/zh-CN.yml: -------------------------------------------------------------------------------- 1 | button: 2 | toggle_dark: 切换深色模式 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electric-fan", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "Electric Fan", 6 | "author": { 7 | "email": "me@yunyoujun.cn", 8 | "name": "YunYouJun", 9 | "url": "https://www.yunyoujun.cn" 10 | }, 11 | "scripts": { 12 | "dev": "vite", 13 | "build": "vite build", 14 | "preview": "vite preview", 15 | "lint": "eslint --ext .js,.ts,.json,.vue --fix ." 16 | }, 17 | "dependencies": { 18 | "@gtm-support/vue-gtm": "^1.2.3", 19 | "@vueuse/core": "^7.1.2", 20 | "@vueuse/head": "^0.7.2", 21 | "nprogress": "^0.2.0", 22 | "vue": "^3.2.17", 23 | "vue-about-me": "^1.2.0", 24 | "vue-i18n": "^9.2.0-beta.6" 25 | }, 26 | "devDependencies": { 27 | "@antfu/eslint-config": "^0.12.0", 28 | "@iconify-json/carbon": "^1.0.12", 29 | "@intlify/vite-plugin-vue-i18n": "^3.2.1", 30 | "@types/markdown-it-link-attributes": "^3.0.1", 31 | "@types/nprogress": "^0.2.0", 32 | "@vitejs/plugin-vue": "^1.10.1", 33 | "@vue/compiler-sfc": "^3.2.23", 34 | "critters": "^0.0.15", 35 | "eslint": "^8.4.0", 36 | "markdown-it-link-attributes": "^3.0.0", 37 | "markdown-it-prism": "^2.2.1", 38 | "sass": "^1.44.0", 39 | "typescript": "^4.5.2", 40 | "unplugin-auto-import": "^0.5.1", 41 | "unplugin-icons": "^0.12.22", 42 | "unplugin-vue-components": "^0.17.4", 43 | "vite": "^2.6.14", 44 | "vite-plugin-inspect": "^0.3.11", 45 | "vite-plugin-md": "^0.11.4", 46 | "vite-plugin-pages": "^0.18.2", 47 | "vite-plugin-pwa": "0.11.10", 48 | "vite-plugin-style-import": "^1.4.0", 49 | "vite-plugin-vue-layouts": "^0.5.0", 50 | "vite-plugin-windicss": "^1.5.4", 51 | "vite-ssg": "^0.16.2" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /public/CNAME: -------------------------------------------------------------------------------- 1 | fan.elpsy.cn 2 | -------------------------------------------------------------------------------- /public/audio/fan.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElpsyCN/electric-fan/ffcb523dfd4ce09e1b1c7d105fcd52b3b39b17ec/public/audio/fan.wav -------------------------------------------------------------------------------- /public/audio/switch.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElpsyCN/electric-fan/ffcb523dfd4ce09e1b1c7d105fcd52b3b39b17ec/public/audio/switch.mp3 -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElpsyCN/electric-fan/ffcb523dfd4ce09e1b1c7d105fcd52b3b39b17ec/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElpsyCN/electric-fan/ffcb523dfd4ce09e1b1c7d105fcd52b3b39b17ec/public/favicon.png -------------------------------------------------------------------------------- /public/img/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElpsyCN/electric-fan/ffcb523dfd4ce09e1b1c7d105fcd52b3b39b17ec/public/img/icons/favicon-16x16.png -------------------------------------------------------------------------------- /public/img/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElpsyCN/electric-fan/ffcb523dfd4ce09e1b1c7d105fcd52b3b39b17ec/public/img/icons/favicon-32x32.png -------------------------------------------------------------------------------- /public/img/icons/pwa-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElpsyCN/electric-fan/ffcb523dfd4ce09e1b1c7d105fcd52b3b39b17ec/public/img/icons/pwa-192x192.png -------------------------------------------------------------------------------- /public/img/icons/pwa-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElpsyCN/electric-fan/ffcb523dfd4ce09e1b1c7d105fcd52b3b39b17ec/public/img/icons/pwa-512x512.png -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElpsyCN/electric-fan/ffcb523dfd4ce09e1b1c7d105fcd52b3b39b17ec/public/logo.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 16 | -------------------------------------------------------------------------------- /src/components/BaseFooter.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 31 | -------------------------------------------------------------------------------- /src/components/BaseHeader.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 12 | -------------------------------------------------------------------------------- /src/components/Fan.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 20 | 21 | 24 | -------------------------------------------------------------------------------- /src/components/FanSwitch.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 98 | -------------------------------------------------------------------------------- /src/composables/dark.ts: -------------------------------------------------------------------------------- 1 | // import { useDark, useToggle } from '@vueuse/core' 2 | 3 | export const isDark = useDark() 4 | export const toggleDark = useToggle(isDark) 5 | -------------------------------------------------------------------------------- /src/composables/index.ts: -------------------------------------------------------------------------------- 1 | export * from './dark' 2 | -------------------------------------------------------------------------------- /src/layouts/default.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { ViteSSG } from 'vite-ssg' 2 | import generatedRoutes from 'virtual:generated-pages' 3 | import { setupLayouts } from 'virtual:generated-layouts' 4 | import App from './App.vue' 5 | 6 | // windicss layers 7 | import 'virtual:windi-base.css' 8 | import 'virtual:windi-components.css' 9 | 10 | // your custom styles here 11 | import './styles/vars.scss' 12 | import './styles/main.scss' 13 | import './styles/index.scss' 14 | 15 | // windicss utilities should be the last style import 16 | import 'virtual:windi-utilities.css' 17 | // windicss devtools support (dev only) 18 | import 'virtual:windi-devtools' 19 | 20 | const routes = setupLayouts(generatedRoutes) 21 | 22 | // https://github.com/antfu/vite-ssg 23 | export const createApp = ViteSSG( 24 | App, 25 | { routes }, 26 | (ctx) => { 27 | // install all modules under `modules/` 28 | Object.values(import.meta.globEager('./modules/*.ts')).map(i => i.install?.(ctx)) 29 | }, 30 | ) 31 | -------------------------------------------------------------------------------- /src/modules/gtm.ts: -------------------------------------------------------------------------------- 1 | import { createGtm } from '@gtm-support/vue-gtm' 2 | import { UserModule } from '~/types' 3 | 4 | // https://github.com/antfu/vite-plugin-pwa#automatic-reload-when-new-content-available 5 | export const install: UserModule = ({ isClient, app }) => { 6 | if (!isClient) return 7 | 8 | app.use( 9 | createGtm({ 10 | id: 'GTM-NMD3456', 11 | }), 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /src/modules/i18n.ts: -------------------------------------------------------------------------------- 1 | import { createI18n } from 'vue-i18n' 2 | import { UserModule } from '~/types' 3 | 4 | // Import i18n resources 5 | // https://vitejs.dev/guide/features.html#glob-import 6 | // 7 | // Don't need this? Try vitesse-lite: https://github.com/antfu/vitesse-lite 8 | const messages = Object.fromEntries( 9 | Object.entries( 10 | import.meta.globEager('../../locales/*.y(a)?ml')) 11 | .map(([key, value]) => { 12 | const yaml = key.endsWith('.yaml') 13 | return [key.slice(14, yaml ? -5 : -4), value.default] 14 | }), 15 | ) 16 | 17 | export const install: UserModule = ({ app }) => { 18 | const i18n = createI18n({ 19 | legacy: false, 20 | locale: 'en', 21 | messages, 22 | }) 23 | 24 | app.use(i18n) 25 | } 26 | -------------------------------------------------------------------------------- /src/modules/nprogress.ts: -------------------------------------------------------------------------------- 1 | import NProgress from 'nprogress' 2 | import { UserModule } from '~/types' 3 | 4 | export const install: UserModule = ({ isClient, router }) => { 5 | if (isClient) { 6 | router.beforeEach(() => { NProgress.start() }) 7 | router.afterEach(() => { NProgress.done() }) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/modules/pwa.ts: -------------------------------------------------------------------------------- 1 | import { UserModule } from '~/types' 2 | 3 | // https://github.com/antfu/vite-plugin-pwa#automatic-reload-when-new-content-available 4 | export const install: UserModule = ({ isClient, router }) => { 5 | if (!isClient) { return } 6 | 7 | router.isReady().then(async() => { 8 | const { registerSW } = await import('virtual:pwa-register') 9 | registerSW({ immediate: true }) 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /src/pages/index.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 19 | -------------------------------------------------------------------------------- /src/shim.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.vue" { 2 | import { defineComponent } from "vue"; 3 | const component: ReturnType; 4 | export default component; 5 | } 6 | -------------------------------------------------------------------------------- /src/stores/index.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from 'vue' 2 | export const store = { 3 | debug: true, 4 | 5 | state: reactive({ 6 | /** 7 | * 风力级别 8 | */ 9 | level: 0, 10 | }), 11 | 12 | setLevel(value: number) { 13 | this.state.level = value 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /src/styles/fan.scss: -------------------------------------------------------------------------------- 1 | @use "sass:math"; 2 | $background-color: var(--fan-color); 3 | $border-color: var(--fan-color); 4 | 5 | $fan-width: 220px; 6 | 7 | $header-width: $fan-width; 8 | $header-border-width: 10px; 9 | 10 | $leaf-border-width: 8px; 11 | $leaf-width: 200px; 12 | 13 | $neck-width: 10px; 14 | $neck-height: 60px; 15 | 16 | $footer-height: 10px; 17 | $footer-width: 150px; 18 | 19 | $circle-width: 10px; 20 | $circle-border-width: 8px; 21 | $circle-position: math.div($leaf-width, 2) - 22 | (math.div($circle-width, 2) + $circle-border-width); 23 | 24 | @keyframes leafsRotate { 25 | 0% { 26 | transform: rotate(0deg); 27 | } 28 | 100% { 29 | transform: rotate(1080deg); 30 | } 31 | } 32 | 33 | .fan-btn { 34 | display: inline-flex; 35 | justify-content: center; 36 | align-items: center; 37 | border-radius: 50%; 38 | width: 3rem; 39 | height: 3rem; 40 | border: 2px solid var(--fan-color); 41 | border-bottom-width: 0.25rem; 42 | margin: 0.5rem; 43 | font-size: 1rem; 44 | 45 | outline: none; 46 | 47 | font-family: monospace; 48 | font-weight: bold; 49 | 50 | transition: 0.2s; 51 | 52 | &:hover { 53 | color: var(--fan-bg-color); 54 | background-color: var(--fan-color); 55 | } 56 | 57 | &:focus { 58 | outline: none; 59 | } 60 | 61 | &.is-active, 62 | &:active { 63 | color: var(--fan-bg-color); 64 | background-color: var(--fan-color); 65 | transform: translateY(0.32rem); 66 | border-bottom-width: 1px; 67 | } 68 | } 69 | 70 | #fan { 71 | width: $fan-width; 72 | height: $header-width + $neck-height + $footer-height; 73 | margin: 2rem auto; 74 | position: relative; 75 | z-index: 4; 76 | .fan-header { 77 | box-sizing: content-box; 78 | 79 | width: $header-width; 80 | height: $header-width; 81 | position: absolute; 82 | left: -$header-border-width; 83 | top: -$header-border-width; 84 | border-radius: 50%; 85 | z-index: 1; 86 | border: solid $header-border-width $border-color; 87 | 88 | .leafs { 89 | z-index: 2; 90 | position: absolute; 91 | animation: leafsRotate 0s infinite linear; 92 | transform-origin: center center; 93 | width: $leaf-width; 94 | height: $leaf-width; 95 | top: $header-border-width; 96 | left: $header-border-width; 97 | 98 | .circle { 99 | box-sizing: content-box; 100 | 101 | width: $circle-width; 102 | height: $circle-width; 103 | border: solid $circle-border-width $border-color; 104 | border-radius: 50%; 105 | position: absolute; 106 | left: $circle-position; 107 | top: $circle-position; 108 | } 109 | 110 | .leaf { 111 | box-sizing: content-box; 112 | 113 | width: 72px; 114 | height: 60px; 115 | border-radius: 20% 50%; 116 | border: $leaf-border-width solid $border-color; 117 | position: absolute; 118 | left: math.div($leaf-width, 2); 119 | top: math.div($leaf-width, 2); 120 | transform-origin: 0% 0%; 121 | } 122 | 123 | .leaf-1 { 124 | @extend .leaf; 125 | } 126 | .leaf-2 { 127 | @extend .leaf; 128 | transform: rotate(120deg); 129 | } 130 | .leaf-3 { 131 | @extend .leaf; 132 | transform: rotate(240deg); 133 | } 134 | } 135 | 136 | .leafs-0 { 137 | @extend .leafs; 138 | animation-duration: 3s; 139 | animation-timing-function: ease-out; 140 | animation-delay: 0s; 141 | animation-iteration-count: 1; 142 | animation-fill-mode: forwards; 143 | } 144 | .leafs-1 { 145 | @extend .leafs; 146 | animation-duration: 2s; 147 | } 148 | .leafs-2 { 149 | @extend .leafs; 150 | animation-duration: 1.5s; 151 | } 152 | .leafs-3 { 153 | @extend .leafs; 154 | animation-duration: 0.8s; 155 | } 156 | } 157 | 158 | .fan-neck { 159 | width: $neck-width; 160 | height: $neck-height; 161 | background: $background-color; 162 | position: absolute; 163 | top: $header-width + 8px; 164 | left: math.div($header-width, 2) - 165 | math.div($neck-width, 2); 166 | z-index: 2; 167 | } 168 | 169 | .fan-footer { 170 | width: $footer-width; 171 | height: $footer-height; 172 | border-radius: 5%; 173 | background: $background-color; 174 | position: absolute; 175 | top: $header-width + $neck-height + 8px; 176 | left: math.div($fan-width, 2) - 177 | math.div($footer-width, 2); 178 | z-index: 3; 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | #app { 2 | font-family: "Avenir", Helvetica, Arial, sans-serif; 3 | -webkit-font-smoothing: antialiased; 4 | -moz-osx-font-smoothing: grayscale; 5 | text-align: center; 6 | color: #2c3e50; 7 | } 8 | 9 | .animate-logo { 10 | animation: iconAnimate 1.5s ease-in-out infinite; 11 | } 12 | -------------------------------------------------------------------------------- /src/styles/main.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | #app { 4 | height: 100%; 5 | margin: 0; 6 | padding: 0; 7 | } 8 | 9 | html.dark { 10 | background: #121212; 11 | } 12 | 13 | #nprogress { 14 | pointer-events: none; 15 | } 16 | 17 | #nprogress .bar { 18 | @apply bg-blue-600 opacity-75; 19 | 20 | position: fixed; 21 | z-index: 1031; 22 | top: 0; 23 | left: 0; 24 | 25 | width: 100%; 26 | height: 2px; 27 | } 28 | 29 | .btn { 30 | @apply px-4 py-1 rounded inline-block 31 | bg-blue-600 text-white cursor-pointer 32 | hover:bg-blue-700 33 | disabled:cursor-default disabled:bg-gray-600 disabled:opacity-50; 34 | } 35 | 36 | .icon-btn { 37 | @apply inline-block cursor-pointer select-none 38 | opacity-75 transition duration-200 ease-in-out 39 | hover:opacity-100 hover:text-blue-600; 40 | font-size: 0.9em; 41 | } 42 | -------------------------------------------------------------------------------- /src/styles/vars.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --fan-color: #333; 3 | --fan-bg-color: white; 4 | } 5 | 6 | html.dark { 7 | --fan-color: white; 8 | --fan-bg-color: #333; 9 | } 10 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | import { ViteSSGContext } from 'vite-ssg' 2 | 3 | export type UserModule = (ctx: ViteSSGContext) => void 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "module": "ESNext", 5 | "target": "es2016", 6 | "lib": ["DOM", "ESNext"], 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "incremental": false, 10 | "skipLibCheck": true, 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "noUnusedLocals": true, 14 | "strictNullChecks": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "types": [ 17 | "vite/client", 18 | "vite-plugin-pages/client", 19 | "vite-plugin-vue-layouts/client" 20 | ], 21 | "paths": { 22 | "~/*": ["src/*"] 23 | } 24 | }, 25 | "exclude": ["dist", "node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { defineConfig } from 'vite' 3 | import Vue from '@vitejs/plugin-vue' 4 | import Pages from 'vite-plugin-pages' 5 | import Layouts from 'vite-plugin-vue-layouts' 6 | import Icons from 'unplugin-icons/vite' 7 | import IconsResolver from 'unplugin-icons/resolver' 8 | import Components from 'unplugin-vue-components/vite' 9 | import AutoImport from 'unplugin-auto-import/vite' 10 | import Markdown from 'vite-plugin-md' 11 | import WindiCSS from 'vite-plugin-windicss' 12 | import { VitePWA } from 'vite-plugin-pwa' 13 | import VueI18n from '@intlify/vite-plugin-vue-i18n' 14 | import Inspect from 'vite-plugin-inspect' 15 | import Prism from 'markdown-it-prism' 16 | import LinkAttributes from 'markdown-it-link-attributes' 17 | 18 | const markdownWrapperClasses = 'prose prose-sm m-auto text-left' 19 | 20 | export default defineConfig({ 21 | resolve: { 22 | alias: { 23 | '~/': `${path.resolve(__dirname, 'src')}/`, 24 | }, 25 | }, 26 | plugins: [ 27 | Vue({ 28 | include: [/\.vue$/, /\.md$/], 29 | }), 30 | 31 | // https://github.com/hannoeru/vite-plugin-pages 32 | Pages({ 33 | extensions: ['vue', 'md'], 34 | }), 35 | 36 | // https://github.com/JohnCampionJr/vite-plugin-vue-layouts 37 | Layouts(), 38 | 39 | // https://github.com/antfu/unplugin-auto-import 40 | AutoImport({ 41 | imports: [ 42 | 'vue', 43 | 'vue-router', 44 | 'vue-i18n', 45 | '@vueuse/head', 46 | '@vueuse/core', 47 | ], 48 | dts: 'src/auto-imports.d.ts', 49 | }), 50 | 51 | // https://github.com/antfu/unplugin-vue-components 52 | Components({ 53 | // allow auto load markdown components under `./src/components/` 54 | extensions: ['vue', 'md'], 55 | 56 | // allow auto import and register components used in markdown 57 | include: [/\.vue$/, /\.vue\?vue/, /\.md$/], 58 | 59 | // custom resolvers 60 | resolvers: [ 61 | // auto import icons 62 | // https://github.com/antfu/unplugin-icons 63 | IconsResolver({ 64 | // componentPrefix: '', 65 | // enabledCollections: ['carbon'] 66 | }), 67 | ], 68 | 69 | dts: 'src/components.d.ts', 70 | }), 71 | 72 | // https://github.com/antfu/unplugin-icons 73 | Icons({ 74 | autoInstall: true 75 | }), 76 | 77 | // https://github.com/antfu/vite-plugin-windicss 78 | WindiCSS({ 79 | safelist: markdownWrapperClasses, 80 | }), 81 | 82 | // https://github.com/antfu/vite-plugin-md 83 | // Don't need this? Try vitesse-lite: https://github.com/antfu/vitesse-lite 84 | Markdown({ 85 | wrapperClasses: markdownWrapperClasses, 86 | headEnabled: true, 87 | markdownItSetup(md) { 88 | // https://prismjs.com/ 89 | // @ts-expect-error types mismatch 90 | md.use(Prism) 91 | // @ts-expect-error types mismatch 92 | md.use(LinkAttributes, { 93 | pattern: /^https?:\/\//, 94 | attrs: { 95 | target: '_blank', 96 | rel: 'noopener', 97 | }, 98 | }) 99 | }, 100 | }), 101 | 102 | // https://github.com/antfu/vite-plugin-pwa 103 | VitePWA({ 104 | registerType: 'autoUpdate', 105 | includeAssets: ['favicon.svg', 'robots.txt', 'safari-pinned-tab.svg'], 106 | manifest: { 107 | name: 'Electric Fan', 108 | short_name: 'Fan', 109 | theme_color: '#ffffff', 110 | icons: [ 111 | { 112 | src: '/img/icons/pwa-192x192.png', 113 | sizes: '192x192', 114 | type: 'image/png', 115 | }, 116 | { 117 | src: '/img/icons/pwa-512x512.png', 118 | sizes: '512x512', 119 | type: 'image/png', 120 | }, 121 | { 122 | src: '/img/icons/pwa-512x512.png', 123 | sizes: '512x512', 124 | type: 'image/png', 125 | purpose: 'any maskable', 126 | }, 127 | ], 128 | }, 129 | }), 130 | 131 | // https://github.com/intlify/bundle-tools/tree/main/packages/vite-plugin-vue-i18n 132 | VueI18n({ 133 | runtimeOnly: true, 134 | compositionOnly: true, 135 | include: [path.resolve(__dirname, 'locales/**')], 136 | }), 137 | 138 | // https://github.com/antfu/vite-plugin-inspect 139 | Inspect({ 140 | // change this to enable inspect for debugging 141 | enabled: false, 142 | }), 143 | ], 144 | 145 | server: { 146 | fs: { 147 | strict: true, 148 | }, 149 | }, 150 | 151 | // https://github.com/antfu/vite-ssg 152 | ssgOptions: { 153 | script: 'async', 154 | formatting: 'minify', 155 | }, 156 | 157 | optimizeDeps: { 158 | include: [ 159 | 'vue', 160 | 'vue-router', 161 | '@vueuse/core', 162 | '@vueuse/head', 163 | ], 164 | exclude: [ 165 | 'vue-demi', 166 | ], 167 | }, 168 | }) 169 | -------------------------------------------------------------------------------- /windi.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'windicss/helpers' 2 | import colors from 'windicss/colors' 3 | import typography from 'windicss/plugin/typography' 4 | 5 | export default defineConfig({ 6 | darkMode: 'class', 7 | // https://windicss.org/posts/v30.html#attributify-mode 8 | attributify: true, 9 | 10 | plugins: [ 11 | typography(), 12 | ], 13 | theme: { 14 | extend: { 15 | typography: { 16 | DEFAULT: { 17 | css: { 18 | maxWidth: '65ch', 19 | color: 'inherit', 20 | a: { 21 | 'color': 'inherit', 22 | 'opacity': 0.75, 23 | 'fontWeight': '500', 24 | 'textDecoration': 'underline', 25 | '&:hover': { 26 | opacity: 1, 27 | color: colors.teal[600], 28 | }, 29 | }, 30 | b: { color: 'inherit' }, 31 | strong: { color: 'inherit' }, 32 | em: { color: 'inherit' }, 33 | h1: { color: 'inherit' }, 34 | h2: { color: 'inherit' }, 35 | h3: { color: 'inherit' }, 36 | h4: { color: 'inherit' }, 37 | code: { color: 'inherit' }, 38 | }, 39 | }, 40 | }, 41 | }, 42 | }, 43 | }) 44 | --------------------------------------------------------------------------------