This commit is contained in:
@@ -8,35 +8,39 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-deploy:
|
build-and-deploy:
|
||||||
runs-on: ubuntu
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v3
|
uses: https://gitea.com/actions/checkout@v3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v2
|
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v2
|
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v4
|
run: |
|
||||||
with:
|
docker build -t ${{ secrets.DOCKER_USERNAME }}/wx-pyq:latest .
|
||||||
context: .
|
docker push ${{ secrets.DOCKER_USERNAME }}/wx-pyq:latest
|
||||||
push: true
|
|
||||||
tags: ${{ secrets.DOCKER_USERNAME }}/wx-pyq:latest
|
|
||||||
|
|
||||||
- name: Deploy to Server
|
- name: Deploy to Server
|
||||||
uses: appleboy/ssh-action@master
|
run: |
|
||||||
with:
|
# Install SSH client
|
||||||
host: ${{ secrets.SERVER_HOST }}
|
if command -v apk > /dev/null; then
|
||||||
username: ${{ secrets.SERVER_USERNAME }}
|
sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
|
||||||
key: ${{ secrets.SERVER_KEY }}
|
apk add --no-cache openssh-client
|
||||||
script: |
|
elif command -v apt-get > /dev/null; then
|
||||||
docker pull ${{ secrets.DOCKER_USERNAME }}/wx-pyq:latest
|
sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list
|
||||||
docker stop wx-pyq || true
|
apt-get update && apt-get install -y openssh-client
|
||||||
docker rm wx-pyq || true
|
fi
|
||||||
docker run -d --name wx-pyq -p 80:80 ${{ secrets.DOCKER_USERNAME }}/wx-pyq:latest
|
|
||||||
|
# Setup SSH key
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
echo "${{ secrets.SERVER_KEY }}" > ~/.ssh/id_rsa
|
||||||
|
chmod 600 ~/.ssh/id_rsa
|
||||||
|
|
||||||
|
# Deploy
|
||||||
|
ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa ${{ secrets.SERVER_USERNAME }}@${{ secrets.SERVER_HOST }} "
|
||||||
|
docker pull ${{ secrets.DOCKER_USERNAME }}/wx-pyq:latest && \
|
||||||
|
(docker stop wx-pyq || true) && \
|
||||||
|
(docker rm wx-pyq || true) && \
|
||||||
|
docker run -d --name wx-pyq -p 3321:80 ${{ secrets.DOCKER_USERNAME }}/wx-pyq:latest
|
||||||
|
"
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ FROM node:18-alpine as builder
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN npm config set registry https://registry.npmmirror.com
|
||||||
|
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
RUN npm ci
|
RUN npm ci
|
||||||
|
|
||||||
|
|||||||
63
src/components/home/CopyCard.tsx
Normal file
63
src/components/home/CopyCard.tsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Button, Toast } from 'antd-mobile';
|
||||||
|
import { Copy, Quote, Sparkles } from 'lucide-react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
interface CopyCardProps {
|
||||||
|
result: string;
|
||||||
|
loading: boolean;
|
||||||
|
scenario?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分离出的文案显示组件
|
||||||
|
* 负责文案的渲染、流式动画效果以及复制功能
|
||||||
|
*/
|
||||||
|
export const CopyCard: React.FC<CopyCardProps> = ({ result, loading, scenario }) => {
|
||||||
|
const handleCopy = () => {
|
||||||
|
if (!result) return;
|
||||||
|
navigator.clipboard.writeText(result).then(() => {
|
||||||
|
Toast.show({ content: '已复制文案', icon: 'success' });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="bg-white dark:bg-slate-900 rounded-3xl p-6 md:p-8 shadow-xl border border-slate-100 dark:border-slate-800 relative mb-6"
|
||||||
|
>
|
||||||
|
{scenario && (
|
||||||
|
<div className="absolute -top-3 left-6 z-10">
|
||||||
|
<span className="bg-indigo-500 text-white text-xs font-bold px-3 py-1 rounded-full shadow-lg flex items-center gap-1">
|
||||||
|
<Sparkles size={12} />
|
||||||
|
{scenario}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Quote className="w-10 h-10 text-indigo-100 dark:text-indigo-900/30 mb-4 -scale-x-100" />
|
||||||
|
|
||||||
|
<div className="text-lg md:text-xl leading-relaxed text-slate-700 dark:text-slate-200 whitespace-pre-wrap font-sans min-h-[120px]">
|
||||||
|
{result}
|
||||||
|
{loading && (
|
||||||
|
<span className="inline-block w-2 h-6 ml-1 animate-pulse bg-indigo-500 align-middle" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 flex justify-end border-t border-slate-50 dark:border-slate-800 pt-4">
|
||||||
|
<Button
|
||||||
|
size="middle"
|
||||||
|
color="primary"
|
||||||
|
fill="solid"
|
||||||
|
onClick={handleCopy}
|
||||||
|
disabled={loading || !result}
|
||||||
|
className="rounded-xl px-6 bg-indigo-600 hover:bg-indigo-700 border-none flex items-center active:scale-95 transition-transform"
|
||||||
|
>
|
||||||
|
<Copy size={18} className="mr-2" />
|
||||||
|
复制文案
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
46
src/components/home/NineGrid.tsx
Normal file
46
src/components/home/NineGrid.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
interface NineGridProps {
|
||||||
|
images: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 专门用于 9 宫格布局的显示组件
|
||||||
|
*/
|
||||||
|
export const NineGrid: React.FC<NineGridProps> = ({ images }) => {
|
||||||
|
// 如果没有图片,不显示任何内容
|
||||||
|
if (!images || images.length === 0) return null;
|
||||||
|
|
||||||
|
// 截取前9张图片
|
||||||
|
const gridItems = images.slice(0, 9);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white/50 dark:bg-slate-900/50 p-4 rounded-3xl border border-slate-100 dark:border-slate-800">
|
||||||
|
<h3 className="text-sm font-bold text-slate-400 mb-3 ml-1 uppercase tracking-wider flex items-center justify-between">
|
||||||
|
<span>图片预览 (九宫格)</span>
|
||||||
|
<span className="text-xs font-normal opacity-70">长按保存图片</span>
|
||||||
|
</h3>
|
||||||
|
<div className="grid grid-cols-3 gap-2 w-full aspect-square max-w-[400px] mx-auto">
|
||||||
|
{gridItems.map((url, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={`${url}-${index}`}
|
||||||
|
initial={{ opacity: 0, scale: 0.8 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
transition={{ delay: index * 0.05 }}
|
||||||
|
className="aspect-square rounded-lg overflow-hidden shadow-sm bg-slate-100 dark:bg-slate-800"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={url}
|
||||||
|
alt={`Grid ${index + 1}`}
|
||||||
|
className="w-full h-full object-cover hover:scale-105 transition-transform duration-500 cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
// Optional: Click to preview or just let user long press
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { Button, Input, Toast, ErrorBlock } from 'antd-mobile';
|
import { Button, Input, Toast, ErrorBlock } from 'antd-mobile';
|
||||||
import { Copy, RefreshCw, Sparkles, Wand2, Quote, UserCircle2, Download, Image as ImageIcon } from 'lucide-react';
|
import { RefreshCw, Sparkles, Wand2, UserCircle2 } from 'lucide-react';
|
||||||
import { useAppStore } from '../store/useAppStore';
|
import { useAppStore } from '../store/useAppStore';
|
||||||
import { generateCopyStream } from '../services/api';
|
import { generateCopyStream } from '../services/api';
|
||||||
import { Header } from '../components/Header';
|
import { Header } from '../components/Header';
|
||||||
import { Layout } from '../components/Layout';
|
import { Layout } from '../components/Layout';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import html2canvas from 'html2canvas';
|
import { CopyCard } from '../components/home/CopyCard';
|
||||||
|
import { NineGrid } from '../components/home/NineGrid';
|
||||||
|
|
||||||
export const Home: React.FC = () => {
|
export const Home: React.FC = () => {
|
||||||
const { templates, preferences, setLastUsedName, posterUrls } = useAppStore();
|
const { templates, preferences, setLastUsedName, posterUrls } = useAppStore();
|
||||||
@@ -16,11 +17,9 @@ export const Home: React.FC = () => {
|
|||||||
const [result, setResult] = useState('');
|
const [result, setResult] = useState('');
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [currentScenario, setCurrentScenario] = useState('');
|
const [currentScenario, setCurrentScenario] = useState('');
|
||||||
const [currentPosterUrl, setCurrentPosterUrl] = useState('');
|
|
||||||
|
|
||||||
// Ref for auto-scrolling to bottom of result
|
// Ref for auto-scrolling to bottom of result
|
||||||
const resultEndRef = useRef<HTMLDivElement>(null);
|
const resultEndRef = useRef<HTMLDivElement>(null);
|
||||||
const posterRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const handleGenerate = async () => {
|
const handleGenerate = async () => {
|
||||||
if (!name.trim()) {
|
if (!name.trim()) {
|
||||||
@@ -33,14 +32,6 @@ export const Home: React.FC = () => {
|
|||||||
setError('');
|
setError('');
|
||||||
setResult('');
|
setResult('');
|
||||||
|
|
||||||
// Randomly select a poster if available
|
|
||||||
if (posterUrls.length > 0) {
|
|
||||||
const randomPoster = posterUrls[Math.floor(Math.random() * posterUrls.length)];
|
|
||||||
setCurrentPosterUrl(randomPoster);
|
|
||||||
} else {
|
|
||||||
setCurrentPosterUrl('');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Filter enabled templates
|
// 1. Filter enabled templates
|
||||||
const enabledTemplates = templates.filter(t => t.isEnabled);
|
const enabledTemplates = templates.filter(t => t.isEnabled);
|
||||||
@@ -73,7 +64,7 @@ export const Home: React.FC = () => {
|
|||||||
},
|
},
|
||||||
(err) => {
|
(err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
setError(err.message || '生成失败,请检查网络或联系管理员');
|
setError((err as Error).message || '生成失败,请检查网络或联系管理员');
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
@@ -88,59 +79,6 @@ export const Home: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyToClipboard = () => {
|
|
||||||
if (!result) return;
|
|
||||||
navigator.clipboard.writeText(result).then(() => {
|
|
||||||
Toast.show({ content: '已复制文案', icon: 'success' });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const copyPosterImage = async () => {
|
|
||||||
if (!posterRef.current || !result) return;
|
|
||||||
try {
|
|
||||||
const canvas = await html2canvas(posterRef.current, {
|
|
||||||
useCORS: true,
|
|
||||||
scale: 2,
|
|
||||||
backgroundColor: null
|
|
||||||
});
|
|
||||||
|
|
||||||
canvas.toBlob(async (blob) => {
|
|
||||||
if (!blob) return;
|
|
||||||
try {
|
|
||||||
// Attempt to write to clipboard
|
|
||||||
const item = new ClipboardItem({ 'image/png': blob });
|
|
||||||
await navigator.clipboard.write([item]);
|
|
||||||
Toast.show({ content: '海报已复制', icon: 'success' });
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Clipboard write failed:', err);
|
|
||||||
Toast.show({ content: '复制失败,请尝试保存', icon: 'fail' });
|
|
||||||
}
|
|
||||||
}, 'image/png');
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Capture failed:', err);
|
|
||||||
Toast.show({ content: '生成图片失败', icon: 'fail' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSaveImage = async () => {
|
|
||||||
if (!posterRef.current || !result) return;
|
|
||||||
try {
|
|
||||||
const canvas = await html2canvas(posterRef.current, {
|
|
||||||
useCORS: true,
|
|
||||||
scale: 2,
|
|
||||||
backgroundColor: null
|
|
||||||
});
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.download = `wx-moments-${Date.now()}.png`;
|
|
||||||
link.href = canvas.toDataURL('image/png');
|
|
||||||
link.click();
|
|
||||||
Toast.show({ content: '海报已保存', icon: 'success' });
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Save failed:', err);
|
|
||||||
Toast.show({ content: '保存失败,请重试', icon: 'fail' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (resultEndRef.current) {
|
if (resultEndRef.current) {
|
||||||
resultEndRef.current.scrollIntoView({ behavior: 'smooth' });
|
resultEndRef.current.scrollIntoView({ behavior: 'smooth' });
|
||||||
@@ -213,19 +151,14 @@ export const Home: React.FC = () => {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Right Section: Result */}
|
{/* Right Section: Result */}
|
||||||
<motion.div
|
<div className="w-full md:w-2/3 min-h-[400px]">
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ delay: 0.1, duration: 0.5, ease: "easeOut" }}
|
|
||||||
className="w-full md:w-2/3 min-h-[400px]"
|
|
||||||
>
|
|
||||||
{error && (
|
{error && (
|
||||||
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }}>
|
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }}>
|
||||||
<ErrorBlock
|
<ErrorBlock
|
||||||
status="default"
|
status="default"
|
||||||
title="生成出错了"
|
title="生成出错了"
|
||||||
description={error}
|
description={error}
|
||||||
className="bg-white/50 dark:bg-slate-900/50 backdrop-blur rounded-2xl p-4 border border-red-200 dark:border-red-900/30"
|
className="bg-white/50 dark:bg-slate-900/50 backdrop-blur rounded-2xl p-4 border border-red-200 dark:border-red-900/30 mb-6"
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
@@ -240,123 +173,40 @@ export const Home: React.FC = () => {
|
|||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
{(result || loading) && (
|
{(result || loading) && (
|
||||||
<motion.div
|
<motion.div
|
||||||
key="result-card"
|
key="result-container"
|
||||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
animate={{ opacity: 1 }}
|
||||||
exit={{ opacity: 0, scale: 0.95, y: -20 }}
|
exit={{ opacity: 0 }}
|
||||||
transition={{ type: "spring", stiffness: 300, damping: 30 }}
|
className="space-y-6"
|
||||||
className="relative"
|
|
||||||
>
|
>
|
||||||
{currentScenario && (
|
{/* 1. Text Card */}
|
||||||
<motion.div
|
<CopyCard
|
||||||
initial={{ opacity: 0, y: -10 }}
|
result={result}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
loading={loading}
|
||||||
className="absolute -top-3 left-6 z-10"
|
scenario={currentScenario}
|
||||||
>
|
/>
|
||||||
<span className="bg-indigo-500 text-white text-xs font-bold px-3 py-1 rounded-full shadow-lg flex items-center gap-1">
|
<div ref={resultEndRef} />
|
||||||
<Sparkles size={12} />
|
|
||||||
{currentScenario}
|
|
||||||
</span>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Poster Container */}
|
|
||||||
<div
|
|
||||||
ref={posterRef}
|
|
||||||
className="relative rounded-3xl overflow-hidden shadow-2xl transition-all aspect-[3/4] md:aspect-[4/5] max-w-[500px] mx-auto bg-white dark:bg-slate-900"
|
|
||||||
>
|
|
||||||
{/* Background Image Layer */}
|
|
||||||
{currentPosterUrl && (
|
|
||||||
<div className="absolute inset-0 z-0">
|
|
||||||
<img
|
|
||||||
src={currentPosterUrl}
|
|
||||||
alt="Poster Background"
|
|
||||||
className="w-full h-full object-cover"
|
|
||||||
crossOrigin="anonymous"
|
|
||||||
/>
|
|
||||||
<div className="absolute inset-0 bg-black/40 backdrop-blur-[1px]" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="relative z-10 h-full flex flex-col p-8 md:p-10">
|
{/* 2. Nine Grid Posters */}
|
||||||
<Quote className={`w-12 h-12 transform -scale-x-100 mb-6 ${currentPosterUrl ? 'text-white/60' : 'text-indigo-100 dark:text-indigo-900/30'}`} />
|
<NineGrid images={posterUrls} />
|
||||||
|
|
||||||
<div className={`flex-1 text-xl md:text-2xl leading-relaxed font-medium whitespace-pre-wrap font-sans overflow-y-auto custom-scrollbar ${currentPosterUrl ? 'text-white drop-shadow-md' : 'text-slate-700 dark:text-slate-200'}`}>
|
|
||||||
{result}
|
|
||||||
{loading && (
|
|
||||||
<span className={`inline-block w-2 h-6 ml-1 animate-pulse align-middle ${currentPosterUrl ? 'bg-white' : 'bg-indigo-500'}`} />
|
|
||||||
)}
|
|
||||||
<div ref={resultEndRef} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={`mt-8 pt-6 border-t ${currentPosterUrl ? 'border-white/20' : 'border-slate-100 dark:border-slate-800'}`}>
|
{/* 3. Global Actions */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex justify-center md:justify-end gap-3 pb-8">
|
||||||
<div className={`text-sm ${currentPosterUrl ? 'text-white/80' : 'text-slate-400'}`}>
|
<Button
|
||||||
Generated by AI
|
size="middle"
|
||||||
</div>
|
fill="none"
|
||||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${currentPosterUrl ? 'bg-white/20' : 'bg-indigo-50'}`}>
|
onClick={handleGenerate}
|
||||||
<Sparkles size={14} className={currentPosterUrl ? 'text-white' : 'text-indigo-500'} />
|
disabled={loading}
|
||||||
</div>
|
className="bg-white/50 dark:bg-slate-800/50 backdrop-blur text-slate-600 dark:text-slate-300 hover:bg-white/80 dark:hover:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700"
|
||||||
</div>
|
>
|
||||||
</div>
|
<RefreshCw size={18} className={`mr-2 ${loading ? 'animate-spin' : ''}`} />
|
||||||
</div>
|
{loading ? '生成中...' : '换个方案'}
|
||||||
</div>
|
</Button>
|
||||||
|
|
||||||
{/* Actions Bar (Outside of Poster Ref) */}
|
|
||||||
<div className="mt-6 flex flex-wrap justify-center md:justify-end gap-3">
|
|
||||||
<Button
|
|
||||||
size="middle"
|
|
||||||
fill="none"
|
|
||||||
onClick={handleGenerate}
|
|
||||||
disabled={loading}
|
|
||||||
className="bg-white/50 dark:bg-slate-800/50 backdrop-blur text-slate-600 dark:text-slate-300 hover:bg-white/80 dark:hover:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700"
|
|
||||||
>
|
|
||||||
<RefreshCw size={18} className={`mr-2 ${loading ? 'animate-spin' : ''}`} />
|
|
||||||
{loading ? '重写' : '换一个'}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
size="middle"
|
|
||||||
color="primary"
|
|
||||||
fill="solid"
|
|
||||||
onClick={copyToClipboard}
|
|
||||||
disabled={loading || !result}
|
|
||||||
className="rounded-xl px-6 bg-indigo-600 hover:bg-indigo-700 border-none shadow-md shadow-indigo-500/20 flex items-center active:scale-95 transition-transform"
|
|
||||||
>
|
|
||||||
<Copy size={18} className="mr-2" />
|
|
||||||
复制文案
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{currentPosterUrl && result && !loading && (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
size="middle"
|
|
||||||
color="warning"
|
|
||||||
fill="solid"
|
|
||||||
onClick={copyPosterImage}
|
|
||||||
className="rounded-xl px-6 bg-amber-500 hover:bg-amber-600 border-none shadow-md shadow-amber-500/20 flex items-center text-white active:scale-95 transition-transform"
|
|
||||||
>
|
|
||||||
<ImageIcon size={18} className="mr-2" />
|
|
||||||
复制海报
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
size="middle"
|
|
||||||
color="success"
|
|
||||||
fill="solid"
|
|
||||||
onClick={handleSaveImage}
|
|
||||||
className="rounded-xl px-6 bg-emerald-500 hover:bg-emerald-600 border-none shadow-md shadow-emerald-500/20 flex items-center text-white active:scale-95 transition-transform"
|
|
||||||
>
|
|
||||||
<Download size={18} className="mr-2" />
|
|
||||||
保存海报
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</motion.div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer className="py-6 text-center text-xs text-slate-400 dark:text-slate-600">
|
<footer className="py-6 text-center text-xs text-slate-400 dark:text-slate-600">
|
||||||
|
|||||||
Reference in New Issue
Block a user