Nest.js从0到1搭建博客系统---React18+Antd+unocss自定义主题(15)

339 阅读4分钟

一程序员去面试,面试官问:“你毕业才两年,这三年工作经验是怎么来的?!”程序员答:“加班。”

效果图

  • 明暗主题

image.png

image.png

  • 切换主题色

image.png

image.png

安装插件

npm i antd
npm i unocss @iconify/json -D

unocss配置

  • 项目根目录创建uno.config.ts配置文件
/*
 * @Author: vhen
 * @Date: 2024-02-02 19:11:19
 * @LastEditTime: 2024-03-05 17:08:08
 * @Description: 现在的努力是为了小时候吹过的牛逼!
 *
 * @FilePath: \react-vhen-blog-admin\uno.config.ts
 *
 */
import {
  defineConfig,
  presetAttributify,
  presetIcons,
  presetTypography,
  presetUno,
  transformerCompileClass,
  transformerDirectives,
  transformerVariantGroup,
} from 'unocss';

import transformerAttributifyJsx from './transformer-attributify-jsx';

export default defineConfig({
  // 自定义快捷方式
  shortcuts: {
    'bg-base': 'bg-[#fff] dark:bg-[#1b1b1f] switch-animation', // 明暗背景色
    'text-base': 'text-[#20202a] dark:text-[#f0f0f0] switch-animation', // 明暗字体样式
  },
  /** 排除 */
  exclude: ['node_modules'],
  presets: [
    presetUno(), // m-10 理解为 margin:10rem 或者 m-10px 理解为 margin:10px
    presetAttributify(),
    presetIcons({
      extraProperties: {
        'display': 'inline-block',
        'width': '1.2em',
        'height': '1.2em',
        'vertical-align': 'middle'
      }
    }),
    presetTypography() // 归因模式 bg="blue-400 hover:blue-500 dark:blue-500 dark:hover:blue-600" 背景颜色的简写
  ],
  transformers: [
    transformerAttributifyJsx(),
    transformerDirectives(),
    transformerVariantGroup(),
    transformerCompileClass()
  ],
  theme: {
    colors: {
      primary: 'var(--primary-color)',

    }
  }
});
  • base.scss 基础样式
// 变量配置
:root {
  /* 默认明亮模式的样式 */
  --v-bg: #fff;
  --v-scrollbar: #f2f2f2;
  --v-scrollbar-hover: #bbb;
  --v-text-color: #333; 
}

html {
  background-color: var(--v-bg);
  color: var(--v-text-color);
  overflow-x: hidden;
  overflow-y: scroll;
}

html.dark {
  /* 黑暗模式的样式 */
  --v-bg: #222;
  --v-scrollbar: #111;
  --v-scrollbar-hover: #333;
  --v-text-color: #fff;
}


html,body{
  font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
  font-synthesis: none;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  -webkit-text-size-adjust: 100%;
  box-sizing: border-box;
  width: 100%;
  height: 100%;
}

#root,.ant-app{
  width: 100%;
  height: 100%
}

// scrollbar
::-webkit-scrollbar {
  width: 8px;
  height: 8px;
  background-color: var(--v-bg);
}

::-webkit-scrollbar-thumb {
  background-color: var(--v-scrollbar);
  border-radius: 8px;
}
::-webkit-scrollbar-thumb:hover {
  background: var(--v-scrollbar-hover);
}

jotai中定义主题

  • 声明ColorMode类型
// '/#/app'
export type ColorMode = "light" | "dark" | "default";

/*
 * @Author: vhen
 * @Date: 2024-02-25 14:39:50
 * @LastEditTime: 2024-02-27 01:16:23
 * @Description: 现在的努力是为了小时候吹过的牛逼!
 * @FilePath: \react-vhen-blog-admin\src\store\app.ts
 *
 */
import { ColorMode } from '/#/app';

import { atom } from 'jotai';
// 切换菜单折叠状态
const collapseAtom = atom(false);
// 切换主题
const themeAtom = atom<ColorMode>('light');
// 主题色
const primaryColorAtom = atom<string>('1890ff');
export {
  collapseAtom, primaryColorAtom, themeAtom
};

封装ThemeMode组件

import { Dropdown } from "antd";
import { useAtom } from 'jotai';

import type { MenuProps } from 'antd';


import { themeAtom } from '@/store/app';


