This commit is contained in:
2025-07-01 23:28:07 +08:00
parent 33f70e0564
commit 55c8f877cd
21 changed files with 901 additions and 1 deletions

32
apps/echo/index.html Normal file
View File

@@ -0,0 +1,32 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/png" href="https://kevisual.xiongxiao.me/root/center/panda.jpg" />
<title>Echo Text</title>
<style>
html,
body {
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
#root {
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="module" src="./src/main.tsx"></script>
</body>
</html>

16
apps/echo/package.json Normal file
View File

@@ -0,0 +1,16 @@
{
"name": "echo",
"version": "0.0.1",
"description": "",
"main": "index.js",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"pub": "ev deploy ./dist -k echo -v 0.0.1 -u "
},
"keywords": [],
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
"license": "MIT",
"packageManager": "pnpm@10.11.1",
"type": "module"
}

26
apps/echo/src/App.tsx Normal file
View File

@@ -0,0 +1,26 @@
import { useEffect } from 'react';
import { initializeCardList } from './store/echo-store';
import { EchoCardList } from './modules/EchoCardList';
import { EchoContentList } from './modules/EchoContentList';
export const App = () => {
return (
<div className='h-full flex flex-col'>
<div className='w-full max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-4 h-full'>
<Echo />
</div>
<EchoContentList />
</div>
);
};
export const Echo = () => {
useEffect(() => {
initializeCardList(10);
}, []);
return (
<div className='h-full overflow-auto scrollbar'>
<EchoCardList />
</div>
);
};

9
apps/echo/src/main.tsx Normal file
View File

@@ -0,0 +1,9 @@
import ReactDOM from 'react-dom/client';
import { App } from './App';
import '@/styles/global.css';
import '@/styles/theme.css';
document.title = 'Echo - AI Pages';
const container = document.getElementById('root');
const root = ReactDOM.createRoot(container!);
root.render(<App />);

View File

@@ -0,0 +1,116 @@
import { useEchoStore } from '../store/echo-store';
import { useShallow } from 'zustand/shallow';
export const EchoCardList = () => {
const echoStore = useEchoStore(
useShallow((state) => ({
cardList: state.cardList,
addModal: state.addModal,
})),
);
const cardList = echoStore.cardList;
if (!cardList || cardList.length === 0) {
return (
<div className='w-full max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6'>
<div className='flex items-center justify-center py-12 sm:py-16'>
<div className='text-center'>
<div className='text-gray-400 text-base sm:text-lg mb-2'></div>
<div className='text-gray-500 text-sm'></div>
</div>
</div>
</div>
);
}
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'short',
day: 'numeric',
});
};
return (
<div className='echo-card-list w-full max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6'>
<div className='grid gap-4 sm:gap-6 grid-cols-1 sm:grid-cols-2'>
{cardList.map((card) => (
<div
key={card.id}
className='bg-white rounded-lg shadow-md hover:shadow-lg transition-shadow duration-300 border border-gray-200 overflow-hidden group flex flex-col h-full'>
{/* 卡片头部 */}
<div className='p-4 sm:p-6 pb-3 sm:pb-4 flex-grow'>
<div className='flex items-start justify-between mb-3'>
<h3 className='text-base sm:text-lg font-semibold text-gray-900 line-clamp-2 flex-1 mr-2 group-hover:text-blue-600 transition-colors leading-tight'>
{card.title}
</h3>
<div className='text-xs text-gray-500 whitespace-nowrap ml-1'>{formatDate(card.createdAt)}</div>
</div>
{/* 标签 */}
{card.tags && card.tags.length > 0 && (
<div className='flex flex-wrap gap-1.5 sm:gap-2 mb-3 sm:mb-4'>
{card.tags.slice(0, 2).map((tag, index) => (
<span key={index} className='inline-flex items-center px-2 sm:px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800'>
{tag}
</span>
))}
{card.tags.length > 2 && (
<span className='inline-flex items-center px-2 sm:px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-600'>
+{card.tags.length - 2}
</span>
)}
</div>
)}
{/* 卡片内容 */}
<p className='text-gray-600 text-sm leading-relaxed line-clamp-3'>{card.summary}</p>
</div>
{/* 卡片底部 */}
<div className='px-4 sm:px-6 py-3 sm:py-4 bg-gray-50 border-t border-gray-100 mt-auto'>
<div className='flex items-center justify-between'>
<div className='flex items-center space-x-2'>
<div className='w-2 h-2 bg-green-400 rounded-full'></div>
<span className='text-xs text-gray-500'></span>
</div>
{card.link && (
<button
onClick={(e) => {
e.preventDefault();
echoStore.addModal?.(card);
}}
className='inline-flex items-center px-2.5 sm:px-3 py-1 sm:py-1.5 bg-blue-600 text-white text-xs font-medium rounded-md hover:bg-blue-700 transition-colors'>
<span className='hidden sm:inline'></span>
<span className='sm:hidden'></span>
<svg className='ml-1 w-3 h-3' fill='none' stroke='currentColor' viewBox='0 0 24 24'>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth={2}
d='M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14'
/>
</svg>
</button>
)}
</div>
</div>
</div>
))}
</div>
</div>
);
};
export const CardDetail = ({ data }: { data: any }) => {
return (
<div className='p-4 sm:p-6'>
<h2 className='text-lg font-semibold text-gray-900 mb-4'>{data.title}</h2>
<p className='text-gray-600 mb-2'>{data.summary}</p>
<div className='text-xs text-gray-500 mb-4'>{new Date(data.createdAt).toLocaleDateString('zh-CN')}</div>
{data.content && <div className='prose max-w-none' dangerouslySetInnerHTML={{ __html: data.content }} />}
</div>
);
};

