feat: 添加用户信息管理功能,更新相关配置和组件
This commit is contained in:
@@ -7,12 +7,10 @@ import pkgs from './package.json';
|
|||||||
import tailwindcss from '@tailwindcss/vite';
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
const isDev = process.env.NODE_ENV === 'development';
|
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' };
|
const apiProxy = { target: target, changeOrigin: true, ws: true, rewriteWsOrigin: true, secure: false, cookieDomainRewrite: 'localhost' };
|
||||||
let proxy = {
|
let proxy = {
|
||||||
'/root/': {
|
'/root/': apiProxy,
|
||||||
target: `${target}/root/`,
|
|
||||||
},
|
|
||||||
'/api': apiProxy,
|
'/api': apiProxy,
|
||||||
'/client': apiProxy,
|
'/client': apiProxy,
|
||||||
};
|
};
|
||||||
@@ -25,14 +23,15 @@ export default defineConfig({
|
|||||||
react(), //
|
react(), //
|
||||||
// sitemap(), // sitemap must be site has a domain
|
// sitemap(), // sitemap must be site has a domain
|
||||||
],
|
],
|
||||||
|
server: {
|
||||||
|
port: 7008,
|
||||||
|
},
|
||||||
vite: {
|
vite: {
|
||||||
plugins: [tailwindcss()],
|
plugins: [tailwindcss()],
|
||||||
define: {
|
define: {
|
||||||
basename: JSON.stringify(basename || ''),
|
BASE_NAME: JSON.stringify(basename || ''),
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
port: 7008,
|
|
||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
allowedHosts: true,
|
allowedHosts: true,
|
||||||
proxy,
|
proxy,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@kevisual/kevisual-home",
|
"name": "@kevisual/kevisual-home",
|
||||||
"version": "0.0.7",
|
"version": "0.0.8",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"basename": "/root/home",
|
"basename": "/root/home",
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
"preview": "astro preview",
|
"preview": "astro preview",
|
||||||
"ui": "pnpm dlx shadcn@latest add ",
|
"ui": "pnpm dlx shadcn@latest add ",
|
||||||
"prepub": "pnpm run build",
|
"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": [],
|
"keywords": [],
|
||||||
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
|
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
|
||||||
|
|||||||
164
src/apps/config/firstLogin.tsx
Normal file
164
src/apps/config/firstLogin.tsx
Normal file
@@ -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 (
|
||||||
|
<div className={`relative ${className}`}>
|
||||||
|
{/* 炫光边框 - 外层发光(黑白色系) */}
|
||||||
|
<div className="absolute -inset-[2px] rounded-2xl opacity-50 blur-xl">
|
||||||
|
<div className="absolute inset-0 rounded-2xl bg-linear-to-r from-gray-100 via-white to-gray-200 animate-gradient-xy" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 边框渐变层(半透明白色) */}
|
||||||
|
<div className="absolute -inset-[1px] rounded-2xl bg-linear-to-r from-white/60 via-white/80 to-white/60 opacity-90" />
|
||||||
|
|
||||||
|
{/* 内容层 - 更透明 */}
|
||||||
|
<div className="relative backdrop-blur-xl bg-black/30 rounded-2xl p-8">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export const App = () => {
|
||||||
|
const firstStore = useFirstStore();
|
||||||
|
const [username, setUsername] = useState<string>("");
|
||||||
|
const [nickname, setNickname] = useState<string>("");
|
||||||
|
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 (
|
||||||
|
<div className="relative min-h-screen">
|
||||||
|
{/* 背景图层 */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 bg-cover bg-center"
|
||||||
|
style={{ backgroundImage: `url(${src})` }}
|
||||||
|
/>
|
||||||
|
{/* 模糊和遮罩层 */}
|
||||||
|
<div className="absolute inset-0 backdrop-blur-sm bg-black/30" />
|
||||||
|
{/* 内容层 */}
|
||||||
|
<div className="relative min-h-screen flex items-center justify-center">
|
||||||
|
<div className="text-white text-lg font-medium">加载中...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative min-h-screen">
|
||||||
|
{/* 背景图层 */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||||||
|
style={{ backgroundImage: `url(${src})` }}
|
||||||
|
/>
|
||||||
|
{/* 模糊和遮罩层 */}
|
||||||
|
<div className="absolute inset-0 backdrop-blur-md bg-black/40" />
|
||||||
|
|
||||||
|
{/* 内容层 */}
|
||||||
|
<div className="relative min-h-screen flex items-center justify-center p-6">
|
||||||
|
<div className="w-full max-w-md">
|
||||||
|
{/* 头像 */}
|
||||||
|
<div className="flex justify-center mb-8">
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-0 bg-white/30 blur-2xl rounded-full" />
|
||||||
|
<img
|
||||||
|
src={firstStore.getAvatar()}
|
||||||
|
alt={firstStore.userInfo?.nickname || firstStore.userInfo?.username || "avatar"}
|
||||||
|
className="relative w-28 h-28 rounded-full border-4 border-white/60 shadow-2xl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 用户信息卡片 - 炫光边框效果 */}
|
||||||
|
<GlowingCard>
|
||||||
|
<h1 className="text-3xl font-bold text-white text-center mb-3 drop-shadow-lg">
|
||||||
|
{nickname || firstStore.userInfo?.username}
|
||||||
|
</h1>
|
||||||
|
<p className="text-white/80 text-center mb-8 text-sm" title="只有第一次可以修改用户名哦~,其他联系管理员修改。">
|
||||||
|
{'只有第一次可以修改用户名哦~'}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
{/* 用户名输入表单 */}
|
||||||
|
<div className="space-y-5">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-white/90 mb-3">
|
||||||
|
昵称
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={nickname ?? ""}
|
||||||
|
onChange={(e) => 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="输入昵称"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-white/90 mb-3">
|
||||||
|
用户名
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={username ?? ""}
|
||||||
|
onChange={(e) => 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="输入用户名"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={!canChange}
|
||||||
|
className={`w-full py-3 px-6 rounded-xl font-medium transition-all duration-200 ${canChange
|
||||||
|
? "bg-linear-to-r from-white/90 to-gray-200 text-black hover:from-white hover:to-gray-100 hover:shadow-white/30 hover:shadow-xl hover:scale-[1.02] active:scale-[0.98]"
|
||||||
|
: "bg-white/10 text-white/30 cursor-not-allowed"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{canChange ? "保存修改" : "不可修改"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</GlowingCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AppProvider = () => {
|
||||||
|
return <AuthProvider>
|
||||||
|
<App />
|
||||||
|
<ToastContainer />
|
||||||
|
</AuthProvider>;
|
||||||
|
}
|
||||||
108
src/apps/config/store.ts
Normal file
108
src/apps/config/store.ts
Normal file
@@ -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<void>;
|
||||||
|
getAvatar(): string;
|
||||||
|
updateUserInfo: (opts?: { username: string; nickname: string }) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useFirstStore = create<FirstState>((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 = `
|
||||||
|
<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="100" height="100" fill="${backgroundColor}"/>
|
||||||
|
<text x="50" y="50" font-family="Arial, sans-serif" font-size="50"
|
||||||
|
font-weight="bold" fill="white" text-anchor="middle"
|
||||||
|
dominant-baseline="central">${firstChar}</text>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 转换为 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 || '更新用户信息失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { basename, wrapBasename } from '@/modules/basename';
|
||||||
import { queryLogin } from '@/modules/query';
|
import { queryLogin } from '@/modules/query';
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
|
|
||||||
@@ -10,6 +11,7 @@ interface UserState {
|
|||||||
orgs?: string[];
|
orgs?: string[];
|
||||||
type?: string;
|
type?: string;
|
||||||
username?: string;
|
username?: string;
|
||||||
|
canChangeUsername?: boolean;
|
||||||
} | null;
|
} | null;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
setOpen: (open: boolean) => void;
|
setOpen: (open: boolean) => void;
|
||||||
@@ -46,6 +48,11 @@ export const useUserStore = create<UserState>((set, get) => ({
|
|||||||
console.log('获取到的用户信息:', res);
|
console.log('获取到的用户信息:', res);
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
set({ user: res.data || null });
|
set({ user: res.data || null });
|
||||||
|
const canChangeUsername = res.data?.canChangeUsername ?? false;
|
||||||
|
if (canChangeUsername) {
|
||||||
|
// 打开修改用户名的页面
|
||||||
|
window.open(wrapBasename('/first'), '_blank');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
init: () => {
|
init: () => {
|
||||||
|
|||||||
BIN
src/assets/user-name-bg.jpg
Normal file
BIN
src/assets/user-name-bg.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
@@ -1,4 +1,13 @@
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
export const basename = BASE_NAME;
|
export const basename = BASE_NAME;
|
||||||
|
|
||||||
console.log(basename);
|
console.log(basename);
|
||||||
|
|
||||||
|
export const wrapBasename = (path: string) => {
|
||||||
|
const hasEnd = path.endsWith('/')
|
||||||
|
if (basename) {
|
||||||
|
return `${basename}${path}` + (hasEnd ? '' : '/');
|
||||||
|
} else {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/pages/first.astro
Normal file
10
src/pages/first.astro
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
import Html from '@/components/html.astro';
|
||||||
|
import { AppProvider } from '@/apps/config/firstLogin';
|
||||||
|
---
|
||||||
|
|
||||||
|
<Html title='可视化平台'>
|
||||||
|
<main>
|
||||||
|
<AppProvider client:only />
|
||||||
|
</main>
|
||||||
|
</Html>
|
||||||
Reference in New Issue
Block a user