Feat(webui): 深色模式添加自动跟随系统,添加格式化配置文件。 (#1109)
* Style(webui): Add format config. Add prettier config to confirm all developer have same code format style. * Feat(WebUI): 深色模式行为优化 新增: 1、添加了「自动跟随系统主题」模式,并设置为默认行为,该模式将实时监控系统/浏览器主题,并同步变化; 2、添加了自动切换模式按钮。 修改: 1、修改按钮深色模式、浅色模式显示为对应当前模式; 2、修改默认主题为自动跟随系统。 * Update prettier config. 限定解析器的部分格式,避免影响其他语言的格式化功能。 * Disassemble some components. 1.move theme hooks to /lib folder; 2.disassemble ThemeButton component; 3.refactor ThemeButton related code.
This commit is contained in:
1 parent
5db2691155
commit
4273edc6c7
9 files changed
+229
-54
No files matched your search
+1
-1
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
"extends": ["next/core-web-vitals", "prettier"]
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
//.prettierignore
|
||||
README.md
|
||||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
/tests/e2e/videos/
|
||||
/tests/e2e/screenshots/
|
||||
|
||||
.github
|
||||
.next
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw*
|
||||
.vscode
|
||||
|
||||
# Lock File
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
||||
# autoGen
|
||||
/autoGen
|
||||
/public/cdn
|
||||
/public/cdnimages
|
||||
@@ -0,0 +1,49 @@
|
||||
# .prettierrc.toml
|
||||
|
||||
# 箭头函数只有一个参数的时候可以忽略括号
|
||||
arrowParens = "avoid"
|
||||
|
||||
# 括号内部不要出现空格
|
||||
bracketSpacing = true
|
||||
|
||||
# 行结束符使用 Unix 格式
|
||||
endOfLine = "lf"
|
||||
|
||||
# true: Put > on the last line instead of at a new line
|
||||
jsxBracketSameLine = false
|
||||
|
||||
# 行宽
|
||||
printWidth = 100
|
||||
|
||||
# 换行方式
|
||||
proseWrap = "preserve"
|
||||
|
||||
# 分号
|
||||
semi = false
|
||||
|
||||
# 使用单引号
|
||||
singleQuote = true
|
||||
|
||||
# 缩进
|
||||
tabWidth = 2
|
||||
|
||||
# 使用 tab 缩进
|
||||
useTabs = false
|
||||
|
||||
# 后置逗号,多行对象、数组在最后一行增加逗号
|
||||
trailingComma = "es5"
|
||||
|
||||
# 解析器,仅指定部分文件格式,避免影响其他语言功能
|
||||
[[overrides]]
|
||||
files = "*.ts"
|
||||
[overrides.options]
|
||||
parser = "typescript"
|
||||
[[overrides]]
|
||||
files = "*.tsx"
|
||||
[overrides.options]
|
||||
parser = "typescript"
|
||||
|
||||
[[overrides]]
|
||||
files = "*.css"
|
||||
[overrides.options]
|
||||
parser = "css"
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
}
|
||||
}
|
||||
+11
-51
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
import "./globals.css";
|
||||
import styles from "./page.module.css";
|
||||
import { SetStateAction, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Button, Nav } from "@douyinfe/semi-ui";
|
||||
import { OnSelectedData } from "@douyinfe/semi-ui/lib/es/navigation";
|
||||
import { Layout as SeLayout } from "@douyinfe/semi-ui/lib/es/layout";
|
||||
@@ -12,14 +12,13 @@ import {
|
||||
IconCustomerSupport,
|
||||
IconDoubleChevronLeft,
|
||||
IconDoubleChevronRight,
|
||||
IconMoon,
|
||||
IconSemiLogo,
|
||||
IconStar,
|
||||
IconSun,
|
||||
IconVideoListStroked,
|
||||
IconHome, IconSetting,
|
||||
} from "@douyinfe/semi-icons";
|
||||
import Image from "next/image";
|
||||
import ThemeButton from "./ui/ThemeButton";
|
||||
import { useSystemTheme, useTheme } from "./lib/utils";
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
@@ -36,13 +35,9 @@ export default function RootLayout({
|
||||
const [openKeys, setOpenKeys] = useState(initOpenKeys);
|
||||
const [selectedKeys, setSelectedKeys] = useState<any>([pathname.slice(1)]);
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
const [mode, setMode] = useState(typeof window !== 'undefined' && localStorage.getItem("mode") || "light");
|
||||
useEffect(() => {
|
||||
localStorage.setItem("mode", mode);
|
||||
if (mode === "dark") {
|
||||
document.body.setAttribute("theme-mode", "dark");
|
||||
}
|
||||
}, [mode]);
|
||||
const [mode, setMode] = useState((typeof window !== 'undefined' && localStorage.getItem('mode')) || 'auto')
|
||||
const systemTheme = useSystemTheme()
|
||||
useTheme(mode,systemTheme)
|
||||
let navStyle = isCollapsed
|
||||
? { height: "100%", overflow: "visible" }
|
||||
: { height: "100%" };
|
||||
@@ -290,7 +285,9 @@ export default function RootLayout({
|
||||
/>
|
||||
</div>
|
||||
</Nav.Header>
|
||||
{footer(mode, setMode)}
|
||||
<Nav.Footer collapseButton={false}>
|
||||
<ThemeButton mode={mode} setMode={setMode} systemTheme={systemTheme} />
|
||||
</Nav.Footer>
|
||||
</Nav>
|
||||
</Sider>
|
||||
<SeLayout style={{ height: "100vh" }}>{children}</SeLayout>
|
||||
@@ -300,44 +297,7 @@ export default function RootLayout({
|
||||
);
|
||||
}
|
||||
|
||||
function footer(
|
||||
mode: string,
|
||||
setMode: {
|
||||
(value: SetStateAction<string>): void;
|
||||
(arg0: string): void;
|
||||
}
|
||||
) {
|
||||
const switchMode = () => {
|
||||
const body = document.body;
|
||||
if (body.hasAttribute("theme-mode")) {
|
||||
body.removeAttribute("theme-mode");
|
||||
setMode("light");
|
||||
} else {
|
||||
body.setAttribute("theme-mode", "dark");
|
||||
setMode("dark");
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Nav.Footer collapseButton={true}>
|
||||
<Button
|
||||
onClick={switchMode}
|
||||
theme="borderless"
|
||||
icon={
|
||||
mode === "light" ? (
|
||||
<IconMoon size="large" />
|
||||
) : (
|
||||
<IconSun size="large" />
|
||||
)
|
||||
}
|
||||
style={{
|
||||
color: "var(--semi-color-text-2)",
|
||||
// marginRight: '12px',
|
||||
// zIndex: 100,
|
||||
}}
|
||||
/>
|
||||
</Nav.Footer>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function isSub(key1: string, key2: string | number) {
|
||||
const routerMap: any = {
|
||||
|
||||
+34
-1
@@ -1,3 +1,5 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
export const responsiveMap = {
|
||||
xs: '(max-width: 575px)',
|
||||
sm: '(min-width: 576px)',
|
||||
@@ -49,4 +51,35 @@ export const humDate = (time: number): string => new Date(time * 1000).toLocaleS
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false
|
||||
}).replaceAll('/', '-')
|
||||
}).replaceAll('/', '-')
|
||||
|
||||
export const useSystemTheme = () => {
|
||||
const [theme, setTheme] = useState<string>('light')
|
||||
useEffect(() => {
|
||||
const getSystemTheme = () =>
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
||||
setTheme(getSystemTheme)
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
const handleChange = () => setTheme(getSystemTheme)
|
||||
mediaQuery.addEventListener('change', handleChange)
|
||||
return () => mediaQuery.removeEventListener('change', handleChange)
|
||||
}, [])
|
||||
return theme
|
||||
}
|
||||
|
||||
export const useTheme = (mode: string, systemTheme: string) => {
|
||||
useEffect(() => {
|
||||
localStorage.setItem('mode', mode)
|
||||
switch (mode) {
|
||||
case 'light':
|
||||
document.body.setAttribute('theme-mode', 'light')
|
||||
break
|
||||
case 'dark':
|
||||
document.body.setAttribute('theme-mode', 'dark')
|
||||
break
|
||||
default:
|
||||
document.body.setAttribute('theme-mode', systemTheme)
|
||||
break
|
||||
}
|
||||
}, [mode, systemTheme])
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
'use client'
|
||||
import { SetStateAction, useEffect, useState } from 'react'
|
||||
import { Button } from '@douyinfe/semi-ui'
|
||||
import { IconMoon, IconSun, IconContrast } from '@douyinfe/semi-icons'
|
||||
|
||||
interface ThemeButtonProps {
|
||||
mode: string
|
||||
setMode: {
|
||||
(value: SetStateAction<string>): void
|
||||
(arg0: string): void
|
||||
}
|
||||
systemTheme: string
|
||||
}
|
||||
|
||||
const ThemeButton: React.FC<ThemeButtonProps> = props => {
|
||||
const [switchTrigger, setSwitchTrigger] = useState(false)
|
||||
const [icon, setIcon] = useState(<IconContrast size="large" />)
|
||||
useEffect(() => {
|
||||
{
|
||||
// 按下按钮切换主题
|
||||
if (typeof window !== 'undefined' && switchTrigger === true) {
|
||||
const body = document.body
|
||||
const currentMode = props.mode
|
||||
let nextMode = currentMode
|
||||
switch (currentMode) {
|
||||
case 'auto':
|
||||
nextMode = 'light'
|
||||
break
|
||||
case 'light':
|
||||
nextMode = 'dark'
|
||||
break
|
||||
default:
|
||||
nextMode = 'auto'
|
||||
break
|
||||
}
|
||||
body.removeAttribute('theme-mode')
|
||||
nextMode === 'auto'
|
||||
? body.setAttribute('theme-mode', props.systemTheme)
|
||||
: body.setAttribute('theme-mode', nextMode)
|
||||
props.setMode(nextMode)
|
||||
setSwitchTrigger(false)
|
||||
}
|
||||
// 更新图标
|
||||
switch (props.mode) {
|
||||
case 'light':
|
||||
setIcon(<IconSun size="large" />)
|
||||
break
|
||||
case 'dark':
|
||||
setIcon(<IconMoon size="large" />)
|
||||
break
|
||||
default:
|
||||
setIcon(<IconContrast size="large" />)
|
||||
break
|
||||
}
|
||||
}
|
||||
}, [props, switchTrigger])
|
||||
|
||||
const switchMode = () => {
|
||||
setSwitchTrigger(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={switchMode}
|
||||
theme="borderless"
|
||||
icon={icon}
|
||||
style={{
|
||||
color: 'var(--semi-color-text-2)',
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default ThemeButton
|
||||
Generated
+14
@@ -27,6 +27,7 @@
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-next": "^14.2.3",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"typescript": "^5.4.5"
|
||||
}
|
||||
},
|
||||
@@ -1795,6 +1796,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-config-prettier": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz",
|
||||
"integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"eslint-config-prettier": "bin/cli.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-import-resolver-node": {
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz",
|
||||
|
||||
+2
-1
@@ -28,6 +28,7 @@
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-next": "^14.2.3",
|
||||
"typescript": "^5.4.5"
|
||||
"typescript": "^5.4.5",
|
||||
"eslint-config-prettier": "^9.1.0"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user