View File

@@ -0,0 +1,53 @@
import { DragModal, useDragSize } from '@/components/a/drag-modal/index.tsx';
import { useMemo } from 'react';
import { useEchoStore } from '../store/echo-store';
import { useShallow } from 'zustand/shallow';
import { X } from 'lucide-react';
import { CardDetail } from './EchoCardList';
export const EchoContentList = () => {
const dragSize = useDragSize();
const echoStore = useEchoStore(
useShallow((state) => ({
modalList: state.modalList,
focusModalId: state.focusModalId,
setFoucusModalId: state.setFoucusModalId,
removeModal: state.removeModal,
})),
);
const modal = useMemo(() => {
const modals = (echoStore.modalList || []).map((item) => {
console.log('render modal', item.id, 'focus id', echoStore.focusModalId);
return (
<DragModal
title={
<div className='flex items-center justify-between pr-2'>
{item.title}
<X
className='inline cursor-pointer'
onClick={(e) => {
e.stopPropagation();
echoStore.removeModal?.(item.id);
}}
/>
</div>
}
key={item.id}
containerClassName={echoStore.focusModalId === item.id ? 'z-30' : ''}
onClose={() => {
echoStore.removeModal?.(item.id);
console.log('remove modal', item.id);
}}
content={
<div>
<CardDetail data={item} />
</div>
}
defaultSize={dragSize.defaultSize}
style={dragSize.style}
/>
);
});
return modals;
}, [echoStore.modalList, echoStore.focusModalId]);
return <div className='w-screen h-screen absolute left-0 top-0 z-1 pointer-events-none'>{modal}</div>;
};

View File

@@ -0,0 +1,8 @@
export const EchoSearch = () => {
return (
<div>
<h2>Echo Search</h2>
<p>This is the Echo Search.</p>
</div>
);
};

View File

