update
This commit is contained in:
24
package.json
24
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@kevisual/kevisual-home",
|
"name": "@kevisual/kevisual-home",
|
||||||
"version": "0.0.5",
|
"version": "0.0.6",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"basename": "/root/home",
|
"basename": "/root/home",
|
||||||
@@ -9,38 +9,42 @@
|
|||||||
"build": "astro build",
|
"build": "astro build",
|
||||||
"preview": "astro preview",
|
"preview": "astro preview",
|
||||||
"ui": "pnpm dlx shadcn@latest add ",
|
"ui": "pnpm dlx shadcn@latest add ",
|
||||||
"pub": "envision deploy ./dist -k home -v 0.0.5 -u"
|
"prepub": "pnpm run build",
|
||||||
|
"pub": "envision deploy ./dist -k home -v 0.0.6 -u -y yes"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
|
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/mdx": "^4.3.10",
|
"@ant-design/x": "^2.0.0",
|
||||||
|
"@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.1",
|
"@kevisual/kv-login": "^0.0.6",
|
||||||
"@kevisual/query": "0.0.29",
|
"@kevisual/query": "0.0.29",
|
||||||
"@kevisual/query-login": "^0.0.6",
|
"@kevisual/query-login": "^0.0.7",
|
||||||
"@kevisual/registry": "^0.0.1",
|
"@kevisual/registry": "^0.0.1",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
|
"@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.15.8",
|
"astro": "^5.16.3",
|
||||||
"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",
|
||||||
"es-toolkit": "^1.42.0",
|
"es-toolkit": "^1.42.0",
|
||||||
"github-markdown-css": "^5.8.1",
|
"github-markdown-css": "^5.8.1",
|
||||||
"lucide-react": "^0.554.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.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-toastify": "^11.0.5",
|
"react-toastify": "^11.0.5",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"zustand": "^5.0.8"
|
"zustand": "^5.0.9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@kevisual/router": "0.0.33",
|
"@kevisual/router": "0.0.33",
|
||||||
@@ -48,14 +52,14 @@
|
|||||||
"@kevisual/types": "^0.0.10",
|
"@kevisual/types": "^0.0.10",
|
||||||
"@tailwindcss/vite": "^4.1.17",
|
"@tailwindcss/vite": "^4.1.17",
|
||||||
"@types/qrcode": "^1.5.6",
|
"@types/qrcode": "^1.5.6",
|
||||||
"@types/react": "^19.2.5",
|
"@types/react": "^19.2.7",
|
||||||
"@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.0",
|
||||||
"tailwindcss": "^4.1.17",
|
"tailwindcss": "^4.1.17",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"vite": "^7.2.2"
|
"vite": "^7.2.4"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
|
|||||||
@@ -74,7 +74,7 @@
|
|||||||
<div class="demo-container">
|
<div class="demo-container">
|
||||||
<div class="login-section">
|
<div class="login-section">
|
||||||
<h2>登录组件</h2>
|
<h2>登录组件</h2>
|
||||||
<kv-login id="loginComponent">
|
<kv-login>
|
||||||
<div id="weixinLogin"></div>
|
<div id="weixinLogin"></div>
|
||||||
</kv-login>
|
</kv-login>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@kevisual/kv-login",
|
"name": "@kevisual/kv-login",
|
||||||
"version": "0.0.3",
|
"version": "0.0.6",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "src/main.ts",
|
"main": "src/main.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -8,24 +8,27 @@
|
|||||||
"build": "vite build --config vite-lib.config.ts",
|
"build": "vite build --config vite-lib.config.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.2 -u -y yes"
|
"pub": "ev deploy ./dist -k kv-login-test -v 0.0.6 -u -y yes"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
|
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"packageManager": "pnpm@10.19.0",
|
"packageManager": "pnpm@10.24.0",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kevisual/query-login": "^0.0.6",
|
"@kevisual/context": "^0.0.4",
|
||||||
|
"@kevisual/query-login": "^0.0.7",
|
||||||
"lit-html": "^3.3.1",
|
"lit-html": "^3.3.1",
|
||||||
"qrcode": "^1.5.4"
|
"qrcode": "^1.5.4"
|
||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./dist/kv-login.es.js",
|
".": "./dist/kv-login.es.js",
|
||||||
"./kv-login.es.js": "./dist/kv-login.es.js",
|
"./kv-login.es.js": "./dist/kv-login.es.js",
|
||||||
"./kv-login.umd.js": "./dist/kv-login.umd.js"
|
"./kv-login.umd.js": "./dist/kv-login.umd.js",
|
||||||
}
|
"./types": "./types/index.d.ts"
|
||||||
|
},
|
||||||
|
"types": "./types/index.d.ts"
|
||||||
}
|
}
|
||||||
@@ -5,3 +5,8 @@
|
|||||||
黑白
|
黑白
|
||||||
|
|
||||||
|
|
||||||
|
```html
|
||||||
|
<kv-login>
|
||||||
|
<div id="weixinLogin"></div>
|
||||||
|
</kv-login>
|
||||||
|
```
|
||||||
@@ -1,2 +1,4 @@
|
|||||||
import './pages/kv-login'
|
import './pages/kv-login'
|
||||||
import './pages/kv-message'
|
import './pages/kv-message'
|
||||||
|
|
||||||
|
export { loginEmitter } from './pages/kv-login'
|
||||||
@@ -1,23 +1,25 @@
|
|||||||
import { query } from './query.ts';
|
import { query } from './query.ts';
|
||||||
import { createMessage } from '../pages/kv-message.ts';
|
import { createMessage } from '../pages/kv-message.ts';
|
||||||
import { WX_MP_APP_ID } from '../pages/kv-login.ts';
|
import { WX_MP_APP_ID } from '../pages/kv-login.ts';
|
||||||
|
import { emit } from './mitt.ts';
|
||||||
export const message = createMessage();
|
export const message = createMessage();
|
||||||
type LoginOpts = {
|
type LoginOpts = {
|
||||||
loginMethod: 'password' | 'phone' | 'wechat' | 'wechat-mp' | 'wechat-mp-ticket',
|
loginMethod: 'password' | 'phone' | 'wechat' | 'wechat-mp' | 'wechat-mp-ticket',
|
||||||
data: any,
|
data: any,
|
||||||
el: HTMLElement
|
el: HTMLElement
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 登录成功后重定向到首页
|
||||||
|
*/
|
||||||
export const redirectHome = () => {
|
export const redirectHome = () => {
|
||||||
console.log('重定向到首页')
|
|
||||||
const href = window.location.href;
|
const href = window.location.href;
|
||||||
const url = new URL(href);
|
const url = new URL(href);
|
||||||
const redirect = url.searchParams.get('redirect');
|
const redirect = url.searchParams.get('redirect');
|
||||||
if (redirect) {
|
if (redirect) {
|
||||||
const href = decodeURIComponent(redirect);
|
const href = decodeURIComponent(redirect);
|
||||||
window.open(href, '_self');
|
window.open(href, '_self');
|
||||||
} else {
|
|
||||||
window.open('/root/home', '_self');
|
|
||||||
}
|
}
|
||||||
|
emit({ type: 'login-success', data: {} });
|
||||||
}
|
}
|
||||||
export const loginHandle = async (opts: LoginOpts) => {
|
export const loginHandle = async (opts: LoginOpts) => {
|
||||||
const { loginMethod, data, el } = opts
|
const { loginMethod, data, el } = opts
|
||||||
|
|||||||
134
packages/kv-login/src/modules/mitt.ts
Normal file
134
packages/kv-login/src/modules/mitt.ts
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
export interface EventData<T = any> {
|
||||||
|
type: string;
|
||||||
|
data: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EventHandler<T = any> = (event: EventData<T>) => void;
|
||||||
|
|
||||||
|
export class EventEmitter {
|
||||||
|
private events: Map<string, Set<EventHandler>> = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 监听事件
|
||||||
|
* @param type 事件类型
|
||||||
|
* @param handler 事件处理函数
|
||||||
|
*/
|
||||||
|
on<T = any>(type: string, handler: EventHandler<T>): void {
|
||||||
|
if (!this.events.has(type)) {
|
||||||
|
this.events.set(type, new Set());
|
||||||
|
}
|
||||||
|
this.events.get(type)!.add(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除事件监听器
|
||||||
|
* @param type 事件类型
|
||||||
|
* @param handler 事件处理函数 (可选,如果不提供则移除该类型的所有监听器)
|
||||||
|
*/
|
||||||
|
off<T = any>(type: string, handler?: EventHandler<T>): void {
|
||||||
|
if (!this.events.has(type)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handler) {
|
||||||
|
this.events.get(type)!.delete(handler);
|
||||||
|
// 如果该类型没有监听器了,删除该类型
|
||||||
|
if (this.events.get(type)!.size === 0) {
|
||||||
|
this.events.delete(type);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 移除该类型的所有监听器
|
||||||
|
this.events.delete(type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 触发事件
|
||||||
|
* @param event 事件对象,包含 type 和 data
|
||||||
|
*/
|
||||||
|
emit<T = any>(event: EventData<T>): void {
|
||||||
|
const { type } = event;
|
||||||
|
|
||||||
|
if (!this.events.has(type)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlers = this.events.get(type)!;
|
||||||
|
handlers.forEach(handler => {
|
||||||
|
try {
|
||||||
|
handler(event);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error in event handler for type "${type}":`, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 触发事件(简化版本,直接传递type和data)
|
||||||
|
* @param type 事件类型
|
||||||
|
* @param data 事件数据
|
||||||
|
*/
|
||||||
|
emitSimple<T = any>(type: string, data: T): void {
|
||||||
|
this.emit({ type, data });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空所有事件监听器
|
||||||
|
*/
|
||||||
|
clear(): void {
|
||||||
|
this.events.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定类型的监听器数量
|
||||||
|
* @param type 事件类型
|
||||||
|
* @returns 监听器数量
|
||||||
|
*/
|
||||||
|
listenerCount(type: string): number {
|
||||||
|
return this.events.get(type)?.size || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有事件类型
|
||||||
|
* @returns 事件类型数组
|
||||||
|
*/
|
||||||
|
eventNames(): string[] {
|
||||||
|
return Array.from(this.events.keys());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否有指定类型的监听器
|
||||||
|
* @param type 事件类型
|
||||||
|
* @returns 是否有监听器
|
||||||
|
*/
|
||||||
|
hasListeners(type: string): boolean {
|
||||||
|
return this.events.has(type) && this.events.get(type)!.size > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 只监听一次事件
|
||||||
|
* @param type 事件类型
|
||||||
|
* @param handler 事件处理函数
|
||||||
|
*/
|
||||||
|
once<T = any>(type: string, handler: EventHandler<T>): void {
|
||||||
|
const onceHandler: EventHandler<T> = (event) => {
|
||||||
|
handler(event);
|
||||||
|
this.off(type, onceHandler);
|
||||||
|
};
|
||||||
|
this.on(type, onceHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建默认的事件发射器实例
|
||||||
|
export const eventEmitter = new EventEmitter();
|
||||||
|
|
||||||
|
// 导出便捷方法
|
||||||
|
export const on = <T = any>(type: string, handler: EventHandler<T>) => eventEmitter.on(type, handler);
|
||||||
|
export const off = <T = any>(type: string, handler?: EventHandler<T>) => eventEmitter.off(type, handler);
|
||||||
|
export const emit = <T = any>(event: EventData<T>) => eventEmitter.emit(event);
|
||||||
|
export const emitSimple = <T = any>(type: string, data: T) => eventEmitter.emitSimple(type, data);
|
||||||
|
export const clear = () => eventEmitter.clear();
|
||||||
|
export const once = <T = any>(type: string, handler: EventHandler<T>) => eventEmitter.once(type, handler);
|
||||||
|
|
||||||
|
// 默认导出
|
||||||
|
export default eventEmitter;
|
||||||
@@ -3,6 +3,9 @@ import { unsafeHTML } from 'lit-html/directives/unsafe-html.js'
|
|||||||
import { loginHandle, checkWechat, getQrCode, checkMpQrCodeLogin } from '../modules/login-handle.ts'
|
import { loginHandle, checkWechat, getQrCode, checkMpQrCodeLogin } from '../modules/login-handle.ts'
|
||||||
import { setWxerwma } from '../modules/wx/ws-login.ts';
|
import { setWxerwma } from '../modules/wx/ws-login.ts';
|
||||||
import { useCreateLoginQRCode } from '../modules/wx-mp/qr.ts';
|
import { useCreateLoginQRCode } from '../modules/wx-mp/qr.ts';
|
||||||
|
import { eventEmitter } from '../modules/mitt.ts';
|
||||||
|
import { useContextKey } from '@kevisual/context'
|
||||||
|
export const loginEmitter = useContextKey('login-emitter', eventEmitter);
|
||||||
export const WX_MP_APP_ID = "wxff97d569b1db16b6";
|
export const WX_MP_APP_ID = "wxff97d569b1db16b6";
|
||||||
interface LoginMethod {
|
interface LoginMethod {
|
||||||
id: LoginMethods
|
id: LoginMethods
|
||||||
@@ -46,7 +49,6 @@ const getLoginMethodByDomain = (): LoginMethod[] => {
|
|||||||
}
|
}
|
||||||
return DefaultLoginMethods.filter(method => methods.includes(method.id))
|
return DefaultLoginMethods.filter(method => methods.includes(method.id))
|
||||||
}
|
}
|
||||||
console.log('可用登录方式:', getLoginMethodByDomain().map(m => m.name).join(', '));
|
|
||||||
class KvLogin extends HTMLElement {
|
class KvLogin extends HTMLElement {
|
||||||
private selectedMethod: LoginMethods = 'password'
|
private selectedMethod: LoginMethods = 'password'
|
||||||
|
|
||||||
@@ -80,7 +82,6 @@ class KvLogin extends HTMLElement {
|
|||||||
}
|
}
|
||||||
private bindEvents() {
|
private bindEvents() {
|
||||||
if (!this.shadowRoot) return
|
if (!this.shadowRoot) return
|
||||||
|
|
||||||
// 使用事件委托来处理登录方式切换
|
// 使用事件委托来处理登录方式切换
|
||||||
this.shadowRoot.addEventListener('click', (e) => {
|
this.shadowRoot.addEventListener('click', (e) => {
|
||||||
const target = e.target as HTMLElement
|
const target = e.target as HTMLElement
|
||||||
@@ -101,6 +102,9 @@ class KvLogin extends HTMLElement {
|
|||||||
this.handleLogin()
|
this.handleLogin()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
loginEmitter.on('login-success', () => {
|
||||||
|
console.log('收到登录成功事件,处理后续逻辑')
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleLogin() {
|
private handleLogin() {
|
||||||
@@ -331,8 +335,8 @@ class KvLogin extends HTMLElement {
|
|||||||
|
|
||||||
.login-methods {
|
.login-methods {
|
||||||
display: flex;
|
display: flex;
|
||||||
background: #f8f9fa;
|
background: #f5f5f5;
|
||||||
border-bottom: 1px solid #e9ecef;
|
border-bottom: 1px solid #000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-method {
|
.login-method {
|
||||||
@@ -350,7 +354,7 @@ class KvLogin extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.login-method:hover {
|
.login-method:hover {
|
||||||
background: #e9ecef;
|
background: #d0d0d0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-method.active {
|
.login-method.active {
|
||||||
@@ -418,7 +422,7 @@ class KvLogin extends HTMLElement {
|
|||||||
.form-group input {
|
.form-group input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
border: 2px solid #e9ecef;
|
border: 2px solid #cccccc;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
transition: border-color 0.3s ease;
|
transition: border-color 0.3s ease;
|
||||||
@@ -481,7 +485,7 @@ class KvLogin extends HTMLElement {
|
|||||||
.qr-container {
|
.qr-container {
|
||||||
width: 340px;
|
width: 340px;
|
||||||
height: 340px;
|
height: 340px;
|
||||||
border: 2px dashed #cccccc;
|
border: 2px solid #000000;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -490,7 +494,7 @@ class KvLogin extends HTMLElement {
|
|||||||
|
|
||||||
.qr-placeholder {
|
.qr-placeholder {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #6c757d;
|
color: #333333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.qr-icon {
|
.qr-icon {
|
||||||
|
|||||||
127
packages/kv-login/types/index.d.ts
vendored
Normal file
127
packages/kv-login/types/index.d.ts
vendored
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
type LoginMethods = 'password' | 'phone' | 'wechat' | 'wechat-mp' | 'wechat-mp-ticket';
|
||||||
|
|
||||||
|
interface KvLoginEventMap {
|
||||||
|
login: CustomEvent<{
|
||||||
|
method: LoginMethods;
|
||||||
|
data: LoginFormData[LoginMethods] | any;
|
||||||
|
}>;
|
||||||
|
/**
|
||||||
|
* 登录方式切换事件
|
||||||
|
*/
|
||||||
|
methodChange: CustomEvent<{
|
||||||
|
method: LoginMethods;
|
||||||
|
previousMethod?: LoginMethods;
|
||||||
|
}>;
|
||||||
|
/**
|
||||||
|
* 登录验证失败事件
|
||||||
|
*/
|
||||||
|
validationError: CustomEvent<{
|
||||||
|
method: LoginMethods;
|
||||||
|
errors: string[];
|
||||||
|
formData: LoginFormData[LoginMethods] | any;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KvLogin extends HTMLElement {
|
||||||
|
/**
|
||||||
|
* 设置登录方式
|
||||||
|
*/
|
||||||
|
setLoginMethods(methods: LoginMethod[]): void;
|
||||||
|
/**
|
||||||
|
* 添加自定义登录方式
|
||||||
|
*/
|
||||||
|
addLoginMethod(method: LoginMethod): void;
|
||||||
|
/**
|
||||||
|
* 移除登录方式
|
||||||
|
*/
|
||||||
|
removeLoginMethod(methodId: LoginMethods): void;
|
||||||
|
/**
|
||||||
|
* 获取当前选中的登录方式
|
||||||
|
*/
|
||||||
|
getSelectedMethod(): LoginMethods;
|
||||||
|
/**
|
||||||
|
* 设置默认登录方式
|
||||||
|
*/
|
||||||
|
setDefaultMethod(methodId: LoginMethods): void;
|
||||||
|
|
||||||
|
addEventListener<K extends keyof KvLoginEventMap>(
|
||||||
|
type: K,
|
||||||
|
listener: (this: KvLogin, ev: KvLoginEventMap[K]) => void,
|
||||||
|
options?: boolean | AddEventListenerOptions
|
||||||
|
): void;
|
||||||
|
removeEventListener<K extends keyof KvLoginEventMap>(
|
||||||
|
type: K,
|
||||||
|
listener: (this: KvLogin, ev: KvLoginEventMap[K]) => void,
|
||||||
|
options?: boolean | EventListenerOptions
|
||||||
|
): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'kv-login': KvLogin;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace JSX {
|
||||||
|
interface IntrinsicElements {
|
||||||
|
'kv-login': KvLoginAttributes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KvLoginAttributes extends React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> {
|
||||||
|
/**
|
||||||
|
* 自定义登录方式配置
|
||||||
|
*/
|
||||||
|
loginMethods?: LoginMethod[];
|
||||||
|
/**
|
||||||
|
* 自定义样式类名
|
||||||
|
*/
|
||||||
|
customClass?: string;
|
||||||
|
/**
|
||||||
|
* 是否显示登录方式选择器
|
||||||
|
*/
|
||||||
|
showMethodSelector?: boolean;
|
||||||
|
/**
|
||||||
|
* 默认选中的登录方式
|
||||||
|
*/
|
||||||
|
defaultMethod?: LoginMethods;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LoginMethod {
|
||||||
|
id: LoginMethods;
|
||||||
|
name: string;
|
||||||
|
icon: string | any; // 可以是emoji字符串、SVG字符串或其他图标类型
|
||||||
|
appid?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
order?: number; // 用于排序
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LoginFormData {
|
||||||
|
password?: {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
phone?: {
|
||||||
|
phone: string;
|
||||||
|
code: string;
|
||||||
|
};
|
||||||
|
wechat?: {
|
||||||
|
wechatCode: string;
|
||||||
|
};
|
||||||
|
'wechat-mp'?: {
|
||||||
|
wechatMpCode: string;
|
||||||
|
};
|
||||||
|
'wechat-mp-ticket'?: {
|
||||||
|
wechatMpCode: string;
|
||||||
|
ticket: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
KvLogin,
|
||||||
|
KvLoginEventMap,
|
||||||
|
KvLoginAttributes,
|
||||||
|
LoginMethods,
|
||||||
|
LoginMethod,
|
||||||
|
LoginFormData
|
||||||
|
};
|
||||||
1931
pnpm-lock.yaml
generated
1931
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
20
src/apps/auth.tsx
Normal file
20
src/apps/auth.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { queryLogin } from "@/modules/query"
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
export const AuthProvider = (props: { children: React.ReactNode }) => {
|
||||||
|
const [isLogin, setIsLogin] = useState<boolean>(false);
|
||||||
|
const init = async () => {
|
||||||
|
const token = await queryLogin.checkLocalToken();
|
||||||
|
if (token) {
|
||||||
|
console.log('User is logged in');
|
||||||
|
setIsLogin(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
useEffect(() => {
|
||||||
|
init();
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{isLogin ? props.children : <div>Please log in to access this application.</div>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
70
src/apps/home/index.tsx
Normal file
70
src/apps/home/index.tsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { app } from '../ai';
|
||||||
|
|
||||||
|
|
||||||
|
import { Sender, XProvider } from '@ant-design/x';
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
import { Nav } from '../nav';
|
||||||
|
|
||||||
|
const useFocus = () => {
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
// Focus the input element inside Sender component
|
||||||
|
const focusInput = () => {
|
||||||
|
if (inputRef.current) {
|
||||||
|
const input = inputRef.current.querySelector('input, textarea') as HTMLInputElement | HTMLTextAreaElement;
|
||||||
|
if (input) {
|
||||||
|
input.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Focus on mount
|
||||||
|
focusInput();
|
||||||
|
|
||||||
|
// Also focus after a short delay to ensure everything is rendered
|
||||||
|
const timeoutId = setTimeout(focusInput, 100);
|
||||||
|
|
||||||
|
return () => clearTimeout(timeoutId);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return inputRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const App = () => {
|
||||||
|
const inputRef = useFocus();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return <div className='container mx-auto p-4'>
|
||||||
|
<div className='fixed bottom-8 w-1/2 justify-self-center' ref={inputRef}>
|
||||||
|
<Sender allowSpeech onSubmit={() => {
|
||||||
|
console.log('Submitted');
|
||||||
|
}} />
|
||||||
|
</div>
|
||||||
|
</div >;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const AppProvider = () => {
|
||||||
|
return <XProvider
|
||||||
|
theme={{
|
||||||
|
token: {
|
||||||
|
colorPrimary: '#000000',
|
||||||
|
colorBgBase: '#ffffff',
|
||||||
|
colorTextBase: '#000000',
|
||||||
|
colorBorder: '#d9d9d9',
|
||||||
|
colorBgContainer: '#ffffff',
|
||||||
|
colorBgElevated: '#ffffff',
|
||||||
|
colorBgLayout: '#ffffff',
|
||||||
|
colorText: '#000000',
|
||||||
|
colorTextSecondary: '#666666',
|
||||||
|
colorTextTertiary: '#999999',
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Nav />
|
||||||
|
<App />
|
||||||
|
</XProvider>;
|
||||||
|
}
|
||||||
80
src/apps/nav/index.tsx
Normal file
80
src/apps/nav/index.tsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useUserStore } from "./store.ts";
|
||||||
|
import { useShallow } from 'zustand/shallow';
|
||||||
|
import '@kevisual/kv-login';
|
||||||
|
import { useContextKey } from "@kevisual/context";
|
||||||
|
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "@/components/ui/dialog";
|
||||||
|
|
||||||
|
export const LoginComponent = ({ onLoginSuccess }: { onLoginSuccess: () => void }) => {
|
||||||
|
useEffect(() => {
|
||||||
|
// 监听登录成功事件
|
||||||
|
const handleLoginSuccess = () => {
|
||||||
|
console.log('监听到登录成功事件,关闭弹窗');
|
||||||
|
onLoginSuccess();
|
||||||
|
};
|
||||||
|
const loginEmitter = useContextKey('login-emitter')
|
||||||
|
console.log('KvLogin Types:', loginEmitter);
|
||||||
|
|
||||||
|
loginEmitter.on('login-success', handleLoginSuccess);
|
||||||
|
|
||||||
|
// 清理监听器
|
||||||
|
return () => {
|
||||||
|
loginEmitter.off('login-success', handleLoginSuccess);
|
||||||
|
};
|
||||||
|
}, [onLoginSuccess]);
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
return (<kv-login><div id="weixinLogin"></div></kv-login>)
|
||||||
|
}
|
||||||
|
export const Nav = () => {
|
||||||
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
|
|
||||||
|
const store = useUserStore(useShallow((state) => ({
|
||||||
|
user: state.user,
|
||||||
|
setUser: state.setUser,
|
||||||
|
clearUser: state.clearUser,
|
||||||
|
queryUser: state.queryUser
|
||||||
|
})));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
store.queryUser();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleLoginSuccess = () => {
|
||||||
|
// 关闭弹窗
|
||||||
|
setIsDialogOpen(false);
|
||||||
|
// 重新查询用户信息
|
||||||
|
};
|
||||||
|
return <header>
|
||||||
|
<nav className="bg-black p-4 text-white flex justify-between">
|
||||||
|
<div className="text-lg font-bold">人生可视化助手</div>
|
||||||
|
<div>
|
||||||
|
{store.user ? (
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
{store.user.avatar && <img src={store.user.avatar} alt="Avatar" className="w-8 h-8 rounded-full" />}
|
||||||
|
<span>{store.user.username}</span>
|
||||||
|
<button
|
||||||
|
onClick={() => store.clearUser()}
|
||||||
|
className="bg-gray-700 text-white px-3 py-1 rounded hover:bg-gray-600 transition-colors"
|
||||||
|
>
|
||||||
|
退出
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<button className="bg-gray-700 text-white px-3 py-1 rounded hover:bg-gray-600 transition-colors">
|
||||||
|
登录
|
||||||
|
</button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader className="text-black text-xl font-bold border-b border-black pb-3">登录</DialogHeader>
|
||||||
|
<LoginComponent onLoginSuccess={handleLoginSuccess} />
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
}
|
||||||
33
src/apps/nav/store.ts
Normal file
33
src/apps/nav/store.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { queryLogin } from '@/modules/query';
|
||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
interface UserState {
|
||||||
|
user: {
|
||||||
|
avatar?: string;
|
||||||
|
description?: string;
|
||||||
|
id?: string;
|
||||||
|
needChangePassword?: boolean;
|
||||||
|
orgs?: string[];
|
||||||
|
type?: string;
|
||||||
|
username?: string;
|
||||||
|
} | null;
|
||||||
|
setUser: (user: UserState['user']) => void;
|
||||||
|
clearUser: () => void;
|
||||||
|
queryUser: () => void;
|
||||||
|
queryMe: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUserStore = create<UserState>((set) => ({
|
||||||
|
user: null,
|
||||||
|
setUser: (user) => set({ user }),
|
||||||
|
clearUser: () => set({ user: null }),
|
||||||
|
queryUser: async () => {
|
||||||
|
const user = await queryLogin.checkLocalUser();
|
||||||
|
set({ user });
|
||||||
|
},
|
||||||
|
queryMe: async () => {
|
||||||
|
const user = await queryLogin.getMe();
|
||||||
|
set({ user });
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import { app } from '../ai';
|
import { app } from '../ai';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { local } from '@/modules/query';
|
import { local } from '@/modules/query';
|
||||||
import '@kevisual/kv-login'
|
import { AuthProvider } from '../auth';
|
||||||
|
|
||||||
const getAppRoutes = () => {
|
const getAppRoutes = () => {
|
||||||
const appRoutes = app.routes.map((route) => {
|
const appRoutes = app.routes.map((route) => {
|
||||||
return {
|
return {
|
||||||
@@ -14,12 +15,6 @@ const getAppRoutes = () => {
|
|||||||
return appRoutes;
|
return appRoutes;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchTest = async () => {
|
|
||||||
const url = 'http://localhost:51015/api/router';
|
|
||||||
const response = await fetch(url);
|
|
||||||
const data = await response.json();
|
|
||||||
console.log('Fetch Test Data:', data);
|
|
||||||
}
|
|
||||||
const fetchLocal = async () => {
|
const fetchLocal = async () => {
|
||||||
const res = await local.post({
|
const res = await local.post({
|
||||||
path: 'client',
|
path: 'client',
|
||||||
@@ -34,6 +29,7 @@ const dynamicImport = async () => {
|
|||||||
console.log('Test Function Output:', module.test());
|
console.log('Test Function Output:', module.test());
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const App = () => {
|
export const App = () => {
|
||||||
const [appRoutes, setAppRoutes] = useState(getAppRoutes());
|
const [appRoutes, setAppRoutes] = useState(getAppRoutes());
|
||||||
|
|
||||||
@@ -53,18 +49,12 @@ export const App = () => {
|
|||||||
setAppRoutes(getAppRoutes());
|
setAppRoutes(getAppRoutes());
|
||||||
}
|
}
|
||||||
}>{JSON.stringify(appRoutes, null, 2)}</pre>
|
}>{JSON.stringify(appRoutes, null, 2)}</pre>
|
||||||
|
|
||||||
<kv-login>
|
|
||||||
<div id="weixinLogin"></div>
|
|
||||||
</kv-login>
|
|
||||||
</div >;
|
</div >;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add custom element to JSX namespace for TypeScript
|
|
||||||
declare global {
|
export const AppProvider = () => {
|
||||||
namespace JSX {
|
return <AuthProvider>
|
||||||
interface IntrinsicElements {
|
<App />
|
||||||
'kv-login': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
|
</AuthProvider>
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
145
src/components/ui/dialog.tsx
Normal file
145
src/components/ui/dialog.tsx
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||||
|
import { XIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { VisuallyHidden } from "./visually-hidden"
|
||||||
|
|
||||||
|
function Dialog({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||||
|
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||||
|
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||||
|
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogClose({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||||
|
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogOverlay({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||||
|
return (
|
||||||
|
<DialogPrimitive.Overlay
|
||||||
|
data-slot="dialog-overlay"
|
||||||
|
className={cn(
|
||||||
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
showCloseButton = true,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||||
|
showCloseButton?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DialogPortal data-slot="dialog-portal">
|
||||||
|
<DialogOverlay />
|
||||||
|
<DialogPrimitive.Content
|
||||||
|
data-slot="dialog-content"
|
||||||
|
className={cn(
|
||||||
|
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<VisuallyHidden>
|
||||||
|
<DialogPrimitive.Title>Dialog</DialogPrimitive.Title>
|
||||||
|
</VisuallyHidden>
|
||||||
|
{children}
|
||||||
|
{showCloseButton && (
|
||||||
|
<DialogPrimitive.Close
|
||||||
|
data-slot="dialog-close"
|
||||||
|
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||||
|
>
|
||||||
|
<XIcon />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
)}
|
||||||
|
</DialogPrimitive.Content>
|
||||||
|
</DialogPortal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="dialog-header"
|
||||||
|
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="dialog-footer"
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogTitle({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||||
|
return (
|
||||||
|
<DialogPrimitive.Title
|
||||||
|
data-slot="dialog-title"
|
||||||
|
className={cn("text-lg leading-none font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||||
|
return (
|
||||||
|
<DialogPrimitive.Description
|
||||||
|
data-slot="dialog-description"
|
||||||
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogOverlay,
|
||||||
|
DialogPortal,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
}
|
||||||
168
src/components/ui/navigation-menu.tsx
Normal file
168
src/components/ui/navigation-menu.tsx
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
|
||||||
|
import { cva } from "class-variance-authority"
|
||||||
|
import { ChevronDownIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function NavigationMenu({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
viewport = true,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {
|
||||||
|
viewport?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<NavigationMenuPrimitive.Root
|
||||||
|
data-slot="navigation-menu"
|
||||||
|
data-viewport={viewport}
|
||||||
|
className={cn(
|
||||||
|
"group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{viewport && <NavigationMenuViewport />}
|
||||||
|
</NavigationMenuPrimitive.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavigationMenuList({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.List>) {
|
||||||
|
return (
|
||||||
|
<NavigationMenuPrimitive.List
|
||||||
|
data-slot="navigation-menu-list"
|
||||||
|
className={cn(
|
||||||
|
"group flex flex-1 list-none items-center justify-center gap-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavigationMenuItem({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<NavigationMenuPrimitive.Item
|
||||||
|
data-slot="navigation-menu-item"
|
||||||
|
className={cn("relative", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const navigationMenuTriggerStyle = cva(
|
||||||
|
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1"
|
||||||
|
)
|
||||||
|
|
||||||
|
function NavigationMenuTrigger({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<NavigationMenuPrimitive.Trigger
|
||||||
|
data-slot="navigation-menu-trigger"
|
||||||
|
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}{" "}
|
||||||
|
<ChevronDownIcon
|
||||||
|
className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</NavigationMenuPrimitive.Trigger>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavigationMenuContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<NavigationMenuPrimitive.Content
|
||||||
|
data-slot="navigation-menu-content"
|
||||||
|
className={cn(
|
||||||
|
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto",
|
||||||
|
"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavigationMenuViewport({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"absolute top-full left-0 isolate z-50 flex justify-center"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<NavigationMenuPrimitive.Viewport
|
||||||
|
data-slot="navigation-menu-viewport"
|
||||||
|
className={cn(
|
||||||
|
"origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavigationMenuLink({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) {
|
||||||
|
return (
|
||||||
|
<NavigationMenuPrimitive.Link
|
||||||
|
data-slot="navigation-menu-link"
|
||||||
|
className={cn(
|
||||||
|
"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavigationMenuIndicator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) {
|
||||||
|
return (
|
||||||
|
<NavigationMenuPrimitive.Indicator
|
||||||
|
data-slot="navigation-menu-indicator"
|
||||||
|
className={cn(
|
||||||
|
"data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" />
|
||||||
|
</NavigationMenuPrimitive.Indicator>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
NavigationMenu,
|
||||||
|
NavigationMenuList,
|
||||||
|
NavigationMenuItem,
|
||||||
|
NavigationMenuContent,
|
||||||
|
NavigationMenuTrigger,
|
||||||
|
NavigationMenuLink,
|
||||||
|
NavigationMenuIndicator,
|
||||||
|
NavigationMenuViewport,
|
||||||
|
navigationMenuTriggerStyle,
|
||||||
|
}
|
||||||
18
src/components/ui/textarea.tsx
Normal file
18
src/components/ui/textarea.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
data-slot="textarea"
|
||||||
|
className={cn(
|
||||||
|
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Textarea }
|
||||||
16
src/components/ui/visually-hidden.tsx
Normal file
16
src/components/ui/visually-hidden.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function VisuallyHidden({ className, ...props }: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"absolute h-px w-px p-0 -m-px overflow-hidden whitespace-nowrap border-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { VisuallyHidden }
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
import { Query } from "@kevisual/query";
|
import { Query } from "@kevisual/query";
|
||||||
|
import { QueryLoginBrowser } from '@kevisual/query-login';
|
||||||
|
|
||||||
export const query = new Query();
|
export const query = new Query();
|
||||||
|
|
||||||
|
export const queryLogin = new QueryLoginBrowser({
|
||||||
|
query
|
||||||
|
})
|
||||||
export const local = new Query({
|
export const local = new Query({
|
||||||
url: '/client/router'
|
url: '/client/router'
|
||||||
});
|
});
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
---
|
---
|
||||||
import Html from '@/components/html.astro';
|
import Html from '@/components/html.astro';
|
||||||
import { App } from '@/apps/web-command';
|
import { AppProvider } from '@/apps/home';
|
||||||
---
|
---
|
||||||
|
|
||||||
<Html>
|
<Html title='可视化平台'>
|
||||||
<main>
|
<main>
|
||||||
<App client:only/>
|
<AppProvider client:only />
|
||||||
</main>
|
</main>
|
||||||
</Html>
|
</Html>
|
||||||
|
|||||||
Reference in New Issue
Block a user