diff --git a/package.json b/package.json index d1a438c..927de28 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,9 @@ "next-themes": "^0.4.6", "react": "^19.2.4", "react-dom": "^19.2.4", - "react-icons": "^5.6.0" + "react-icons": "^5.6.0", + "react-lorem-ipsum": "^1.4.10", + "zustand": "^5.0.12" }, "devDependencies": { "@eslint/js": "^9.39.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8e10530..b3ffd56 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,7 +19,7 @@ importers: version: 11.14.0(@types/react@19.2.14)(react@19.2.4) antd: specifier: ^6.3.5 - version: 6.3.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 6.3.5(date-fns@2.30.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -32,6 +32,12 @@ importers: react-icons: specifier: ^5.6.0 version: 5.6.0(react@19.2.4) + react-lorem-ipsum: + specifier: ^1.4.10 + version: 1.4.10 + zustand: + specifier: ^5.0.12 + version: 5.0.12(@types/react@19.2.14)(react@19.2.4) devDependencies: '@eslint/js': specifier: ^9.39.4 @@ -1151,6 +1157,10 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + date-fns@2.30.0: + resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} + engines: {node: '>=0.11'} + dayjs@1.11.20: resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==} @@ -1583,6 +1593,9 @@ packages: react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + react-lorem-ipsum@1.4.10: + resolution: {integrity: sha512-PCEuauLZTGJkm++oxJeUycEgQOBg3qmmD7pn5sUyq9e0OvvNbW32RKQsSgalAYGUIbS91UkDgxvt6iHVAsqYJw==} + react@19.2.4: resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} engines: {node: '>=0.10.0'} @@ -1773,6 +1786,24 @@ packages: zod@4.3.6: resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + zustand@5.0.12: + resolution: {integrity: sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + snapshots: '@ant-design/colors@8.0.1': @@ -2364,7 +2395,7 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@rc-component/picker@1.9.1(dayjs@1.11.20)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@rc-component/picker@1.9.1(date-fns@2.30.0)(dayjs@1.11.20)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@rc-component/overflow': 1.0.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@rc-component/resize-observer': 1.1.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -2374,6 +2405,7 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) optionalDependencies: + date-fns: 2.30.0 dayjs: 1.11.20 '@rc-component/portal@2.2.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': @@ -3297,7 +3329,7 @@ snapshots: dependencies: color-convert: 2.0.1 - antd@6.3.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + antd@6.3.5(date-fns@2.30.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@ant-design/colors': 8.0.1 '@ant-design/cssinjs': 2.1.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -3323,7 +3355,7 @@ snapshots: '@rc-component/mutate-observer': 2.0.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@rc-component/notification': 1.2.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@rc-component/pagination': 1.2.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@rc-component/picker': 1.9.1(dayjs@1.11.20)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@rc-component/picker': 1.9.1(date-fns@2.30.0)(dayjs@1.11.20)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@rc-component/progress': 1.0.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@rc-component/qrcode': 1.1.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@rc-component/rate': 1.0.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -3426,6 +3458,11 @@ snapshots: csstype@3.2.3: {} + date-fns@2.30.0: + dependencies: + '@babel/runtime': 7.29.2 + optional: true + dayjs@1.11.20: {} debug@4.4.3: @@ -3799,6 +3836,8 @@ snapshots: react-is@18.3.1: {} + react-lorem-ipsum@1.4.10: {} + react@19.2.4: {} resolve-from@4.0.0: {} @@ -3942,3 +3981,8 @@ snapshots: zod: 4.3.6 zod@4.3.6: {} + + zustand@5.0.12(@types/react@19.2.14)(react@19.2.4): + optionalDependencies: + '@types/react': 19.2.14 + react: 19.2.4 diff --git a/src/App.css b/src/App.css index 508f9f1..e69de29 100644 --- a/src/App.css +++ b/src/App.css @@ -1,70 +0,0 @@ -.height_40 { - height: 40px; -} - -.height_40_center { - height: 40px; - line-height: 40px; -} - -.display_inline_block { - display: inline-block; -} - -.text_center { - text-align: center; -} - -.width_300 { - width: 300px; -} - -.width_400 { - width: 400px; -} - -.margin_3_0 { - margin: 3px 0; -} - -.margin_10_20 { - margin: 10px 20px; -} - -.margin_10_0 { - margin: 10px 0; -} - -.margin_0_20 { - margin: 0 20px; -} - -.margin_0_10 { - margin: 0 10px; -} - -.margin_10_10 { - margin: 10px; -} - -.margin_20_10 { - margin: 20px 10px; -} - -.margin_0_5 { - margin: 0 5px; -} - -.width_600 { - width: 600px; -} - -.margin_left_600 { - margin-left: 600px; -} - -.right-align-container { - margin-left: auto; - display: flex; - align-items: center; -} \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index ce1c484..6a511b5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,147 +1,14 @@ // import { useState } from 'react' import './App.css' +import { Provider } from "@/components/ui/provider" -import { HomeOutlined, SettingOutlined, ApiOutlined, FolderOutlined, ClearOutlined, UserOutlined, CommentOutlined, AlertOutlined, UploadOutlined } from '@ant-design/icons'; -import type { MenuProps } from 'antd'; -import { Menu } from 'antd'; -import React, { useState } from 'react'; -import { Input, Avatar } from 'antd'; -import { SearchOutlined } from '@ant-design/icons'; - -const items: MenuProps['items'] = [ - - { - label: '主页', - key: 'home', - icon: , - }, - { - label: '工具大全', - key: 'tools', - icon: , - }, - { - label: '文章资源', - key: 'articles', - icon: , - }, - { - label: '消息', - key: 'xx', - icon: , - children:[ - { - label: '回复我的', - key: 'xx1', - }, - { - label: '@我的', - key: 'xx2', - }, - { - label: '收到的赞', - key: 'xx3', - }, - { - label: '系统消息', - key: 'xx4', - }, - { - label: '我的消息', - key: 'xx5', - } - ] - }, - { - label: 'bug反馈', - key: 'bug', - icon: , - }, - { - label: (} - />), - key: 'search', - disabled: true, - className: "right-align-container", - }, - { - label: ( - - } className='margin_3_0'/> - - 用户: 10086 - - ), - key: 'home', - children: [ - { - className: "width_300", - type: 'group', - label: ( -
用户10086
- ), - children: [ - { - label: '个人中心', - key: 'setting:1', - icon: - }, - { - label: '个人设置', - key: 'setting:2', - icon: , - }, - - { - label: '浏览历史', - key: 'setting:2', - icon: - }, - { - label: ( -
- ), - disabled: true, - key: 'divider', - }, - { - label: '退出登录', - key: 'setting:3', - icon: - } - ], - } - ], - className: "right-align-container", - }, -]; - +import Navbar from '@/components/Navbar/Navbar'; const App: React.FC = () => { - const [current, setCurrent] = useState('mail'); - - const onClick: MenuProps['onClick'] = e => { - console.log('click ', e); - setCurrent(e.key); - }; return ( -
- - - - -
+ + + ); }; diff --git a/src/components/Navbar/Navbar.css b/src/components/Navbar/Navbar.css new file mode 100644 index 0000000..ca57e66 --- /dev/null +++ b/src/components/Navbar/Navbar.css @@ -0,0 +1,16 @@ +.right-align-container { + margin-left: auto; + display: flex; + align-items: center; +} + +.not-font-select { + -webkit-user-select: none; + /* Safari */ + -moz-user-select: none; + /* Firefox */ + -ms-user-select: none; + /* Internet Explorer */ + user-select: none; + /* 标准语法 */ +} \ No newline at end of file diff --git a/src/components/Navbar/Navbar.tsx b/src/components/Navbar/Navbar.tsx new file mode 100644 index 0000000..8d1b523 --- /dev/null +++ b/src/components/Navbar/Navbar.tsx @@ -0,0 +1,568 @@ +import { + HomeOutlined, + SettingOutlined, + ApiOutlined, + FolderOutlined, + ClearOutlined, + UserOutlined, + CommentOutlined, + AlertOutlined, + UploadOutlined, + ClockCircleTwoTone, + PlusCircleTwoTone, + ControlTwoTone, + MessageTwoTone, +} from "@ant-design/icons"; +import type { MenuProps } from "antd"; +import { Menu } from "antd"; +import React, { useState } from "react"; +import { Input, Avatar } from "antd"; +import { SearchOutlined } from "@ant-design/icons"; + +import "./Navbar.css"; + +import { Dialog, Portal, createOverlay, CloseButton } from "@chakra-ui/react"; +import { PasswordInput } from "@/components/ui/password-input"; + +import { Input as ChakraInput, InputGroup, Box } from "@chakra-ui/react"; + +import { QRCode, Button } from "antd"; +// import { PasswordInput } from "@/components/ui/password-input" + +import { Tabs } from "antd"; + +import { Space } from "antd"; + +import { Flex } from "antd"; + +interface DialogProps { + title: string; + description?: string; + content?: React.ReactNode; +} + +const login_dialog = createOverlay((props) => { + const { ...rest } = props; + return ( + + + + + + + + + +
+ + 扫描二维码登录 + + +
+ 使用APP扫码登录 +
+
+ 扫描登录或者扫码下载APP +
+
+
+
+ + + 账号密码登录 + + ), + key: "1", + children: ( + + + + + + + + + + + + + + + + + + ), + }, + { + label: ( + + 邮箱验证码登录 + + ), + key: "2", + children: ( + + + + + + + + + + + + + + + + + ), + }, + ]} + /> + + +
+ 其他登录方式 +
+ + + + + + +
+
+
+
+ +
+ 未注册过账号的手机号/邮箱,我们将自动帮你注册账号{" "} +
+
+ 登录或完成注册即代表你同意 + + 和 + +
+
+
+
+
+
+ ); +}); + +// login_dialog.open({}); + +const Navbar: React.FC = () => { + const [current, setCurrent] = useState("mail"); + const [isLogin, setIsLogin] = useState(false); + + const items: MenuProps["items"] = [ + { + label: "主页", + key: "home", + icon: , + }, + { + label: "工具大全", + key: "tools", + icon: , + }, + { + label: "文章资源", + key: "articles", + icon: , + }, + { + label: "消息", + key: "xx", + icon: , + children: [ + { + label: "回复我的", + key: "xx1", + }, + { + label: "@我的", + key: "xx2", + }, + { + label: "收到的赞", + key: "xx3", + }, + { + label: "系统消息", + key: "xx4", + }, + { + label: "我的消息", + key: "xx5", + }, + ], + }, + { + label: "bug反馈", + key: "bug", + icon: , + }, + { + label: ( + } + /> + ), + key: "search", + disabled: true, + className: "right-align-container", + }, + { + label: ( + { + login_dialog.open({}); + }} + > + } className="margin_3_0" /> + + 用户: 10086 + + + ), + key: "my", + children: isLogin + ? [ + { + className: "width_300", + type: "group", + label: ( +
用户10086
+ ), + children: [ + { + label: "个人中心", + key: "setting:1", + icon: , + }, + { + label: "个人设置", + key: "setting:2", + icon: , + }, + + { + label: "浏览历史", + key: "setting:3", + icon: , + }, + { + label: ( +
+ ), + disabled: true, + key: "divider", + }, + { + label: "退出登录", + key: "setting:4", + icon: , + }, + ], + }, + ] + : [ + { + label: ( +
+ 登录后你可以: +
111
+
+ ), + key: "login", + disabled: true, + }, + { + label: ( + <> + {" "} + 你发布评论后,也可以收到别人的回复 + + ), + disabled: true, + }, + { + label: ( + <> + {" "} + 查看自己的历史浏览记录 + + ), + disabled: true, + }, + { + label: ( + <> + {" "} + 收藏需要的工具和文章 + + ), + disabled: true, + }, + { + label: ( + <> + {" "} + 部分登录才可以使用的功能 + + ), + disabled: true, + }, + { + label: ( + + ), + key: "divider", + disabled: true, + }, + { + label: ( +
+ 首次登录?{" "} + +
+ ), + disabled: true, + }, + ], + className: "right-align-container", + }, + ]; + + const onClick: MenuProps["onClick"] = (e) => { + console.log("click ", e); + setCurrent(e.key); + + if (e.key == "home") { + login_dialog.open({}); + } + }; + return ( +
+ + +
+ ); +}; + +export default Navbar; diff --git a/src/components/ui/password-input.tsx b/src/components/ui/password-input.tsx new file mode 100644 index 0000000..3b9af80 --- /dev/null +++ b/src/components/ui/password-input.tsx @@ -0,0 +1,159 @@ +"use client" + +import type { + ButtonProps, + GroupProps, + InputProps, + StackProps, +} from "@chakra-ui/react" +import { + Box, + HStack, + IconButton, + Input, + InputGroup, + Stack, + mergeRefs, + useControllableState, +} from "@chakra-ui/react" +import * as React from "react" +import { LuEye, LuEyeOff } from "react-icons/lu" + +export interface PasswordVisibilityProps { + /** + * The default visibility state of the password input. + */ + defaultVisible?: boolean + /** + * The controlled visibility state of the password input. + */ + visible?: boolean + /** + * Callback invoked when the visibility state changes. + */ + onVisibleChange?: (visible: boolean) => void + /** + * Custom icons for the visibility toggle button. + */ + visibilityIcon?: { on: React.ReactNode; off: React.ReactNode } +} + +export interface PasswordInputProps + extends InputProps, + PasswordVisibilityProps { + rootProps?: GroupProps +} + +export const PasswordInput = React.forwardRef< + HTMLInputElement, + PasswordInputProps +>(function PasswordInput(props, ref) { + const { + rootProps, + defaultVisible, + visible: visibleProp, + onVisibleChange, + visibilityIcon = { on: , off: }, + ...rest + } = props + + const [visible, setVisible] = useControllableState({ + value: visibleProp, + defaultValue: defaultVisible || false, + onChange: onVisibleChange, + }) + + const inputRef = React.useRef(null) + + return ( + { + if (rest.disabled) return + if (e.button !== 0) return + e.preventDefault() + setVisible(!visible) + }} + > + {visible ? visibilityIcon.off : visibilityIcon.on} + + } + {...rootProps} + > + + + ) +}) + +const VisibilityTrigger = React.forwardRef( + function VisibilityTrigger(props, ref) { + return ( + + ) + }, +) + +interface PasswordStrengthMeterProps extends StackProps { + max?: number + value: number +} + +export const PasswordStrengthMeter = React.forwardRef< + HTMLDivElement, + PasswordStrengthMeterProps +>(function PasswordStrengthMeter(props, ref) { + const { max = 4, value, ...rest } = props + + const percent = (value / max) * 100 + const { label, colorPalette } = getColorPalette(percent) + + return ( + + + {Array.from({ length: max }).map((_, index) => ( + + ))} + + {label && {label}} + + ) +}) + +function getColorPalette(percent: number) { + switch (true) { + case percent < 33: + return { label: "Low", colorPalette: "red" } + case percent < 66: + return { label: "Medium", colorPalette: "orange" } + default: + return { label: "High", colorPalette: "green" } + } +} diff --git a/src/css.t.ts b/src/css.t.ts new file mode 100644 index 0000000..e29340f --- /dev/null +++ b/src/css.t.ts @@ -0,0 +1,4 @@ +declare module '*.css' { + const classes: { [key: string]: string }; + export default classes; +} \ No newline at end of file diff --git a/src/global.css b/src/global.css new file mode 100644 index 0000000..7965aa2 --- /dev/null +++ b/src/global.css @@ -0,0 +1,64 @@ +.height_40 { + height: 40px; +} + +.height_40_center { + height: 40px; + line-height: 40px; +} + +.display_inline_block { + display: inline-block; +} + +.text_center { + text-align: center; +} + +.width_300 { + width: 300px; +} + +.width_400 { + width: 400px; +} + +.margin_3_0 { + margin: 3px 0; +} + +.margin_10_20 { + margin: 10px 20px; +} + +.margin_10_0 { + margin: 10px 0; +} + +.margin_0_20 { + margin: 0 20px; +} + +.margin_0_10 { + margin: 0 10px; +} + +.margin_10_10 { + margin: 10px; +} + +.margin_20_10 { + margin: 20px 10px; +} + +.margin_0_5 { + margin: 0 5px; +} + +.width_600 { + width: 600px; +} + +.margin_left_600 { + margin-left: 600px; +} \ No newline at end of file diff --git a/src/main.tsx b/src/main.tsx index 28ede35..ca1efa2 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -4,6 +4,8 @@ import './index.css' import 'antd/dist/antd.css'; import App from './App.tsx' +import './global.css' + createRoot(document.getElementById('root')!).render( diff --git a/src/stores/Store.ts b/src/stores/Store.ts new file mode 100644 index 0000000..e69de29 diff --git a/tsconfig.json b/tsconfig.json index 1ffef60..14b7c11 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,27 @@ +// { +// "files": [], +// "references": [ +// { "path": "./tsconfig.app.json" }, +// { "path": "./tsconfig.node.json" } +// ] +// } { - "files": [], - "references": [ - { "path": "./tsconfig.app.json" }, - { "path": "./tsconfig.node.json" } - ] -} + "compilerOptions": { + "types": ["node"], + "target": "ES2023", + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "noEmit": true, + "jsx": "react-jsx", + "paths": { + "@/*": ["./src/*"] + }, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true + }, + "include": ["src"] +} \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index 3a219a0..e886103 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,5 +1,6 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' +import path from 'path'; // https://vite.dev/config/ export default defineConfig({ @@ -10,4 +11,9 @@ export default defineConfig({ ], }, plugins: [react()], + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), // 实际上使用了,但可能没被检测到 + }, + }, })