This commit is contained in:
2025-12-04 17:50:00 +08:00
parent a51a366f00
commit d217c8cec1
12 changed files with 632 additions and 514 deletions

View File

@@ -17,13 +17,13 @@
"license": "MIT", "license": "MIT",
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@ant-design/x": "^2.0.0", "@ant-design/x": "^2.0.1",
"@astrojs/mdx": "^4.3.12", "@astrojs/mdx": "^4.3.12",
"@astrojs/react": "^4.4.2", "@astrojs/react": "^4.4.2",
"@astrojs/sitemap": "^3.6.0", "@astrojs/sitemap": "^3.6.0",
"@floating-ui/dom": "^1.7.4", "@floating-ui/dom": "^1.7.4",
"@kevisual/context": "^0.0.4", "@kevisual/context": "^0.0.4",
"@kevisual/kv-login": "^0.0.6", "@kevisual/kv-login": "^0.0.7",
"@kevisual/query": "0.0.29", "@kevisual/query": "0.0.29",
"@kevisual/query-login": "^0.0.7", "@kevisual/query-login": "^0.0.7",
"@kevisual/registry": "^0.0.1", "@kevisual/registry": "^0.0.1",
@@ -31,7 +31,7 @@
"@radix-ui/react-navigation-menu": "^1.2.14", "@radix-ui/react-navigation-menu": "^1.2.14",
"@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-slot": "^1.2.4",
"@tailwindcss/vite": "^4.1.17", "@tailwindcss/vite": "^4.1.17",
"astro": "^5.16.3", "astro": "^5.16.4",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dayjs": "^1.11.19", "dayjs": "^1.11.19",
@@ -40,8 +40,8 @@
"lucide-react": "^0.555.0", "lucide-react": "^0.555.0",
"nanoid": "^5.1.6", "nanoid": "^5.1.6",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"react": "^19.2.0", "react": "^19.2.1",
"react-dom": "^19.2.0", "react-dom": "^19.2.1",
"react-toastify": "^11.0.5", "react-toastify": "^11.0.5",
"tailwind-merge": "^3.4.0", "tailwind-merge": "^3.4.0",
"zustand": "^5.0.9" "zustand": "^5.0.9"
@@ -56,10 +56,10 @@
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@vitejs/plugin-basic-ssl": "^2.1.0", "@vitejs/plugin-basic-ssl": "^2.1.0",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"react": "^19.2.0", "react": "^19.2.1",
"tailwindcss": "^4.1.17", "tailwindcss": "^4.1.17",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
"vite": "^7.2.4" "vite": "^7.2.6"
}, },
"pnpm": { "pnpm": {
"onlyBuiltDependencies": [ "onlyBuiltDependencies": [

View File

@@ -0,0 +1,15 @@
import { build } from 'bun';
await build({
entrypoints: ["./src/main.ts"],
outdir: './dist',
target: 'browser',
format: 'esm',
naming: {
entry: 'app.js',
},
minify: false,
sourcemap: false,
});
console.log('✅ Build complete: dist/app.js');

View File

@@ -1,11 +1,12 @@
{ {
"name": "@kevisual/kv-login", "name": "@kevisual/kv-login",
"version": "0.0.6", "version": "0.0.7",
"description": "", "description": "",
"main": "src/main.ts", "main": "src/main.ts",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build --config vite-lib.config.ts", "build": "bun bun.config.ts",
"postbuild": "dts -i src/main.ts -o app.d.ts",
"build:test": "vite build", "build:test": "vite build",
"prepub": "rm -rf ./dist && pnpm run build:test", "prepub": "rm -rf ./dist && pnpm run build:test",
"pub": "ev deploy ./dist -k kv-login-test -v 0.0.6 -u -y yes" "pub": "ev deploy ./dist -k kv-login-test -v 0.0.6 -u -y yes"
@@ -25,10 +26,11 @@
"qrcode": "^1.5.4" "qrcode": "^1.5.4"
}, },
"exports": { "exports": {
".": "./dist/kv-login.es.js", ".": "./dist/app.js",
"./kv-login.es.js": "./dist/kv-login.es.js",
"./kv-login.umd.js": "./dist/kv-login.umd.js",
"./types": "./types/index.d.ts" "./types": "./types/index.d.ts"
}, },
"types": "./types/index.d.ts" "types": "./types/index.d.ts",
"devDependencies": {
"@types/bun": "^1.3.3"
}
} }

View File

@@ -19,7 +19,11 @@ export const redirectHome = () => {
const href = decodeURIComponent(redirect); const href = decodeURIComponent(redirect);
window.open(href, '_self'); window.open(href, '_self');
} }
// 从url上清除 code 参数, 清除 state 参数
emit({ type: 'login-success', data: {} }); emit({ type: 'login-success', data: {} });
setTimeout(() => {
clearCode();
}, 1500);
} }
export const loginHandle = async (opts: LoginOpts) => { export const loginHandle = async (opts: LoginOpts) => {
const { loginMethod, data, el } = opts const { loginMethod, data, el } = opts

View File

@@ -1,12 +0,0 @@
import { defineConfig } from 'vite';
const entry = './src/main.ts';
export default defineConfig({
build: {
lib: {
entry,
name: 'KvLogin',
fileName: (format) => `kv-login.${format}.js`,
}
}
});

944
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

15
src/apps/home/chat.ts Normal file
View File

@@ -0,0 +1,15 @@
import { query } from '@/modules/query';
import { toast } from 'react-toastify';
export const postChat = async (question: string) => {
const res = await query.post({
path: 'noco-life',
key: 'chat',
payload: { question },
});
if (res.code === 200) {
return res.data?.content;
} else {
toast.error(res.message || 'Failed to get chat response');
}
return '';
}

View File

@@ -2,9 +2,12 @@ import { app } from '../ai';
import { Sender, XProvider } from '@ant-design/x'; import { Sender, XProvider } from '@ant-design/x';
import { useEffect, useRef } from 'react'; import { useEffect, useRef, useState } from 'react';
import { postChat } from './chat';
import { Nav } from '../nav'; import { Nav } from '../nav';
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { useHomeStore } from './store';
const useFocus = () => { const useFocus = () => {
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
@@ -31,17 +34,29 @@ const useFocus = () => {
return inputRef; return inputRef;
} }
export const App = () => { export const App = () => {
const inputRef = useFocus(); const inputRef = useFocus();
const [content, setContent] = useState<string>('');
const { inputValue, setInputValue, isLoading, setLoading } = useHomeStore();
return <div className='container mx-auto px-4 py-4 md:p-4'>
<div className='md:top-20 left-0 mb-4 right-0 w-full mx-auto px-4 md:px-0' ref={inputRef}>
return <div className='container mx-auto p-4'> <Sender
<div className='fixed bottom-8 w-1/2 justify-self-center' ref={inputRef}> allowSpeech
<Sender allowSpeech onSubmit={() => { value={inputValue}
console.log('Submitted'); onChange={setInputValue}
}} /> loading={isLoading}
onSubmit={async (message) => {
console.log('Submitted', message);
setLoading(true);
const res = await postChat(message);
setContent(res || ' ');
setLoading(false);
}}
/>
</div>
<div className='mb-20 md:mb-16 px-2 md:px-0'>
{content}
</div> </div>
</div >; </div >;
} }
@@ -66,5 +81,6 @@ export const AppProvider = () => {
> >
<Nav /> <Nav />
<App /> <App />
<ToastContainer />
</XProvider>; </XProvider>;
} }

27
src/apps/home/store.ts Normal file
View File

@@ -0,0 +1,27 @@
/**
* @title Home Store
* @description 管理 home 页面的输入框数据和加载状态
* @tags zustand, state-management, input, loading
* @createdAt 2025-12-04
*/
import { create } from 'zustand';
interface HomeState {
// 输入框内容
inputValue: string;
// 加载状态
isLoading: boolean;
// Actions
setInputValue: (value: string) => void;
setLoading: (loading: boolean) => void;
}
export const useHomeStore = create<HomeState>((set) => ({
inputValue: '',
isLoading: false,
setInputValue: (value) => set({ inputValue: value }),
setLoading: (loading) => set({ isLoading: loading }),
}));

View File

@@ -12,6 +12,7 @@ export const LoginComponent = ({ onLoginSuccess }: { onLoginSuccess: () => void
const handleLoginSuccess = () => { const handleLoginSuccess = () => {
console.log('监听到登录成功事件,关闭弹窗'); console.log('监听到登录成功事件,关闭弹窗');
onLoginSuccess(); onLoginSuccess();
}; };
const loginEmitter = useContextKey('login-emitter') const loginEmitter = useContextKey('login-emitter')
console.log('KvLogin Types:', loginEmitter); console.log('KvLogin Types:', loginEmitter);
@@ -28,23 +29,24 @@ export const LoginComponent = ({ onLoginSuccess }: { onLoginSuccess: () => void
return (<kv-login><div id="weixinLogin"></div></kv-login>) return (<kv-login><div id="weixinLogin"></div></kv-login>)
} }
export const Nav = () => { export const Nav = () => {
const [isDialogOpen, setIsDialogOpen] = useState(false);
const store = useUserStore(useShallow((state) => ({ const store = useUserStore(useShallow((state) => ({
user: state.user, user: state.user,
open: state.open,
setOpen: state.setOpen,
setUser: state.setUser, setUser: state.setUser,
clearUser: state.clearUser, clearUser: state.clearUser,
queryUser: state.queryUser queryUser: state.queryUser,
init: state.init,
}))); })));
useEffect(() => { useEffect(() => {
store.queryUser(); store.queryUser();
store.init();
}, []); }, []);
const handleLoginSuccess = () => { const handleLoginSuccess = () => {
// 关闭弹窗 store.setOpen(false);
setIsDialogOpen(false); store.queryUser();
// 重新查询用户信息
}; };
return <header> return <header>
<nav className="bg-black p-4 text-white flex justify-between"> <nav className="bg-black p-4 text-white flex justify-between">
@@ -62,7 +64,7 @@ export const Nav = () => {
</button> </button>
</div> </div>
) : ( ) : (
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}> <Dialog open={store.open} onOpenChange={store.setOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<button className="bg-gray-700 text-white px-3 py-1 rounded hover:bg-gray-600 transition-colors"> <button className="bg-gray-700 text-white px-3 py-1 rounded hover:bg-gray-600 transition-colors">

View File

@@ -11,23 +11,50 @@ interface UserState {
type?: string; type?: string;
username?: string; username?: string;
} | null; } | null;
open: boolean;
setOpen: (open: boolean) => void;
setUser: (user: UserState['user']) => void; setUser: (user: UserState['user']) => void;
clearUser: () => void; clearUser: () => Promise<void>;
queryUser: () => void; queryUser: () => void;
queryMe: () => void; queryMe: (token?: string) => void;
init: () => void;
} }
export const useUserStore = create<UserState>((set) => ({ export const useUserStore = create<UserState>((set, get) => ({
user: null, user: null,
open: false,
setOpen: (open) => set({ open }),
setUser: (user) => set({ user }), setUser: (user) => set({ user }),
clearUser: () => set({ user: null }), clearUser: async () => {
await queryLogin.logout()
set({ user: null });
},
queryUser: async () => { queryUser: async () => {
const user = await queryLogin.checkLocalUser(); const user = await queryLogin.checkLocalUser();
set({ user }); console.log('查询到的用户信息:', user);
if (!user) {
const token = localStorage.getItem('token');
if (token) {
get().queryMe(token);
}
} else {
set({ user });
}
}, },
queryMe: async () => { queryMe: async (token?: string) => {
const user = await queryLogin.getMe(); const res = await queryLogin.getMe(token);
set({ user }); console.log('获取到的用户信息:', res);
if (res.code === 200) {
set({ user: res.data || null });
}
},
init: () => {
const url = new URL(window.location.href);
const code = url.searchParams.get('code');
const state = url.searchParams.get('state');
if (code && state) {
set({ open: true })
}
} }
})); }));

View File

@@ -1,11 +1,11 @@
import { Query } from "@kevisual/query"; import { QueryClient } from "@kevisual/query";
import { QueryLoginBrowser } from '@kevisual/query-login'; import { QueryLoginBrowser } from '@kevisual/query-login';
export const query = new Query(); export const query = new QueryClient();
export const queryLogin = new QueryLoginBrowser({ export const queryLogin = new QueryLoginBrowser({
query query
}) })
export const local = new Query({ export const local = new QueryClient({
url: '/client/router' url: '/client/router'
}); });