diff --git a/astro.config.mjs b/astro.config.mjs index e03e766..d2a6226 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -7,12 +7,10 @@ import pkgs from './package.json'; import tailwindcss from '@tailwindcss/vite'; const isDev = process.env.NODE_ENV === 'development'; -let target = process.env.VITE_API_URL || 'http://localhost:51015'; +let target = process.env.VITE_API_URL || 'http://localhost:51515'; const apiProxy = { target: target, changeOrigin: true, ws: true, rewriteWsOrigin: true, secure: false, cookieDomainRewrite: 'localhost' }; let proxy = { - '/root/': { - target: `${target}/root/`, - }, + '/root/': apiProxy, '/api': apiProxy, '/client': apiProxy, }; @@ -25,14 +23,15 @@ export default defineConfig({ react(), // // sitemap(), // sitemap must be site has a domain ], - + server: { + port: 7008, + }, vite: { plugins: [tailwindcss()], define: { - basename: JSON.stringify(basename || ''), + BASE_NAME: JSON.stringify(basename || ''), }, server: { - port: 7008, host: '0.0.0.0', allowedHosts: true, proxy, diff --git a/package.json b/package.json index 8df293d..53a8d8f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kevisual/kevisual-home", - "version": "0.0.7", + "version": "0.0.8", "description": "", "main": "index.js", "basename": "/root/home", @@ -10,7 +10,7 @@ "preview": "astro preview", "ui": "pnpm dlx shadcn@latest add ", "prepub": "pnpm run build", - "pub": "envision deploy ./dist -k home -v 0.0.7 -u -y yes" + "pub": "envision deploy ./dist -k home -v 0.0.8 -u -y yes" }, "keywords": [], "author": "abearxiong (https://www.xiongxiao.me)", diff --git a/src/apps/config/firstLogin.tsx b/src/apps/config/firstLogin.tsx new file mode 100644 index 0000000..26957c5 --- /dev/null +++ b/src/apps/config/firstLogin.tsx @@ -0,0 +1,164 @@ +import { useEffect, useState } from "react"; +import { AuthProvider } from "../auth"; +import { useFirstStore } from "./store"; +// @ts-ignore +import UserNameBg from '../../assets/user-name-bg.jpg' +import { ToastContainer } from "react-toastify"; +console.log(UserNameBg); +const src = UserNameBg.src; + +// 炫光边框卡片组件 - 黑白色系 +const GlowingCard = ({ children, className = "" }: { children: React.ReactNode; className?: string }) => { + return ( +
+ {/* 炫光边框 - 外层发光(黑白色系) */} +
+
+
+ + {/* 边框渐变层(半透明白色) */} +
+ + {/* 内容层 - 更透明 */} +
+ {children} +
+
+ ); +}; +export const App = () => { + const firstStore = useFirstStore(); + const [username, setUsername] = useState(""); + const [nickname, setNickname] = useState(""); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + firstStore.getMe().finally(() => setIsLoading(false)); + }, []); + + useEffect(() => { + if (firstStore.userInfo) { + setUsername(firstStore.userInfo.username); + setNickname(firstStore.userInfo.nickname); + } + }, [firstStore.userInfo]); + + const canChange = firstStore.userInfo?.canChangeUsername ?? false; + + const handleSubmit = async () => { + // TODO: 实现更新用户名和昵称的逻辑 + // console.log("Update username to:", username, "nickname to:", nickname); + const res = await firstStore.updateUserInfo({ + username, + nickname, + }); + + }; + + if (isLoading) { + return ( +
+ {/* 背景图层 */} +
+ {/* 模糊和遮罩层 */} +
+ {/* 内容层 */} +
+
加载中...
+
+
+ ); + } + + return ( +
+ {/* 背景图层 */} +
+ {/* 模糊和遮罩层 */} +
+ + {/* 内容层 */} +
+
+ {/* 头像 */} +
+
+
+ {firstStore.userInfo?.nickname +
+
+ + {/* 用户信息卡片 - 炫光边框效果 */} + +

+ {nickname || firstStore.userInfo?.username} +

+

+ {'只有第一次可以修改用户名哦~'} +

+ + + {/* 用户名输入表单 */} +
+
+ + setNickname(e.target.value)} + disabled={!canChange} + className="w-full px-5 py-3 rounded-xl border-2 focus:outline-none focus:ring-2 focus:ring-white/50 focus:border-white/50 backdrop-blur-sm bg-white/5 text-white placeholder-white/40 border-white/20 disabled:bg-white/5 disabled:cursor-not-allowed disabled:border-white/10 disabled:text-white/40 transition-all duration-200" + placeholder="输入昵称" + /> +
+ +
+ + setUsername(e.target.value)} + disabled={!canChange} + className="w-full px-5 py-3 rounded-xl border-2 focus:outline-none focus:ring-2 focus:ring-white/50 focus:border-white/50 backdrop-blur-sm bg-white/5 text-white placeholder-white/40 border-white/20 disabled:bg-white/5 disabled:cursor-not-allowed disabled:border-white/10 disabled:text-white/40 transition-all duration-200" + placeholder="输入用户名" + /> +
+ + +
+
+
+
+
+ ); +}; + +export const AppProvider = () => { + return + + + ; +} \ No newline at end of file diff --git a/src/apps/config/store.ts b/src/apps/config/store.ts new file mode 100644 index 0000000..357d240 --- /dev/null +++ b/src/apps/config/store.ts @@ -0,0 +1,108 @@ +import { create } from 'zustand'; +import { queryLogin } from '@/modules/query'; +import { toast } from 'react-toastify'; + +type UserInfo = { + avatar: string | null; + canChangeUsername: boolean; + description: string; + id: string; + needChangePassword: boolean; + nickname: string; + orgs: string[]; + type: string; + username: string; +} +interface FirstState { + userInfo?: UserInfo; + getMe(): Promise; + getAvatar(): string; + updateUserInfo: (opts?: { username: string; nickname: string }) => Promise; +} + +export const useFirstStore = create((set, get) => ({ + userInfo: undefined, + getMe: async () => { + const res = await queryLogin.getMe(); + console.log('User info:', res); + set({ userInfo: res.data }); + }, + getAvatar: () => { + const { userInfo } = get(); + + if (!userInfo) { + return ''; + } + + // 如果已有 avatar,直接返回 + if (userInfo.avatar) { + return userInfo.avatar; + } + + // 使用 nickname 或 username 的第一个字符 + const firstChar = (userInfo.nickname || userInfo.username || 'U') + .trim() + .charAt(0) + .toUpperCase(); + + // 根据用户名生成稳定的颜色 + const stringToColor = (str: string): string => { + let hash = 0; + for (let i = 0; i < str.length; i++) { + hash = str.charCodeAt(i) + ((hash << 5) - hash); + } + const hue = Math.abs(hash % 360); + return `hsl(${hue}, 65%, 55%)`; + }; + + const backgroundColor = stringToColor(userInfo.username + userInfo.id); + + // 创建 SVG 头像 + const svg = ` + + + ${firstChar} + + `; + + // 转换为 data URL(现代方法,不使用已弃用的 unescape) + const svgBase64 = btoa( + encodeURIComponent(svg) + .replace(/%([0-9A-F]{2})/g, (_, p1) => + String.fromCharCode(parseInt(p1, 16)) + ) + ); + return `data:image/svg+xml;base64,${svgBase64}`; + }, + updateUserInfo: async (opts?: { username: string; nickname: string }) => { + if (!opts) return; + const newUsername = opts.username; + const newNickname = opts.nickname; + const update = { + username: newUsername, + nickname: newNickname + } + const res = await queryLogin.post({ + path: 'user', + key: 'updateSelf', + data: update, + }) + if (res.code === 200) { + const currentInfo = get().userInfo; + if (currentInfo) { + set({ + userInfo: { + ...currentInfo, + username: newUsername, + nickname: newNickname + } + }); + toast.success('更新用户信息成功, 请手动关闭页面'); + } else { + toast.error(res.message || '更新用户信息失败'); + } + } + } +})); diff --git a/src/apps/nav/store.ts b/src/apps/nav/store.ts index 9eebadc..86a9bdf 100644 --- a/src/apps/nav/store.ts +++ b/src/apps/nav/store.ts @@ -1,3 +1,4 @@ +import { basename, wrapBasename } from '@/modules/basename'; import { queryLogin } from '@/modules/query'; import { create } from 'zustand'; @@ -10,6 +11,7 @@ interface UserState { orgs?: string[]; type?: string; username?: string; + canChangeUsername?: boolean; } | null; open: boolean; setOpen: (open: boolean) => void; @@ -46,6 +48,11 @@ export const useUserStore = create((set, get) => ({ console.log('获取到的用户信息:', res); if (res.code === 200) { set({ user: res.data || null }); + const canChangeUsername = res.data?.canChangeUsername ?? false; + if (canChangeUsername) { + // 打开修改用户名的页面 + window.open(wrapBasename('/first'), '_blank'); + } } }, init: () => { diff --git a/src/assets/user-name-bg.jpg b/src/assets/user-name-bg.jpg new file mode 100644 index 0000000..2457456 Binary files /dev/null and b/src/assets/user-name-bg.jpg differ diff --git a/src/modules/basename.ts b/src/modules/basename.ts index e647be9..788806a 100644 --- a/src/modules/basename.ts +++ b/src/modules/basename.ts @@ -1,4 +1,13 @@ // @ts-ignore export const basename = BASE_NAME; -console.log(basename); \ No newline at end of file +console.log(basename); + +export const wrapBasename = (path: string) => { + const hasEnd = path.endsWith('/') + if (basename) { + return `${basename}${path}` + (hasEnd ? '' : '/'); + } else { + return path; + } +} \ No newline at end of file diff --git a/src/pages/first.astro b/src/pages/first.astro new file mode 100644 index 0000000..ef559e0 --- /dev/null +++ b/src/pages/first.astro @@ -0,0 +1,10 @@ +--- +import Html from '@/components/html.astro'; +import { AppProvider } from '@/apps/config/firstLogin'; +--- + + +
+ +
+