generated from template/astro-template
	UPDATE
This commit is contained in:
		
							
								
								
									
										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/react-dom": "^19.1.6",
 | 
				
			||||||
    "@types/sortablejs": "^1.15.8",
 | 
					    "@types/sortablejs": "^1.15.8",
 | 
				
			||||||
    "@vitejs/plugin-basic-ssl": "^2.0.0",
 | 
					    "@vitejs/plugin-basic-ssl": "^2.0.0",
 | 
				
			||||||
 | 
					    "@vitejs/plugin-react": "^4.4.1",
 | 
				
			||||||
    "commander": "^14.0.0",
 | 
					    "commander": "^14.0.0",
 | 
				
			||||||
    "crypto-js": "^4.2.0",
 | 
					    "crypto-js": "^4.2.0",
 | 
				
			||||||
    "dotenv": "^16.5.0",
 | 
					    "dotenv": "^16.5.0",
 | 
				
			||||||
@@ -88,6 +89,7 @@
 | 
				
			|||||||
    "path-browserify-esm": "^1.0.6",
 | 
					    "path-browserify-esm": "^1.0.6",
 | 
				
			||||||
    "tailwindcss": "^4.1.8",
 | 
					    "tailwindcss": "^4.1.8",
 | 
				
			||||||
    "tw-animate-css": "^1.3.4",
 | 
					    "tw-animate-css": "^1.3.4",
 | 
				
			||||||
 | 
					    "vite": "^6.3.5",
 | 
				
			||||||
    "vite-plugin-remote-assets": "^2.0.0"
 | 
					    "vite-plugin-remote-assets": "^2.0.0"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "packageManager": "pnpm@10.11.1"
 | 
					  "packageManager": "pnpm@10.11.1"
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										6
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							@@ -183,6 +183,9 @@ importers:
 | 
				
			|||||||
      '@vitejs/plugin-basic-ssl':
 | 
					      '@vitejs/plugin-basic-ssl':
 | 
				
			||||||
        specifier: ^2.0.0
 | 
					        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))
 | 
					        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:
 | 
					      commander:
 | 
				
			||||||
        specifier: ^14.0.0
 | 
					        specifier: ^14.0.0
 | 
				
			||||||
        version: 14.0.0
 | 
					        version: 14.0.0
 | 
				
			||||||
@@ -207,6 +210,9 @@ importers:
 | 
				
			|||||||
      tw-animate-css:
 | 
					      tw-animate-css:
 | 
				
			||||||
        specifier: ^1.3.4
 | 
					        specifier: ^1.3.4
 | 
				
			||||||
        version: 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:
 | 
					      vite-plugin-remote-assets:
 | 
				
			||||||
        specifier: ^2.0.0
 | 
					        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))
 | 
					        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,
 | 
					        y: 0,
 | 
				
			||||||
      }}>
 | 
					      }}>
 | 
				
			||||||
      <div
 | 
					      <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}
 | 
					        ref={dragRef}
 | 
				
			||||||
        style={props.style}>
 | 
					        style={props.style}>
 | 
				
			||||||
        <div className={clsxMerge('handle cursor-move border-b border-gray-200 py-2 px-4', props.handleClassName)}>{props.title || 'Move'}</div>
 | 
					        <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 'tailwindcss';
 | 
				
			||||||
@import "tw-animate-css";
 | 
					@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 *));
 | 
					@custom-variant dark (&:is(.dark *));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@theme inline {
 | 
					@theme inline {
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user