feat(h5): 重构深色模式与首页, 统一协议与版本号

清理脚手架冗余; 深色模式全局跟随系统 + iOS 深灰背景; 首页 logo/欢迎语/版本号; DaxPay logo; 版本 4.0.0 + 改名 daxpay-h5
This commit is contained in:
bootx
2026-06-23 22:28:15 +08:00
parent a2a44751d4
commit add03103dc
27 changed files with 152 additions and 1299 deletions

View File

@@ -1,7 +1,7 @@
{
"name": "daxpay-gateway-ui",
"name": "daxpay-h5",
"type": "module",
"version": "3.1.0",
"version": "4.0.0",
"private": true,
"packageManager": "pnpm@10.32.1",
"license": "LGPL-3.0-or-later",
@@ -27,16 +27,13 @@
"lint:lint-staged": "lint-staged"
},
"dependencies": {
"@vueuse/core": "^14.3.0",
"axios": "^1.18.1",
"date-fns": "^3.0.6",
"lodash-es": "^4.18.1",
"nprogress": "^0.2.0",
"pinia": "^3.0.4",
"pinia-plugin-persistedstate": "^4.7.1",
"qs": "^6.15.2",
"vant": "^4.9.24",
"vconsole": "^3.15.1",
"vue": "^3.5.38",
"vue-router": "^5.1.0"
},
@@ -59,7 +56,6 @@
"@vitejs/plugin-vue": "^6.0.7",
"autoprefixer": "^10.5.0",
"cross-env": "^7.0.3",
"cz-git": "^1.13.1",
"dotenv": "^16.3.1",
"eslint": "^9.39.4",
"eslint-plugin-format": "^2.0.1",
@@ -71,7 +67,6 @@
"postcss": "^8.5.15",
"postcss-mobile-forever": "^4.0.0",
"rimraf": "^6.1.3",
"rollup": "^4.62.2",
"rollup-plugin-visualizer": "^5.11.0",
"simple-git-hooks": "^2.13.1",
"terser": "^5.48.0",
@@ -80,8 +75,8 @@
"unocss": "^66.7.2",
"unplugin-auto-import": "^21.0.0",
"unplugin-vue-components": "^32.1.0",
"vconsole": "^3.15.1",
"vite": "^8.1.0",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-html": "^3.2.2",
"vite-plugin-svg-icons": "^2.0.1",
"vue-tsc": "^3.3.5"
@@ -99,10 +94,5 @@
},
"lint-staged": {
"*": "eslint --fix"
},
"config": {
"commitizen": {
"path": "node_modules/cz-git"
}
}
}

View File