const ThemeMode: React.FC = () => {
  const [Theme, setTheme] = useAtom(themeAtom);
  const iconMap = {
    light: 'i-material-symbols:light-mode-outline',
    dark: 'i-tdesign:mode-dark',
    default: 'i-material-symbols:desktop-windows-outline-rounded',
  };
  const items: MenuProps['items'] = [
    {
      key: "1",
      label: <span>light</span>,
      onClick: () => setTheme('light'),
    },
    {
      key: "2",
      label: <span>dark</span>,
      onClick: () => setTheme('dark'),
    },
    {
      key: "3",
      label: <span>default</span>,
      onClick: () => setTheme('default'),
    }
  ];
  return (
    <Dropdown menu={{ items }} placement="bottom" trigger={["click"]} arrow>
      <i text-lg className={iconMap[Theme]} />
    </Dropdown>
  )
}

export default ThemeMode;

image.png

封装Hook usePreferredDark来监听主题改变

prefers-color-scheme 这个CSS3媒体查询特性用来检测用户是否设置亮色(light)或暗色(dark) 的主题色

在JavaScript 检测中,最关键的是使用 Window.matchMedia() API. 这个函数检测 document 是否匹配对应的媒体查询并返回一个 MediaQueryList 对象. 通过返回对象, 可以检测 document 是否匹配媒体查询

import { useState } from 'react'

export function usePreferredDark() {
  const [matches, setMatches] = useState(window.matchMedia('(prefers-color-scheme: dark)').matches)

  window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
    setMatches(e.matches)
  })

  return matches
}

封装Hook useDark来判断网页是否为暗黑模式

/*
 * @Author: vhen
 * @Date: 2024-02-27 01:13:24
 * @LastEditTime: 2024-02-27 01:16:43
 * @Description: 现在的努力是为了小时候吹过的牛逼!
 * @FilePath: \react-vhen-blog-admin\src\hooks\useDark.ts
 *
 */
import { useMemo } from 'react';

import { ColorMode } from '/#/app';

import { usePreferredDark } from './usePreferredDark';

export function useDark(mode: ColorMode) {
  const preferredDark = usePreferredDark()
  const isDark = useMemo(() => {
    return mode === 'dark' || (preferredDark && mode !== 'light')
  }, [mode, preferredDark])

  return isDark
}

App.tsx 配置主题

DOMTokenList 接口的 toggle()  方法从列表中删除一个给定的标记并返回 false。如果标记不存在,则添加并且函数返回 true

/* eslint-disable @typescript-eslint/no-unused-vars */
import { App, ConfigProvider, theme } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import { useAtom } from 'jotai';
import { FC, ReactElement, useEffect } from 'react';
import { HashRouter, useRoutes } from 'react-router-dom';

import type { ThemeConfig } from 'antd';

import { useDark } from '@/hooks/useDark';
import allRoutes from '@/router';
import RouterGuard from '@/router/RouterGuard';
import { primaryColorAtom, themeAtom } from '@/store/app';

const Router = ({ routes }: { routes: any }) => useRoutes(routes);

const AppWrapper: FC = (): ReactElement => {

  const [ThemeMode] = useAtom(themeAtom);
  const [primaryColor] = useAtom(primaryColorAtom);

  const isDark = useDark(ThemeMode!);
  useEffect(() => {
    document.documentElement.classList.toggle("dark", isDark);
  }, [isDark]);
  const config: ThemeConfig = {
    token: {
      colorPrimary: primaryColor
    },
    algorithm: isDark ? theme.darkAlgorithm : theme.defaultAlgorithm,
  };
  return (
    <ConfigProvider locale={zhCN} theme={config}>
      <App>
        <HashRouter>
          {/* 路由守卫鉴权与拦截器 */}
          <RouterGuard key="appraisal">
            <Router routes={allRoutes} />
          </RouterGuard>
        </HashRouter>
      </App>
    </ConfigProvider>
  );
};
export default AppWrapper;

主题色调用

import { Button, ColorPicker } from "antd";
import { useAtom } from "jotai";

import ThemeMode from '@/components/Theme/ThemeMode';
import { primaryColorAtom } from '@/store/app';

const Home: React.FC = () => {
  const [primaryColor, setPrimaryColor] = useAtom(primaryColorAtom);
  return (
    <div>
      <div mb-4> Home</div>
      <div mb-4 pl-6 dark:text-color>
        <ThemeMode />

      </div>
     <div mb-4>
     <ColorPicker
          value={primaryColor}
          onChange={(_, c) => setPrimaryColor(c)}
        />
     </div>
        <Button type="primary">primary</Button>
        <Button ml-4>default</Button>
    </div>
  );
};

export default Home;

image.png