Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 12 additions & 19 deletions packages/key-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,32 @@
"version": "0.1.0",
"description": "Shared UI components for KeyApp and MiniApps",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"main": "./src/index.ts",
"module": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
"import": "./src/index.ts",
"types": "./src/index.ts"
},
"./address-display": {
"types": "./dist/address-display/index.d.ts",
"import": "./dist/address-display/index.js",
"require": "./dist/address-display/index.cjs"
"import": "./src/address-display/index.ts",
"types": "./src/address-display/index.ts"
},
"./amount-display": {
"types": "./dist/amount-display/index.d.ts",
"import": "./dist/amount-display/index.js",
"require": "./dist/amount-display/index.cjs"
"import": "./src/amount-display/index.ts",
"types": "./src/amount-display/index.ts"
},
"./token-icon": {
"types": "./dist/token-icon/index.d.ts",
"import": "./dist/token-icon/index.js",
"require": "./dist/token-icon/index.cjs"
"import": "./src/token-icon/index.ts",
"types": "./src/token-icon/index.ts"
},
"./styles": "./src/styles/base.css"
},
"files": [
"dist",
"src/styles"
"src"
],
"scripts": {
"build": "vite build",
"dev": "vite build --watch",
"typecheck": "tsc --noEmit",
"typecheck:run": "tsc --noEmit",
"test": "vitest",
Expand Down
18 changes: 8 additions & 10 deletions packages/key-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,21 @@
"version": "0.1.0",
"description": "Utility functions for KeyApp UI components",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"main": "./src/index.ts",
"module": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
"import": "./src/index.ts",
"types": "./src/index.ts"
},
"./*": {
"import": "./dist/*.js",
"require": "./dist/*.cjs",
"types": "./dist/*.d.ts"
"import": "./src/*.ts",
"types": "./src/*.ts"
}
},
"files": [
"dist"
"src"
],
"scripts": {
"build": "vite build",
Expand Down
7 changes: 4 additions & 3 deletions scripts/vite-plugin-miniapps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,12 +305,13 @@ function scanRemoteMiniappsForBuild(miniappsPath: string): Array<MiniappManifest

try {
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8')) as MiniappManifest;
const baseUrl = `./${entry.name}/`;
remoteApps.push({
...manifest,
dirName: entry.name,
url: `./${entry.name}/`,
icon: `./${entry.name}/${manifest.icon}`,
screenshots: manifest.screenshots?.map((s) => `./${entry.name}/${s}`) ?? [],
url: baseUrl,
icon: manifest.icon.startsWith('http') ? manifest.icon : new URL(manifest.icon, baseUrl).href,
screenshots: manifest.screenshots?.map((s) => (s.startsWith('http') ? s : new URL(s, baseUrl).href)) ?? [],
});
} catch {
console.warn(`[miniapps] ${entry.name}: invalid remote manifest.json, skipping`);
Expand Down
162 changes: 84 additions & 78 deletions src/components/common/swiper-sync-context.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,26 @@
/**
* Swiper 同步 Context
*
* 简单存储 Swiper 实例,让组件使用 Controller 模块自动同步
*/

import {
createContext,
useContext,
useRef,
useState,
useCallback,
useEffect,
type ReactNode,
type MutableRefObject,
} from 'react';
import type { Swiper as SwiperType } from 'swiper';

/** Swiper 同步 Context 值 */
interface SwiperMemberState {
swiper: SwiperType;
initialIndex: number;
}

interface SwiperSyncContextValue {
/** Swiper 实例注册表 */
swipersRef: MutableRefObject<Map<string, SwiperType>>;
/** 监听器注册表 */
listenersRef: MutableRefObject<Set<() => void>>;
/** 注册 Swiper */
register: (id: string, swiper: SwiperType) => void;
/** 注销 Swiper */
membersRef: MutableRefObject<Map<string, SwiperMemberState>>;
readyPairsRef: MutableRefObject<Set<string>>;
register: (id: string, state: SwiperMemberState) => void;
unregister: (id: string) => void;
/** 订阅变更 */
subscribe: (listener: () => void) => () => void;
tryConnect: (selfId: string, targetId: string) => void;
isReady: (selfId: string, targetId: string) => boolean;
}

const SwiperSyncContext = createContext<SwiperSyncContextValue | null>(null);
Expand All @@ -36,87 +29,100 @@ interface SwiperSyncProviderProps {
children: ReactNode;
}

/**
* Swiper 同步 Provider
*/
const getPairKey = (id1: string, id2: string) => [id1, id2].sort().join(':');

export function SwiperSyncProvider({ children }: SwiperSyncProviderProps) {
const swipersRef = useRef<Map<string, SwiperType>>(new Map());
const listenersRef = useRef<Set<() => void>>(new Set());
const membersRef = useRef<Map<string, SwiperMemberState>>(new Map());
const readyPairsRef = useRef<Set<string>>(new Set());

const notify = useCallback(() => {
listenersRef.current.forEach(listener => listener());
const register = useCallback((id: string, state: SwiperMemberState) => {
membersRef.current.set(id, state);
}, []);

const register = useCallback((id: string, swiper: SwiperType) => {
swipersRef.current.set(id, swiper);
notify();
}, [notify]);

const unregister = useCallback((id: string) => {
swipersRef.current.delete(id);
notify();
}, [notify]);
const state = membersRef.current.get(id);
if (state?.swiper && !state.swiper.destroyed && state.swiper.controller) {
state.swiper.controller.control = undefined;
}
membersRef.current.delete(id);
readyPairsRef.current.forEach((key) => {
if (key.includes(id)) readyPairsRef.current.delete(key);
});
}, []);

const subscribe = useCallback((listener: () => void) => {
listenersRef.current.add(listener);
return () => listenersRef.current.delete(listener);
const tryConnect = useCallback((selfId: string, targetId: string) => {
const pairKey = getPairKey(selfId, targetId);
if (readyPairsRef.current.has(pairKey)) return;

const self = membersRef.current.get(selfId);
const target = membersRef.current.get(targetId);

if (!self || !target) return;
if (self.swiper.destroyed || target.swiper.destroyed) return;

// 先标记 ready,再执行操作,这样 slideTo 触发的 onSlideChange 能通过 isReady 检查
readyPairsRef.current.add(pairKey);

self.swiper.controller.control = target.swiper;
target.swiper.controller.control = self.swiper;

self.swiper.slideTo(self.initialIndex, 0, false);
target.swiper.slideTo(target.initialIndex, 0, false);
}, []);

const isReady = useCallback((selfId: string, targetId: string) => {
return readyPairsRef.current.has(getPairKey(selfId, targetId));
}, []);

return (
<SwiperSyncContext.Provider value={{ swipersRef, listenersRef, register, unregister, subscribe }}>
<SwiperSyncContext.Provider value={{ membersRef, readyPairsRef, register, unregister, tryConnect, isReady }}>
{children}
</SwiperSyncContext.Provider>
);
}

/**
* 注册 Swiper 并获取要控制的其他 Swiper
*
* 用法:
* ```tsx
* const { onSwiper, controlledSwiper } = useSwiperMember('main', 'indicator');
*
* <Swiper
* modules={[Controller]}
* controller={{ control: controlledSwiper }}
* onSwiper={onSwiper}
* >
* ```
*/
export function useSwiperMember(selfId: string, targetId: string) {
interface UseSwiperMemberOptions {
initialIndex: number;
onSlideChange?: (swiper: SwiperType) => void;
}

export function useSwiperMember(selfId: string, targetId: string, options: UseSwiperMemberOptions) {
const ctx = useContext(SwiperSyncContext);
if (!ctx) {
throw new Error('useSwiperMember must be used within SwiperSyncProvider');
}

const [controlledSwiper, setControlledSwiper] = useState<SwiperType | undefined>(() => {
const target = ctx.swipersRef.current.get(targetId);
return target && !target.destroyed ? target : undefined;
});
const optionsRef = useRef(options);
optionsRef.current = options;

const onSwiper = useCallback((swiper: SwiperType) => {
ctx.register(selfId, swiper);
}, [ctx, selfId]);
const ctxRef = useRef(ctx);
ctxRef.current = ctx;

const selfIdRef = useRef(selfId);
selfIdRef.current = selfId;

const onSwiper = useCallback(
(swiper: SwiperType) => {
ctx.register(selfId, {
swiper,
initialIndex: optionsRef.current.initialIndex,
});
ctx.tryConnect(selfId, targetId);
},
[ctx, selfId, targetId],
);

const onSlideChange = useCallback(
(swiper: SwiperType) => {
if (!ctx.isReady(selfId, targetId)) return;
optionsRef.current.onSlideChange?.(swiper);
},
[ctx, selfId, targetId],
);

// 订阅变更,当目标 Swiper 注册时更新
useEffect(() => {
const updateTarget = () => {
const target = ctx.swipersRef.current.get(targetId);
const validTarget = target && !target.destroyed ? target : undefined;
setControlledSwiper(prev => prev !== validTarget ? validTarget : prev);
};

// 初始检查
updateTarget();

// 订阅变更
return ctx.subscribe(updateTarget);
}, [ctx, targetId]);

return {
onSwiper,
controlledSwiper,
};
}
return () => ctxRef.current.unregister(selfIdRef.current);
}, []);

export type { SwiperSyncContextValue };
return { onSwiper, onSlideChange };
}
Loading