@@ -0,0 +1,70 @@
import { create } from 'zustand';
export type CardList = {
id: string;
title: string;
summary: string;
createdAt: string;
tags: string[];
link: string;
[key: string]: any;
};
interface EchoStore {
searchQuery: string;
setSearchQuery: (query: string) => void;
cardList?: CardList[];
focusModalId?: string;
setFoucusModalId?: (id: string) => void;
modalList?: CardList[];
addModal?: (card: CardList) => void;
removeModal?: (id: string) => void;
}
export const useEchoStore = create<EchoStore>((set) => ({
searchQuery: '',
setSearchQuery: (query) => set({ searchQuery: query }),
cardList: [],
focusModalId: undefined,
setFoucusModalId: (id) => set({ focusModalId: id }),
modalList: [],
addModal: (card) => {
//如果存在在modalList中则不添加 设置focusModalId为当前card的id
set((state) => {
const exists = state.modalList?.some((item) => item.id === card.id);
if (exists) {
return { focusModalId: card.id };
}
return {
modalList: [...(state.modalList || []), card],
focusModalId: card.id,
};
});
},
removeModal: (id) => {
set((state) => {
const modalList = state.modalList?.filter((item) => item.id !== id);
const focusModalId = modalList?.length ? modalList[0].id : undefined;
return { modalList, focusModalId };
});
},
}));
export const generateCardList = (count: number): CardList[] => {
return Array.from({ length: count }, (_, index) => ({
id: `card-${index + 1}`,
title: `Card Title ${index + 1}`,
summary: `This is a summary for card ${index + 1}.`,
createdAt: new Date().toISOString(),
tags: [`tag${index + 1}`, `tag${index + 2}`],
link: `https://example.com/card-${index + 1}`,
}));
};
export const initializeCardList = (count: number) => {
const cardList = generateCardList(count);
useEchoStore.setState({ cardList });
const two = cardList.slice(0, 2);
// useEchoStore.setState({ modalList: two }); // Initialize with first 2 cards as modals
// useEchoStore.setState({ focusModalId: two[0]?.id }); // Set the first modal as focused
};

40
apps/echo/src/styles.css Normal file
View File

@@ -0,0 +1,40 @@
@import 'tailwindcss';
@source "../../src/**/*.{js,ts,jsx,tsx}";
@source "../../../apps/echo/src/**/*.{js,ts,jsx,tsx}";
@source "../../../packages/components/**/*.{js,ts,jsx,tsx}";
@utility scrollbar {
overflow: auto;
/* 整个滚动条 */
&::-webkit-scrollbar {
width: 3px;
height: 3px;
}
&::-webkit-scrollbar-track {
background-color: var(--color-scrollbar-track);
}
/* 滚动条有滑块的轨道部分 */
&::-webkit-scrollbar-track-piece {
background-color: transparent;
border-radius: 1px;
}
/* 滚动条滑块(竖向:vertical 横向:horizontal) */
&::-webkit-scrollbar-thumb {
cursor: pointer;
background-color: var(--color-scrollbar-thumb);
border-radius: 5px;
}
/* 滚动条滑块hover */
&::-webkit-scrollbar-thumb:hover {
background-color: var(--color-scrollbar-thumb-hover);
}
/* 同时有垂直和水平滚动条时交汇的部分 */
&::-webkit-scrollbar-corner {
display: block; /* 修复交汇时出现的白块 */
}
}

15
apps/echo/tsconfig.json Normal file
View File

@@ -0,0 +1,15 @@
{
"extends": "@kevisual/types/json/frontend.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": [
"../../src/*"
]
}
},
"include": [
"src/**/*",
"../../src/**/*"
]
}

40
apps/echo/vite.config.js Normal file
View File

@@ -0,0 +1,40 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
import tailwindcss from '@tailwindcss/vite';
const plugins = [react(), tailwindcss()];
let target = process.env.VITE_API_URL || 'https://kevisual.xiongxiao.me';
const apiProxy = { target: target, changeOrigin: true, ws: true, rewriteWsOrigin: true, secure: false, cookieDomainRewrite: 'localhost' };
let proxy = {
'/root/': apiProxy,
'/user/login/': apiProxy,
'/api': apiProxy,
'/client': apiProxy,
};
/**
* @see https://vitejs.dev/config/
*/
export default defineConfig(() => {
return {
plugins,
resolve: {
alias: {
'@': path.resolve(__dirname, '../../src'),
},
},
base: './',
define: {
DEV_SERVER: JSON.stringify(process.env.NODE_ENV === 'development'),
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
},
build: {
target: 'modules',
},
server: {
port: 7008,
host: '0.0.0.0',
allowedHosts: true,
proxy,
},
};
});