generated from template/astro-template
UPDATE
This commit is contained in:
parent
33f70e0564
commit
55c8f877cd
32
apps/create-audio/index.html
Normal file
32
apps/create-audio/index.html
Normal 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/create-audio/package.json
Normal file
16
apps/create-audio/package.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "create-audio",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"pub": "ev deploy ./dist -k create-audio -v 0.0.1 -u "
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
|
||||
"license": "MIT",
|
||||
"packageManager": "pnpm@10.11.1",
|
||||
"type": "module"
|
||||
}
|
350
apps/create-audio/src/App.tsx
Normal file
350
apps/create-audio/src/App.tsx
Normal file
@ -0,0 +1,350 @@
|
||||
import { ToastContainer, toast } from 'react-toastify';
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { query } from '@/modules/query';
|
||||
import { nanoid } from 'nanoid';
|
||||
export const postCreateVideos = async ({ text }) => {
|
||||
return await query.post({
|
||||
path: 'aliyun-ai',
|
||||
key: 'createVideos',
|
||||
payload: {
|
||||
text,
|
||||
model: '',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const App = () => {
|
||||
return (
|
||||
<>
|
||||
<CreateVideos />
|
||||
<ToastContainer />
|
||||
</>
|
||||
);
|
||||
};
|
||||
const initGetText = () => {
|
||||
let text = sessionStorage.getItem('createVideosText');
|
||||
if (text) {
|
||||
sessionStorage.removeItem('createVideosText');
|
||||
return text;
|
||||
}
|
||||
text = localStorage.getItem('createVideosText');
|
||||
if (text) {
|
||||
localStorage.removeItem('createVideosText');
|
||||
return text;
|
||||
}
|
||||
const url = new URL(window.location.href);
|
||||
text = url.searchParams.get('text');
|
||||
if (text) {
|
||||
text = decodeURIComponent(text);
|
||||
return text;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const downloadAudio = (url: string, filename?: string) => {
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
const generatedFilename = filename || nanoid(10) + '.wav'; // Generate a random filename
|
||||
link.download = generatedFilename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
export const CreateVideos = () => {
|
||||
const [url, setUrl] = useState<string>('');
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [text, setText] = useState<string>('');
|
||||
const [isPlaying, setIsPlaying] = useState<boolean>(false);
|
||||
const audioRef = useRef<HTMLAudioElement>(null);
|
||||
useEffect(() => {
|
||||
const initialText = initGetText();
|
||||
if (initialText) {
|
||||
setText(initialText);
|
||||
}
|
||||
}, []);
|
||||
const handleGenerateAudio = async () => {
|
||||
if (!text.trim()) {
|
||||
toast.error('请输入要转换的文字内容');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await postCreateVideos({
|
||||
text: text.trim(),
|
||||
});
|
||||
|
||||
if (response?.data?.audioUrl) {
|
||||
setUrl(response.data.audioUrl);
|
||||
toast.success('音频生成成功!');
|
||||
} else {
|
||||
toast.error('音频生成失败,请稍后重试');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('生成音频时出错:', error);
|
||||
toast.error('生成音频时出错,请稍后重试');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePlay = () => {
|
||||
if (audioRef.current) {
|
||||
if (isPlaying) {
|
||||
audioRef.current.pause();
|
||||
setIsPlaying(false);
|
||||
} else {
|
||||
audioRef.current.play();
|
||||
setIsPlaying(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleStop = () => {
|
||||
if (audioRef.current) {
|
||||
audioRef.current.pause();
|
||||
audioRef.current.currentTime = 0;
|
||||
setIsPlaying(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAudioEnded = () => {
|
||||
setIsPlaying(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
maxWidth: '800px',
|
||||
margin: '0 auto',
|
||||
padding: '40px 20px',
|
||||
fontFamily: 'Arial, sans-serif',
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
borderRadius: '16px',
|
||||
padding: '40px',
|
||||
color: 'white',
|
||||
marginBottom: '30px',
|
||||
}}>
|
||||
<h1
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
margin: '0 0 20px 0',
|
||||
fontSize: '2.5rem',
|
||||
fontWeight: 'bold',
|
||||
}}>
|
||||
AI 文字转音频
|
||||
</h1>
|
||||
<p
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
margin: '0',
|
||||
fontSize: '1.1rem',
|
||||
opacity: '0.9',
|
||||
}}>
|
||||
输入文字内容,即可生成高质量的AI语音
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
background: 'white',
|
||||
borderRadius: '12px',
|
||||
padding: '30px',
|
||||
boxShadow: '0 4px 20px rgba(0,0,0,0.1)',
|
||||
border: '1px solid #e5e7eb',
|
||||
}}>
|
||||
<div style={{ marginBottom: '25px' }}>
|
||||
<label
|
||||
style={{
|
||||
display: 'block',
|
||||
marginBottom: '10px',
|
||||
fontSize: '16px',
|
||||
fontWeight: '600',
|
||||
color: '#374151',
|
||||
}}>
|
||||
输入文字内容:
|
||||
</label>
|
||||
<textarea
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
placeholder='请输入要转换为语音的文字内容...'
|
||||
rows={6}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px 16px',
|
||||
border: '2px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
fontSize: '16px',
|
||||
resize: 'vertical',
|
||||
outline: 'none',
|
||||
transition: 'border-color 0.2s ease',
|
||||
fontFamily: 'inherit',
|
||||
}}
|
||||
onFocus={(e) => (e.target.style.borderColor = '#667eea')}
|
||||
onBlur={(e) => (e.target.style.borderColor = '#e5e7eb')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '25px' }}>
|
||||
<button
|
||||
onClick={handleGenerateAudio}
|
||||
disabled={loading || !text.trim()}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '14px 28px',
|
||||
background: loading ? '#9ca3af' : 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
fontSize: '16px',
|
||||
fontWeight: '600',
|
||||
cursor: loading ? 'not-allowed' : 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
transform: loading ? 'none' : 'translateY(0)',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!loading) {
|
||||
(e.target as HTMLButtonElement).style.transform = 'translateY(-2px)';
|
||||
(e.target as HTMLButtonElement).style.boxShadow = '0 8px 25px rgba(102, 126, 234, 0.4)';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!loading) {
|
||||
(e.target as HTMLButtonElement).style.transform = 'translateY(0)';
|
||||
(e.target as HTMLButtonElement).style.boxShadow = 'none';
|
||||
}
|
||||
}}>
|
||||
{loading ? '生成中...' : '生成音频'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{url && (
|
||||
<div
|
||||
style={{
|
||||
background: '#f8fafc',
|
||||
border: '1px solid #e2e8f0',
|
||||
borderRadius: '8px',
|
||||
padding: '20px',
|
||||
}}>
|
||||
<h3
|
||||
style={{
|
||||
margin: '0 0 15px 0',
|
||||
fontSize: '18px',
|
||||
color: '#374151',
|
||||
}}>
|
||||
生成的音频:
|
||||
</h3>
|
||||
|
||||
<div
|
||||
style={{
|
||||
background: 'white',
|
||||
borderRadius: '8px',
|
||||
padding: '15px',
|
||||
marginBottom: '15px',
|
||||
border: '1px solid #e5e7eb',
|
||||
}}>
|
||||
<p
|
||||
style={{
|
||||
margin: '0 0 10px 0',
|
||||
fontSize: '14px',
|
||||
color: '#6b7280',
|
||||
wordBreak: 'break-all',
|
||||
}}
|
||||
onClick={() => {
|
||||
// copy
|
||||
navigator.clipboard.writeText(url);
|
||||
toast.success('音频链接已复制到剪贴板');
|
||||
}}>
|
||||
音频链接:{url}
|
||||
</p>
|
||||
<audio ref={audioRef} src={url} onEnded={handleAudioEnded} style={{ display: 'none' }} />
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: '12px',
|
||||
justifyContent: 'center',
|
||||
}}>
|
||||
<button
|
||||
onClick={handlePlay}
|
||||
style={{
|
||||
padding: '10px 20px',
|
||||
background: isPlaying ? '#ef4444' : '#10b981',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
minWidth: '80px',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
(e.target as HTMLButtonElement).style.opacity = '0.9';
|
||||
(e.target as HTMLButtonElement).style.transform = 'translateY(-1px)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
(e.target as HTMLButtonElement).style.opacity = '1';
|
||||
(e.target as HTMLButtonElement).style.transform = 'translateY(0)';
|
||||
}}>
|
||||
{isPlaying ? '暂停' : '播放'}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleStop}
|
||||
style={{
|
||||
padding: '10px 20px',
|
||||
background: '#6b7280',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
minWidth: '80px',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
(e.target as HTMLButtonElement).style.opacity = '0.9';
|
||||
(e.target as HTMLButtonElement).style.transform = 'translateY(-1px)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
(e.target as HTMLButtonElement).style.opacity = '1';
|
||||
(e.target as HTMLButtonElement).style.transform = 'translateY(0)';
|
||||
}}>
|
||||
停止
|
||||
</button>
|
||||
<button
|
||||
onClick={() => downloadAudio(url)}
|
||||
style={{
|
||||
padding: '10px 20px',
|
||||
background: '#3b82f6',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
minWidth: '80px',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
(e.target as HTMLButtonElement).style.opacity = '0.9';
|
||||
(e.target as HTMLButtonElement).style.transform = 'translateY(-1px)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
(e.target as HTMLButtonElement).style.opacity = '1';
|
||||
(e.target as HTMLButtonElement).style.transform = 'translateY(0)';
|
||||
}}>
|
||||
下载音频
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
9
apps/create-audio/src/main.tsx
Normal file
9
apps/create-audio/src/main.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { App } from './App.tsx';
|
||||
import '@/styles/global.css';
|
||||
import '@/styles/theme.css';
|
||||
|
||||
document.title = 'Create Audio - AI Pages';
|
||||
const container = document.getElementById('root');
|
||||
const root = ReactDOM.createRoot(container!);
|
||||
root.render(<App />);
|
15
apps/create-audio/tsconfig.json
Normal file
15
apps/create-audio/tsconfig.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"extends": "@kevisual/types/json/frontend.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"../../src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"../../src/**/*"
|
||||
]
|
||||
}
|
41
apps/create-audio/vite.config.js
Normal file
41
apps/create-audio/vite.config.js
Normal file
@ -0,0 +1,41 @@
|
||||
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';
|
||||
console.log('API Target:', target);
|
||||
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: 7009,
|
||||
host: '0.0.0.0',
|
||||
allowedHosts: true,
|
||||
proxy,
|
||||
},
|
||||
};
|
||||
});
|
32
apps/echo/index.html
Normal file
32
apps/echo/index.html
Normal 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
16
apps/echo/package.json
Normal 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
26
apps/echo/src/App.tsx
Normal 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
9
apps/echo/src/main.tsx
Normal 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 />);
|
116
apps/echo/src/modules/EchoCardList.tsx
Normal file
116
apps/echo/src/modules/EchoCardList.tsx
Normal 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>
|
||||
);
|
||||
};
|
53
apps/echo/src/modules/EchoContentList.tsx
Normal file
53
apps/echo/src/modules/EchoContentList.tsx
Normal 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>;
|
||||
};
|
8
apps/echo/src/modules/EchoSearch.tsx
Normal file
8
apps/echo/src/modules/EchoSearch.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
export const EchoSearch = () => {
|
||||
return (
|
||||
<div>
|
||||
<h2>Echo Search</h2>
|
||||
<p>This is the Echo Search.</p>
|
||||
</div>
|
||||
);
|
||||
};
|
70
apps/echo/src/store/echo-store.ts
Normal file
70
apps/echo/src/store/echo-store.ts
Normal 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
40
apps/echo/src/styles.css
Normal 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
15
apps/echo/tsconfig.json
Normal 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
40
apps/echo/vite.config.js
Normal 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,
|
||||
},
|
||||
};
|
||||
});
|
@ -80,6 +80,7 @@
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@types/sortablejs": "^1.15.8",
|
||||
"@vitejs/plugin-basic-ssl": "^2.0.0",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"commander": "^14.0.0",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dotenv": "^16.5.0",
|
||||
@ -88,6 +89,7 @@
|
||||
"path-browserify-esm": "^1.0.6",
|
||||
"tailwindcss": "^4.1.8",
|
||||
"tw-animate-css": "^1.3.4",
|
||||
"vite": "^6.3.5",
|
||||
"vite-plugin-remote-assets": "^2.0.0"
|
||||
},
|
||||
"packageManager": "pnpm@10.11.1"
|
||||
|
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@ -183,6 +183,9 @@ importers:
|
||||
'@vitejs/plugin-basic-ssl':
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.51.0))
|
||||
'@vitejs/plugin-react':
|
||||
specifier: ^4.4.1
|
||||
version: 4.4.1(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.51.0))
|
||||
commander:
|
||||
specifier: ^14.0.0
|
||||
version: 14.0.0
|
||||
@ -207,6 +210,9 @@ importers:
|
||||
tw-animate-css:
|
||||
specifier: ^1.3.4
|
||||
version: 1.3.4
|
||||
vite:
|
||||
specifier: ^6.3.5
|
||||
version: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.51.0)
|
||||
vite-plugin-remote-assets:
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.51.0))
|
||||
|
@ -41,7 +41,7 @@ export const DragModal = (props: DragModalProps) => {
|
||||
y: 0,
|
||||
}}>
|
||||
<div
|
||||
className={clsxMerge('absolute top-0 left-0 bg-white rounded-md border border-gray-200 shadow-sm', props.focus ? 'z-30' : '', props.containerClassName)}
|
||||
className={clsxMerge('absolute top-0 left-0 bg-white rounded-md border border-gray-200 shadow-sm pointer-events-auto', props.focus ? 'z-30' : '', props.containerClassName)}
|
||||
ref={dragRef}
|
||||
style={props.style}>
|
||||
<div className={clsxMerge('handle cursor-move border-b border-gray-200 py-2 px-4', props.handleClassName)}>{props.title || 'Move'}</div>
|
||||
|
@ -1,5 +1,9 @@
|
||||
@import 'tailwindcss';
|
||||
@import "tw-animate-css";
|
||||
|
||||
@source "../../src/**/*.{js,ts,jsx,tsx}";
|
||||
@source "../../../apps/echo/src/**/*.{js,ts,jsx,tsx}";
|
||||
@source "../../../packages/components/**/*.{js,ts,jsx,tsx}";
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
|
Loading…
x
Reference in New Issue
Block a user