@@ -1,38 +1,21 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 87 100"
>
<defs>
<linearGradient x1="0" y1="0" x2="0" y2="1" id="gradient23" gradientTransform="rotate(-90 .5 .5)">
<stop offset="0" stop-color="#AAE8FF" stop-opacity="1"></stop>
<stop offset="1" stop-color="#BFDCFF" stop-opacity="1"></stop>
</linearGradient>
</defs>
<path fill="url(#gradient23)" d="M 86.45 25.05Q 86.05 25.9 85.5 26.25L 44.9 49.7 44.9 51.75 41.9 51.75 41.9 49.7 1.25 26.25Q 0.9 26.05 0.65 25.55L 0.45 25.1 0.45 75.2 0.5 75.75Q 0.7 76.35 1.1 76.55L 41.75 100 41.85 59.85 44.95 59.85 45.05 100 85.65 76.55Q 86.15 76.2 86.35 75.7L 86.45 75.2 86.45 25.05 Z"></path>
<defs>
<linearGradient x1="0" y1="0" x2="0" y2="1" id="gradient24" gradientTransform="rotate(-90 .5 .5)">
<stop offset="0" stop-color="#C2FFF8" stop-opacity="1"></stop>
<stop offset="1" stop-color="#AADAFF" stop-opacity="1"></stop>
</linearGradient>
</defs>
<path fill="url(#gradient24)" d="M 86.6 25.1Q 86.6 24.4 85.8 23.95L 45.15 0.5Q 44.35 0 43.15 0 42 0 41.2 0.5L 0.85 23.95Q 0 24.4 0 25.1 0 25.75 0.85 26.25L 41.45 49.7Q 42.3 50.15 43.45 50.15 44.65 50.15 45.45 49.7L 85.8 26.25Q 86.6 25.75 86.6 25.1 Z"></path>
<defs>
<linearGradient x1="0" y1="0" x2="0" y2="1" id="gradient25" gradientTransform="rotate(-90 .5 .5)">
<stop offset="0" stop-color="#4573FF" stop-opacity="1"></stop>
<stop offset="1" stop-color="#005FB9" stop-opacity="1"></stop>
</linearGradient>
</defs>
<path fill="url(#gradient25)" d="M 67.9 35.85Q 67.8 36.35 67.5 36.55L 43.25 50.2 43.25 79.3 67.6 65.3Q 67.95 65.1 67.9 64.5L 67.9 35.85 Z"></path>
<defs>
<linearGradient x1="0" y1="0" x2="0" y2="1" id="gradient26" gradientTransform="rotate(-90 .5 .5)">
<stop offset="0" stop-color="#2EFFF8" stop-opacity="1"></stop>
<stop offset="1" stop-color="#4599FF" stop-opacity="1"></stop>
</linearGradient>
</defs>
<path fill="url(#gradient26)" d="M 19.2 36.55Q 19 36.4 18.9 36.1L 18.8 35.85 18.85 64.85Q 18.9 65.15 19.15 65.3L 43.45 79.3 43.45 50.2 19.2 36.55 Z"></path>
<defs>
<linearGradient x1="0" y1="0" x2="0" y2="1" id="gradient27" gradientTransform="rotate(-90 .5 .5)">
<stop offset="0" stop-color="#2EFFF8" stop-opacity="1"></stop>
<stop offset="1" stop-color="#4599FF" stop-opacity="1"></stop>
</linearGradient>
</defs>
<path fill="url(#gradient27)" d="M 67.6 35.2L 44.4 21.8Q 43.9 21.55 43.25 21.55 42.55 21.55 42.1 21.8L 19.05 35.2Q 18.6 35.5 18.6 35.9 18.6 36.25 19.05 36.55L 42.25 49.95Q 42.7 50.2 43.4 50.2 44.1 50.2 44.55 49.95L 67.6 36.55Q 68.1 36.25 68.05 35.9 68.05 35.5 67.6 35.2 Z"></path>
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 567 513" width="567" height="513">
<title>dax-pay-无汉字-svg</title>
<style>
.s0 { fill: #ebb54b }
.s1 { fill: #403f3b }
</style>
<g id="#ffffffff">
</g>
<g id="#ebb54bff">
<path id="Layer" class="s0" d="m286.3 29.2c28.6-12.8 60.3-20.1 91.7-17.2 22.5 2.3 44.5 10.6 62.5 24.4 16 11.9 28.2 28.3 37.3 45.9 17.9 35.1 23 75.4 21.3 114.4-7.5 0.4-15 0.1-22.5 0-0.5-8.5-0.1-17.1-0.7-25.7-0.7-21.2-4.4-42.3-11.7-62.2-6.1-16.7-15.2-32.5-27.8-45.2-16.3-16.2-38.4-26.7-61.4-28.4-27.6-3-55.2 4.4-80.4 15.5-18.5 8.1-35.5 19.2-52.1 30.7-40.8 29-74.9 66.7-103.4 107.6-2.1 2.8-4.1 6.1-7.5 7.3-8.2 2-16.5 0.5-24.7 0.3 13.3-22.4 29-43.2 45.8-63 13.4-15.8 27.7-30.9 43.3-44.6 26.6-24.6 57-45.3 90.3-59.8z"/>
<path id="Layer" class="s0" d="m71.9 333.6c7.3-1.4 14.8-0.9 22.2-0.2 1.1 22.7 3.8 45.6 11.4 67.2 6.5 18.4 16.4 35.8 30.6 49.4 12.9 12.3 29.4 20.9 47 24.5 13.7 3.1 28 3 41.9 1.3 18.1-2.4 35.6-8.3 52.3-15.4 18.1-7.7 34.5-18.5 50.5-29.6 35.6-25.6 66.2-57.6 92.4-92.6 2.4-3.7 6.6-5.3 10.9-5.3 6.7-0.6 13.4 0.5 20.2 0.6-7.6 13.2-17.2 25.2-26.8 37-23.5 28.7-50.1 55.1-80 77.1-31.3 22.8-66.3 41.6-104.5 49.5-16.1 3.3-32.6 3-48.9 2.2-19.8-1.7-39.2-8.6-55.5-19.9-23.7-16.2-40.9-40.5-50.7-67.2-7.5-19.9-11.5-41.1-13.2-62.2-0.5-5.5-0.3-10.9 0.2-16.4z"/>
</g>
<g id="#403f3bff">
<path id="Layer" fill-rule="evenodd" class="s1" d="m29.1 203.6c7.2-1.3 14.6-0.9 21.9-0.9 12.3 0.2 25.1-0.8 36.8 3.5 7.2 2.5 12.8 8.5 15.2 15.7 3.8 11.1 2.2 23 0 34.3-2.9 13.8-8.8 27.7-20.1 36.6-11 8.6-25.4 10.7-38.9 10.5-10.9 0.2-21.7-0.1-32.6-0.1 3.7-25.5 8.8-50.7 13.1-76.1 1.4-7.9 2.3-15.8 4.6-23.5zm16.5 27.8c-3 17.7-6 35.3-9.1 53 10.2 0.1 21.5 1.3 30.5-4.5 8.5-5.8 11.8-16.2 13.7-25.8 1.2-6.3 1.6-12.8 0.8-19.2-0.6-4.9-3.8-9.7-8.6-11.5-7.9-2.8-16.3-2.4-24.5-2q-1.9 4.9-2.8 10z"/>
<path id="Layer" fill-rule="evenodd" class="s1" d="m308.3 203.5c12.5-1.7 25.2-0.5 37.8-0.7 9 0.6 19.3 1.3 25.9 8.2 5.8 6.2 5.2 15.3 4.3 23.1-1.6 9.6-4.1 19.8-11 27.1-6.5 6.9-16.1 9.7-25.2 10.6-7.5 0.7-14.9 0.7-22.3 1-1.9 10.1-3.7 20.3-5.4 30.4q-11.1 0.1-22.2 0c4.5-27.1 9.6-54.2 14.2-81.3 1-6.2 1.8-12.5 3.9-18.4zm19.2 17c-3.7 11.1-4.6 22.8-6.6 34.2 7.8-0.1 16 0.9 23.3-2.3 6.8-3.3 9.2-11.4 9.5-18.4 0.4-4.2-0.3-9.2-4.3-11.5-6.8-3.1-14.6-3-21.9-2z"/>
<path id="Layer" fill-rule="evenodd" class="s1" d="m142.3 225.4c2.6-1.5 5.8-1.1 8.6-1.5 10.4-0.3 21.3-0.8 31.1 3.2 4.9 2 9 6.3 9.6 11.7 1 8.5-1.2 16.8-2.6 25.1-2.3 13.1-4.8 26.2-7 39.4q-9.4-0.1-18.8 0c-0.3-2.5-0.5-5-0.8-7.5-6.2 3.8-12.7 8-20.2 8.7-7.5 0.6-15.6 0.6-22.2-3.4-4-2.2-6.6-6.6-7-11.1-1.1-9.6 1.9-20.1 9.5-26.4 6.6-5.3 15.3-6.7 23.6-7.2 7.8-0.2 15.6 0 23.4-0.1-0.4-4.2 1.1-9.3-2.1-12.6-9.2-4.1-19.6-1.7-29.2-1 1.1-5.8 1.3-12 4.1-17.3zm-3.6 47.4c-4.6 2.7-7.2 11.1-2 14.5 7.9 3.1 17.3 1.3 24.2-3.4 4.3-3 4.9-8.5 6.1-13.2-9.4-0.3-19.4-1.7-28.3 2.1z"/>
<path id="Layer" fill-rule="evenodd" class="s1" d="m409.2 225.5c8.1-2.3 16.6-1.7 24.8-1.6 8.3 0.6 17.8 2 23 9.1 4.1 6.7 2.4 14.8 1.3 22.1-3 16-5.8 32.1-8.7 48.1q-9.4 0.1-18.8 0-0.3-3.7-0.6-7.5c-10.9 8-25.4 11.5-38.5 7.5-5.6-1.6-10.3-6.4-11.1-12.2-1.9-11.8 3.3-25.6 14.6-30.7 13.3-5.9 28-3.5 42-4-0.3-4.3 1.2-10.5-3.3-13.1-9-3.3-18.7-1.1-28-0.6 0.7-5.8 1.7-11.5 3.3-17.1zm-2.6 47c-3.9 2.3-5.7 7.3-4.8 11.6 0.9 3.5 4.9 4.4 8.1 4.6 6.5 0.3 13.3-1.3 18.7-5.1 4.2-2.9 4.7-8.4 6.1-12.9-9.3-0.3-19.2-1.7-28.1 1.8z"/>
<path id="Layer" class="s1" d="m210.3 225.4c7.7-0.6 15.5-1.1 23.2 0 5.5 7.5 7.7 16.8 11.7 25.1 5.7-6.1 10.5-13 15.6-19.6 2.1-2.6 4.6-5.6 8.2-5.8 6.7-0.7 13.5-0.3 20.2 0.4-9.7 13-20.9 24.9-31 37.6 6.2 13.5 13.3 26.5 19.3 40.1-8.3 0.1-16.5 0-24.7 0.1-3.9-9-7.7-18.1-12.1-26.8-5.8 6.6-11.1 13.7-16.5 20.6-2.1 2.6-4.4 5.8-8.1 6.1-7.1 0.6-14.2 0.2-21.4 0 10.6-13.6 22.2-26.3 33.4-39.3-5.2-13.2-12.4-25.4-17.8-38.5z"/>
<path id="Layer" class="s1" d="m476.1 225c7.4-0.3 14.8-0.1 22.2-0.2 3.4 18.6 5.5 37.5 9.1 56.1 10-18.4 18.2-37.7 28.1-56.1 7.2-0.1 14.4 0.3 21.6 0.6-10 20.5-21.7 40.2-32.4 60.3-4.6 8.4-9 16.8-14.3 24.7-5 7.2-11.1 14.2-19.3 17.8-7.8 3.5-16.5 3.9-24.9 4.3 1-5.9 2.1-11.7 3.2-17.6 5.6-1.1 11.6-1.8 16.5-5 2.6-1.8 5-4.5 5-7.8 0.1-4.5-1-8.9-1.9-13.2-3.4-15-6.7-30-10-44.9-1.4-6.3-3.1-12.6-2.9-19z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -1,38 +1,21 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 87 100"
>
<defs>
<linearGradient x1="0" y1="0" x2="0" y2="1" id="gradient23" gradientTransform="rotate(-90 .5 .5)">
<stop offset="0" stop-color="#AAE8FF" stop-opacity="1"></stop>
<stop offset="1" stop-color="#BFDCFF" stop-opacity="1"></stop>
</linearGradient>
</defs>
<path fill="url(#gradient23)" d="M 86.45 25.05Q 86.05 25.9 85.5 26.25L 44.9 49.7 44.9 51.75 41.9 51.75 41.9 49.7 1.25 26.25Q 0.9 26.05 0.65 25.55L 0.45 25.1 0.45 75.2 0.5 75.75Q 0.7 76.35 1.1 76.55L 41.75 100 41.85 59.85 44.95 59.85 45.05 100 85.65 76.55Q 86.15 76.2 86.35 75.7L 86.45 75.2 86.45 25.05 Z"></path>
<defs>
<linearGradient x1="0" y1="0" x2="0" y2="1" id="gradient24" gradientTransform="rotate(-90 .5 .5)">
<stop offset="0" stop-color="#C2FFF8" stop-opacity="1"></stop>
<stop offset="1" stop-color="#AADAFF" stop-opacity="1"></stop>
</linearGradient>
</defs>
<path fill="url(#gradient24)" d="M 86.6 25.1Q 86.6 24.4 85.8 23.95L 45.15 0.5Q 44.35 0 43.15 0 42 0 41.2 0.5L 0.85 23.95Q 0 24.4 0 25.1 0 25.75 0.85 26.25L 41.45 49.7Q 42.3 50.15 43.45 50.15 44.65 50.15 45.45 49.7L 85.8 26.25Q 86.6 25.75 86.6 25.1 Z"></path>
<defs>
<linearGradient x1="0" y1="0" x2="0" y2="1" id="gradient25" gradientTransform="rotate(-90 .5 .5)">
<stop offset="0" stop-color="#4573FF" stop-opacity="1"></stop>
<stop offset="1" stop-color="#005FB9" stop-opacity="1"></stop>
</linearGradient>
</defs>
<path fill="url(#gradient25)" d="M 67.9 35.85Q 67.8 36.35 67.5 36.55L 43.25 50.2 43.25 79.3 67.6 65.3Q 67.95 65.1 67.9 64.5L 67.9 35.85 Z"></path>
<defs>
<linearGradient x1="0" y1="0" x2="0" y2="1" id="gradient26" gradientTransform="rotate(-90 .5 .5)">
<stop offset="0" stop-color="#2EFFF8" stop-opacity="1"></stop>
<stop offset="1" stop-color="#4599FF" stop-opacity="1"></stop>
</linearGradient>
</defs>
<path fill="url(#gradient26)" d="M 19.2 36.55Q 19 36.4 18.9 36.1L 18.8 35.85 18.85 64.85Q 18.9 65.15 19.15 65.3L 43.45 79.3 43.45 50.2 19.2 36.55 Z"></path>
<defs>
<linearGradient x1="0" y1="0" x2="0" y2="1" id="gradient27" gradientTransform="rotate(-90 .5 .5)">
<stop offset="0" stop-color="#2EFFF8" stop-opacity="1"></stop>
<stop offset="1" stop-color="#4599FF" stop-opacity="1"></stop>
</linearGradient>
</defs>
<path fill="url(#gradient27)" d="M 67.6 35.2L 44.4 21.8Q 43.9 21.55 43.25 21.55 42.55 21.55 42.1 21.8L 19.05 35.2Q 18.6 35.5 18.6 35.9 18.6 36.25 19.05 36.55L 42.25 49.95Q 42.7 50.2 43.4 50.2 44.1 50.2 44.55 49.95L 67.6 36.55Q 68.1 36.25 68.05 35.9 68.05 35.5 67.6 35.2 Z"></path>
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 567 513" width="567" height="513">
<title>dax-pay-无汉字-svg</title>
<style>
.s0 { fill: #ebb54b }
.s1 { fill: #403f3b }
</style>
<g id="#ffffffff">
</g>
<g id="#ebb54bff">
<path id="Layer" class="s0" d="m286.3 29.2c28.6-12.8 60.3-20.1 91.7-17.2 22.5 2.3 44.5 10.6 62.5 24.4 16 11.9 28.2 28.3 37.3 45.9 17.9 35.1 23 75.4 21.3 114.4-7.5 0.4-15 0.1-22.5 0-0.5-8.5-0.1-17.1-0.7-25.7-0.7-21.2-4.4-42.3-11.7-62.2-6.1-16.7-15.2-32.5-27.8-45.2-16.3-16.2-38.4-26.7-61.4-28.4-27.6-3-55.2 4.4-80.4 15.5-18.5 8.1-35.5 19.2-52.1 30.7-40.8 29-74.9 66.7-103.4 107.6-2.1 2.8-4.1 6.1-7.5 7.3-8.2 2-16.5 0.5-24.7 0.3 13.3-22.4 29-43.2 45.8-63 13.4-15.8 27.7-30.9 43.3-44.6 26.6-24.6 57-45.3 90.3-59.8z"/>
<path id="Layer" class="s0" d="m71.9 333.6c7.3-1.4 14.8-0.9 22.2-0.2 1.1 22.7 3.8 45.6 11.4 67.2 6.5 18.4 16.4 35.8 30.6 49.4 12.9 12.3 29.4 20.9 47 24.5 13.7 3.1 28 3 41.9 1.3 18.1-2.4 35.6-8.3 52.3-15.4 18.1-7.7 34.5-18.5 50.5-29.6 35.6-25.6 66.2-57.6 92.4-92.6 2.4-3.7 6.6-5.3 10.9-5.3 6.7-0.6 13.4 0.5 20.2 0.6-7.6 13.2-17.2 25.2-26.8 37-23.5 28.7-50.1 55.1-80 77.1-31.3 22.8-66.3 41.6-104.5 49.5-16.1 3.3-32.6 3-48.9 2.2-19.8-1.7-39.2-8.6-55.5-19.9-23.7-16.2-40.9-40.5-50.7-67.2-7.5-19.9-11.5-41.1-13.2-62.2-0.5-5.5-0.3-10.9 0.2-16.4z"/>
</g>
<g id="#403f3bff">
<path id="Layer" fill-rule="evenodd" class="s1" d="m29.1 203.6c7.2-1.3 14.6-0.9 21.9-0.9 12.3 0.2 25.1-0.8 36.8 3.5 7.2 2.5 12.8 8.5 15.2 15.7 3.8 11.1 2.2 23 0 34.3-2.9 13.8-8.8 27.7-20.1 36.6-11 8.6-25.4 10.7-38.9 10.5-10.9 0.2-21.7-0.1-32.6-0.1 3.7-25.5 8.8-50.7 13.1-76.1 1.4-7.9 2.3-15.8 4.6-23.5zm16.5 27.8c-3 17.7-6 35.3-9.1 53 10.2 0.1 21.5 1.3 30.5-4.5 8.5-5.8 11.8-16.2 13.7-25.8 1.2-6.3 1.6-12.8 0.8-19.2-0.6-4.9-3.8-9.7-8.6-11.5-7.9-2.8-16.3-2.4-24.5-2q-1.9 4.9-2.8 10z"/>
<path id="Layer" fill-rule="evenodd" class="s1" d="m308.3 203.5c12.5-1.7 25.2-0.5 37.8-0.7 9 0.6 19.3 1.3 25.9 8.2 5.8 6.2 5.2 15.3 4.3 23.1-1.6 9.6-4.1 19.8-11 27.1-6.5 6.9-16.1 9.7-25.2 10.6-7.5 0.7-14.9 0.7-22.3 1-1.9 10.1-3.7 20.3-5.4 30.4q-11.1 0.1-22.2 0c4.5-27.1 9.6-54.2 14.2-81.3 1-6.2 1.8-12.5 3.9-18.4zm19.2 17c-3.7 11.1-4.6 22.8-6.6 34.2 7.8-0.1 16 0.9 23.3-2.3 6.8-3.3 9.2-11.4 9.5-18.4 0.4-4.2-0.3-9.2-4.3-11.5-6.8-3.1-14.6-3-21.9-2z"/>
<path id="Layer" fill-rule="evenodd" class="s1" d="m142.3 225.4c2.6-1.5 5.8-1.1 8.6-1.5 10.4-0.3 21.3-0.8 31.1 3.2 4.9 2 9 6.3 9.6 11.7 1 8.5-1.2 16.8-2.6 25.1-2.3 13.1-4.8 26.2-7 39.4q-9.4-0.1-18.8 0c-0.3-2.5-0.5-5-0.8-7.5-6.2 3.8-12.7 8-20.2 8.7-7.5 0.6-15.6 0.6-22.2-3.4-4-2.2-6.6-6.6-7-11.1-1.1-9.6 1.9-20.1 9.5-26.4 6.6-5.3 15.3-6.7 23.6-7.2 7.8-0.2 15.6 0 23.4-0.1-0.4-4.2 1.1-9.3-2.1-12.6-9.2-4.1-19.6-1.7-29.2-1 1.1-5.8 1.3-12 4.1-17.3zm-3.6 47.4c-4.6 2.7-7.2 11.1-2 14.5 7.9 3.1 17.3 1.3 24.2-3.4 4.3-3 4.9-8.5 6.1-13.2-9.4-0.3-19.4-1.7-28.3 2.1z"/>
<path id="Layer" fill-rule="evenodd" class="s1" d="m409.2 225.5c8.1-2.3 16.6-1.7 24.8-1.6 8.3 0.6 17.8 2 23 9.1 4.1 6.7 2.4 14.8 1.3 22.1-3 16-5.8 32.1-8.7 48.1q-9.4 0.1-18.8 0-0.3-3.7-0.6-7.5c-10.9 8-25.4 11.5-38.5 7.5-5.6-1.6-10.3-6.4-11.1-12.2-1.9-11.8 3.3-25.6 14.6-30.7 13.3-5.9 28-3.5 42-4-0.3-4.3 1.2-10.5-3.3-13.1-9-3.3-18.7-1.1-28-0.6 0.7-5.8 1.7-11.5 3.3-17.1zm-2.6 47c-3.9 2.3-5.7 7.3-4.8 11.6 0.9 3.5 4.9 4.4 8.1 4.6 6.5 0.3 13.3-1.3 18.7-5.1 4.2-2.9 4.7-8.4 6.1-12.9-9.3-0.3-19.2-1.7-28.1 1.8z"/>
<path id="Layer" class="s1" d="m210.3 225.4c7.7-0.6 15.5-1.1 23.2 0 5.5 7.5 7.7 16.8 11.7 25.1 5.7-6.1 10.5-13 15.6-19.6 2.1-2.6 4.6-5.6 8.2-5.8 6.7-0.7 13.5-0.3 20.2 0.4-9.7 13-20.9 24.9-31 37.6 6.2 13.5 13.3 26.5 19.3 40.1-8.3 0.1-16.5 0-24.7 0.1-3.9-9-7.7-18.1-12.1-26.8-5.8 6.6-11.1 13.7-16.5 20.6-2.1 2.6-4.4 5.8-8.1 6.1-7.1 0.6-14.2 0.2-21.4 0 10.6-13.6 22.2-26.3 33.4-39.3-5.2-13.2-12.4-25.4-17.8-38.5z"/>
<path id="Layer" class="s1" d="m476.1 225c7.4-0.3 14.8-0.1 22.2-0.2 3.4 18.6 5.5 37.5 9.1 56.1 10-18.4 18.2-37.7 28.1-56.1 7.2-0.1 14.4 0.3 21.6 0.6-10 20.5-21.7 40.2-32.4 60.3-4.6 8.4-9 16.8-14.3 24.7-5 7.2-11.1 14.2-19.3 17.8-7.8 3.5-16.5 3.9-24.9 4.3 1-5.9 2.1-11.7 3.2-17.6 5.6-1.1 11.6-1.8 16.5-5 2.6-1.8 5-4.5 5-7.8 0.1-4.5-1-8.9-1.9-13.2-3.4-15-6.7-30-10-44.9-1.4-6.3-3.1-12.6-2.9-19z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -1,28 +0,0 @@
export enum sizeEnum {
XS = 'XS',
SM = 'SM',
MD = 'MD',
LG = 'LG',
XL = 'XL',
XXL = 'XXL',
}
export enum screenEnum {
XS = 480,
SM = 576,
MD = 768,
LG = 992,
XL = 1200,
XXL = 1600,
}
const screenMap = new Map<sizeEnum, number>()
screenMap.set(sizeEnum.XS, screenEnum.XS)
screenMap.set(sizeEnum.SM, screenEnum.SM)
screenMap.set(sizeEnum.MD, screenEnum.MD)
screenMap.set(sizeEnum.LG, screenEnum.LG)
screenMap.set(sizeEnum.XL, screenEnum.XL)
screenMap.set(sizeEnum.XXL, screenEnum.XXL)
export { screenMap }

View File

@@ -1,14 +0,0 @@
// token key
export const TOKEN_KEY = 'TOKEN'
// user info key
export const USER_INFO_KEY = 'USER__INFO__'
// role info key
export const ROLES_KEY = 'ROLES__KEY__'
// base global local key
export const BASE_LOCAL_CACHE_KEY = 'LOCAL__CACHE__KEY__'
// base global session key
export const BASE_SESSION_CACHE_KEY = 'SESSION__CACHE__KEY__'

View File

@@ -1,48 +0,0 @@
import { tryOnUnmounted } from '@vueuse/core'
import { ref, watch } from 'vue'
import { isFunction } from '@/utils/is'
export function useTimeoutFn(handle: Fn<any>, wait: number, native = false) {
if (!isFunction(handle)) {
throw new Error('handle is not Function!')
}
const { readyRef, stop, start } = useTimeoutRef(wait)
if (native) {
handle()
}
else {
watch(
readyRef,
(maturity) => {
maturity && handle()
},
{ immediate: false },
)
}
return { readyRef, stop, start }
}
export function useTimeoutRef(wait: number) {
const readyRef = ref(false)
let timer: TimeoutHandle
function stop(): void {
readyRef.value = false
timer && window.clearTimeout(timer)
}
function start(): void {
stop()
timer = setTimeout(() => {
readyRef.value = true
}, wait)
}
start()
tryOnUnmounted(stop)
return { readyRef, stop, start }
}

View File

@@ -1,95 +0,0 @@
import type { ComputedRef } from 'vue'
import { computed, ref, unref } from 'vue'
import { screenEnum, screenMap, sizeEnum } from '@/enums/breakpointEnum'
import { useEventListener } from '@/hooks/event/useEventListener'
let globalScreenRef: ComputedRef<sizeEnum | undefined>
let globalWidthRef: ComputedRef<number>
let globalRealWidthRef: ComputedRef<number>
export interface CreateCallbackParams {
screen: ComputedRef<sizeEnum | undefined>
width: ComputedRef<number>
realWidth: ComputedRef<number>
screenEnum: typeof screenEnum
screenMap: Map<sizeEnum, number>
sizeEnum: typeof sizeEnum
}
export function useBreakpoint() {
return {
screenRef: computed(() => unref(globalScreenRef)),
widthRef: globalWidthRef,
screenEnum,
realWidthRef: globalRealWidthRef,
}
}
// Just call it once
export function createBreakpointListen(fn?: (opt: CreateCallbackParams) => void) {
const screenRef = ref<sizeEnum>(sizeEnum.XL)
const realWidthRef = ref(window.innerWidth)
function getWindowWidth() {
const width = document.body.clientWidth
const xs = screenMap.get(sizeEnum.XS)!
const sm = screenMap.get(sizeEnum.SM)!
const md = screenMap.get(sizeEnum.MD)!
const lg = screenMap.get(sizeEnum.LG)!
const xl = screenMap.get(sizeEnum.XL)!
if (width < xs) {
screenRef.value = sizeEnum.XS
}
else if (width < sm) {
screenRef.value = sizeEnum.SM
}
else if (width < md) {
screenRef.value = sizeEnum.MD
}
else if (width < lg) {
screenRef.value = sizeEnum.LG
}
else if (width < xl) {
screenRef.value = sizeEnum.XL
}
else {
screenRef.value = sizeEnum.XXL
}
realWidthRef.value = width
}
useEventListener({
el: window,
name: 'resize',
listener: () => {
getWindowWidth()
resizeFn()
},
// wait: 100,
})
getWindowWidth()
globalScreenRef = computed(() => unref(screenRef))
globalWidthRef = computed((): number => screenMap.get(unref(screenRef)!)!)
globalRealWidthRef = computed((): number => unref(realWidthRef))
function resizeFn() {
fn?.({
screen: globalScreenRef,
width: globalWidthRef,
realWidth: globalRealWidthRef,
screenEnum,
screenMap,
sizeEnum,
})
}
resizeFn()
return {
screenRef: globalScreenRef,
screenEnum,
widthRef: globalWidthRef,
realWidthRef: globalRealWidthRef,
}
}

View File

@@ -1,61 +0,0 @@
import type { Ref } from 'vue'
import { useDebounceFn, useThrottleFn } from '@vueuse/core'
import { ref, unref, watch } from 'vue'
export type RemoveEventFn = () => void
export interface UseEventParams {
el?: Element | Ref<Element | undefined> | Window | any
name: string
listener: EventListener
options?: boolean | AddEventListenerOptions
autoRemove?: boolean
isDebounce?: boolean
wait?: number
}
export function useEventListener({
el = window,
name,
listener,
options,
autoRemove = true,
isDebounce = true,
wait = 80,
}: UseEventParams): { removeEvent: RemoveEventFn } {
let remove: RemoveEventFn = () => {
}
const isAddRef = ref(false)
if (el) {
const element: Ref<Element> = ref(el as Element)
const handler = isDebounce ? useDebounceFn(listener, wait) : useThrottleFn(listener, wait)
const realHandler = wait ? handler : listener
const removeEventListener = (e: Element) => {
isAddRef.value = true
e.removeEventListener(name, realHandler, options)
}
const addEventListener = (e: Element) => e.addEventListener(name, realHandler, options)
const removeWatch = watch(
element,
(v, _ov, cleanUp) => {
if (v) {
!unref(isAddRef) && addEventListener(v)
cleanUp(() => {
autoRemove && removeEventListener(v)
})
}
},
{ immediate: true },
)
remove = () => {
removeEventListener(element.value)
removeWatch()
}
}
return { removeEvent: remove }
}

View File

@@ -1,35 +0,0 @@
import { tryOnMounted, tryOnUnmounted, useDebounceFn } from '@vueuse/core'
interface WindowSizeOptions {
once?: boolean
immediate?: boolean
listenerOptions?: AddEventListenerOptions | boolean
}
export function useWindowSizeFn<T>(fn: Fn<T>, wait = 150, options?: WindowSizeOptions) {
let handler = () => {
fn()
}
const handleSize = useDebounceFn(handler, wait)
handler = handleSize
const start = () => {
if (options && options.immediate) {
handler()
}
window.addEventListener('resize', handler)
}
const stop = () => {
window.removeEventListener('resize', handler)
}
tryOnMounted(() => {
start()
})
tryOnUnmounted(() => {
stop()
})
return [start, stop]
}

View File

@@ -1,3 +0,0 @@
import { useAsync } from './use-async'
export { useAsync }

View File

@@ -4,7 +4,8 @@ import { useDesignSettingStore } from '@/store/modules/designSetting'
export function useDesignSetting() {
const designStore = useDesignSettingStore()
const getDarkMode = computed(() => designStore.darkMode)
// 当前实际生效主题(跟随系统 prefers-color-scheme 派生)
const getDarkMode = computed(() => designStore.getDarkMode)
const getAppTheme = computed(() => designStore.appTheme)

View File

@@ -1,16 +0,0 @@
import { isReactive, isRef } from 'vue'
function setLoading(loading, val) {
if (loading !== undefined && isRef(loading)) {
loading.value = val
}
else if (loading !== undefined && isReactive(loading)) {
loading.loading = val
}
}
export async function useAsync(func: Promise<any>, loading: any): Promise<any> {
setLoading(loading, true)
return await func.finally(() => setLoading(loading, false))
}

View File

@@ -1,23 +0,0 @@
import { debounce } from 'lodash-es'
import { onMounted, onUnmounted, ref } from 'vue'
/**
* description: 获取页面宽度
*/
export function useDomWidth() {
const domWidth = ref(window.innerWidth)
function resize() {
domWidth.value = document.body.clientWidth
}
onMounted(() => {
window.addEventListener('resize', debounce(resize, 80))
})
onUnmounted(() => {
window.removeEventListener('resize', resize)
})
return domWidth
}

View File

@@ -1,30 +0,0 @@
import { onMounted, onUnmounted, ref } from 'vue'
/**
* @description 用户网络是否可用
*/
export function useOnline() {
const online = ref(true)
const showStatus = (val) => {
online.value = typeof val == 'boolean' ? val : val.target.online
}
// 在页面加载后,设置正确的网络状态
navigator.onLine ? showStatus(true) : showStatus(false)
onMounted(() => {
// 开始监听网络状态的变化
window.addEventListener('online', showStatus)
window.addEventListener('offline', showStatus)
})
onUnmounted(() => {
// 移除监听网络状态的变化
window.removeEventListener('online', showStatus)
window.removeEventListener('offline', showStatus)
})
return { online }
}

View File

@@ -1,55 +0,0 @@
import { onMounted, onUnmounted, ref } from 'vue'
/**
* @description 获取本地时间
*/
export function useTime() {
let timer // 定时器
const year = ref(0) // 年份
const month = ref(0) // 月份
const week = ref('') // 星期几
const day = ref(0) // 天数
const hour = ref<number | string>(0) // 小时
const minute = ref<number | string>(0) // 分钟
const second = ref(0) // 秒
// 更新时间
const updateTime = () => {
const date = new Date()
year.value = date.getFullYear()
month.value = date.getMonth() + 1
week.value = '日一二三四五六'.charAt(date.getDay())
day.value = date.getDate()
hour.value
= (`${date.getHours()}`)?.padStart(2, '0')
|| new Intl.NumberFormat(undefined, { minimumIntegerDigits: 2 }).format(date.getHours())
minute.value
= (`${date.getMinutes()}`)?.padStart(2, '0')
|| new Intl.NumberFormat(undefined, { minimumIntegerDigits: 2 }).format(date.getMinutes())
second.value = date.getSeconds()
}
// 原生时间格式化
// new Intl.DateTimeFormat('zh', {
// year: 'numeric',
// month: '2-digit',
// day: '2-digit',
// hour: '2-digit',
// minute: '2-digit',
// second: '2-digit',
// hour12: false
// }).format(new Date())
updateTime()
onMounted(() => {
clearInterval(timer)
timer = setInterval(updateTime, 1000)
})
onUnmounted(() => {
clearInterval(timer)
})
return { month, day, hour, minute, second, week }
}

View File

@@ -1,525 +0,0 @@
<template>
<div class="floating-toolbar-wrap">
<nav
ref="toolbarRef"
class="floating-toolbar"
:class="{ dark: isDarkTheme }"
:style="toolbarCssVars"
aria-label="主导航"
>
<div class="ambient-glow" />
<div class="film-grain" />
<div
ref="indicatorRef"
class="active-indicator"
:style="{
transform: `translateX(${indicatorX}px)`,
opacity: activeNavIndex >= 0 ? 1 : 0,
}"
>
<div class="ring-glow" />
<div class="ring-clip">
<div class="ring-spin" />
</div>
<div class="inner-plate" />
</div>
<button
v-for="(item, idx) in navItems"
:key="item.path"
:ref="(el) => setNavRef(el, idx)"
type="button"
class="nav-btn"
:class="{
'active': idx === activeNavIndex,
'with-divider': idx < navItems.length - 1,
'nav-bounce': navBounceIndex === idx,
}"
:aria-label="item.label"
@click="handleNavClick(idx, item.path)"
>
<i :class="item.icon" />
</button>
<button
v-if="props.showDarkModeToggle"
type="button"
class="nav-btn toggle-btn"
:class="{ bounce: toggleBouncing }"
aria-label="切换主题"
@click="toggleTheme"
>
<span class="toggle-icon-wrap">
<i class="i-ph:sun-dim sun-icon" :class="{ hidden: darkModel }" />
<i class="i-ph:moon moon-icon" :class="{ show: darkModel }" />
</span>
</button>
</nav>
</div>
</template>
<script setup lang="ts">
import { useDark } from '@vueuse/core'
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { isNavigationFailure, NavigationFailureType, useRoute, useRouter } from 'vue-router'
import { useDesignSettingStore } from '@/store/modules/designSetting'
interface NavItem {
label: string
path: string
icon: string
}
interface Props {
items?: NavItem[]
showDarkModeToggle?: boolean
}
const props = withDefaults(defineProps<Props>(), {
items: () => [
{ label: 'Home', path: '/home/index', icon: 'i-ph:house-line' },
],
showDarkModeToggle: true,
})
const designStore = useDesignSettingStore()
const currentRoute = useRoute()
const router = useRouter()
const navItems = computed(() => props.items)
const activeNavIndex = computed(() => {
const path = currentRoute.path
const items = navItems.value
if (items.length === 0) {
return -1
}
const index = items.findIndex((item) => {
const basePath = item.path.replace(/\/index$/, '')
return path === item.path || path.startsWith(basePath)
})
return index >= 0 ? index : 0
})
const toolbarRef = ref<HTMLElement | null>(null)
const indicatorRef = ref<HTMLElement | null>(null)
const navRefs = ref<(HTMLElement | null)[]>([])
const indicatorX = ref(0)
const isDark = useDark({
valueDark: 'dark',
valueLight: 'light',
disableTransition: false,
})
const darkModel = computed({
get: () => isDark.value || designStore.darkMode === 'dark',
set: (value: boolean) => {
isDark.value = value
designStore.setDarkMode(value ? 'dark' : 'light')
},
})
const isDarkTheme = computed(() => darkModel.value)
const toolbarColumns = computed(() => Math.max(1, navItems.value.length + (props.showDarkModeToggle ? 1 : 0)))
const toolbarCssVars = computed(() => ({
'--accent-color': designStore.appTheme,
'--accent-soft-color': `${designStore.appTheme}33`,
'--toolbar-max-width': toolbarColumns.value >= 5 ? '430px' : '340px',
'gridTemplateColumns': `repeat(${toolbarColumns.value}, 1fr)`,
}))
const toggleBouncing = ref(false)
const navBounceIndex = ref<number | null>(null)
let toggleTimer: number | null = null
let navBounceTimer: number | null = null
let resizeObserver: ResizeObserver | null = null
function setNavRef(el: Element | { $el?: Element } | null, index: number) {
if (!el) {
navRefs.value[index] = null
return
}
const element = el instanceof HTMLElement
? el
: ('$el' in el ? (el.$el as HTMLElement | undefined) : undefined)
navRefs.value[index] = element ?? null
}
function updateIndicatorPosition() {
const indicator = indicatorRef.value
const target = navRefs.value[activeNavIndex.value]
if (!indicator || !target) {
return
}
const indicatorWidth = indicator.offsetWidth || 48
indicatorX.value = target.offsetLeft + target.offsetWidth / 2 - indicatorWidth / 2
}
function scheduleIndicatorUpdate() {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
updateIndicatorPosition()
})
})
}
function bindResizeObserver() {
if (typeof ResizeObserver === 'undefined') {
return
}
if (resizeObserver) {
resizeObserver.disconnect()
}
resizeObserver = new ResizeObserver(() => {
scheduleIndicatorUpdate()
})
if (toolbarRef.value) {
resizeObserver.observe(toolbarRef.value)
}
if (indicatorRef.value) {
resizeObserver.observe(indicatorRef.value)
}
for (const navEl of navRefs.value) {
if (navEl) {
resizeObserver.observe(navEl)
}
}
}
function normalizePath(path: string) {
return path.replace(/\/+$/, '') || '/'
}
function goNav(path: string) {
// Avoid redundant navigation warnings when clicking current tab.
if (normalizePath(currentRoute.path) === normalizePath(path)) {
return
}
router.push(path).catch((err) => {
if (!isNavigationFailure(err, NavigationFailureType.duplicated)) {
throw err
}
})
}
function handleNavClick(index: number, path: string) {
navBounceIndex.value = null
if (navBounceTimer) {
window.clearTimeout(navBounceTimer)
}
void nextTick(() => {
navBounceIndex.value = index
navBounceTimer = window.setTimeout(() => {
navBounceIndex.value = null
}, 420)
})
goNav(path)
}
function toggleTheme() {
if (!props.showDarkModeToggle) {
return
}
toggleBouncing.value = false
if (toggleTimer) {
window.clearTimeout(toggleTimer)
}
void nextTick(() => {
toggleBouncing.value = true
toggleTimer = window.setTimeout(() => {
toggleBouncing.value = false
}, 420)
})
darkModel.value = !darkModel.value
}
watch(
() => currentRoute.path,
async () => {
await nextTick()
bindResizeObserver()
scheduleIndicatorUpdate()
},
{ immediate: false },
)
watch(activeNavIndex, async () => {
await nextTick()
scheduleIndicatorUpdate()
})
onMounted(async () => {
await nextTick()
bindResizeObserver()
scheduleIndicatorUpdate()
window.addEventListener('resize', scheduleIndicatorUpdate)
const storeDark = designStore.darkMode === 'dark'
if (isDark.value !== storeDark) {
isDark.value = storeDark
}
})
onBeforeUnmount(() => {
if (toggleTimer) {
window.clearTimeout(toggleTimer)
}
if (navBounceTimer) {
window.clearTimeout(navBounceTimer)
}
if (resizeObserver) {
resizeObserver.disconnect()
resizeObserver = null
}
window.removeEventListener('resize', scheduleIndicatorUpdate as () => void)
})
</script>
<style scoped lang="less">
.floating-toolbar-wrap {
pointer-events: none;
position: fixed;
z-index: 20;
left: 0;
right: 0;
bottom: calc(14px + env(safe-area-inset-bottom));
display: flex;
justify-content: center;
}
.floating-toolbar {
pointer-events: auto;
position: relative;
width: min(94vw, var(--toolbar-max-width));
height: 62px;
display: grid;
grid-template-columns: repeat(4, 1fr);
align-items: center;
padding: 6px;
border-radius: 31px;
background: rgba(16, 20, 30, 0.86);
border: 1px solid rgba(255, 255, 255, 0.08);
backdrop-filter: blur(20px) saturate(140%);
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.35);
overflow: hidden;
}
.floating-toolbar.dark {
background: rgba(16, 20, 30, 0.86);
border-color: rgba(255, 255, 255, 0.08);
}
.floating-toolbar:not(.dark) {
background: rgba(255, 255, 255, 0.9);
border-color: rgba(24, 39, 75, 0.12);
box-shadow: 0 6px 16px rgba(41, 58, 88, 0.08);
}
.ambient-glow {
position: absolute;
inset: -18px;
background: radial-gradient(72% 100% at 50% 112%, var(--accent-soft-color), transparent 70%);
pointer-events: none;
}
.film-grain {
position: absolute;
inset: 0;
opacity: 0.06;
pointer-events: none;
mix-blend-mode: soft-light;
background-image:
radial-gradient(circle at 22% 28%, rgba(255, 255, 255, 0.38) 0.35px, transparent 1px),
radial-gradient(circle at 78% 72%, rgba(255, 255, 255, 0.26) 0.4px, transparent 1px);
background-size:
4px 4px,
5px 5px;
}
.active-indicator {
position: absolute;
left: 0;
top: 50%;
width: 62px;
height: 38px;
margin-top: -19px;
transition: transform 0.28s ease;
pointer-events: none;
}
.ring-glow {
position: absolute;
inset: 0;
border-radius: 22px;
background: var(--accent-soft-color);
opacity: 0.22;
filter: none;
box-shadow: none;
}
.ring-clip {
position: absolute;
inset: 0;
border-radius: 22px;
overflow: visible;
}
.ring-spin {
position: absolute;
inset: 0;
border-radius: 22px;
border: 1px solid var(--accent-color);
opacity: 0.35;
animation: none;
background: transparent;
}
.inner-plate {
position: absolute;
inset: 0;
border-radius: 22px;
background: transparent;
}
.floating-toolbar:not(.dark) .inner-plate {
background: rgba(255, 255, 255, 0.92);
border: 1px solid rgba(24, 39, 75, 0.1);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.8),
0 2px 8px rgba(31, 41, 55, 0.12);
}
.nav-btn {
position: relative;
z-index: 2;
width: 100%;
height: 100%;
border: none;
background: transparent;
display: grid;
place-items: center;
color: rgba(255, 255, 255, 0.45);
font-size: 20px;
cursor: pointer;
transition: color 0.25s ease;
}
.floating-toolbar:not(.dark) .nav-btn {
color: rgba(44, 59, 88, 0.72);
}
.nav-btn.active {
color: var(--accent-color);
}
.floating-toolbar:not(.dark) .nav-btn.active {
color: #1f2937;
}
.nav-btn.with-divider::after {
content: '';
position: absolute;
right: 0;
top: 15px;
width: 1px;
height: 30px;
background: linear-gradient(180deg, transparent, rgba(255, 255, 255, 0.14), transparent);
}
.floating-toolbar:not(.dark) .nav-btn.with-divider::after {
background: linear-gradient(180deg, transparent, rgba(36, 54, 88, 0.12), transparent);
}
.theme-switch-wrap {
position: relative;
z-index: 2;
display: grid;
place-items: center;
}
.toggle-btn {
color: rgba(255, 255, 255, 0.6);
}
.floating-toolbar:not(.dark) .toggle-btn {
color: rgba(44, 59, 88, 0.78);
}
.floating-toolbar:not(.dark) .ring-glow {
background: rgba(255, 255, 255, 0.95);
opacity: 0.75;
}
.floating-toolbar:not(.dark) .ring-spin {
border-color: rgba(24, 39, 75, 0.14);
opacity: 1;
}
.toggle-icon-wrap {
position: relative;
width: 22px;
height: 22px;
}
.sun-icon,
.moon-icon {
position: absolute;
inset: 0;
transition:
opacity 0.28s ease,
transform 0.32s ease;
}
.sun-icon.hidden {
opacity: 0;
transform: rotate(90deg) scale(0.6);
}
.moon-icon {
opacity: 0;
transform: rotate(-90deg) scale(0.6);
}
.moon-icon.show {
opacity: 1;
transform: rotate(0deg) scale(1);
}
.toggle-btn.bounce {
animation: toggle-bounce 0.42s cubic-bezier(0.34, 1.2, 0.64, 1);
}
.nav-btn.nav-bounce i {
animation: nav-icon-bounce 0.42s cubic-bezier(0.34, 1.2, 0.64, 1);
}
@keyframes toggle-bounce {
0% {
transform: scale(1);
}
35% {
transform: scale(1.25);
}
70% {
transform: scale(0.95);
}
100% {
transform: scale(1);
}
}
@keyframes nav-icon-bounce {
0% {
transform: scale(1);
}
35% {
transform: scale(1.2);
}
70% {
transform: scale(0.92);
}
100% {
transform: scale(1);
}
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<div
class="layout-shell h-screen flex flex-col"
:class="{ dark: designStore.darkMode === 'dark' }"
:class="{ dark: designStore.getDarkMode === 'dark' }"
>
<RouterView class="flex-1 overflow-x-hidden">
<template #default="{ Component, route }">
@@ -11,8 +11,6 @@
<component :is="Component" v-else :key="route.fullPath" />
</template>
</RouterView>
<FloatingNavBar :items="tabbarItems" :show-dark-mode-toggle="true" />
</div>
</template>
@@ -20,17 +18,11 @@
import { computed } from 'vue'
import { useDesignSettingStore } from '@/store/modules/designSetting'
import { useRouteStore } from '@/store/modules/route'
import FloatingNavBar from './components/FloatingNavBar.vue'
const routeStore = useRouteStore()
const designStore = useDesignSettingStore()
const keepAliveComponents = computed(() => routeStore.keepAliveComponents)
const tabbarItems = [
{ label: 'Home', path: '/home/index', icon: 'i-ph:house-line' },
{ label: 'Theme', path: '/themeSetting', icon: 'i-ph:palette' },
]
</script>
<style scoped lang="less">
@@ -39,6 +31,6 @@ const tabbarItems = [
}
.layout-shell.dark {
background: #000000;
background: #1c1c1e;
}
</style>

View File

@@ -1,5 +1,6 @@
import { createApp } from 'vue'
import { setupStore } from '@/store'
import { useDesignSettingWithOut } from '@/store/modules/designSetting'
import App from './App.vue'
import router, { setupRouter } from './router'
import 'virtual:uno.css'
@@ -27,6 +28,8 @@ async function bootstrap() {
const app = createApp(App)
// 挂载状态管理
setupStore(app)
// 初始化全局主题:跟随系统 prefers-color-scheme不支持手动修改
useDesignSettingWithOut().initSystemListener()
// 挂载路由
setupRouter(app)
await router.isReady()

View File

@@ -1,15 +0,0 @@
export default {
upload: {
// 考虑接口规范不同
apiSetting: {
// 集合字段名
infoField: 'result',
// 图片地址字段名
imgField: 'imagePath',
},
// 最大上传图片大小
maxSize: 1,
// 图片上传类型
fileType: ['image/png', 'image/jpg', 'image/jpeg', 'image/gif', 'image/svg+xml'],
},
}

View File

@@ -1,8 +1,8 @@
// app theme preset color
export interface DesignSettingState {
// 系统主题
darkMode: 'light' | 'dark'
// 系统是否深色(跟随 prefers-color-scheme只读派生不持久化
systemPrefersDark: boolean
// 系统风格
appTheme: string
// 系统内置风格
@@ -36,8 +36,8 @@ export const appThemeList: string[] = [
]
const setting: DesignSettingState = {
// 深色主题
darkMode: 'dark',
// 系统是否深色(初始 false由 initSystemListener 读取系统并覆盖)
systemPrefersDark: false,
// 系统主题色
appTheme: '#5d9dfe',
// 系统内置主题色列表

View File

@@ -3,19 +3,30 @@ import { defineStore } from 'pinia'
import designSetting from '@/settings/designSetting'
import { store } from '@/store'
const { darkMode, appTheme, appThemeList, isPageAnimate, pageAnimateType } = designSetting
const { systemPrefersDark, appTheme, appThemeList, isPageAnimate, pageAnimateType } = designSetting
// 是否已绑定系统主题监听,避免重复绑定
let systemListenerBound = false
/** 同步 <html> 的 dark / light class驱动全局深色样式common.less 的 html.dark / html.light */
function syncHtmlThemeClass(isDark: boolean) {
const html = document.documentElement
html.classList.toggle('dark', isDark)
html.classList.toggle('light', !isDark)
}
export const useDesignSettingStore = defineStore('app-design-setting', {
state: (): DesignSettingState => ({
darkMode,
systemPrefersDark,
appTheme,
appThemeList,
isPageAnimate,
pageAnimateType,
}),
getters: {
// 当前实际生效的主题(跟随系统 prefers-color-scheme
getDarkMode(): 'light' | 'dark' {
return this.darkMode
return this.systemPrefersDark ? 'dark' : 'light'
},
getAppTheme(): string {
return this.appTheme
@@ -31,17 +42,40 @@ export const useDesignSettingStore = defineStore('app-design-setting', {
},
},
actions: {
setDarkMode(mode: 'light' | 'dark'): void {
this.darkMode = mode
/**
* 初始化系统主题监听:读取 prefers-color-scheme 并监听其变化
* 全局跟随系统,不支持手动修改
*/
initSystemListener() {
// 非浏览器环境(如 SSR仅按当前 state 同步一次
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
syncHtmlThemeClass(this.systemPrefersDark)
return
}
// 首次读取系统主题并同步到 <html>
this.systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
syncHtmlThemeClass(this.systemPrefersDark)
// 已绑定过则不再重复添加监听(应用生命周期内常驻)
if (systemListenerBound) {
return
}
systemListenerBound = true
const mql = window.matchMedia('(prefers-color-scheme: dark)')
const handler = (e: MediaQueryListEvent) => {
this.systemPrefersDark = e.matches
syncHtmlThemeClass(this.systemPrefersDark)
}
mql.addEventListener('change', handler)
},
setPageAnimateType(type: string): void {
this.pageAnimateType = type
},
},
// 持久化
// 仅持久化用户可配置项,跟随系统的 systemPrefersDark 不持久化
persist: {
key: 'DESIGN-SETTING',
storage: localStorage,
pick: ['appTheme', 'appThemeList', 'isPageAnimate', 'pageAnimateType'],
},
})

View File

@@ -1 +0,0 @@
export const DESIGN_SETTING = 'DESIGN-SETTING' // 当前用户主题信息

View File

@@ -16,8 +16,13 @@ html.dark {
color-scheme: dark !important;
}
// 覆盖 vant 深色背景变量vant 默认 --van-background 为 #000对比度过强
--van-background: #1c1c1e;
--van-background-2: #1c1c1e;
--van-background-3: #2c2c2e;
body {
background-color: #000000;
background-color: #1c1c1e;
color: rgba(255, 255, 255, 0.9) !important;
}
}
@@ -108,9 +113,9 @@ html.light {
html.dark {
.my-card {
backdrop-filter: blur(12px);
background: rgba(18, 18, 18, 0.85);
background: rgba(44, 44, 46, 0.85);
border: 1px solid rgba(255, 255, 255, 0.08);
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3);
}
.van-cell-group--inset {
@@ -120,7 +125,7 @@ html.dark {
}
.van-cell-group--inset .van-cell {
background: rgba(18, 18, 18, 0.6);
background: #2c2c2e;
}
.van-cell-group .van-cell::after {

View File

@@ -1,12 +0,0 @@
import { format } from 'date-fns'
const DATE_TIME_FORMAT = 'yyyy-MM-dd HH:mm:ss'
const DATE_FORMAT = 'YYYY-MM-DD '
export function formatToDateTime(date: number | Date, formatStr = DATE_TIME_FORMAT): string {
return format(date, formatStr)
}
export function formatToDate(date: number | Date, formatStr = DATE_FORMAT): string {
return format(date, formatStr)
}

View File

@@ -1,199 +0,0 @@
/* eslint-disable ts/ban-ts-comment */
import type { FunctionArgs } from '@vueuse/core'
import { upperFirst } from 'lodash-es'
export interface ViewportOffsetResult {
left: number
top: number
right: number
bottom: number
rightIncludeBody: number
bottomIncludeBody: number
}
export function getBoundingClientRect(element: Element): DOMRect | number {
if (!element || !element.getBoundingClientRect) {
return 0
}
return element.getBoundingClientRect()
}
function trim(string: string) {
return (string || '').replace(/^\s+|\s+$/g, '')
}
/* istanbul ignore next */
export function hasClass(el: Element, cls: string) {
if (!el || !cls) {
return false
}
if (cls.includes(' ')) {
throw new Error('className should not contain space.')
}
if (el.classList) {
return el.classList.contains(cls)
}
else {
return (` ${el.className} `).includes(` ${cls} `)
}
}
/* istanbul ignore next */
export function addClass(el: Element, cls: string) {
if (!el) {
return
}
let curClass = el.className
const classes = (cls || '').split(' ')
for (let i = 0, j = classes.length; i < j; i++) {
const clsName = classes[i]
if (!clsName) {
continue
}
if (el.classList) {
el.classList.add(clsName)
}
else if (!hasClass(el, clsName)) {
curClass += ` ${clsName}`
}
}
if (!el.classList) {
el.className = curClass
}
}
/* istanbul ignore next */
export function removeClass(el: Element, cls: string) {
if (!el || !cls) {
return
}
const classes = cls.split(' ')
let curClass = ` ${el.className} `
for (let i = 0, j = classes.length; i < j; i++) {
const clsName = classes[i]
if (!clsName) {
continue
}
if (el.classList) {
el.classList.remove(clsName)
}
else if (hasClass(el, clsName)) {
curClass = curClass.replace(` ${clsName} `, ' ')
}
}
if (!el.classList) {
el.className = trim(curClass)
}
}
/**
* Get the left and top offset of the current element
* left: the distance between the leftmost element and the left side of the document
* top: the distance from the top of the element to the top of the document
* right: the distance from the far right of the element to the right of the document
* bottom: the distance from the bottom of the element to the bottom of the document
* rightIncludeBody: the distance between the leftmost element and the right side of the document
* bottomIncludeBody: the distance from the bottom of the element to the bottom of the document
*
* @description:
*/
export function getViewportOffset(element: Element): ViewportOffsetResult {
const doc = document.documentElement
const docScrollLeft = doc.scrollLeft
const docScrollTop = doc.scrollTop
const docClientLeft = doc.clientLeft
const docClientTop = doc.clientTop
const pageXOffset = window.pageXOffset
const pageYOffset = window.pageYOffset
const box = getBoundingClientRect(element)
const { left: retLeft, top: rectTop, width: rectWidth, height: rectHeight } = box as DOMRect
const scrollLeft = (pageXOffset || docScrollLeft) - (docClientLeft || 0)
const scrollTop = (pageYOffset || docScrollTop) - (docClientTop || 0)
const offsetLeft = retLeft + pageXOffset
const offsetTop = rectTop + pageYOffset
const left = offsetLeft - scrollLeft
const top = offsetTop - scrollTop
const clientWidth = window.document.documentElement.clientWidth
const clientHeight = window.document.documentElement.clientHeight
return {
left,
top,
right: clientWidth - rectWidth - left,
bottom: clientHeight - rectHeight - top,
rightIncludeBody: clientWidth - left,
bottomIncludeBody: clientHeight - top,
}
}
export function hackCss(attr: string, value: string) {
const prefix: string[] = ['webkit', 'Moz', 'ms', 'OT']
const styleObj: any = {}
prefix.forEach((item) => {
styleObj[`${item}${upperFirst(attr)}`] = value
})
return {
...styleObj,
[attr]: value,
}
}
/* istanbul ignore next */
export function on(
element: Element | HTMLElement | Document | Window,
event: string,
handler: EventListenerOrEventListenerObject,
): void {
if (element && event && handler) {
element.addEventListener(event, handler, false)
}
}
/* istanbul ignore next */
export function off(
element: Element | HTMLElement | Document | Window,
event: string,
handler: Fn,
): void {
if (element && event && handler) {
element.removeEventListener(event, handler, false)
}
}
/* istanbul ignore next */
export function once(el: HTMLElement, event: string, fn: EventListener): void {
const listener = function (this: any, ...args: unknown[]) {
if (fn) {
// @ts-expect-error
fn.apply(this, args)
}
off(el, event, listener)
}
on(el, event, listener)
}
export function useRafThrottle<T extends FunctionArgs>(fn: T): T {
let locked = false
// @ts-expect-error
return function (...args: any[]) {
if (locked) {
return
}
locked = true
window.requestAnimationFrame(() => {
// @ts-expect-error
fn.apply(this, args)
locked = false
})
}
}

View File

@@ -1,14 +1,29 @@
<template>
<div class="home">
<van-nav-bar title="DaxPay" />
<div class="home__body">
<van-empty description="DaxPay H5 — 等待业务开发" />
<div class="home__welcome">
<!-- DaxPay 文字 logo默认主题色时显示 -->
<Logo />
<!-- 欢迎语 -->
<div class="home__title">
欢迎使用 DaxPay
</div>
</div>
</div>
<!-- 底部项目信息 -->
<div class="home__footer">
<p>版本号: v{{ version }}</p>
<p>构建时间: {{ lastBuildTime }}</p>
</div>
</div>
</template>
<script setup lang="ts">
// 占位首页:业务开发时替换为实际首页
import Logo from '@/components/Logo.vue'
// 由 vite define 注入的项目信息
const { pkg, lastBuildTime } = __APP_INFO__
const version = pkg.version
</script>
<style scoped lang="less">
@@ -23,5 +38,36 @@
align-items: center;
justify-content: center;
}
// logo + 欢迎语纵向居中
&__welcome {
display: flex;
flex-direction: column;
align-items: center;
}
// 欢迎语(参考商业版 text-2xl font-black mt-12
&__title {
margin-top: 12px;
text-align: center;
font-size: 24px;
font-weight: 900;
}
// 底部项目信息(版本号 / 构建时间,参考商业版 fixed bottom-6 text-xs opacity-50
&__footer {
position: fixed;
bottom: 24px;
left: 0;
right: 0;
text-align: center;
font-size: 12px;
opacity: 0.5;
line-height: 1.6;
p {
margin: 0;
}
}
}
</style>

View File

@@ -1,14 +1,5 @@
<template>
<div>
<van-divider>主题模式</van-divider>
<van-cell-group inset>
<van-cell center title="暗黑模式">
<template #right-icon>
<van-switch v-model="getDarkMode" size="22" />
</template>
</van-cell>
</van-cell-group>
<van-divider>系统主题色</van-divider>
<div flex="~" justify="center">
<div grid="~ cols-8 gap-2">
@@ -67,26 +58,11 @@
</template>
<script setup lang="ts">
import { useDark } from '@vueuse/core'
import { animates as animateOptions } from '@/settings/animateSetting'
import { useDesignSettingStore } from '@/store/modules/designSetting'
const designStore = useDesignSettingStore()
const isDark = useDark({
valueDark: 'dark',
valueLight: 'light',
disableTransition: false,
})
const getDarkMode = computed({
get: () => isDark.value,
set: (value: boolean) => {
isDark.value = value
designStore.setDarkMode(value ? 'dark' : 'light')
},
})
function togTheme(color: string) {
designStore.appTheme = color
}