refactor: 页面重构

This commit is contained in:
Blizzard
2026-03-30 16:26:42 +08:00
parent a269f8893f
commit 8132edf6c1
20 changed files with 6018 additions and 774 deletions
+16 -2
View File
@@ -40,8 +40,8 @@ export const updateProgramApi = (data: Partial<RadioProgram>): Promise<void> =>
request.post('/radio/program/update', data);
export const deleteProgramApi = (data: { ids: (string | number)[] }): Promise<void> =>
request.post('/radio/program/delete', data);
export const generateTtsApi = (id: string | number): Promise<void> =>
request.get('/radio/program/generate-tts', { params: { id } });
export const generateTtsApi = (id: string | number, speaker?: string): Promise<void> =>
request.get('/radio/program/generate-tts', { params: { id, speaker } });
// --- VIP API ---
export const getVipConfigDetailApi = (): Promise<Record<string, unknown>> => request.post('/vip/config/detail');
@@ -72,3 +72,17 @@ export const getVipStatsApi = (params: Record<string, unknown> = {}): Promise<an
// --- User API ---
export const getRadioUserListApi = (params: RadioUserListParams): Promise<PageResult<RadioUserItem>> =>
request.get('/radio/user/list', { params });
// --- Voice API ---
export const getVoiceListApi = (data: import('../types/radio').VoiceListParams): Promise<PageResult<import('../types/radio').RadioVoice>> =>
request.post('/radio/voice/list', data);
export const saveVoiceApi = (data: Partial<import('../types/radio').VoiceFormData>): Promise<void> =>
request.post('/radio/voice/save', data);
export const updateVoiceApi = (data: Partial<import('../types/radio').VoiceFormData>): Promise<void> =>
request.post('/radio/voice/update', data);
export const deleteVoiceApi = (data: { ids: (string | number)[] }): Promise<void> =>
request.post('/radio/voice/delete', data);
export const setDefaultVoiceApi = (data: { id: string | number }): Promise<void> =>
request.post('/radio/voice/set-default', data, { params: { id: data.id } });
export const getVoiceOptionsApi = (): Promise<import('../types/radio').RadioVoice[]> =>
request.get('/radio/voice/options');
+51
View File
@@ -0,0 +1,51 @@
import { useEffect, useRef } from 'react';
import { useInView, useMotionValue, useSpring } from 'framer-motion';
export function AnimatedCounter({
value,
format = 'number',
className = ''
}: {
value: number | string;
format?: 'number' | 'currency' | 'percent';
className?: string;
}) {
const ref = useRef<HTMLSpanElement>(null);
const motionValue = useMotionValue(0);
const springValue = useSpring(motionValue, {
stiffness: 50,
damping: 20,
mass: 1,
});
const isInView = useInView(ref, { once: true, margin: '-50px' });
useEffect(() => {
if (isInView) {
const numericValue = typeof value === 'string' ? parseFloat(value.replace(/[^0-9.-]+/g, '')) : value;
if (!isNaN(numericValue)) {
motionValue.set(numericValue);
}
}
}, [value, isInView, motionValue]);
useEffect(() => {
const unsubscribe = springValue.on('change', (latest) => {
if (ref.current) {
if (format === 'currency') {
ref.current.textContent = `¥${latest.toFixed(2)}`;
} else if (format === 'percent') {
ref.current.textContent = `${latest.toFixed(1)}%`;
} else {
ref.current.textContent = Math.round(latest).toLocaleString();
}
}
});
return () => unsubscribe();
}, [springValue, format]);
return (
<span ref={ref} className={className}>
{typeof value === 'string' && isNaN(parseFloat(value.replace(/[^0-9.-]+/g, ''))) ? value : (format === 'currency' ? '¥0.00' : '0')}
</span>
);
}
+69
View File
@@ -0,0 +1,69 @@
import { motion } from 'framer-motion';
interface EmptyStateProps {
title?: string;
description?: string;
className?: string;
}
export function EmptyState({ title = "暂无数据", description = "这里暂时什么都没有", className = "" }: EmptyStateProps) {
return (
<div className={`flex flex-col items-center justify-center p-12 text-center h-full w-full min-h-[300px] ${className}`}>
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5, ease: "easeOut" }}
className="relative mb-6"
>
<div className="absolute inset-0 bg-gray-100 rounded-full blur-2xl opacity-50 animate-pulse" />
<svg width="120" height="120" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg" className="relative z-10">
<motion.path
d="M30 45C30 36.7157 36.7157 30 45 30H75C83.2843 30 90 36.7157 90 45V75C90 83.2843 83.2843 90 75 90H45C36.7157 90 30 83.2843 30 75V45Z"
fill="#F3F4F6"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
/>
<motion.path
d="M45 45H75M45 60H65M45 75H55"
stroke="#D1D5DB"
strokeWidth="4"
strokeLinecap="round"
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ delay: 0.5, duration: 0.8, ease: "easeOut" }}
/>
<motion.circle
cx="85" cy="85" r="15" fill="#FFFFFF" stroke="#E5E7EB" strokeWidth="4"
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.8, type: "spring", stiffness: 200 }}
/>
<motion.path
d="M80 85L83 88L90 81"
stroke="#9CA3AF" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ delay: 1.1, duration: 0.4 }}
/>
</svg>
</motion.div>
<motion.h3
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="text-lg font-bold text-gray-900 mb-2"
>
{title}
</motion.h3>
<motion.p
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="text-sm font-medium text-gray-400 max-w-[250px]"
>
{description}
</motion.p>
</div>
);
}
+253
View File
@@ -0,0 +1,253 @@
import { useState, useEffect, useRef } from 'react';
interface PupilProps {
size?: number;
maxDistance?: number;
pupilColor?: string;
forceLookX?: number;
forceLookY?: number;
}
const Pupil = ({
size = 12, maxDistance = 5, pupilColor = 'black',
forceLookX, forceLookY,
}: PupilProps) => {
const [mouseX, setMouseX] = useState(0);
const [mouseY, setMouseY] = useState(0);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const h = (e: MouseEvent) => { setMouseX(e.clientX); setMouseY(e.clientY); };
window.addEventListener('mousemove', h);
return () => window.removeEventListener('mousemove', h);
}, []);
const calc = () => {
if (!ref.current) return { x: 0, y: 0 };
if (forceLookX !== undefined && forceLookY !== undefined) return { x: forceLookX, y: forceLookY };
const r = ref.current.getBoundingClientRect();
const cx = r.left + r.width / 2, cy = r.top + r.height / 2;
const dx = mouseX - cx, dy = mouseY - cy;
const dist = Math.min(Math.sqrt(dx * dx + dy * dy), maxDistance);
const a = Math.atan2(dy, dx);
return { x: Math.cos(a) * dist, y: Math.sin(a) * dist };
};
const p = calc();
return (
<div ref={ref} className="rounded-full" style={{
width: size, height: size, backgroundColor: pupilColor,
transform: `translate(${p.x}px, ${p.y}px)`, transition: 'transform 0.1s ease-out',
}} />
);
};
interface EyeBallProps {
size?: number; pupilSize?: number; maxDistance?: number;
eyeColor?: string; pupilColor?: string; isBlinking?: boolean;
forceLookX?: number; forceLookY?: number;
}
const EyeBall = ({
size = 48, pupilSize = 16, maxDistance = 10,
eyeColor = 'white', pupilColor = 'black', isBlinking = false,
forceLookX, forceLookY,
}: EyeBallProps) => {
const [mouseX, setMouseX] = useState(0);
const [mouseY, setMouseY] = useState(0);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const h = (e: MouseEvent) => { setMouseX(e.clientX); setMouseY(e.clientY); };
window.addEventListener('mousemove', h);
return () => window.removeEventListener('mousemove', h);
}, []);
const calc = () => {
if (!ref.current) return { x: 0, y: 0 };
if (forceLookX !== undefined && forceLookY !== undefined) return { x: forceLookX, y: forceLookY };
const r = ref.current.getBoundingClientRect();
const cx = r.left + r.width / 2, cy = r.top + r.height / 2;
const dx = mouseX - cx, dy = mouseY - cy;
const dist = Math.min(Math.sqrt(dx * dx + dy * dy), maxDistance);
const a = Math.atan2(dy, dx);
return { x: Math.cos(a) * dist, y: Math.sin(a) * dist };
};
const p = calc();
return (
<div ref={ref} className="rounded-full flex items-center justify-center transition-all duration-150"
style={{ width: size, height: isBlinking ? 2 : size, backgroundColor: eyeColor, overflow: 'hidden' }}>
{!isBlinking && (
<div className="rounded-full" style={{
width: pupilSize, height: pupilSize, backgroundColor: pupilColor,
transform: `translate(${p.x}px, ${p.y}px)`, transition: 'transform 0.1s ease-out',
}} />
)}
</div>
);
};
interface Props { isTyping?: boolean; showPassword?: boolean; passwordLength?: number; }
export default function LoginCharacters({ isTyping = false, showPassword = false, passwordLength = 0 }: Props) {
const [mouseX, setMouseX] = useState(0);
const [mouseY, setMouseY] = useState(0);
const [isPurpleBlink, setIsPurpleBlink] = useState(false);
const [isBlackBlink, setIsBlackBlink] = useState(false);
const [isLooking, setIsLooking] = useState(false);
const [isPurplePeek, setIsPurplePeek] = useState(false);
const purpleRef = useRef<HTMLDivElement>(null);
const blackRef = useRef<HTMLDivElement>(null);
const yellowRef = useRef<HTMLDivElement>(null);
const orangeRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const h = (e: MouseEvent) => { setMouseX(e.clientX); setMouseY(e.clientY); };
window.addEventListener('mousemove', h);
return () => window.removeEventListener('mousemove', h);
}, []);
// Purple blink
useEffect(() => {
const schedule = (): ReturnType<typeof setTimeout> => setTimeout(() => {
setIsPurpleBlink(true);
setTimeout(() => { setIsPurpleBlink(false); schedule(); }, 150);
}, Math.random() * 4000 + 3000);
const t = schedule(); return () => clearTimeout(t);
}, []);
// Black blink
useEffect(() => {
const schedule = (): ReturnType<typeof setTimeout> => setTimeout(() => {
setIsBlackBlink(true);
setTimeout(() => { setIsBlackBlink(false); schedule(); }, 150);
}, Math.random() * 4000 + 3000);
const t = schedule(); return () => clearTimeout(t);
}, []);
// Look at each other when typing
useEffect(() => {
if (isTyping) { setIsLooking(true); const t = setTimeout(() => setIsLooking(false), 800); return () => clearTimeout(t); }
else setIsLooking(false);
}, [isTyping]);
// Purple peek when password visible
useEffect(() => {
if (passwordLength > 0 && showPassword) {
const t = setTimeout(() => {
setIsPurplePeek(true);
setTimeout(() => setIsPurplePeek(false), 800);
}, Math.random() * 3000 + 2000);
return () => clearTimeout(t);
} else setIsPurplePeek(false);
}, [passwordLength, showPassword, isPurplePeek]);
const calcPos = (ref: React.RefObject<HTMLDivElement | null>) => {
if (!ref.current) return { faceX: 0, faceY: 0, bodySkew: 0 };
const r = ref.current.getBoundingClientRect();
const cx = r.left + r.width / 2, cy = r.top + r.height / 3;
const dx = mouseX - cx, dy = mouseY - cy;
return {
faceX: Math.max(-15, Math.min(15, dx / 20)),
faceY: Math.max(-10, Math.min(10, dy / 30)),
bodySkew: Math.max(-6, Math.min(6, -dx / 120)),
};
};
const pp = calcPos(purpleRef), bp = calcPos(blackRef);
const yp = calcPos(yellowRef), op = calcPos(orangeRef);
const hiding = passwordLength > 0 && !showPassword;
const showing = passwordLength > 0 && showPassword;
return (
<div className="relative w-full" style={{ height: 300 }}>
{/* Purple tall - back */}
<div ref={purpleRef} className="absolute bottom-0 transition-all duration-700 ease-in-out" style={{
left: '12%', width: '33%', height: (isTyping || hiding) ? '110%' : '100%',
backgroundColor: '#6C3FF5', borderRadius: '10px 10px 0 0', zIndex: 1,
transform: showing ? 'skewX(0deg)' : (isTyping || hiding)
? `skewX(${(pp.bodySkew || 0) - 12}deg) translateX(40px)` : `skewX(${pp.bodySkew || 0}deg)`,
transformOrigin: 'bottom center',
}}>
<div className="absolute flex gap-8 transition-all duration-700 ease-in-out" style={{
left: showing ? 20 : isLooking ? 55 : 45 + pp.faceX,
top: showing ? 35 : isLooking ? 65 : 40 + pp.faceY,
}}>
<EyeBall size={18} pupilSize={7} maxDistance={5} eyeColor="white" pupilColor="#2D2D2D"
isBlinking={isPurpleBlink}
forceLookX={showing ? (isPurplePeek ? 4 : -4) : isLooking ? 3 : undefined}
forceLookY={showing ? (isPurplePeek ? 5 : -4) : isLooking ? 4 : undefined} />
<EyeBall size={18} pupilSize={7} maxDistance={5} eyeColor="white" pupilColor="#2D2D2D"
isBlinking={isPurpleBlink}
forceLookX={showing ? (isPurplePeek ? 4 : -4) : isLooking ? 3 : undefined}
forceLookY={showing ? (isPurplePeek ? 5 : -4) : isLooking ? 4 : undefined} />
</div>
</div>
{/* Black - middle */}
<div ref={blackRef} className="absolute bottom-0 transition-all duration-700 ease-in-out" style={{
left: '44%', width: '22%', height: '77%',
backgroundColor: '#2D2D2D', borderRadius: '8px 8px 0 0', zIndex: 2,
transform: showing ? 'skewX(0deg)' : isLooking
? `skewX(${(bp.bodySkew || 0) * 1.5 + 10}deg) translateX(20px)`
: (isTyping || hiding) ? `skewX(${(bp.bodySkew || 0) * 1.5}deg)` : `skewX(${bp.bodySkew || 0}deg)`,
transformOrigin: 'bottom center',
}}>
<div className="absolute flex gap-6 transition-all duration-700 ease-in-out" style={{
left: showing ? 10 : isLooking ? 32 : 26 + bp.faceX,
top: showing ? 28 : isLooking ? 12 : 32 + bp.faceY,
}}>
<EyeBall size={16} pupilSize={6} maxDistance={4} eyeColor="white" pupilColor="#2D2D2D"
isBlinking={isBlackBlink}
forceLookX={showing ? -4 : isLooking ? 0 : undefined}
forceLookY={showing ? -4 : isLooking ? -4 : undefined} />
<EyeBall size={16} pupilSize={6} maxDistance={4} eyeColor="white" pupilColor="#2D2D2D"
isBlinking={isBlackBlink}
forceLookX={showing ? -4 : isLooking ? 0 : undefined}
forceLookY={showing ? -4 : isLooking ? -4 : undefined} />
</div>
</div>
{/* Orange semi-circle - front left */}
<div ref={orangeRef} className="absolute bottom-0 transition-all duration-700 ease-in-out" style={{
left: '0%', width: '44%', height: '50%', backgroundColor: '#FF9B6B',
borderRadius: '120px 120px 0 0', zIndex: 3,
transform: showing ? 'skewX(0deg)' : `skewX(${op.bodySkew || 0}deg)`,
transformOrigin: 'bottom center',
}}>
<div className="absolute flex gap-8 transition-all duration-200 ease-out" style={{
left: showing ? '20%' : `calc(34% + ${op.faceX || 0}px)`,
top: showing ? '42%' : `calc(45% + ${op.faceY || 0}px)`,
}}>
<Pupil size={12} maxDistance={5} pupilColor="#2D2D2D"
forceLookX={showing ? -5 : undefined} forceLookY={showing ? -4 : undefined} />
<Pupil size={12} maxDistance={5} pupilColor="#2D2D2D"
forceLookX={showing ? -5 : undefined} forceLookY={showing ? -4 : undefined} />
</div>
</div>
{/* Yellow rounded - front right */}
<div ref={yellowRef} className="absolute bottom-0 transition-all duration-700 ease-in-out" style={{
left: '56%', width: '26%', height: '58%', backgroundColor: '#E8D754',
borderRadius: '70px 70px 0 0', zIndex: 4,
transform: showing ? 'skewX(0deg)' : `skewX(${yp.bodySkew || 0}deg)`,
transformOrigin: 'bottom center',
}}>
<div className="absolute flex gap-6 transition-all duration-200 ease-out" style={{
left: showing ? '14%' : `calc(37% + ${yp.faceX || 0}px)`,
top: showing ? '15%' : `calc(17% + ${yp.faceY || 0}px)`,
}}>
<Pupil size={12} maxDistance={5} pupilColor="#2D2D2D"
forceLookX={showing ? -5 : undefined} forceLookY={showing ? -4 : undefined} />
<Pupil size={12} maxDistance={5} pupilColor="#2D2D2D"
forceLookX={showing ? -5 : undefined} forceLookY={showing ? -4 : undefined} />
</div>
{/* mouth */}
<div className="absolute w-16 h-[4px] bg-[#2D2D2D] rounded-full transition-all duration-200 ease-out"
style={{
left: showing ? '7%' : `calc(28% + ${yp.faceX || 0}px)`,
top: showing ? '38%' : `calc(38% + ${yp.faceY || 0}px)`,
}} />
</div>
</div>
);
}
+95 -81
View File
@@ -46,31 +46,25 @@
}
:root {
--radius: 1.5rem;
/* 24px */
--background: #FAF5E6;
/* 暖奶油色 */
--foreground: #4A3A2C;
/* 深暖棕色 */
--card: rgba(255, 253, 235, 0.85);
/* 浅黄油色 */
--card-foreground: #4A3A2C;
--popover: rgba(255, 253, 235, 0.9);
--popover-foreground: #4A3A2C;
--primary: #D28F4F;
/* 柔和的琥珀色 */
--radius: 0.75rem; /* 12px for modern rounded look */
--background: #F9FAFB; /* gray-50 */
--foreground: #111827; /* gray-900 */
--card: #FFFFFF;
--card-foreground: #111827;
--popover: #FFFFFF;
--popover-foreground: #111827;
--primary: #111827;
--primary-foreground: #FFFFFF;
--secondary: #6A7F6A;
/* 灰绿色 */
--secondary-foreground: #FFFFFF;
--muted: #F2EDE4;
--muted-foreground: #8C7E6C;
--accent: rgba(210, 143, 79, 0.1);
--accent-foreground: #D28F4F;
--destructive: #A64452;
--border: rgba(74, 58, 44, 0.08);
--input: rgba(74, 58, 44, 0.05);
--ring: #D28F4F;
--secondary: #F3F4F6; /* gray-100 */
--secondary-foreground: #111827;
--muted: #F3F4F6;
--muted-foreground: #6B7280; /* gray-500 */
--accent: #F3F4F6;
--accent-foreground: #111827;
--destructive: #EF4444; /* red-500 */
--border: #E5E7EB; /* gray-200 */
--input: #E5E7EB;
--ring: #111827;
/* Custom Design Tokens */
--morning-gradient: linear-gradient(135deg, #FDE68A 0%, #FBBF24 50%, #7DD3FC 100%);
@@ -91,24 +85,24 @@
}
.dark {
--background: #121212;
--foreground: #D4D4D4;
--card: #1E1E1E;
--card-foreground: #D4D4D4;
--popover: #1E1E1E;
--popover-foreground: #D4D4D4;
--primary: #D28F4F;
--primary-foreground: #FFFFFF;
--secondary: #6A7F6A;
--secondary-foreground: #FFFFFF;
--muted: #262626;
--muted-foreground: #737373;
--accent: rgba(210, 143, 79, 0.2);
--accent-foreground: #D28F4F;
--destructive: #EF4444;
--border: #262626;
--input: #262626;
--ring: #D28F4F;
--background: #030712; /* gray-950 */
--foreground: #F9FAFB;
--card: #111827; /* gray-900 */
--card-foreground: #F9FAFB;
--popover: #111827;
--popover-foreground: #F9FAFB;
--primary: #F9FAFB;
--primary-foreground: #111827;
--secondary: #1F2937; /* gray-800 */
--secondary-foreground: #F9FAFB;
--muted: #1F2937;
--muted-foreground: #9CA3AF;
--accent: #1F2937;
--accent-foreground: #F9FAFB;
--destructive: #7F1D1D;
--border: #374151; /* gray-700 */
--input: #374151;
--ring: #F9FAFB;
--glass-bg: rgba(30, 30, 30, 0.8);
--glass-border: rgba(255, 255, 255, 0.05);
@@ -162,43 +156,11 @@
overflow: hidden;
}
/* 暖色噪点纹理 */
.warm-noise::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0.06;
pointer-events: none;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E");
z-index: 0;
}
.sidebar-noise {
position: relative;
background-color: var(--sidebar-twilight);
}
.sidebar-noise::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0.03;
pointer-events: none;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E");
}
/* 侧边栏选中光晕 */
.sidebar-halo {
position: relative;
box-shadow: 0 0 15px rgba(210, 143, 79, 0.3);
border: 1px solid rgba(210, 143, 79, 0.4) !important;
}
/* Classes nullified for new aesthetic */
.warm-noise::before { display: none; }
.sidebar-noise { background-color: transparent; }
.sidebar-noise::before { display: none; }
.sidebar-halo { }
.audio-wave {
display: flex;
@@ -263,11 +225,63 @@
body {
@apply bg-background text-foreground font-sans;
background-image: radial-gradient(at 0% 0%, rgba(210, 143, 79, 0.05) 0px, transparent 50%),
radial-gradient(at 100% 100%, rgba(106, 127, 106, 0.05) 0px, transparent 50%);
}
}
/* --- Premium Animations & Shadows --- */
.shadow-soft {
box-shadow: 0 4px 40px -4px rgba(0, 0, 0, 0.04), 0 2px 10px -2px rgba(0,0,0,0.02);
}
.shadow-glass {
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.05);
}
.shadow-hover-spring {
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.shadow-hover-spring:hover {
transform: translateY(-4px) scale(1.01);
box-shadow: 0 20px 40px -5px rgba(0, 0, 0, 0.08), 0 10px 20px -5px rgba(0, 0, 0, 0.04);
}
/* 1px semi-transparent inner border for glass effect */
.border-glass {
border: 1px solid rgba(255, 255, 255, 0.6);
background: linear-gradient(135deg, rgba(255,255,255,0.9), rgba(255,255,255,0.6));
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
}
.dark .border-glass {
border: 1px solid rgba(255, 255, 255, 0.08);
background: linear-gradient(135deg, rgba(30,30,30,0.9), rgba(15,15,15,0.8));
}
/* Button Sweep Animation */
.btn-sweep {
position: relative;
overflow: hidden;
z-index: 1;
}
.btn-sweep::after {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 50%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
transform: skewX(-20deg);
transition: all 0.6s ease;
z-index: -1;
}
.btn-sweep:hover::after {
left: 150%;
}
.custom-scrollbar::-webkit-scrollbar {
width: 5px;
}
+46 -45
View File
@@ -10,7 +10,8 @@ import {
LogOut,
User as UserIcon,
ChevronDown,
Crown
Crown,
Volume2
} from 'lucide-react';
import { Button } from '../components/ui/button';
import {
@@ -51,60 +52,61 @@ export default function AdminLayout() {
{ name: '节目管理', path: '/radio/program', icon: Disc3 },
{ name: 'VIP配置', path: '/radio/vip', icon: Crown },
{ name: '用户管理', path: '/radio/user', icon: UserIcon },
{ name: '音色管理', path: '/radio/voice', icon: Volume2 },
{ name: '文件管理', path: '/system/oss', icon: FolderOpen },
];
return (
<div className="flex h-screen bg-[#FAF5E6] dark:bg-[#1A1A1A] overflow-hidden font-sans warm-noise">
<div className="flex h-screen bg-gray-50 overflow-hidden font-sans">
{/* Sidebar */}
<aside className="fixed inset-y-0 left-0 z-50 w-64 sidebar-noise text-slate-100 hidden md:flex flex-col shadow-2xl border-r border-white/5">
<div className="flex items-center h-20 px-6 border-b border-white/5 shrink-0">
<div className="w-12 h-12 rounded-2xl overflow-hidden mr-3 shadow-lg shadow-orange-500/20 ring-1 ring-white/20 flex items-center justify-center bg-white">
<img src="/favicon.jpg" alt="logo" className="w-full h-full object-cover p-1 bg-white" />
<aside className="fixed inset-y-0 left-0 z-50 w-64 bg-white hidden md:flex flex-col border-r border-gray-200">
<div className="flex items-center h-16 px-6 border-b border-gray-200 shrink-0">
<div className="w-8 h-8 rounded-lg overflow-hidden mr-3 flex items-center justify-center bg-gray-100">
<img src="/favicon.jpg" alt="logo" className="w-full h-full object-cover" />
</div>
<div className="flex flex-col">
<span className="font-black text-lg tracking-tight leading-none text-white"></span>
<span className="text-[10px] uppercase tracking-[0.2em] text-white/40 mt-1">广</span>
<span className="font-bold text-base tracking-tight leading-none text-gray-900"></span>
<span className="text-[10px] uppercase tracking-widest text-gray-400 mt-1"></span>
</div>
</div>
<div className="flex-1 px-4 py-8 space-y-4 overflow-y-auto custom-scrollbar">
<div className="flex-1 px-4 py-6 space-y-1.5 overflow-y-auto">
{navItems.map((item) => {
const isActive = location.pathname === item.path || (item.path !== '/' && location.pathname.startsWith(item.path));
return (
<Link key={item.path} to={item.path}>
<Button
variant="ghost"
className={`w-full justify-start h-12 px-4 rounded-2xl transition-all duration-300 group relative overflow-hidden ${isActive
? 'text-white bg-white/10 sidebar-halo'
: 'text-white/50 hover:text-white hover:bg-white/5'
className={`w-full justify-start h-11 px-4 rounded-xl transition-all duration-300 group relative overflow-hidden ${isActive
? 'bg-white shadow-soft ring-1 ring-gray-100 text-gray-900 font-bold'
: 'text-gray-500 hover:bg-gray-50/80 hover:text-gray-800'
}`}
>
<item.icon className={`w-5 h-5 mr-3 shrink-0 transition-transform duration-300 ${isActive ? 'text-[#D28F4F]' : 'group-hover:text-[#D28F4F] group-hover:scale-110'}`} />
<span className="font-bold tracking-wide">{item.name}</span>
{isActive && (
<div className="ml-auto w-1.5 h-1.5 rounded-full bg-[#D28F4F] shadow-[0_0_12px_#D28F4F]" />
<div className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-6 bg-gray-900 rounded-r-full shadow-[0_0_10px_rgba(17,24,39,0.3)]" />
)}
<item.icon className={`w-[18px] h-[18px] mr-3 shrink-0 transition-transform duration-300 ${isActive ? 'text-gray-900 scale-110' : 'text-gray-400 group-hover:text-gray-600'}`} />
<span className={`tracking-wide ${isActive ? 'font-bold' : 'font-medium'}`}>{item.name}</span>
</Button>
</Link>
)
})}
</div>
<div className="p-4 border-t border-white/5 shrink-0">
<div className="bg-white/5 backdrop-blur-md rounded-[2rem] p-4 flex items-center space-x-3 border border-white/10">
<div className="p-4 border-t border-gray-200 shrink-0">
<div className="rounded-xl p-3 flex items-center space-x-3 bg-gray-50 hover:bg-gray-100 transition-colors">
<div className="relative">
<Avatar className="h-10 w-10 ring-2 ring-white/10">
<Avatar className="h-9 w-9 border border-gray-200">
<AvatarImage src={userInfo?.avatar || ''} />
<AvatarFallback className="bg-white/10 text-white/80">
<UserIcon className="w-5 h-5" />
<AvatarFallback className="bg-white text-gray-600">
<UserIcon className="w-4 h-4" />
</AvatarFallback>
</Avatar>
<div className="absolute -bottom-0.5 -right-0.5 w-3 h-3 bg-emerald-500 rounded-full border-2 border-[#263238] animate-pulse" />
<div className="absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 bg-green-500 rounded-full border-2 border-white" />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-black truncate text-white">{userInfo?.nickName || '管理员'}</p>
<p className="text-[10px] text-white/30 uppercase tracking-tighter truncate font-bold">{userInfo?.account || 'admin'}</p>
<p className="text-sm font-semibold truncate text-gray-900">{userInfo?.nickName || '管理员'}</p>
<p className="text-xs text-gray-500 truncate">{userInfo?.account || 'admin'}</p>
</div>
</div>
</div>
@@ -113,47 +115,47 @@ export default function AdminLayout() {
{/* Main Area */}
<div className="flex-1 md:ml-64 flex flex-col h-full overflow-hidden relative">
{/* Navbar */}
<header className="h-20 flex items-center justify-between px-8 bg-[#FAF5E6]/60 dark:bg-black/40 backdrop-blur-xl border-b border-[#4A3A2C]/10 shrink-0 z-40 sticky top-0 warm-noise">
<header className="h-16 flex items-center justify-between px-6 bg-white/80 backdrop-blur-md border-b border-gray-200 shrink-0 z-40 sticky top-0">
<div className="flex items-center space-x-4">
<div className="md:hidden">
<img src="/favicon.jpg" alt="logo" className="w-8 h-8 rounded-lg object-cover" />
</div>
<div className="flex flex-col">
<h2 className="text-[10px] font-black text-[#D28F4F]/60 uppercase tracking-[0.3em] leading-none mb-1"></h2>
<p className="text-xl font-black tracking-tight text-[#4A3A2C]">
<h2 className="text-[10px] font-bold text-gray-400 uppercase tracking-wider leading-none mb-1"></h2>
<p className="text-lg font-bold tracking-tight text-gray-900">
{navItems.find(i => i.path === location.pathname)?.name || '控制台'}
</p>
</div>
</div>
<div className="flex items-center space-x-6">
<div className="flex items-center space-x-2 bg-[#D28F4F]/5 border border-[#D28F4F]/10 px-4 py-2 rounded-2xl">
<div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse shadow-[0_0_8px_#10b981]" />
<span className="text-[10px] font-black text-[#8C7E6C] uppercase tracking-widest">线</span>
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-2 bg-green-50 px-3 py-1.5 rounded-full border border-green-100">
<div className="w-1.5 h-1.5 rounded-full bg-green-500" />
<span className="text-[11px] font-medium text-green-700">线</span>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-12 px-2 rounded-2xl hover:bg-white/40 flex items-center space-x-3 transition-all">
<Avatar className="h-10 w-10 ring-2 ring-[#D28F4F]/20 shadow-lg">
<Button variant="ghost" className="h-10 px-2 rounded-lg hover:bg-gray-100 flex items-center space-x-2">
<Avatar className="h-8 w-8">
<AvatarImage src={userInfo?.avatar || ''} />
<AvatarFallback className="bg-[#FAF5E6]"><UserIcon className="text-[#D28F4F]" /></AvatarFallback>
<AvatarFallback className="bg-gray-100"><UserIcon className="w-4 h-4 text-gray-600" /></AvatarFallback>
</Avatar>
<ChevronDown className="w-4 h-4 text-[#8C7E6C]" />
<ChevronDown className="w-4 h-4 text-gray-400" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-64 mt-2 rounded-[2.5rem] p-2 glass-card warm-noise border-[#D28F4F]/10" align="end">
<DropdownMenuLabel className="font-normal px-4 py-6">
<DropdownMenuContent className="w-56 mt-2 rounded-xl p-1 bg-white border-gray-200 shadow-lg" align="end">
<DropdownMenuLabel className="font-normal px-3 py-3">
<div className="flex flex-col space-y-1">
<p className="text-lg font-black text-[#4A3A2C]">{userInfo?.nickName || '管理员'}</p>
<p className="text-xs text-[#8C7E6C] font-bold tracking-wide">{userInfo?.account || 'admin'}</p>
<p className="text-sm font-semibold text-gray-900">{userInfo?.nickName || '管理员'}</p>
<p className="text-xs text-gray-500">{userInfo?.account || 'admin'}</p>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator className="bg-[#4A3A2C]/5" />
<DropdownMenuSeparator className="bg-gray-100" />
<DropdownMenuItem
onClick={handleLogout}
className="cursor-pointer text-rose-600 focus:bg-rose-50 focus:text-rose-700 rounded-2xl p-4 font-black transition-all"
className="cursor-pointer text-red-600 focus:bg-red-50 focus:text-red-700 rounded-lg p-2.5 font-medium"
>
<LogOut className="mr-3 h-5 w-5" />
<span>退</span>
<LogOut className="mr-2 h-4 w-4" />
<span>退</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -161,9 +163,8 @@ export default function AdminLayout() {
</header>
{/* Page Content */}
<main className="flex-1 overflow-y-auto p-6 md:p-8 scroll-smooth relative">
<div className="absolute top-0 left-0 w-full h-[500px] bg-gradient-to-b from-[#D28F4F]/5 to-transparent pointer-events-none" />
<div className="w-full h-full relative">
<main className="flex-1 overflow-y-auto p-6 md:p-8 scroll-smooth">
<div className="w-full h-full max-w-[1400px] mx-auto">
<Outlet />
</div>
</main>
+26 -21
View File
@@ -19,6 +19,7 @@ import {
Coins
} from 'lucide-react';
import { motion } from 'framer-motion';
import { AnimatedCounter } from '@/components/AnimatedCounter.tsx';
import {
AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend
} from 'recharts';
@@ -40,11 +41,11 @@ import type { RadioChannel } from '@/types/radio.ts';
// ─── Color Palette ───
const COLORS = {
listen: '#D28F4F', // primary (Orange)
sub: '#6A7F6A', // secondary (Green)
renew: '#C86354', // distinct warm red
muted: '#8C7E6C',
chart: ['#D28F4F', '#6A7F6A', '#C86354', '#E8B878', '#8BAE8B'],
listen: '#0f172a', // Slate-900 (primary)
sub: '#3b82f6', // Blue-500
renew: '#10b981', // Emerald-500
muted: '#9ca3af', // Gray-400
chart: ['#0f172a', '#3b82f6', '#10b981', '#f59e0b', '#6366f1'],
};
export default function Dashboard() {
@@ -244,7 +245,7 @@ export default function Dashboard() {
value: vipStats.activeVipUsers,
icon: Crown,
subLabel: '当前生效的尊享会员',
color: '#F59E0B',
color: '#f59e0b',
trend: vipStats.newVipOrders > 0 ? `+${vipStats.newVipOrders} 订单` : undefined,
},
{
@@ -252,7 +253,7 @@ export default function Dashboard() {
value: `¥${(vipStats.vipRevenue / 100).toFixed(2)}`,
icon: Coins,
subLabel: '区间内会员特权变现',
color: '#D28F4F',
color: '#0f172a',
},
];
@@ -265,8 +266,7 @@ export default function Dashboard() {
};
return (
<div className="space-y-8 pb-20 overflow-x-hidden bg-background min-h-screen text-foreground p-6 md:p-10 font-sans relative">
<div className="absolute inset-0 warm-noise" />
<div className="space-y-8 pb-20 overflow-x-hidden min-h-screen text-gray-900 font-sans relative">
{/* ═══ Header ═══ */}
<header className="relative z-10 flex flex-col xl:flex-row xl:items-end justify-between gap-6 pb-6 border-b border-border">
@@ -340,7 +340,7 @@ export default function Dashboard() {
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: idx * 0.08 }}
className="bg-card rounded-2xl p-5 border border-border shadow-sm hover:shadow-md transition-all duration-300 flex flex-col gap-3 group"
className="bg-white rounded-[2rem] p-6 border border-gray-100/50 shadow-soft shadow-hover-spring flex flex-col gap-3 group relative overflow-hidden"
>
<div className="flex justify-between items-start">
<p className="text-xs font-semibold text-muted-foreground">{stat.name}</p>
@@ -348,8 +348,13 @@ export default function Dashboard() {
<stat.icon className="w-4 h-4" style={{ color: stat.color }} />
</div>
</div>
<p className="text-2xl md:text-3xl font-bold text-foreground leading-none">
{loading ? '—' : stat.value.toLocaleString()}
<p className="text-2xl md:text-3xl font-bold text-gray-900 leading-none">
{loading ? '—' : (
<AnimatedCounter
value={stat.value}
format={typeof stat.value === 'string' && stat.value.startsWith('¥') ? 'currency' : 'number'}
/>
)}
</p>
<div className="flex justify-between items-center">
<span className="text-[11px] text-muted-foreground">{stat.subLabel}</span>
@@ -372,7 +377,7 @@ export default function Dashboard() {
animate={{ opacity: 1 }}
transition={{ duration: 0.5 }}
>
<div className="bg-card rounded-2xl p-6 border border-border shadow-sm flex flex-col gap-4">
<div className="bg-white rounded-[2rem] p-8 border-glass shadow-soft flex flex-col gap-4">
{/* Chart header */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
@@ -449,7 +454,7 @@ export default function Dashboard() {
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.15 }}
>
<div className="bg-card rounded-2xl p-6 border border-border shadow-sm h-full flex flex-col">
<div className="bg-white rounded-[2rem] p-8 border-glass shadow-soft h-full flex flex-col">
<div className="flex items-center justify-between mb-6 pb-4 border-b border-border">
<h3 className="text-sm font-bold text-foreground"></h3>
<TrendingUp className="w-4 h-4 text-muted-foreground" />
@@ -457,10 +462,10 @@ export default function Dashboard() {
{/* LTV highlight */}
{funnelData && (
<div className="mb-6 rounded-xl bg-primary/10 border border-primary/20 p-4 text-center">
<p className="text-xs text-muted-foreground mb-1"> (LTV)</p>
<p className="text-2xl font-bold" style={{ color: COLORS.listen }}>
¥{(funnelData.ltv / 100).toFixed(2)}
<div className="mb-6 rounded-2xl bg-gray-50/50 border border-gray-100 p-5 text-center shadow-inner group transition-all duration-300 hover:bg-white hover:shadow-soft">
<p className="text-xs text-gray-500 mb-2 transition-colors group-hover:text-gray-900"> (LTV)</p>
<p className="text-3xl font-black" style={{ color: COLORS.listen }}>
<AnimatedCounter value={funnelData.ltv / 100} format="currency" />
</p>
</div>
)}
@@ -481,10 +486,10 @@ export default function Dashboard() {
<span className="text-xs font-bold text-foreground">{step.label}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-sm font-bold text-foreground">{step.value.toLocaleString()}</span>
<span className="text-sm font-bold text-gray-900"><AnimatedCounter value={step.value}/></span>
{convRate && (
<span className="text-[10px] font-semibold text-muted-foreground bg-muted px-1.5 py-0.5 rounded">
{convRate}%
<span className="text-[10px] font-semibold text-gray-500 bg-gray-50 px-2 py-0.5 rounded-full border border-gray-100">
<AnimatedCounter value={convRate} format="percent" />
</span>
)}
</div>
+189 -256
View File
@@ -6,8 +6,8 @@ import { toast } from 'sonner';
import { Button } from '../../components/ui/button';
import { Input } from '../../components/ui/input';
import { Label } from '../../components/ui/label';
import { ShieldCheck, Disc3, ArrowRight, Heart, Sparkles, Waves } from 'lucide-react';
import { motion } from 'framer-motion';
import { Disc3, Eye, EyeOff } from 'lucide-react';
import LoginCharacters from '@/components/LoginCharacters';
export default function Login() {
const navigate = useNavigate();
@@ -20,6 +20,8 @@ export default function Login() {
const [captchaId, setCaptchaId] = useState('');
const [captchaImage, setCaptchaImage] = useState('');
const [loading, setLoading] = useState(false);
const [passwordFocused, setPasswordFocused] = useState(false);
const [passwordVisible, setPasswordVisible] = useState(false);
const mounted = useRef(false);
@@ -52,12 +54,7 @@ export default function Login() {
try {
setLoading(true);
const res = await loginApi({
account,
password,
captcha,
captchaId
});
const res = await loginApi({ account, password, captcha, captchaId });
toast.success('欢迎回来,首席播音官');
setToken(res.token);
setUserInfo(res.user);
@@ -72,260 +69,196 @@ export default function Login() {
};
return (
<div className="min-h-screen flex items-center justify-center bg-[#FAF5E6] relative overflow-hidden font-sans warm-noise selection:bg-[#D28F4F]/30 selection:text-[#4A3A2C]">
{/* Ambient Background Elements */}
<div className="absolute top-[-20%] left-[-10%] w-[60%] h-[60%] bg-gradient-to-br from-[#D28F4F]/15 via-transparent to-transparent rounded-full blur-[160px] animate-pulse" />
<div className="absolute bottom-[-15%] right-[-5%] w-[55%] h-[55%] bg-gradient-to-tr from-[#A64452]/10 via-transparent to-transparent rounded-full blur-[140px] animate-pulse transition-duration-[4s]" />
{/* Animated Floating Particles */}
<div className="absolute inset-0 pointer-events-none opacity-20">
<motion.div
animate={{ y: [0, -40, 0], opacity: [0.1, 0.4, 0.1] }}
transition={{ duration: 10, repeat: Infinity }}
className="absolute top-1/4 left-1/4 w-32 h-32 bg-[#D28F4F] rounded-full blur-[80px]"
/>
<motion.div
animate={{ y: [0, 50, 0], opacity: [0.1, 0.3, 0.1] }}
transition={{ duration: 12, repeat: Infinity, delay: 1 }}
className="absolute bottom-1/4 right-1/3 w-40 h-40 bg-[#A64452] rounded-full blur-[90px]"
/>
</div>
<motion.div
initial={{ opacity: 0, scale: 0.98, y: 15 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
transition={{ duration: 1.2, ease: [0.22, 1, 0.36, 1] }}
className="w-full max-w-[1240px] grid grid-cols-1 lg:grid-cols-2 bg-white/40 backdrop-blur-3xl rounded-[4.5rem] shadow-[0_40px_100px_-20px_rgba(74,58,44,0.12)] border border-white/60 overflow-hidden relative z-10 m-6 ring-1 ring-[#D28F4F]/5"
>
{/* Visual Side (Japanese Zen/Retro Radio Vibe) */}
<div className="hidden lg:flex flex-col justify-between p-20 sidebar-noise text-white relative overflow-hidden group">
<div className="absolute inset-0 bg-[#4A3A2C]/10 mix-blend-overlay pointer-events-none" />
<div className="absolute top-[-30%] right-[-20%] w-[120%] h-[120%] bg-gradient-to-bl from-[#D28F4F]/40 via-transparent to-transparent rounded-full blur-[120px] group-hover:scale-110 transition-transform duration-[4s]" />
<div className="relative z-10">
<motion.div
initial={{ x: -30, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
transition={{ delay: 0.4, duration: 0.8 }}
className="flex items-center gap-6"
>
<div className="w-20 h-20 bg-white rounded-[2.5rem] flex items-center justify-center shadow-2xl shadow-[#D28F4F]/30 ring-1 ring-white/40 overflow-hidden relative group p-2.5">
<img src="/favicon.jpg" alt="logo" className="w-full h-full object-cover rounded-[1.8rem]" />
<div className="absolute inset-0 bg-[#D28F4F] opacity-0 group-hover:opacity-20 transition-opacity" />
</div>
<div className="flex flex-col">
<span className="text-3xl font-black tracking-tightest leading-none drop-shadow-sm"></span>
<div className="flex items-center gap-3 mt-2">
<span className="text-[10px] uppercase font-black tracking-[0.4em] opacity-50 px-2 py-0.5 rounded-full border border-white/20">QUAN SHENG HUI</span>
</div>
</div>
</motion.div>
<div className="mt-28 space-y-10">
<motion.h2
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.6, duration: 1 }}
className="text-6xl font-black text-white leading-[1.05] tracking-tighter"
>
<span className="text-white/40 italic font-serif mx-1"></span><br />
<span className="text-transparent bg-clip-text bg-gradient-to-r from-[#D28F4F] to-[#E29A66] italic px-2"></span>
</motion.h2>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.8 }}
className="flex flex-col gap-6"
>
<p className="text-white/60 text-xl font-medium max-w-sm leading-relaxed border-l-3 border-[#D28F4F] pl-8">
<br />
</p>
<div className="flex gap-4 items-center">
<div className="h-px w-12 bg-white/20" />
<span className="text-[10px] font-black uppercase tracking-[0.4em] text-white/30">System v2.5.0 Premium</span>
</div>
</motion.div>
<div className="min-h-screen flex items-center justify-center bg-gray-100 p-6">
<div className="w-full max-w-[1100px] grid grid-cols-1 lg:grid-cols-2
bg-white rounded-3xl shadow-2xl shadow-gray-300/40
overflow-hidden">
{/* Left - Characters Banner */}
<div className="relative hidden lg:flex flex-col justify-between
bg-gradient-to-br from-gray-400 via-gray-500 to-gray-600
p-10 text-white rounded-l-3xl">
{/* Logo */}
<div className="relative z-20 flex items-center gap-3">
<div className="w-9 h-9 bg-white/15 backdrop-blur-sm rounded-lg
flex items-center justify-center overflow-hidden p-1">
<img src="/favicon.jpg" alt="logo"
className="w-full h-full object-cover rounded-md" />
</div>
<span className="text-lg font-semibold tracking-tight"></span>
</div>
<div className="relative z-10 flex items-end justify-between">
<div className="flex flex-col">
<div className="flex items-center gap-3 mb-2">
<Waves className="w-5 h-5 text-[#D28F4F]" />
<span className="text-[10px] uppercase font-black tracking-widest text-[#D28F4F]">Live Interaction Index</span>
</div>
<div className="text-4xl font-black flex items-baseline gap-2">
99.9%
<span className="text-xs text-white/40 font-bold uppercase">Up-time</span>
</div>
</div>
<div className="bg-white/10 backdrop-blur-xl rounded-[2rem] px-6 py-4 border border-white/10 flex items-center gap-4 group cursor-help hover:bg-white/20 transition-all">
<div className="w-3 h-3 rounded-full bg-emerald-500 shadow-[0_0_12px_#10b981]" />
<span className="text-sm font-black tracking-wide text-white/80 flex items-center gap-2">
<ShieldCheck className="w-4 h-4 text-[#D28F4F]" />
</span>
</div>
{/* Characters */}
<div className="relative z-20 flex items-end justify-center"
style={{ height: 320 }}>
<LoginCharacters
isTyping={passwordFocused}
showPassword={passwordVisible}
passwordLength={password.length}
/>
</div>
{/* Bottom */}
<div className="relative z-20 text-xs text-white/40">
© 2025 · QUAN SHENG HUI
</div>
{/* Decorative */}
<div className="absolute inset-0 rounded-l-3xl" style={{
backgroundImage: 'radial-gradient(circle, rgba(255,255,255,0.04) 1px, transparent 1px)',
backgroundSize: '20px 20px',
}} />
<div className="absolute top-1/4 right-1/4 w-48 h-48
bg-gray-400/20 rounded-full blur-3xl" />
<div className="absolute bottom-1/4 left-1/4 w-64 h-64
bg-gray-300/20 rounded-full blur-3xl" />
</div>
{/* Form Side (Clean Japanese Creamy Minimalist) */}
<div className="p-12 lg:p-24 flex flex-col justify-center bg-[#FAF5E6]/30 backdrop-blur-2xl relative overflow-hidden group">
<div className="absolute top-0 right-0 w-64 h-64 bg-[#D28F4F]/5 rounded-full blur-[100px] pointer-events-none" />
<div className="max-w-md mx-auto w-full relative z-10">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5 }}
className="mb-16"
>
<div className="flex items-center gap-4 mb-3">
<Sparkles className="w-5 h-5 text-[#D28F4F]" />
<span className="text-[11px] font-black text-[#D28F4F] uppercase tracking-[0.5em] opacity-70">Identity Authentication</span>
</div>
<h3 className="text-5xl font-black text-[#4A3A2C] tracking-tight leading-tight"></h3>
<div className="h-1.5 w-24 bg-gradient-to-r from-[#D28F4F] to-transparent rounded-full mt-6" />
</motion.div>
<form onSubmit={handleLogin} className="space-y-12">
<div className="space-y-10">
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.7 }}
className="space-y-4"
>
<Label className="text-[12px] uppercase font-black tracking-[0.2em] text-[#8C7E6C] ml-2 flex items-center gap-2">
<div className="w-1 h-1 rounded-full bg-[#D28F4F]" />
</Label>
<Input
placeholder="Admin / Operator ID"
className="h-20 rounded-[2rem] border-none bg-white shadow-[0_8px_30px_rgb(0,0,0,0.04)] font-bold text-[#4A3A2C] focus:ring-4 ring-[#D28F4F]/15 transition-all pl-10 placeholder:text-[#8C7E6C]/30 text-lg group-hover:shadow-[0_8px_30px_rgba(210,143,79,0.08)]"
value={account}
onChange={(e) => setAccount(e.target.value)}
disabled={loading}
/>
</motion.div>
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.8 }}
className="space-y-4"
>
<Label className="text-[12px] uppercase font-black tracking-[0.2em] text-[#8C7E6C] ml-2 flex items-center gap-2">
<div className="w-1 h-1 rounded-full bg-[#D28F4F]" />
</Label>
<Input
type="password"
placeholder="Secure Passphrase"
className="h-20 rounded-[2rem] border-none bg-white shadow-[0_8px_30px_rgb(0,0,0,0.04)] font-bold text-[#4A3A2C] focus:ring-4 ring-[#D28F4F]/15 transition-all pl-10 placeholder:text-[#8C7E6C]/30 text-lg group-hover:shadow-[0_8px_30px_rgba(210,143,79,0.08)]"
value={password}
onChange={(e) => setPassword(e.target.value)}
disabled={loading}
/>
</motion.div>
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.9 }}
className="space-y-4"
>
<Label className="text-[12px] uppercase font-black tracking-[0.2em] text-[#8C7E6C] ml-2 flex items-center gap-2">
<div className="w-1 h-1 rounded-full bg-[#D28F4F]" />
</Label>
<div className="flex gap-5">
<Input
placeholder="Code"
className="h-20 rounded-[2rem] border-none bg-white shadow-[0_8px_30px_rgb(0,0,0,0.04)] font-bold text-[#4A3A2C] focus:ring-4 ring-[#D28F4F]/15 transition-all pl-10 flex-1 placeholder:text-[#8C7E6C]/30 text-lg"
value={captcha}
onChange={(e) => setCaptcha(e.target.value)}
disabled={loading}
/>
<div
className="h-20 w-44 bg-white rounded-[2rem] p-3 shadow-[0_8px_30px_rgb(0,0,0,0.04)] border border-[#D28F4F]/10 cursor-pointer hover:ring-4 ring-[#D28F4F]/20 transition-all flex items-center justify-center overflow-hidden group/captcha"
onClick={fetchCaptcha}
>
{captchaImage ? (
<img src={captchaImage} alt="captcha" className="h-full w-full object-contain filter group-hover/captcha:scale-110 transition-transform duration-500 contrast-125" />
) : (
<Disc3 className="w-6 h-6 text-[#D28F4F]/40 animate-spin" />
)}
</div>
</div>
</motion.div>
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 1.1 }}
>
<Button
type="submit"
disabled={loading}
className="w-full h-24 rounded-[2.5rem] bg-gradient-to-r from-[#D28F4F] to-[#A64452] hover:scale-[1.02] active:scale-[0.98] transition-all shadow-2xl shadow-[#D28F4F]/30 border-none group overflow-hidden relative"
>
<div className="relative z-10 flex items-center justify-center gap-6 text-2xl font-black tracking-tight text-white">
{loading ? (
<>
<Disc3 className="w-8 h-8 animate-spin" />
<span>...</span>
</>
) : (
<>
<span></span>
<div className="w-10 h-10 rounded-full bg-white/20 flex items-center justify-center group-hover:bg-white/30 transition-colors">
<ArrowRight className="w-6 h-6 group-hover:translate-x-1.5 transition-transform" />
</div>
</>
)}
</div>
<div className="absolute inset-0 bg-white/10 opacity-0 group-hover:opacity-100 transition-opacity" />
{/* Liquid gradient animation effect */}
<motion.div
className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent -translate-x-full"
animate={{ translateX: ['-100%', '200%'] }}
transition={{ duration: 2.5, repeat: Infinity, ease: "linear" }}
/>
</Button>
</motion.div>
</form>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 1.3 }}
className="mt-20 pt-10 border-t border-[#4A3A2C]/5 flex items-center justify-between"
>
<div className="flex items-center gap-3">
<span className="w-2 h-2 rounded-full bg-[#A64452]/20 border border-[#A64452]/40" />
<span className="text-[10px] font-black text-[#8C7E6C]/50 uppercase tracking-[0.25em]">Crafted For Visual Harmony</span>
</div>
<div className="flex items-center gap-2">
<Heart className="w-3.5 h-3.5 text-[#A64452] fill-[#A64452]/30 animate-pulse" />
</div>
</motion.div>
</div>
</div>
</motion.div>
{/* Decorative Kanji/Background elements for Japanese Zen Vibe */}
<div className="absolute top-20 left-20 pointer-events-none select-none opacity-[0.03] flex flex-col items-center">
<span className="text-[200px] font-serif leading-none"></span>
<span className="text-[100px] font-serif leading-none mt-[-40px]"></span>
</div>
<div className="absolute bottom-12 right-12 flex gap-4 opacity-[0.05] pointer-events-none">
<div className="w-4 h-32 bg-[#D28F4F] rounded-full blur-[1px]" />
<div className="w-4 h-48 bg-[#D28F4F] rounded-full mt-10 blur-[1px]" />
<div className="w-4 h-32 bg-[#D28F4F] rounded-full mt-4 blur-[1px]" />
{/* Right - Login Form */}
<LoginForm
account={account} setAccount={setAccount}
password={password} setPassword={setPassword}
captcha={captcha} setCaptcha={setCaptcha}
captchaImage={captchaImage}
passwordVisible={passwordVisible} setPasswordVisible={setPasswordVisible}
passwordFocused={passwordFocused} setPasswordFocused={setPasswordFocused}
loading={loading}
onSubmit={handleLogin}
onRefreshCaptcha={fetchCaptcha}
/>
</div>
</div>
);
}
/* ──── Clean Login Form ──── */
interface LoginFormProps {
account: string; setAccount: (v: string) => void;
password: string; setPassword: (v: string) => void;
captcha: string; setCaptcha: (v: string) => void;
captchaImage: string;
passwordVisible: boolean; setPasswordVisible: (v: boolean) => void;
passwordFocused: boolean; setPasswordFocused: (v: boolean) => void;
loading: boolean;
onSubmit: (e: React.FormEvent) => void;
onRefreshCaptcha: () => void;
}
function LoginForm({
account, setAccount, password, setPassword,
captcha, setCaptcha, captchaImage,
passwordVisible, setPasswordVisible,
setPasswordFocused,
loading, onSubmit, onRefreshCaptcha,
}: LoginFormProps) {
return (
<div className="flex items-center justify-center px-8 py-12 bg-white rounded-r-3xl">
<div className="w-full max-w-[420px]">
{/* Mobile logo */}
<div className="lg:hidden flex items-center justify-center gap-2 mb-12">
<img src="/favicon.jpg" alt="logo"
className="w-8 h-8 rounded-lg object-cover" />
<span className="text-lg font-semibold"></span>
</div>
{/* Header */}
<div className="text-center mb-10">
<h1 className="text-3xl font-bold tracking-tight mb-2
text-gray-900">
</h1>
<p className="text-gray-500 text-sm"></p>
</div>
{/* Form */}
<form onSubmit={onSubmit} className="space-y-5">
<div className="space-y-2">
<Label className="text-sm font-medium text-gray-700">
</Label>
<Input
placeholder="请输入管理员账号"
className="h-12 bg-white border-gray-200
focus:border-gray-900 focus:ring-0
text-gray-900 placeholder:text-gray-400"
value={account}
onChange={(e) => setAccount(e.target.value)}
disabled={loading}
/>
</div>
<div className="space-y-2">
<Label className="text-sm font-medium text-gray-700">
</Label>
<div className="relative">
<Input
type={passwordVisible ? 'text' : 'password'}
placeholder="••••••••"
className="h-12 pr-10 bg-white border-gray-200
focus:border-gray-900 focus:ring-0
text-gray-900 placeholder:text-gray-400"
value={password}
onChange={(e) => setPassword(e.target.value)}
onFocus={() => setPasswordFocused(true)}
onBlur={() => setPasswordFocused(false)}
disabled={loading}
/>
<button type="button"
onClick={() => setPasswordVisible(!passwordVisible)}
className="absolute right-3 top-1/2 -translate-y-1/2
text-gray-400 hover:text-gray-700 transition-colors"
tabIndex={-1}>
{passwordVisible
? <Eye className="w-5 h-5" />
: <EyeOff className="w-5 h-5" />}
</button>
</div>
</div>
<div className="space-y-2">
<Label className="text-sm font-medium text-gray-700">
</Label>
<div className="flex gap-3">
<Input
placeholder="请输入验证码"
className="h-12 flex-1 bg-white border-gray-200
focus:border-gray-900 focus:ring-0
text-gray-900 placeholder:text-gray-400"
value={captcha}
onChange={(e) => setCaptcha(e.target.value)}
disabled={loading}
/>
<div className="h-12 w-36 border border-gray-200
rounded-md overflow-hidden cursor-pointer
hover:border-gray-400 transition-colors
flex items-center justify-center bg-gray-50"
onClick={onRefreshCaptcha}>
{captchaImage ? (
<img src={captchaImage} alt="captcha"
className="h-full w-full object-contain" />
) : (
<Disc3 className="w-5 h-5 text-gray-400
animate-spin" />
)}
</div>
</div>
</div>
<Button type="submit" disabled={loading}
className="w-full h-12 text-base font-bold
bg-gradient-to-r from-indigo-500 to-purple-600
hover:from-indigo-600 hover:to-purple-700
text-white border-0 shadow-lg shadow-indigo-500/30
rounded-xl transition-all duration-300 hover:scale-[1.02] btn-sweep">
{loading ? (
<span className="flex items-center gap-2">
<Disc3 className="w-5 h-5 animate-spin" />
...
</span>
) : '登 录'}
</Button>
</form>
</div>
</div>
);
+50 -50
View File
@@ -146,84 +146,84 @@ export default function Category() {
>
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6 px-2">
<div>
<h1 className="text-3xl md:text-4xl font-black tracking-tight text-[#4A3A2C] flex items-center gap-4">
<h1 className="text-3xl md:text-4xl font-black tracking-tight text-[#111827] flex items-center gap-4">
<div className="w-3 h-3 rounded-full bg-[#D28F4F] shadow-[0_0_20px_rgba(210,143,79,0.5)] animate-pulse" />
<div className="w-3 h-3 rounded-full bg-[#111827] shadow-[0_0_20px_rgba(210,143,79,0.5)] animate-pulse" />
</h1>
<p className="text-[#8C7E6C] font-medium mt-2 text-sm md:text-base"></p>
<p className="text-[#6b7280] font-medium mt-2 text-sm md:text-base"></p>
</div>
<Button
onClick={handleOpenAdd}
className="rounded-[1.2rem] md:rounded-[1.5rem] h-12 md:h-14 px-6 md:px-8 font-black shadow-xl shadow-[#D28F4F]/20 hover:scale-105 transition-all bg-gradient-to-r from-[#D28F4F] to-[#A64452] border-none group"
className="rounded-[1.2rem] md:rounded-[1.5rem] h-12 md:h-14 px-6 md:px-8 font-black shadow-xl shadow-[#111827]/20 hover:scale-105 transition-all bg-gradient-to-r from-[#111827] to-[#A64452] border-none group"
>
<Plus className="w-5 h-5 mr-2 md:mr-3 group-hover:rotate-90 transition-transform" />
</Button>
</div>
<Card className="glass-card warm-noise border-none shadow-glass rounded-[1.5rem] md:rounded-[2.5rem] overflow-hidden min-h-[500px] md:min-h-[600px] relative z-10">
<CardHeader className="p-6 md:p-10 border-b border-[#4A3A2C]/5 flex flex-col md:flex-row md:items-center justify-between gap-6 bg-[#FAF5E6]/40">
<Card className="glass-card border-none shadow-glass rounded-[1.5rem] md:rounded-[2.5rem] overflow-hidden min-h-[500px] md:min-h-[600px] relative z-10">
<CardHeader className="p-6 md:p-10 border-b border-[#111827]/5 flex flex-col md:flex-row md:items-center justify-between gap-6 bg-[#f9fafb]/40">
<div className="flex items-center gap-4">
<div className="p-2.5 md:p-3 rounded-xl md:rounded-2xl bg-[#D28F4F]/10">
<Layers className="w-5 h-5 md:w-6 md:h-6 text-[#D28F4F]" />
<div className="p-2.5 md:p-3 rounded-xl md:rounded-2xl bg-[#111827]/10">
<Layers className="w-5 h-5 md:w-6 md:h-6 text-[#111827]" />
</div>
<CardTitle className="text-xl md:text-2xl font-black tracking-tight text-[#4A3A2C]"></CardTitle>
<CardTitle className="text-xl md:text-2xl font-black tracking-tight text-[#111827]"></CardTitle>
</div>
<div className="relative group w-full max-w-sm">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-[#8C7E6C] group-focus-within:text-[#D28F4F] transition-colors" />
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-[#6b7280] group-focus-within:text-[#111827] transition-colors" />
<Input
placeholder="搜索分类名称..."
value={searchName}
onChange={(e) => setSearchName(e.target.value)}
className="pl-12 h-10 md:h-12 rounded-xl md:rounded-2xl border-none bg-[#FAF5E6]/80 focus:bg-white shadow-inner transition-all font-bold text-[#4A3A2C]"
className="pl-12 h-10 md:h-12 rounded-xl md:rounded-2xl border-none bg-[#f9fafb]/80 focus:bg-white shadow-inner transition-all font-bold text-[#111827]"
/>
</div>
</CardHeader>
<CardContent className="p-0">
<div className="overflow-x-auto overflow-y-hidden">
<Table className="min-w-[800px]">
<TableHeader className="bg-[#FAF5E6]/50">
<TableRow className="hover:bg-transparent border-[#4A3A2C]/5">
<TableHead className="w-[100px] pl-10 py-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#8C7E6C]"></TableHead>
<TableHead className="py-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#8C7E6C]"></TableHead>
<TableHead className="py-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#8C7E6C]"></TableHead>
<TableHead className="py-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#8C7E6C] text-center"></TableHead>
<TableHead className="py-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#8C7E6C] text-center"></TableHead>
<TableHead className="text-right pr-10 py-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#8C7E6C]"></TableHead>
<TableHeader className="bg-[#f9fafb]/50">
<TableRow className="hover:bg-transparent border-[#111827]/5">
<TableHead className="w-[100px] pl-10 py-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#6b7280]"></TableHead>
<TableHead className="py-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#6b7280]"></TableHead>
<TableHead className="py-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#6b7280]"></TableHead>
<TableHead className="py-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#6b7280] text-center"></TableHead>
<TableHead className="py-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#6b7280] text-center"></TableHead>
<TableHead className="text-right pr-10 py-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#6b7280]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading && data.length === 0 ? (
<TableRow><TableCell colSpan={6} className="h-96 text-center text-[#8C7E6C] font-black uppercase"><Disc3 className="w-12 h-12 mx-auto animate-spin-slow mb-4 text-[#D28F4F]/50" />...</TableCell></TableRow>
<TableRow><TableCell colSpan={6} className="h-96 text-center text-[#6b7280] font-black uppercase"><Disc3 className="w-12 h-12 mx-auto animate-spin-slow mb-4 text-[#111827]/50" />...</TableCell></TableRow>
) : data.length === 0 ? (
<TableRow><TableCell colSpan={6} className="h-96 text-center text-[#8C7E6C] text-sm font-black tracking-widest uppercase"></TableCell></TableRow>
<TableRow><TableCell colSpan={6} className="h-96 text-center text-[#6b7280] text-sm font-black tracking-widest uppercase"></TableCell></TableRow>
) : (
data.map((item, index) => (
<TableRow
key={item.id}
className="group border-[#4A3A2C]/5 hover:bg-[#FAF5E6]/80 transition-all duration-300 transform md:hover:scale-[1.002]"
className="group border-[#111827]/5 hover:bg-[#f9fafb]/80 transition-all duration-300 transform md:hover:scale-[1.002]"
>
<TableCell className="pl-10 py-6">
<div className="w-10 h-10 rounded-xl bg-white shadow-sm flex items-center justify-center border border-[#4A3A2C]/5 font-black text-[#D28F4F] text-xs">
<div className="w-10 h-10 rounded-xl bg-white shadow-sm flex items-center justify-center border border-[#111827]/5 font-black text-[#111827] text-xs">
{(page - 1) * pageSize + index + 1}
</div>
</TableCell>
<TableCell className="py-6">
<div className="flex items-center gap-3">
<Tag className="w-4 h-4 text-[#D28F4F]/30" />
<Tag className="w-4 h-4 text-[#111827]/30" />
<div>
<p className="font-black text-[#4A3A2C] md:group-hover:text-[#D28F4F] transition-colors text-base tracking-tight">{item.name}</p>
<p className="text-[10px] font-bold text-[#8C7E6C]/50 mt-0.5 tracking-tighter">ID: {item.id}</p>
<p className="font-black text-[#111827] md:group-hover:text-[#111827] transition-colors text-base tracking-tight">{item.name}</p>
<p className="text-[10px] font-bold text-[#6b7280]/50 mt-0.5 tracking-tighter">ID: {item.id}</p>
</div>
</div>
</TableCell>
<TableCell className="py-6 max-w-[350px]">
<p className="text-[12px] font-bold text-[#8C7E6C] line-clamp-2 italic leading-relaxed">
<p className="text-[12px] font-bold text-[#6b7280] line-clamp-2 italic leading-relaxed">
{item.description || "— 暂无描述 —"}
</p>
</TableCell>
<TableCell className="py-6 text-center">
<span className="text-[10px] md:text-[11px] font-black px-3 md:px-4 py-1.5 rounded-full bg-[#FAF5E6] text-[#D28F4F] shadow-sm border border-[#D28F4F]/10">
<span className="text-[10px] md:text-[11px] font-black px-3 md:px-4 py-1.5 rounded-full bg-[#f9fafb] text-[#111827] shadow-sm border border-[#111827]/10">
LV.{item.sort}
</span>
</TableCell>
@@ -232,7 +232,7 @@ export default function Category() {
? 'bg-emerald-50 text-emerald-700 border-emerald-200'
: 'bg-rose-50 text-rose-600 border-rose-200'
}`}>
<div className={`w-1.5 h-1.5 rounded-full mr-2 ${item.status === 1 ? 'bg-emerald-500 animate-pulse' : 'bg-rose-500'}`} />
<div className={`w-1.5 h-1.5 rounded-full mr-2 ${item.status === 1 ? 'bg-emerald-500 animate-pulse' : 'bg-gray-900'}`} />
{item.status === 1 ? '启用' : '隐藏'}
</div>
</TableCell>
@@ -242,7 +242,7 @@ export default function Category() {
variant="ghost"
size="icon"
onClick={() => handleOpenEdit(item)}
className="w-10 h-10 md:w-12 md:h-12 rounded-xl md:rounded-[1.2rem] hover:bg-white hover:text-[#D28F4F] transition-all border border-[#4A3A2C]/5 shadow-sm hover:shadow-md"
className="w-10 h-10 md:w-12 md:h-12 rounded-xl md:rounded-[1.2rem] hover:bg-white hover:text-[#111827] transition-all border border-[#111827]/5 shadow-sm hover:shadow-md"
>
<Edit className="w-5 h-5" />
</Button>
@@ -250,7 +250,7 @@ export default function Category() {
variant="ghost"
size="icon"
onClick={() => handleDeleteClick(item.id)}
className="w-10 h-10 md:w-12 md:h-12 rounded-xl md:rounded-[1.2rem] hover:bg-rose-50 hover:text-rose-600 transition-all border border-[#4A3A2C]/5 shadow-sm"
className="w-10 h-10 md:w-12 md:h-12 rounded-xl md:rounded-[1.2rem] hover:bg-rose-50 hover:text-rose-600 transition-all border border-[#111827]/5 shadow-sm"
>
<Trash2 className="w-5 h-5" />
</Button>
@@ -263,17 +263,17 @@ export default function Category() {
</Table>
</div>
</CardContent>
<div className="p-6 md:p-10 border-t border-[#4A3A2C]/5 flex flex-col md:flex-row items-center justify-between gap-6 bg-[#FAF5E6]/40 backdrop-blur-md">
<div className="p-6 md:p-10 border-t border-[#111827]/5 flex flex-col md:flex-row items-center justify-between gap-6 bg-[#f9fafb]/40 backdrop-blur-md">
<div className="flex items-center gap-3">
<span className="text-[10px] font-black text-[#8C7E6C] uppercase tracking-widest hidden sm:inline">:</span>
<span className="px-4 py-1.5 bg-white shadow-sm rounded-full text-[10px] md:text-[11px] font-black text-[#D28F4F]"> {total} </span>
<span className="text-[10px] font-black text-[#6b7280] uppercase tracking-widest hidden sm:inline">:</span>
<span className="px-4 py-1.5 bg-white shadow-sm rounded-full text-[10px] md:text-[11px] font-black text-[#111827]"> {total} </span>
</div>
<div className="flex items-center gap-4">
<Button
variant="ghost"
onClick={() => setPage(p => Math.max(1, p - 1))}
disabled={page === 1}
className="rounded-xl md:rounded-2xl h-10 md:h-11 px-4 md:px-6 font-black uppercase tracking-widest text-[10px] hover:bg-white text-[#8C7E6C] hover:text-[#D28F4F]"
className="rounded-xl md:rounded-2xl h-10 md:h-11 px-4 md:px-6 font-black uppercase tracking-widest text-[10px] hover:bg-white text-[#6b7280] hover:text-[#111827]"
>
</Button>
@@ -281,7 +281,7 @@ export default function Category() {
variant="ghost"
onClick={() => setPage(p => p + 1)}
disabled={data.length < pageSize}
className="rounded-xl md:rounded-2xl h-10 md:h-11 px-4 md:px-6 font-black uppercase tracking-widest text-[10px] hover:bg-white text-[#8C7E6C] hover:text-[#D28F4F]"
className="rounded-xl md:rounded-2xl h-10 md:h-11 px-4 md:px-6 font-black uppercase tracking-widest text-[10px] hover:bg-white text-[#6b7280] hover:text-[#111827]"
>
</Button>
@@ -290,26 +290,26 @@ export default function Category() {
</Card>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="max-w-[95vw] sm:max-w-[600px] glass-card warm-noise border-none rounded-[1.5rem] md:rounded-[3rem] p-0 overflow-hidden shadow-2xl">
<div className="bg-gradient-to-r from-[#D28F4F] to-[#A64452] p-8 md:p-12 text-white relative">
<DialogContent className="max-w-[95vw] sm:max-w-[600px] glass-card border-none rounded-[1.5rem] md:rounded-[3rem] p-0 overflow-hidden shadow-2xl">
<div className="bg-gradient-to-r from-[#111827] to-[#A64452] p-8 md:p-12 text-white relative">
<DialogTitle className="text-2xl md:text-3xl font-black tracking-tight">{isEdit ? '编辑分类方案' : '定义新分类架构'}</DialogTitle>
<p className="text-white/60 text-[10px] md:text-xs font-black uppercase tracking-[0.3em] mt-3">Architecture Blueprinting</p>
<Disc3 className="absolute right-6 md:right-12 top-1/2 -translate-y-1/2 w-16 h-16 md:w-24 md:h-24 text-white/10 animate-spin-slow hidden sm:block" />
</div>
<div className="p-6 md:p-12 space-y-8 md:space-y-10 max-h-[70vh] overflow-y-auto custom-scrollbar bg-[#FAF5E6]/60 backdrop-blur-3xl">
<div className="p-6 md:p-12 space-y-8 md:space-y-10 max-h-[70vh] overflow-y-auto custom-scrollbar bg-[#f9fafb]/60 backdrop-blur-3xl">
<div className="space-y-6 md:space-y-8">
<div className="space-y-2 md:space-y-3">
<Label className="text-[10px] md:text-[11px] uppercase font-black tracking-widest text-[#8C7E6C] ml-1"></Label>
<Label className="text-[10px] md:text-[11px] uppercase font-black tracking-widest text-[#6b7280] ml-1"></Label>
<Input
placeholder="输入辨识度高的名称..."
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className="h-14 md:h-16 rounded-[1.2rem] md:rounded-[1.5rem] border-none bg-white shadow-sm font-bold text-[#4A3A2C] focus:ring-4 ring-[#D28F4F]/10 transition-all pl-6 md:pl-8 text-base md:text-lg"
className="h-14 md:h-16 rounded-[1.2rem] md:rounded-[1.5rem] border-none bg-white shadow-sm font-bold text-[#111827] focus:ring-4 ring-[#111827]/10 transition-all pl-6 md:pl-8 text-base md:text-lg"
/>
</div>
<div className="grid grid-cols-2 gap-4 md:gap-6">
<div className="space-y-2 md:space-y-3">
<Label className="text-[10px] md:text-[11px] uppercase font-black tracking-widest text-[#8C7E6C] ml-1"></Label>
<Label className="text-[10px] md:text-[11px] uppercase font-black tracking-widest text-[#6b7280] ml-1"></Label>
<Input
type="number"
value={formData.sort ?? ""}
@@ -317,13 +317,13 @@ export default function Category() {
const val = e.target.value;
setFormData({ ...formData, sort: val === "" ? "" : parseInt(val) });
}}
className="h-14 md:h-16 rounded-[1.2rem] md:rounded-[1.5rem] border-none bg-white shadow-sm font-bold text-center text-[#4A3A2C] focus:ring-4 ring-[#D28F4F]/10 transition-all text-base md:text-lg"
className="h-14 md:h-16 rounded-[1.2rem] md:rounded-[1.5rem] border-none bg-white shadow-sm font-bold text-center text-[#111827] focus:ring-4 ring-[#111827]/10 transition-all text-base md:text-lg"
/>
</div>
<div className="space-y-2 md:space-y-3">
<Label className="text-[10px] md:text-[11px] uppercase font-black tracking-widest text-[#8C7E6C] ml-1"></Label>
<Label className="text-[10px] md:text-[11px] uppercase font-black tracking-widest text-[#6b7280] ml-1"></Label>
<select
className="w-full h-14 md:h-16 rounded-[1.2rem] md:rounded-[1.5rem] bg-white shadow-sm px-4 md:px-8 font-bold text-[#4A3A2C] border-none focus:ring-4 ring-[#D28F4F]/10 transition-all outline-none text-base md:text-lg appearance-none cursor-pointer"
className="w-full h-14 md:h-16 rounded-[1.2rem] md:rounded-[1.5rem] bg-white shadow-sm px-4 md:px-8 font-bold text-[#111827] border-none focus:ring-4 ring-[#111827]/10 transition-all outline-none text-base md:text-lg appearance-none cursor-pointer"
value={formData.status}
onChange={e => setFormData({ ...formData, status: parseInt(e.target.value) })}
>
@@ -334,26 +334,26 @@ export default function Category() {
</div>
</div>
<div className="space-y-2 md:space-y-3">
<Label className="text-[10px] md:text-[11px] uppercase font-black tracking-widest text-[#8C7E6C] ml-1"></Label>
<Label className="text-[10px] md:text-[11px] uppercase font-black tracking-widest text-[#6b7280] ml-1"></Label>
<textarea
className="w-full min-h-[120px] md:min-h-[140px] rounded-[1.2rem] md:rounded-[2rem] border-none bg-white shadow-sm p-6 md:p-8 font-bold text-[#4A3A2C] focus:ring-4 ring-[#D28F4F]/10 transition-all outline-none resize-none placeholder:text-[#8C7E6C]/30 text-base md:text-lg"
className="w-full min-h-[120px] md:min-h-[140px] rounded-[1.2rem] md:rounded-[2rem] border-none bg-white shadow-sm p-6 md:p-8 font-bold text-[#111827] focus:ring-4 ring-[#111827]/10 transition-all outline-none resize-none placeholder:text-[#6b7280]/30 text-base md:text-lg"
placeholder="描述此分类涵盖的内容风格与核心特质..."
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
/>
</div>
</div>
<div className="p-8 md:p-10 bg-white/40 backdrop-blur-xl flex flex-col sm:flex-row gap-4 sm:justify-between items-center border-t border-[#4A3A2C]/5">
<div className="p-8 md:p-10 bg-white/40 backdrop-blur-xl flex flex-col sm:flex-row gap-4 sm:justify-between items-center border-t border-[#111827]/5">
<Button
variant="ghost"
onClick={() => setOpen(false)}
className="w-full sm:w-auto rounded-[1rem] md:rounded-[1.5rem] h-12 md:h-14 px-10 font-black uppercase tracking-[0.2em] text-[10px] md:text-xs hover:bg-white text-[#8C7E6C]"
className="w-full sm:w-auto rounded-[1rem] md:rounded-[1.5rem] h-12 md:h-14 px-10 font-black uppercase tracking-[0.2em] text-[10px] md:text-xs hover:bg-white text-[#6b7280]"
>
</Button>
<Button
onClick={handleSubmit}
className="w-full sm:w-auto rounded-[1rem] md:rounded-[1.5rem] h-12 md:h-14 px-10 md:px-12 font-black shadow-2xl shadow-[#D28F4F]/30 hover:scale-105 transition-all bg-gradient-to-r from-[#D28F4F] to-[#A64452] border-none text-white text-[11px] md:text-sm"
className="w-full sm:w-auto rounded-[1rem] md:rounded-[1.5rem] h-12 md:h-14 px-10 md:px-12 font-black shadow-2xl shadow-[#111827]/30 hover:scale-105 transition-all bg-gradient-to-r from-[#111827] to-[#A64452] border-none text-white text-[11px] md:text-sm"
>
{isEdit ? '保存架构变更' : '部署新架构节点'}
</Button>
+77 -77
View File
@@ -229,15 +229,15 @@ export default function Channel() {
>
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6 px-2">
<div>
<h1 className="text-3xl md:text-4xl font-black tracking-tight text-[#4A3A2C] flex items-center gap-4">
<h1 className="text-3xl md:text-4xl font-black tracking-tight text-[#111827] flex items-center gap-4">
<Smile className="w-8 h-8 text-[#D28F4F] animate-bounce" />
<Smile className="w-8 h-8 text-[#111827] animate-bounce" />
</h1>
<p className="text-[#8C7E6C] font-medium mt-2 text-sm md:text-base"> Emoji </p>
<p className="text-[#6b7280] font-medium mt-2 text-sm md:text-base"> Emoji </p>
</div>
<Button
onClick={handleOpenAdd}
className="rounded-[1.2rem] md:rounded-[1.5rem] h-12 md:h-14 px-6 md:px-8 font-black shadow-xl shadow-[#D28F4F]/20 hover:scale-105 transition-all bg-gradient-to-r from-[#D28F4F] to-[#A64452] border-none group"
className="rounded-[1.2rem] md:rounded-[1.5rem] h-12 md:h-14 px-6 md:px-8 font-black shadow-xl shadow-[#111827]/20 hover:scale-105 transition-all bg-gradient-to-r from-[#111827] to-[#A64452] border-none group"
>
<Plus className="w-5 h-5 mr-2 md:mr-3 group-hover:rotate-90 transition-transform" />
@@ -246,20 +246,20 @@ export default function Channel() {
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 md:gap-8 items-start">
{/* Sidebar Categories */}
<Card className="lg:col-span-3 glass-card warm-noise border-none shadow-glass rounded-[1.5rem] md:rounded-[2.5rem] overflow-hidden relative lg:sticky lg:top-28">
<CardHeader className="p-4 md:p-8 border-b border-[#4A3A2C]/5 bg-[#FAF5E6]/40">
<CardTitle className="text-xs font-black uppercase tracking-[0.3em] text-[#8C7E6C] flex items-center gap-3">
<Card className="lg:col-span-3 glass-card border-none shadow-glass rounded-[1.5rem] md:rounded-[2.5rem] overflow-hidden relative lg:sticky lg:top-28">
<CardHeader className="p-4 md:p-8 border-b border-[#111827]/5 bg-[#f9fafb]/40">
<CardTitle className="text-xs font-black uppercase tracking-[0.3em] text-[#6b7280] flex items-center gap-3">
<LayoutGrid className="w-4 h-4" />
</CardTitle>
</CardHeader>
<CardContent className="p-4 bg-[#FAF5E6]/20">
<CardContent className="p-4 bg-[#f9fafb]/20">
<div className="flex flex-col gap-2 max-h-[60vh] overflow-y-auto custom-scrollbar">
<button
onClick={() => setSelectedCategoryId("")}
className={`w-full text-left px-5 py-4 rounded-3xl text-sm font-black transition-all flex items-center justify-between group ${selectedCategoryId === ""
? 'bg-[#D28F4F] text-white shadow-lg'
: 'text-[#8C7E6C] hover:bg-white/60 hover:text-[#4A3A2C]'
? 'bg-[#111827] text-white shadow-lg'
: 'text-[#6b7280] hover:bg-white/60 hover:text-[#111827]'
}`}
>
@@ -273,8 +273,8 @@ export default function Channel() {
key={catId}
onClick={() => setSelectedCategoryId(catId)}
className={`w-full text-left px-5 py-4 rounded-3xl text-sm font-black transition-all flex items-center justify-between group ${isSelected
? 'bg-[#4A3A2C] text-white shadow-lg'
: 'text-[#8C7E6C] hover:bg-white/60 hover:text-[#4A3A2C]'
? 'bg-[#111827] text-white shadow-lg'
: 'text-[#6b7280] hover:bg-white/60 hover:text-[#111827]'
}`}
>
<span className="truncate">{category.name}</span>
@@ -287,74 +287,74 @@ export default function Channel() {
</Card>
{/* Main Table Content */}
<Card className="lg:col-span-9 glass-card warm-noise border-none shadow-glass rounded-[1.5rem] md:rounded-[2.5rem] overflow-hidden min-h-[500px] md:min-h-[700px] relative z-10">
<CardHeader className="p-6 md:p-10 border-b border-[#4A3A2C]/5 flex flex-col md:flex-row md:items-center justify-between gap-6 bg-[#FAF5E6]/40">
<Card className="lg:col-span-9 glass-card border-none shadow-glass rounded-[1.5rem] md:rounded-[2.5rem] overflow-hidden min-h-[500px] md:min-h-[700px] relative z-10">
<CardHeader className="p-6 md:p-10 border-b border-[#111827]/5 flex flex-col md:flex-row md:items-center justify-between gap-6 bg-[#f9fafb]/40">
<div className="flex items-center gap-4">
<div className="w-1.5 h-6 md:h-8 bg-[#D28F4F] rounded-full shadow-[0_0_12px_#D28F4F]" />
<CardTitle className="text-xl md:text-2xl font-black tracking-tight text-[#4A3A2C]">
<div className="w-1.5 h-6 md:h-8 bg-[#111827] rounded-full shadow-[0_0_12px_#111827]" />
<CardTitle className="text-xl md:text-2xl font-black tracking-tight text-[#111827]">
{selectedCategoryId ? categories.find(c => c.id === selectedCategoryId)?.name : '电台频道全集'}
</CardTitle>
</div>
<div className="relative group w-full max-w-sm">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-[#8C7E6C] group-focus-within:text-[#D28F4F] transition-colors" />
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-[#6b7280] group-focus-within:text-[#111827] transition-colors" />
<Input
placeholder="搜索频道名称..."
value={searchName}
onChange={(e) => setSearchName(e.target.value)}
className="pl-12 h-10 md:h-12 rounded-xl md:rounded-2xl border-none bg-[#FAF5E6]/80 focus:bg-white shadow-inner transition-all font-bold text-[#4A3A2C]"
className="pl-12 h-10 md:h-12 rounded-xl md:rounded-2xl border-none bg-[#f9fafb]/80 focus:bg-white shadow-inner transition-all font-bold text-[#111827]"
/>
</div>
</CardHeader>
<CardContent className="p-0">
<div className="overflow-x-auto overflow-y-hidden">
<Table className="min-w-[800px]">
<TableHeader className="bg-[#FAF5E6]/50">
<TableRow className="hover:bg-transparent border-[#4A3A2C]/5">
<TableHead className="w-[100px] pl-10 py-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#8C7E6C]">Emoji </TableHead>
<TableHead className="py-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#8C7E6C]"></TableHead>
<TableHead className="py-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#8C7E6C]"> ()</TableHead>
<TableHead className="py-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#8C7E6C] text-center"></TableHead>
<TableHead className="py-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#8C7E6C]"></TableHead>
<TableHead className="text-right pr-10 py-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#8C7E6C]"></TableHead>
<TableHeader className="bg-[#f9fafb]/50">
<TableRow className="hover:bg-transparent border-[#111827]/5">
<TableHead className="w-[100px] pl-10 py-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#6b7280]">Emoji </TableHead>
<TableHead className="py-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#6b7280]"></TableHead>
<TableHead className="py-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#6b7280]"> ()</TableHead>
<TableHead className="py-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#6b7280] text-center"></TableHead>
<TableHead className="py-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#6b7280]"></TableHead>
<TableHead className="text-right pr-10 py-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#6b7280]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow><TableCell colSpan={6} className="h-96 text-center text-[#8C7E6C] font-black uppercase"><Palette className="w-12 h-12 mx-auto animate-spin-slow mb-4 text-[#D28F4F]/50" />...</TableCell></TableRow>
<TableRow><TableCell colSpan={6} className="h-96 text-center text-[#6b7280] font-black uppercase"><Palette className="w-12 h-12 mx-auto animate-spin-slow mb-4 text-[#111827]/50" />...</TableCell></TableRow>
) : data.length === 0 ? (
<TableRow><TableCell colSpan={6} className="h-96 text-center text-[#8C7E6C] text-sm font-black tracking-widest uppercase"></TableCell></TableRow>
<TableRow><TableCell colSpan={6} className="h-96 text-center text-[#6b7280] text-sm font-black tracking-widest uppercase"></TableCell></TableRow>
) : (
data.map((item) => (
<TableRow
key={item.id}
className="group border-[#4A3A2C]/5 hover:bg-[#FAF5E6]/80 transition-all duration-300 transform md:hover:scale-[1.002] relative"
className="group border-[#111827]/5 hover:bg-[#f9fafb]/80 transition-all duration-300 transform md:hover:scale-[1.002] relative"
>
<TableCell className="pl-10 py-6">
<div className="w-16 h-16 md:w-20 md:h-20 rounded-2xl md:rounded-[2rem] bg-white shadow-sm flex items-center justify-center text-3xl md:text-5xl group-hover:scale-110 group-hover:rotate-6 transition-all border border-[#FAF5E6]">
<div className="w-16 h-16 md:w-20 md:h-20 rounded-2xl md:rounded-[2rem] bg-white shadow-sm flex items-center justify-center text-3xl md:text-5xl group-hover:scale-110 group-hover:rotate-6 transition-all border border-[#f9fafb]">
{item.cover || '📻'}
</div>
</TableCell>
<TableCell className="py-6">
<div className="max-w-[200px]">
<p className="font-black text-[#4A3A2C] md:group-hover:text-[#D28F4F] transition-colors text-base md:text-lg tracking-tight">{item.name}</p>
<p className="text-[11px] font-bold text-[#8C7E6C]/60 mt-1 line-clamp-1 italic">#{categories.find(c => c.id === String(item.categoryId))?.name || '未分类'}</p>
<p className="font-black text-[#111827] md:group-hover:text-[#111827] transition-colors text-base md:text-lg tracking-tight">{item.name}</p>
<p className="text-[11px] font-bold text-[#6b7280]/60 mt-1 line-clamp-1 italic">#{categories.find(c => c.id === String(item.categoryId))?.name || '未分类'}</p>
</div>
</TableCell>
<TableCell className="py-6">
<div className="flex gap-4">
<div className="text-center group/price md:hover:translate-y-[-2px] transition-transform">
<p className="text-[10px] font-black text-[#8C7E6C]/50 uppercase"></p>
<p className="text-xs md:text-sm font-black text-[#4A3A2C]">¥{(item.monthlyPrice / 100).toFixed(2).replace(/\.00$/, '')}</p>
<p className="text-[10px] font-black text-[#6b7280]/50 uppercase"></p>
<p className="text-xs md:text-sm font-black text-[#111827]">¥{(item.monthlyPrice / 100).toFixed(2).replace(/\.00$/, '')}</p>
</div>
<div className="w-px h-8 bg-[#4A3A2C]/5 mt-1" />
<div className="w-px h-8 bg-[#111827]/5 mt-1" />
<div className="text-center group/price md:hover:translate-y-[-2px] transition-transform">
<p className="text-[10px] font-black text-[#8C7E6C]/50 uppercase"></p>
<p className="text-xs md:text-sm font-black text-[#4A3A2C]">¥{(item.quarterlyPrice / 100).toFixed(2).replace(/\.00$/, '')}</p>
<p className="text-[10px] font-black text-[#6b7280]/50 uppercase"></p>
<p className="text-xs md:text-sm font-black text-[#111827]">¥{(item.quarterlyPrice / 100).toFixed(2).replace(/\.00$/, '')}</p>
</div>
<div className="w-px h-8 bg-[#4A3A2C]/5 mt-1" />
<div className="w-px h-8 bg-[#111827]/5 mt-1" />
<div className="text-center group/price md:hover:translate-y-[-2px] transition-transform">
<p className="text-[10px] font-black text-[#D28F4F]/60 uppercase"></p>
<p className="text-xs md:text-sm font-black text-[#D28F4F]">¥{(item.annualPrice / 100).toFixed(2).replace(/\.00$/, '')}</p>
<p className="text-[10px] font-black text-[#111827]/60 uppercase"></p>
<p className="text-xs md:text-sm font-black text-[#111827]">¥{(item.annualPrice / 100).toFixed(2).replace(/\.00$/, '')}</p>
</div>
</div>
</TableCell>
@@ -362,7 +362,7 @@ export default function Channel() {
<div className="flex justify-center">
{item.isVipOnly === 1 ? (
<div className="flex items-center gap-1.5 px-4 py-1.5 rounded-full bg-amber-50 text-amber-700 border border-amber-200 shadow-sm">
<Crown className="w-3.5 h-3.5 fill-amber-500" />
<Crown className="w-3.5 h-3.5 fill-gray-800" />
<span className="text-[10px] font-black uppercase">VIP </span>
</div>
) : item.isFree === 1 ? (
@@ -371,7 +371,7 @@ export default function Channel() {
<span className="text-[10px] font-black uppercase tracking-tighter"></span>
</div>
) : (
<span className="text-[11px] font-black text-[#8C7E6C]/40 uppercase tracking-widest text-shadow-sm">访</span>
<span className="text-[11px] font-black text-[#6b7280]/40 uppercase tracking-widest text-shadow-sm">访</span>
)}
</div>
</TableCell>
@@ -380,7 +380,7 @@ export default function Channel() {
? 'bg-emerald-50 text-emerald-700 border-emerald-200'
: 'bg-rose-50 text-rose-600 border-rose-200'
}`}>
<div className={`w-1.5 h-1.5 rounded-full mr-2 ${item.status === 1 ? 'bg-emerald-500 animate-pulse' : 'bg-rose-500'}`} />
<div className={`w-1.5 h-1.5 rounded-full mr-2 ${item.status === 1 ? 'bg-emerald-500 animate-pulse' : 'bg-gray-900'}`} />
{item.status === 1 ? '启用' : '下架'}
</div>
</TableCell>
@@ -390,7 +390,7 @@ export default function Channel() {
variant="ghost"
size="icon"
onClick={() => handleOpenEdit(item)}
className="w-10 h-10 md:w-12 md:h-12 rounded-[1rem] md:rounded-[1.2rem] hover:bg-white hover:text-[#D28F4F] transition-all border border-[#4A3A2C]/5 shadow-sm hover:shadow-md"
className="w-10 h-10 md:w-12 md:h-12 rounded-[1rem] md:rounded-[1.2rem] hover:bg-white hover:text-[#111827] transition-all border border-[#111827]/5 shadow-sm hover:shadow-md"
>
<Edit className="w-5 h-5" />
</Button>
@@ -398,7 +398,7 @@ export default function Channel() {
variant="ghost"
size="icon"
onClick={() => handleDeleteClick(item.id)}
className="w-10 h-10 md:w-12 md:h-12 rounded-[1rem] md:rounded-[1.2rem] hover:bg-rose-50 hover:text-rose-600 transition-all border border-[#4A3A2C]/5 shadow-sm"
className="w-10 h-10 md:w-12 md:h-12 rounded-[1rem] md:rounded-[1.2rem] hover:bg-rose-50 hover:text-rose-600 transition-all border border-[#111827]/5 shadow-sm"
>
<Trash2 className="w-5 h-5" />
</Button>
@@ -417,25 +417,25 @@ export default function Channel() {
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
className="p-6 md:p-10 border-t border-[#4A3A2C]/5 flex items-center justify-center bg-[#FAF5E6]/40 backdrop-blur-md"
className="p-6 md:p-10 border-t border-[#111827]/5 flex items-center justify-center bg-[#f9fafb]/40 backdrop-blur-md"
>
<div className="flex items-center gap-4 md:gap-6">
<Button
variant="ghost"
onClick={() => setPage(p => Math.max(1, p - 1))}
disabled={page === 1}
className="rounded-xl md:rounded-2xl px-4 md:px-8 h-10 md:h-12 font-black uppercase tracking-widest text-[10px] md:text-[11px] hover:bg-white text-[#8C7E6C] hover:text-[#D28F4F]"
className="rounded-xl md:rounded-2xl px-4 md:px-8 h-10 md:h-12 font-black uppercase tracking-widest text-[10px] md:text-[11px] hover:bg-white text-[#6b7280] hover:text-[#111827]"
>
</Button>
<div className="px-4 md:px-8 py-2 md:py-2.5 bg-white/60 backdrop-blur-md rounded-full shadow-inner border border-[#D28F4F]/10 text-[10px] md:text-[11px] font-black uppercase tracking-[0.2em] text-[#8C7E6C] whitespace-nowrap">
<span className="text-[#D28F4F]">{page}</span> / {Math.ceil(total / pageSize)}
<div className="px-4 md:px-8 py-2 md:py-2.5 bg-white/60 backdrop-blur-md rounded-full shadow-inner border border-[#111827]/10 text-[10px] md:text-[11px] font-black uppercase tracking-[0.2em] text-[#6b7280] whitespace-nowrap">
<span className="text-[#111827]">{page}</span> / {Math.ceil(total / pageSize)}
</div>
<Button
variant="ghost"
onClick={() => setPage(p => p + 1)}
disabled={data.length < pageSize}
className="rounded-xl md:rounded-2xl px-4 md:px-8 h-10 md:h-12 font-black uppercase tracking-widest text-[10px] md:text-[11px] hover:bg-white text-[#8C7E6C] hover:text-[#D28F4F]"
className="rounded-xl md:rounded-2xl px-4 md:px-8 h-10 md:h-12 font-black uppercase tracking-widest text-[10px] md:text-[11px] hover:bg-white text-[#6b7280] hover:text-[#111827]"
>
</Button>
@@ -448,8 +448,8 @@ export default function Channel() {
{/* Responsive Modal / Dialog */}
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="max-w-[95vw] sm:max-w-[850px] glass-card warm-noise border-none rounded-[2rem] md:rounded-[4rem] p-0 overflow-hidden shadow-2xl">
<div className="bg-gradient-to-r from-[#D28F4F] to-[#A64452] p-8 md:p-14 text-white relative">
<DialogContent className="max-w-[95vw] sm:max-w-[850px] glass-card border-none rounded-[2rem] md:rounded-[4rem] p-0 overflow-hidden shadow-2xl">
<div className="bg-gradient-to-r from-[#111827] to-[#A64452] p-8 md:p-14 text-white relative">
<div className="flex flex-col md:flex-row md:items-center gap-8">
<motion.div
animate={{ rotate: [0, 5, -5, 0] }}
@@ -465,21 +465,21 @@ export default function Channel() {
</div>
</div>
<div className="p-6 md:p-14 space-y-10 md:space-y-12 max-h-[60vh] overflow-y-auto custom-scrollbar bg-[#FAF5E6]/60 backdrop-blur-3xl">
<div className="p-6 md:p-14 space-y-10 md:space-y-12 max-h-[60vh] overflow-y-auto custom-scrollbar bg-[#f9fafb]/60 backdrop-blur-3xl">
<div className="grid grid-cols-1 md:grid-cols-2 gap-10 md:gap-14">
{/* Form Left Side */}
<div className="space-y-8">
<section className="space-y-4">
<Label className="text-[10px] md:text-[11px] uppercase font-black tracking-widest text-[#D28F4F] ml-1 flex items-center gap-2">
<Label className="text-[10px] md:text-[11px] uppercase font-black tracking-widest text-[#111827] ml-1 flex items-center gap-2">
<Smile className="w-4 h-4" />
Emoji
</Label>
<div className="grid grid-cols-5 md:grid-cols-10 gap-2 p-4 rounded-[2rem] bg-white/40 border border-[#D28F4F]/10">
<div className="grid grid-cols-5 md:grid-cols-10 gap-2 p-4 rounded-[2rem] bg-white/40 border border-[#111827]/10">
{EMOJI_LIST.map(emoji => (
<button
key={emoji}
onClick={() => setFormData({ ...formData, cover: emoji })}
className={`w-10 h-10 md:w-12 md:h-12 flex items-center justify-center text-xl md:text-2xl rounded-xl transition-all ${formData.cover === emoji ? 'bg-[#D28F4F] shadow-lg scale-110' : 'hover:bg-white/60'}`}
className={`w-10 h-10 md:w-12 md:h-12 flex items-center justify-center text-xl md:text-2xl rounded-xl transition-all ${formData.cover === emoji ? 'bg-[#111827] shadow-lg scale-110' : 'hover:bg-white/60'}`}
>
{emoji}
</button>
@@ -488,19 +488,19 @@ export default function Channel() {
</section>
<div className="space-y-4">
<Label className="text-[10px] md:text-[11px] uppercase font-black tracking-widest text-[#8C7E6C] ml-1"></Label>
<Label className="text-[10px] md:text-[11px] uppercase font-black tracking-widest text-[#6b7280] ml-1"></Label>
<Input
placeholder="输入辨识度高的名称..."
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className="h-14 md:h-16 rounded-2xl md:rounded-3xl border-none bg-white shadow-sm font-bold text-[#4A3A2C] focus:ring-4 ring-[#D28F4F]/10 transition-all pl-6 md:pl-8 text-lg"
className="h-14 md:h-16 rounded-2xl md:rounded-3xl border-none bg-white shadow-sm font-bold text-[#111827] focus:ring-4 ring-[#111827]/10 transition-all pl-6 md:pl-8 text-lg"
/>
</div>
<div className="space-y-4">
<Label className="text-[10px] md:text-[11px] uppercase font-black tracking-widest text-[#8C7E6C] ml-1"></Label>
<Label className="text-[10px] md:text-[11px] uppercase font-black tracking-widest text-[#6b7280] ml-1"></Label>
<select
className="w-full h-14 md:h-16 rounded-2xl md:rounded-3xl bg-white shadow-sm px-6 md:px-8 font-bold text-[#4A3A2C] border-none focus:ring-4 ring-[#D28F4F]/10 transition-all outline-none text-lg appearance-none cursor-pointer"
className="w-full h-14 md:h-16 rounded-2xl md:rounded-3xl bg-white shadow-sm px-6 md:px-8 font-bold text-[#111827] border-none focus:ring-4 ring-[#111827]/10 transition-all outline-none text-lg appearance-none cursor-pointer"
value={formData.categoryId}
onChange={e => setFormData({ ...formData, categoryId: e.target.value })}
>
@@ -514,34 +514,34 @@ export default function Channel() {
{/* Form Right Side */}
<div className="space-y-8">
<div className="rounded-[2.5rem] md:rounded-[3rem] border border-[#D28F4F]/10 shadow-sm bg-white/40 p-8 md:p-10 space-y-8">
<Label className="text-[10px] md:text-[11px] uppercase font-black tracking-widest text-[#D28F4F] flex items-center gap-2">
<div className="rounded-[2.5rem] md:rounded-[3rem] border border-[#111827]/10 shadow-sm bg-white/40 p-8 md:p-10 space-y-8">
<Label className="text-[10px] md:text-[11px] uppercase font-black tracking-widest text-[#111827] flex items-center gap-2">
<CalendarDays className="w-4 h-4" />
()
</Label>
<div className="space-y-4">
<div className="flex items-center gap-6 bg-white p-4 rounded-3xl shadow-sm">
<span className="text-[11px] font-black text-[#8C7E6C] uppercase w-12 text-center"></span>
<span className="text-[11px] font-black text-[#6b7280] uppercase w-12 text-center"></span>
<Input
type="number"
step="0.01"
value={toYuan(formData.monthlyPrice)}
onChange={e => setFormData({ ...formData, monthlyPrice: toCents(e.target.value) })}
className="h-12 border-none shadow-none font-black text-xl text-right flex-1 bg-transparent text-[#4A3A2C]"
className="h-12 border-none shadow-none font-black text-xl text-right flex-1 bg-transparent text-[#111827]"
/>
</div>
<div className="flex items-center gap-6 bg-white p-4 rounded-3xl shadow-sm">
<span className="text-[11px] font-black text-[#8C7E6C] uppercase w-12 text-center"></span>
<span className="text-[11px] font-black text-[#6b7280] uppercase w-12 text-center"></span>
<Input
type="number"
step="0.01"
value={toYuan(formData.quarterlyPrice)}
onChange={e => setFormData({ ...formData, quarterlyPrice: toCents(e.target.value) })}
className="h-12 border-none shadow-none font-black text-xl text-right flex-1 bg-transparent text-[#4A3A2C]"
className="h-12 border-none shadow-none font-black text-xl text-right flex-1 bg-transparent text-[#111827]"
/>
</div>
<div className="flex items-center gap-6 bg-gradient-to-r from-[#D28F4F]/10 to-[#A64452]/10 p-4 rounded-3xl ring-2 ring-[#D28F4F]/20 shadow-sm">
<span className="text-[11px] font-black text-[#D28F4F] uppercase w-12 text-center font-serif italic"></span>
<div className="flex items-center gap-6 bg-gradient-to-r from-[#111827]/10 to-[#A64452]/10 p-4 rounded-3xl ring-2 ring-[#111827]/20 shadow-sm">
<span className="text-[11px] font-black text-[#111827] uppercase w-12 text-center font-serif italic"></span>
<Input
type="number"
step="0.01"
@@ -555,7 +555,7 @@ export default function Channel() {
<div className="grid grid-cols-2 gap-6">
<div className="space-y-3">
<Label className="text-[10px] md:text-[11px] uppercase font-black tracking-widest text-[#8C7E6C] ml-1"></Label>
<Label className="text-[10px] md:text-[11px] uppercase font-black tracking-widest text-[#6b7280] ml-1"></Label>
<Input
type="number"
value={formData.sort ?? ""}
@@ -563,13 +563,13 @@ export default function Channel() {
const val = e.target.value;
setFormData({ ...formData, sort: val === "" ? 0 : parseInt(val) });
}}
className="h-14 md:h-16 rounded-3xl border-none bg-white shadow-sm font-black text-center text-[#4A3A2C] focus:ring-4 ring-[#D28F4F]/10 transition-all text-xl"
className="h-14 md:h-16 rounded-3xl border-none bg-white shadow-sm font-black text-center text-[#111827] focus:ring-4 ring-[#111827]/10 transition-all text-xl"
/>
</div>
<div className="space-y-3">
<Label className="text-[10px] md:text-[11px] uppercase font-black tracking-widest text-[#8C7E6C] ml-1"></Label>
<Label className="text-[10px] md:text-[11px] uppercase font-black tracking-widest text-[#6b7280] ml-1"></Label>
<select
className="w-full h-14 md:h-16 rounded-3xl bg-white shadow-sm px-6 font-bold text-[#4A3A2C] border-none focus:ring-4 ring-[#D28F4F]/10 transition-all outline-none text-base appearance-none cursor-pointer"
className="w-full h-14 md:h-16 rounded-3xl bg-white shadow-sm px-6 font-bold text-[#111827] border-none focus:ring-4 ring-[#111827]/10 transition-all outline-none text-base appearance-none cursor-pointer"
value={formData.isVipOnly === 1 ? 'vip' : formData.isFree === 1 ? 'free' : 'standard'}
onChange={e => {
const val = e.target.value;
@@ -593,9 +593,9 @@ export default function Channel() {
{/* Textareas */}
<div className="space-y-6">
<Label className="text-[10px] md:text-[11px] uppercase font-black tracking-widest text-[#8C7E6C] ml-1"></Label>
<Label className="text-[10px] md:text-[11px] uppercase font-black tracking-widest text-[#6b7280] ml-1"></Label>
<textarea
className="w-full min-h-[140px] rounded-[2.5rem] border-none bg-white shadow-sm p-8 font-bold text-[#4A3A2C] focus:ring-4 ring-[#D28F4F]/10 transition-all outline-none resize-none placeholder:text-[#8C7E6C]/30 text-lg"
className="w-full min-h-[140px] rounded-[2.5rem] border-none bg-white shadow-sm p-8 font-bold text-[#111827] focus:ring-4 ring-[#111827]/10 transition-all outline-none resize-none placeholder:text-[#6b7280]/30 text-lg"
placeholder="描述此频道的独特魅力与听众受众..."
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
@@ -604,17 +604,17 @@ export default function Channel() {
</div>
{/* Action Buttons */}
<div className="p-8 md:p-14 bg-white/60 backdrop-blur-2xl flex flex-col sm:flex-row gap-6 sm:justify-between items-center border-t border-[#4A3A2C]/5 font-black">
<div className="p-8 md:p-14 bg-white/60 backdrop-blur-2xl flex flex-col sm:flex-row gap-6 sm:justify-between items-center border-t border-[#111827]/5 font-black">
<Button
variant="ghost"
onClick={() => setOpen(false)}
className="w-full sm:w-auto rounded-3xl h-14 md:h-16 px-12 uppercase tracking-[0.3em] text-xs hover:bg-white text-[#8C7E6C]"
className="w-full sm:w-auto rounded-3xl h-14 md:h-16 px-12 uppercase tracking-[0.3em] text-xs hover:bg-white text-[#6b7280]"
>
</Button>
<Button
onClick={handleSubmit}
className="w-full sm:w-auto rounded-3xl h-14 md:h-16 px-14 md:px-16 shadow-2xl shadow-[#D28F4F]/30 hover:scale-105 transition-all bg-gradient-to-r from-[#D28F4F] to-[#A64452] border-none text-white text-base"
className="w-full sm:w-auto rounded-3xl h-14 md:h-16 px-14 md:px-16 shadow-2xl shadow-[#111827]/30 hover:scale-105 transition-all bg-gradient-to-r from-[#111827] to-[#A64452] border-none text-white text-base"
>
{isEdit ? '注入数据更新' : '立即发布频道'}
</Button>
+140 -80
View File
@@ -5,11 +5,12 @@ import {
updateProgramApi,
deleteProgramApi,
getChannelListApi,
generateTtsApi, getAllCategoryListApi
generateTtsApi, getAllCategoryListApi, getVoiceOptionsApi
} from '@/api/radio.ts';
import type {RadioProgram, RadioChannel, RadioCategory, ProgramFormData} from '@/types/radio.ts';
import type {RadioProgram, RadioChannel, RadioCategory, ProgramFormData, RadioVoice} from '@/types/radio.ts';
import {FileUploader} from '@/components/FileUploader.tsx';
import {DeleteConfirm} from '@/components/DeleteConfirm.tsx';
import {EmptyState} from '@/components/EmptyState.tsx';
import {Button} from '@/components/ui/button.tsx';
import {Input} from '@/components/ui/input.tsx';
import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from '@/components/ui/table.tsx';
@@ -82,6 +83,11 @@ export default function Program() {
const [playingId, setPlayingId] = useState<string | null>(null);
const audioRef = useRef<HTMLAudioElement | null>(null);
const [voices, setVoices] = useState<RadioVoice[]>([]);
const [ttsOpen, setTtsOpen] = useState(false);
const [ttsProgramId, setTtsProgramId] = useState<string | null>(null);
const [selectedSpeaker, setSelectedSpeaker] = useState<string>('');
const fetchChannels = async () => {
try {
const res = await getChannelListApi({pageSize: 100});
@@ -102,6 +108,20 @@ export default function Program() {
}
}
const fetchVoices = async () => {
try {
const res = await getVoiceOptionsApi();
const list = Array.isArray(res) ? res : ('list' in res ? (res as any).list : []);
setVoices(list);
if (list.length > 0) {
const def = list.find((v: RadioVoice) => v.isDefault === 1);
setSelectedSpeaker(def ? def.speakerId : list[0].speakerId);
}
} catch (e) {
console.error(e);
}
}
const toggleCat = (catId: string) => {
setExpandedCats(prev => {
const next = new Set(prev);
@@ -131,6 +151,7 @@ export default function Program() {
useEffect(() => {
fetchChannels();
fetchCategories();
fetchVoices();
}, []);
useEffect(() => {
@@ -198,13 +219,22 @@ export default function Program() {
}
};
const handleGenerateTts = async (id: string) => {
const handleOpenTts = (id: string) => {
setTtsProgramId(id);
setTtsOpen(true);
};
const confirmGenerateTts = async () => {
if (!ttsProgramId) return;
try {
await generateTtsApi(id);
await generateTtsApi(ttsProgramId, selectedSpeaker);
toast.success('语音生成任务已提交');
fetchData();
} catch (e) {
console.error(e);
} finally {
setTtsOpen(false);
setTtsProgramId(null);
}
};
@@ -266,17 +296,17 @@ export default function Program() {
</span>
<div className="w-2 h-2 rounded-full bg-[#A64452] animate-ping"/>
</div>
<h1 className="text-4xl md:text-5xl font-black tracking-tighter text-[#4A3A2C] leading-tight">
<h1 className="text-4xl md:text-5xl font-black tracking-tighter text-[#111827] leading-tight">
<span
className="text-transparent bg-clip-text bg-gradient-to-r from-[#D28F4F] to-[#A64452] mx-2 italic font-serif"></span>
className="text-transparent bg-clip-text bg-gradient-to-r from-[#111827] to-[#A64452] mx-2 italic font-serif"></span>
</h1>
<p className="text-[#8C7E6C] font-medium mt-4 text-sm md:text-base max-w-xl">
<p className="text-[#6b7280] font-medium mt-4 text-sm md:text-base max-w-xl">
Emoji
</p>
</div>
<Button
onClick={handleOpenAdd}
className="h-16 px-10 rounded-[2rem] bg-[#4A3A2C] hover:bg-[#D28F4F] text-white font-black shadow-2xl transition-all hover:scale-105 active:scale-95 group border-none"
className="h-16 px-10 rounded-[2rem] bg-[#111827] hover:bg-[#111827] text-white font-black shadow-2xl transition-all hover:scale-105 active:scale-95 group border-none"
>
<Plus className="w-5 h-5 mr-3 group-hover:rotate-90 transition-transform"/>
@@ -286,10 +316,10 @@ export default function Program() {
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 items-start">
{/* Category → Channel Tree Sidebar */}
<Card
className="lg:col-span-3 glass-card warm-noise border-none shadow-glass rounded-[2.5rem] overflow-hidden lg:sticky lg:top-28">
<CardHeader className="p-8 border-b border-[#4A3A2C]/5 bg-[#FAF5E6]/40">
className="lg:col-span-3 glass-card border-none shadow-glass rounded-[2.5rem] overflow-hidden lg:sticky lg:top-28">
<CardHeader className="p-8 border-b border-[#111827]/5 bg-[#f9fafb]/40">
<CardTitle
className="text-xs font-black uppercase tracking-[0.3em] text-[#8C7E6C] flex items-center gap-3">
className="text-xs font-black uppercase tracking-[0.3em] text-[#6b7280] flex items-center gap-3">
<FolderOpen className="w-4 h-4"/>
/
</CardTitle>
@@ -297,7 +327,7 @@ export default function Program() {
<div className="p-4 space-y-1 max-h-[60vh] overflow-y-auto custom-scrollbar">
<button
onClick={() => setSelectedChannelId("")}
className={`w-full text-left px-6 py-4 rounded-3xl text-sm font-black transition-all flex items-center justify-between group ${selectedChannelId === "" ? 'bg-[#D28F4F] text-white shadow-xl' : 'text-[#8C7E6C] hover:bg-white/60'}`}
className={`w-full text-left px-6 py-4 rounded-3xl text-sm font-black transition-all flex items-center justify-between group ${selectedChannelId === "" ? 'bg-[#111827] text-white shadow-xl' : 'text-[#6b7280] hover:bg-white/60'}`}
>
<Disc3
@@ -316,7 +346,7 @@ export default function Program() {
<div key={catId} className="mt-1">
<button
onClick={() => toggleCat(catId)}
className={`w-full text-left px-5 py-3 rounded-2xl text-xs font-black uppercase tracking-[0.15em] transition-all flex items-center gap-3 ${hasSelectedChild ? 'text-[#D28F4F] bg-[#D28F4F]/5' : 'text-[#8C7E6C]/70 hover:bg-white/40 hover:text-[#4A3A2C]'}`}
className={`w-full text-left px-5 py-3 rounded-2xl text-xs font-black uppercase tracking-[0.15em] transition-all flex items-center gap-3 ${hasSelectedChild ? 'text-[#111827] bg-[#111827]/5' : 'text-[#6b7280]/70 hover:bg-white/40 hover:text-[#111827]'}`}
>
{isExpanded
? <ChevronDown className="w-3.5 h-3.5 shrink-0"/>
@@ -325,12 +355,12 @@ export default function Program() {
<span className="ml-auto text-[10px] opacity-50">{catChannels.length}</span>
</button>
{isExpanded && (
<div className="ml-4 pl-3 border-l-2 border-[#D28F4F]/10 space-y-1 mt-1">
<div className="ml-4 pl-3 border-l-2 border-[#111827]/10 space-y-1 mt-1">
{catChannels.map((channel) => {
const chId = String(channel.id);
return (
<button key={chId} onClick={() => setSelectedChannelId(chId)}
className={`w-full text-left px-5 py-3 rounded-2xl text-sm font-bold transition-all flex items-center justify-between group ${selectedChannelId === chId ? 'bg-[#4A3A2C] text-white shadow-lg' : 'text-[#8C7E6C] hover:bg-white/60'}`}>
className={`w-full text-left px-5 py-3 rounded-2xl text-sm font-bold transition-all flex items-center justify-between group ${selectedChannelId === chId ? 'bg-[#111827] text-white shadow-lg' : 'text-[#6b7280] hover:bg-white/60'}`}>
<div className="flex items-center gap-2.5">
<span
className="text-base shrink-0">{channel.cover || '📻'}</span>
@@ -353,15 +383,15 @@ export default function Program() {
const uncategorized = channels.filter((ch) => !catIds.has(String(ch.categoryId)));
if (uncategorized.length === 0) return null;
return (
<div className="mt-2 pt-2 border-t border-[#4A3A2C]/5">
<div className="mt-2 pt-2 border-t border-[#111827]/5">
<div
className="px-5 py-2 text-[10px] font-black uppercase tracking-[0.15em] text-[#8C7E6C]/40">
className="px-5 py-2 text-[10px] font-black uppercase tracking-[0.15em] text-[#6b7280]/40">
</div>
{uncategorized.map((channel) => {
const chId = String(channel.id);
return (
<button key={chId} onClick={() => setSelectedChannelId(chId)}
className={`w-full text-left px-6 py-3 rounded-2xl text-sm font-bold transition-all flex items-center justify-between group ${selectedChannelId === chId ? 'bg-[#4A3A2C] text-white shadow-lg' : 'text-[#8C7E6C] hover:bg-white/60'}`}>
className={`w-full text-left px-6 py-3 rounded-2xl text-sm font-bold transition-all flex items-center justify-between group ${selectedChannelId === chId ? 'bg-[#111827] text-white shadow-lg' : 'text-[#6b7280] hover:bg-white/60'}`}>
<div className="flex items-center gap-2.5">
<span className="text-base shrink-0">{channel.cover || '📻'}</span>
<span className="truncate text-[13px]">{channel.name}</span>
@@ -379,72 +409,77 @@ export default function Program() {
{/* Programs Table */}
<Card
className="lg:col-span-9 glass-card warm-noise border-none shadow-glass rounded-[3rem] overflow-hidden min-h-[600px] relative z-10 bg-white/40">
className="lg:col-span-9 glass-card border-none shadow-glass rounded-[3rem] overflow-hidden min-h-[600px] relative z-10 bg-white/40">
<CardHeader
className="p-10 border-b border-[#4A3A2C]/5 flex flex-col md:flex-row md:items-center justify-between gap-8 bg-[#FAF5E6]/60 backdrop-blur-md">
className="p-10 border-b border-[#111827]/5 flex flex-col md:flex-row md:items-center justify-between gap-8 bg-[#f9fafb]/60 backdrop-blur-md">
<div className="flex items-center gap-6">
<div className="p-4 rounded-[1.5rem] bg-white shadow-xl rotate-[-3deg]">
<Music className="w-6 h-6 text-[#A64452]"/>
</div>
<div>
<CardTitle
className="text-2xl font-black text-[#4A3A2C] tracking-tight"></CardTitle>
<p className="text-[10px] font-black uppercase text-[#8C7E6C]/60 tracking-[0.2em] mt-1">Total {total} Episodes
className="text-2xl font-black text-[#111827] tracking-tight"></CardTitle>
<p className="text-[10px] font-black uppercase text-[#6b7280]/60 tracking-[0.2em] mt-1">Total {total} Episodes
recorded</p>
</div>
</div>
<div className="relative group w-full max-w-sm">
<Search
className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-[#8C7E6C] group-focus-within:text-[#D28F4F] transition-colors"/>
className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-[#6b7280] group-focus-within:text-[#111827] transition-colors"/>
<Input
placeholder="按标题检索内容..."
value={searchTitle}
onChange={(e) => setSearchTitle(e.target.value)}
className="pl-12 h-14 rounded-3xl border-none bg-white shadow-inner transition-all font-bold text-[#4A3A2C] placeholder:opacity-30"
className="pl-12 h-14 rounded-3xl border-none bg-white shadow-inner transition-all font-bold text-[#111827] placeholder:opacity-30"
/>
</div>
</CardHeader>
<CardContent className="p-0">
<div className="overflow-x-auto overflow-y-hidden">
<Table className="min-w-[1200px]">
<TableHeader className="bg-[#FAF5E6]/40">
<TableRow className="hover:bg-transparent border-[#4A3A2C]/5">
<TableHeader className="bg-[#f9fafb]/40">
<TableRow className="hover:bg-transparent border-[#111827]/5">
<TableHead
className="w-[80px] pl-10 py-6 text-[10px] font-black uppercase tracking-[0.3em] text-[#8C7E6C]"></TableHead>
className="w-[80px] pl-10 py-6 text-[10px] font-black uppercase tracking-[0.3em] text-[#6b7280]"></TableHead>
<TableHead
className="py-6 text-[10px] font-black uppercase tracking-[0.3em] text-[#8C7E6C]"></TableHead>
className="py-6 text-[10px] font-black uppercase tracking-[0.3em] text-[#6b7280]"></TableHead>
<TableHead
className="py-6 text-[10px] font-black uppercase tracking-[0.3em] text-[#8C7E6C]"></TableHead>
className="py-6 text-[10px] font-black uppercase tracking-[0.3em] text-[#6b7280]"></TableHead>
<TableHead
className="py-6 text-[10px] font-black uppercase tracking-[0.3em] text-[#8C7E6C] text-center"></TableHead>
className="py-6 text-[10px] font-black uppercase tracking-[0.3em] text-[#6b7280] text-center"></TableHead>
<TableHead
className="text-right pr-10 py-6 text-[10px] font-black uppercase tracking-[0.3em] text-[#8C7E6C]"></TableHead>
className="text-right pr-10 py-6 text-[10px] font-black uppercase tracking-[0.3em] text-[#6b7280]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow><TableCell colSpan={5}
className="h-96 text-center text-[#8C7E6C] font-black uppercase tracking-[0.5em]"><Disc3
className="h-96 text-center text-[#6b7280] font-black uppercase tracking-[0.5em]"><Disc3
className="w-16 h-16 mx-auto animate-spin-slow mb-6 text-[#A64452]/40"/>Synchronizing
Session...</TableCell></TableRow>
) : data.length === 0 ? (
<TableRow><TableCell colSpan={5}
className="h-96 text-center text-[#8C7E6C] text-xs font-black tracking-[0.5em] uppercase">No
Audio Units Found</TableCell></TableRow>
<TableRow>
<TableCell colSpan={5} className="p-0 border-none">
<EmptyState title="没找到任何节目录音" description="这里空空如也,尝试切换频道或点击上方按钮录制一段新声音吧。" className="py-20" />
</TableCell>
</TableRow>
) : (
data.map((item) => (
<TableRow
data.map((item, idx) => (
<motion.tr
key={item.id}
className="group border-[#4A3A2C]/5 hover:bg-white/80 transition-all duration-500"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: idx * 0.05 }}
className="group border-[#111827]/5 hover:bg-white transition-all duration-300 shadow-sm hover:shadow-soft"
>
<TableCell className="pl-10 py-8">
<div className="relative group/cover">
<div
className="w-16 h-16 md:w-20 md:h-20 rounded-3xl bg-white shadow-2xl flex items-center justify-center text-3xl md:text-5xl group-hover:scale-110 group-hover:rotate-6 transition-all duration-500 border border-[#FAF5E6] z-10 relative">
className="w-16 h-16 md:w-20 md:h-20 rounded-3xl bg-white shadow-2xl flex items-center justify-center text-3xl md:text-5xl group-hover:scale-110 group-hover:rotate-6 transition-all duration-500 border border-[#f9fafb] z-10 relative">
{item.cover || '🎵'}
</div>
<div
className="absolute -inset-2 bg-gradient-to-br from-[#D28F4F]/20 to-[#A64452]/20 blur-2xl opacity-0 group-hover/cover:opacity-100 transition-opacity"/>
className="absolute -inset-2 bg-gradient-to-br from-[#111827]/20 to-[#A64452]/20 blur-2xl opacity-0 group-hover/cover:opacity-100 transition-opacity"/>
</div>
</TableCell>
<TableCell className="py-8">
@@ -452,28 +487,28 @@ export default function Program() {
<div className="flex items-center gap-4">
<button
onClick={() => togglePlay(item)}
className={`w-10 h-10 rounded-full flex items-center justify-center transition-all ${playingId === item.id ? 'bg-[#A64452] text-white animate-pulse' : 'bg-[#4A3A2C] text-white hover:bg-[#D28F4F] shadow-lg hover:scale-110'}`}
className={`w-10 h-10 rounded-full flex items-center justify-center transition-all ${playingId === item.id ? 'bg-[#A64452] text-white animate-pulse' : 'bg-[#111827] text-white hover:bg-[#111827] shadow-lg hover:scale-110'}`}
>
{playingId === item.id ?
<Pause className="w-4 h-4 fill-current"/> :
<Play className="w-4 h-4 fill-current ml-0.5"/>}
</button>
<div>
<h4 className="text-lg font-black text-[#4A3A2C] group-hover:text-[#D28F4F] transition-colors line-clamp-1">{item.title}</h4>
<p className="text-[10px] font-black uppercase text-[#8C7E6C]/50 tracking-widest flex items-center gap-2 mt-1">
<h4 className="text-lg font-black text-[#111827] group-hover:text-[#111827] transition-colors line-clamp-1">{item.title}</h4>
<p className="text-[10px] font-black uppercase text-[#6b7280]/50 tracking-widest flex items-center gap-2 mt-1">
<Tag className="w-3 h-3 text-[#A64452]/40"/>
{channels.find(c => c.id === String(item.channelId))?.name || '公域单元'}
</p>
</div>
</div>
<p className="text-xs font-medium text-[#8C7E6C] line-clamp-3 max-w-[500px] italic leading-relaxed pl-14 opacity-80 group-hover:opacity-100 transition-opacity">
<p className="text-xs font-medium text-[#6b7280] line-clamp-3 max-w-[500px] italic leading-relaxed pl-14 opacity-80 group-hover:opacity-100 transition-opacity">
{item.description || "— 此单元尚未注入灵魂描述 —"}
</p>
</div>
</TableCell>
<TableCell className="py-8">
<div className="space-y-3">
<div className="flex items-center gap-3 text-[#8C7E6C]">
<div className="flex items-center gap-3 text-[#6b7280]">
<Clock className="w-3.5 h-3.5"/>
<span
className="text-sm font-black tabular-nums">{formatDuration(item.duration)}</span>
@@ -481,7 +516,7 @@ export default function Program() {
<div className="flex flex-wrap gap-2">
{(item.tags || "").split(/[, ]/).filter((t: string) => t).map((tag: string, i: number) => (
<span key={i}
className="px-3 py-1 bg-white border border-[#4A3A2C]/5 rounded-full text-[9px] font-black text-[#8C7E6C] hover:border-[#D28F4F]/30 hover:text-[#D28F4F] transition-all cursor-default">
className="px-3 py-1 bg-white border border-[#111827]/5 rounded-full text-[9px] font-black text-[#6b7280] hover:border-[#111827]/30 hover:text-[#111827] transition-all cursor-default">
{tag}
</span>
))}
@@ -495,7 +530,7 @@ export default function Program() {
: 'bg-rose-50 text-rose-600 border-rose-100/50'
}`}>
<div
className={`w-2 h-2 rounded-full mr-2.5 ${item.status === 1 ? 'bg-emerald-500 animate-pulse shadow-[0_0_8px_#10b981]' : 'bg-rose-500'}`}/>
className={`w-2 h-2 rounded-full mr-2.5 ${item.status === 1 ? 'bg-emerald-500 animate-pulse shadow-[0_0_8px_#10b981]' : 'bg-gray-900'}`}/>
{item.status === 1 ? '正在发布' : '仓库封存'}
</div>
</TableCell>
@@ -508,9 +543,9 @@ export default function Program() {
<Button
variant={hasAudio || isGenerating ? "outline" : "default"}
size="sm"
onClick={() => handleGenerateTts(item.id)}
onClick={() => handleOpenTts(item.id)}
disabled={hasAudio || isGenerating}
className={`h-9 px-4 rounded-xl shadow-sm transition-all border border-[#4A3A2C]/5 ${hasAudio || isGenerating ? 'text-[#8C7E6C]/50 bg-gray-50/50 cursor-not-allowed' : 'bg-[#FAF5E6] text-[#D28F4F] hover:bg-white hover:shadow-md hover:text-[#A64452]'}`}
className={`h-9 px-4 rounded-xl shadow-sm transition-all border border-[#111827]/5 ${hasAudio || isGenerating ? 'text-[#6b7280]/50 bg-gray-50/50 cursor-not-allowed' : 'bg-[#f9fafb] text-[#111827] hover:bg-white hover:shadow-md hover:text-[#A64452]'}`}
title={hasAudio ? "语音文件已存在" : (isGenerating ? "正在生成中" : "生成语音文件")}
>
{isGenerating ? (
@@ -528,7 +563,7 @@ export default function Program() {
variant="ghost"
size="sm"
onClick={() => handleOpenEdit(item)}
className="h-9 px-4 rounded-xl bg-[#f8f9fa] hover:bg-white hover:text-[#D28F4F] shadow-sm transition-all border border-[#4A3A2C]/5"
className="h-9 px-4 rounded-xl bg-[#f8f9fa] hover:bg-white hover:text-[#111827] shadow-sm transition-all border border-[#111827]/5"
>
<Edit className="w-4 h-4 mr-1.5"/>
@@ -537,14 +572,14 @@ export default function Program() {
variant="ghost"
size="sm"
onClick={() => handleDeleteClick(item.id)}
className="h-9 px-4 rounded-xl bg-[#fff0f0] hover:bg-rose-50 hover:text-rose-600 shadow-sm transition-all border border-[#4A3A2C]/5"
className="h-9 px-4 rounded-xl bg-[#fff0f0] hover:bg-rose-50 hover:text-rose-600 shadow-sm transition-all border border-[#111827]/5"
>
<Trash2 className="w-4 h-4 mr-1.5"/>
</Button>
</div>
</TableCell>
</TableRow>
</motion.tr>
))
)}
</TableBody>
@@ -552,29 +587,29 @@ export default function Program() {
</div>
</CardContent>
<div
className="p-10 border-t border-[#4A3A2C]/5 flex flex-col sm:flex-row items-center justify-between gap-8 bg-[#FAF5E6]/40 backdrop-blur-xl">
className="p-10 border-t border-[#111827]/5 flex flex-col sm:flex-row items-center justify-between gap-8 bg-[#f9fafb]/40 backdrop-blur-xl">
<div className="flex items-center gap-4">
<Headphones className="w-5 h-5 text-[#D28F4F] opacity-40"/>
<span className="text-[10px] font-black text-[#8C7E6C] uppercase tracking-[0.4em]">Audio Master Control</span>
<Headphones className="w-5 h-5 text-[#111827] opacity-40"/>
<span className="text-[10px] font-black text-[#6b7280] uppercase tracking-[0.4em]">Audio Master Control</span>
</div>
<div className="flex items-center gap-4">
<Button
variant="ghost"
onClick={() => setPage(p => Math.max(1, p - 1))}
disabled={page === 1}
className="rounded-2xl h-12 px-8 font-black uppercase tracking-widest text-[11px] hover:bg-white text-[#8C7E6C] hover:text-[#A64452] border border-transparent hover:border-[#A64452]/10"
className="rounded-2xl h-12 px-8 font-black uppercase tracking-widest text-[11px] hover:bg-white text-[#6b7280] hover:text-[#A64452] border border-transparent hover:border-[#A64452]/10"
>
</Button>
<div
className="px-6 py-2 bg-white/50 rounded-full font-black text-[11px] text-[#4A3A2C] shadow-inner border border-white">
className="px-6 py-2 bg-white/50 rounded-full font-black text-[11px] text-[#111827] shadow-inner border border-white">
{page}
</div>
<Button
variant="ghost"
onClick={() => setPage(p => p + 1)}
disabled={data.length < pageSize}
className="rounded-2xl h-12 px-8 font-black uppercase tracking-widest text-[11px] hover:bg-white text-[#8C7E6C] hover:text-[#A64452] border border-transparent hover:border-[#A64452]/10"
className="rounded-2xl h-12 px-8 font-black uppercase tracking-widest text-[11px] hover:bg-white text-[#6b7280] hover:text-[#A64452] border border-transparent hover:border-[#A64452]/10"
>
</Button>
@@ -586,19 +621,19 @@ export default function Program() {
{/* Program Edit/Create Modal */}
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent
className="max-w-[95vw] sm:max-w-[900px] glass-card warm-noise border-none rounded-[3rem] p-0 overflow-hidden shadow-2xl flex flex-col md:flex-row">
className="max-w-[95vw] sm:max-w-[900px] glass-card border-none rounded-[3rem] p-0 overflow-hidden shadow-2xl flex flex-col md:flex-row">
{/* Artistic Side Sidebar */}
<div
className="md:w-1/3 bg-gradient-to-b from-[#4A3A2C] to-[#2D241C] p-10 md:p-14 text-white space-y-12">
className="md:w-1/3 bg-gradient-to-b from-[#111827] to-[#2D241C] p-10 md:p-14 text-white space-y-12">
<div className="space-y-4">
<span className="text-[10px] font-black uppercase tracking-[0.5em] text-[#D28F4F]">Visual Anchor</span>
<span className="text-[10px] font-black uppercase tracking-[0.5em] text-[#111827]">Visual Anchor</span>
<motion.div
animate={{y: [0, -10, 0]}}
transition={{repeat: Infinity, duration: 3, ease: "easeInOut"}}
className="w-full aspect-square rounded-[3rem] bg-white/5 backdrop-blur-3xl border border-white/10 shadow-3xl flex items-center justify-center text-8xl md:text-9xl group relative overflow-hidden"
>
<div
className="absolute inset-0 bg-gradient-to-br from-[#D28F4F]/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity"/>
className="absolute inset-0 bg-gradient-to-br from-[#111827]/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity"/>
{formData.cover}
</motion.div>
</div>
@@ -618,32 +653,32 @@ export default function Program() {
{/* Main Form Fields */}
<div
className="flex-1 p-10 md:p-14 space-y-10 bg-[#FAF5E6]/95 backdrop-blur-3xl overflow-y-auto max-h-[85vh] custom-scrollbar">
className="flex-1 p-10 md:p-14 space-y-10 bg-[#f9fafb]/95 backdrop-blur-3xl overflow-y-auto max-h-[85vh] custom-scrollbar">
<div className="space-y-2">
<DialogTitle className="text-4xl font-black text-[#4A3A2C] tracking-tighter">
<DialogTitle className="text-4xl font-black text-[#111827] tracking-tighter">
{isEdit ? '深度打磨单元' : '开启声音共鸣'}
</DialogTitle>
<p className="text-[#8C7E6C] text-xs font-black uppercase tracking-[0.3em]">Audio
<p className="text-[#6b7280] text-xs font-black uppercase tracking-[0.3em]">Audio
Engineering Workshop</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-10">
<div className="space-y-8">
<div className="space-y-4">
<Label className="text-[10px] uppercase font-black tracking-widest text-[#D28F4F]">1.
<Label className="text-[10px] uppercase font-black tracking-widest text-[#111827]">1.
</Label>
<Input
placeholder="输入引发共鸣的标题..."
value={formData.title}
onChange={(e) => setFormData({...formData, title: e.target.value})}
className="h-16 rounded-[1.5rem] md:rounded-[2rem] border-none bg-white shadow-xl px-10 font-bold text-[#4A3A2C] text-lg focus:ring-4 ring-[#D28F4F]/10 transition-all"
className="h-16 rounded-[1.5rem] md:rounded-[2rem] border-none bg-white shadow-xl px-10 font-bold text-[#111827] text-lg focus:ring-4 ring-[#111827]/10 transition-all"
/>
</div>
<div className="space-y-4">
<Label className="text-[10px] uppercase font-black tracking-widest text-[#D28F4F]">2.
<Label className="text-[10px] uppercase font-black tracking-widest text-[#111827]">2.
</Label>
<select
className="w-full h-16 rounded-[1.5rem] md:rounded-[2rem] bg-white shadow-xl px-10 font-bold text-[#4A3A2C] border-none outline-none appearance-none cursor-pointer focus:ring-4 ring-[#D28F4F]/10 transition-all"
className="w-full h-16 rounded-[1.5rem] md:rounded-[2rem] bg-white shadow-xl px-10 font-bold text-[#111827] border-none outline-none appearance-none cursor-pointer focus:ring-4 ring-[#111827]/10 transition-all"
value={formData.channelId}
onChange={e => setFormData({...formData, channelId: e.target.value})}
>
@@ -655,7 +690,7 @@ export default function Program() {
</select>
</div>
<div className="space-y-4">
<Label className="text-[10px] uppercase font-black tracking-widest text-[#D28F4F]">3.
<Label className="text-[10px] uppercase font-black tracking-widest text-[#111827]">3.
()</Label>
<div className="grid grid-cols-2 gap-4">
<Input
@@ -666,13 +701,13 @@ export default function Program() {
...formData,
duration: parseInt(e.target.value) || 0
})}
className="h-16 rounded-3xl border-none bg-white shadow-xl px-8 font-black text-center text-[#4A3A2C]"
className="h-16 rounded-3xl border-none bg-white shadow-xl px-8 font-black text-center text-[#111827]"
/>
<Input
placeholder="标签(空格分隔)"
value={formData.tags}
onChange={e => setFormData({...formData, tags: e.target.value})}
className="h-16 rounded-3xl border-none bg-white shadow-xl px-6 font-bold text-[#4A3A2C]"
className="h-16 rounded-3xl border-none bg-white shadow-xl px-6 font-bold text-[#111827]"
/>
</div>
</div>
@@ -680,18 +715,18 @@ export default function Program() {
<div className="space-y-6">
<Label
className="text-[10px] uppercase font-black tracking-widest text-[#D28F4F] flex items-center gap-2">
className="text-[10px] uppercase font-black tracking-widest text-[#111827] flex items-center gap-2">
<Smile className="w-4 h-4"/>
4. Emoji
</Label>
<div
className="grid grid-cols-6 lg:grid-cols-8 gap-2 p-6 rounded-[2.5rem] bg-white/60 shadow-inner border border-[#D28F4F]/10">
className="grid grid-cols-6 lg:grid-cols-8 gap-2 p-6 rounded-[2.5rem] bg-white/60 shadow-inner border border-[#111827]/10">
{EMOJI_LIST.map(emoji => (
<button
key={emoji}
type="button"
onClick={() => setFormData({...formData, cover: emoji})}
className={`w-10 h-10 md:w-11 md:h-11 flex items-center justify-center text-xl rounded-2xl transition-all ${formData.cover === emoji ? 'bg-[#D28F4F] text-white shadow-lg scale-110 rotate-12' : 'hover:bg-white hover:shadow-md'}`}
className={`w-10 h-10 md:w-11 md:h-11 flex items-center justify-center text-xl rounded-2xl transition-all ${formData.cover === emoji ? 'bg-[#111827] text-white shadow-lg scale-110 rotate-12' : 'hover:bg-white hover:shadow-md'}`}
>
{emoji}
</button>
@@ -701,17 +736,17 @@ export default function Program() {
</div>
<div className="space-y-6">
<Label className="text-[10px] uppercase font-black tracking-widest text-[#D28F4F]">5.
<Label className="text-[10px] uppercase font-black tracking-widest text-[#111827]">5.
</Label>
<div className="space-y-4">
<textarea
className="w-full min-h-[100px] rounded-3xl border-none bg-white shadow-xl p-8 font-bold text-[#4A3A2C] focus:ring-4 ring-[#D28F4F]/10 transition-all outline-none resize-none placeholder:text-[#8C7E6C]/30 text-base italic"
className="w-full min-h-[100px] rounded-3xl border-none bg-white shadow-xl p-8 font-bold text-[#111827] focus:ring-4 ring-[#111827]/10 transition-all outline-none resize-none placeholder:text-[#6b7280]/30 text-base italic"
placeholder="描述此单元的氛围、背景与核心价值..."
value={formData.description}
onChange={(e) => setFormData({...formData, description: e.target.value})}
/>
<textarea
className="w-full min-h-[160px] rounded-3xl border-none bg-white shadow-xl p-8 font-bold text-[#4A3A2C] focus:ring-4 ring-[#D28F4F]/10 transition-all outline-none resize-none placeholder:text-[#8C7E6C]/30 text-lg"
className="w-full min-h-[160px] rounded-3xl border-none bg-white shadow-xl p-8 font-bold text-[#111827] focus:ring-4 ring-[#111827]/10 transition-all outline-none resize-none placeholder:text-[#6b7280]/30 text-lg"
placeholder="在这里输入播报的核心文案或详细内容板块..."
value={formData.content}
onChange={(e) => setFormData({...formData, content: e.target.value})}
@@ -723,13 +758,13 @@ export default function Program() {
<Button
variant="ghost"
onClick={() => setOpen(false)}
className="w-full sm:w-auto rounded-3xl h-16 px-12 font-black uppercase tracking-[0.4em] text-xs hover:bg-white text-[#8C7E6C]"
className="w-full sm:w-auto rounded-3xl h-16 px-12 font-black uppercase tracking-[0.4em] text-xs hover:bg-white text-[#6b7280]"
>
</Button>
<Button
onClick={handleSubmit}
className="w-full sm:w-auto rounded-3xl h-16 px-16 shadow-3xl shadow-[#A64452]/20 hover:scale-[1.02] transition-all bg-gradient-to-r from-[#4A3A2C] to-[#2D241C] border-none text-[#D28F4F] font-black text-lg"
className="w-full sm:w-auto rounded-3xl h-16 px-16 shadow-3xl shadow-[#A64452]/20 hover:scale-[1.02] transition-all bg-gradient-to-r from-[#111827] to-[#2D241C] border-none text-[#111827] font-black text-lg"
>
{isEdit ? '注入数据进化' : '正式发布声音单元'}
<ArrowRight className="ml-4 w-5 h-5 shadow-2xl"/>
@@ -739,6 +774,31 @@ export default function Program() {
</DialogContent>
</Dialog>
<Dialog open={ttsOpen} onOpenChange={setTtsOpen}>
<DialogContent className="max-w-[400px] glass-card border-none rounded-[2rem] p-6 shadow-2xl bg-[#f9fafb] backdrop-blur-3xl">
<DialogTitle className="text-xl font-black text-[#111827] mb-4">()</DialogTitle>
<div className="space-y-4">
<Label className="text-xs uppercase font-black text-[#6b7280]">Speaker</Label>
<select
className="w-full h-12 rounded-xl bg-white shadow-sm px-4 font-bold text-[#111827] border-none outline-none focus:ring-2 ring-[#111827]/20 cursor-pointer"
value={selectedSpeaker}
onChange={(e) => setSelectedSpeaker(e.target.value)}
>
{voices.map(v => (
<option key={v.id} value={v.speakerId}>
{v.name} ({v.gender === 'male' ? '男' : v.gender === 'female' ? '女' : '中性'}) {v.isDefault ? ' [默认]' : ''}
</option>
))}
<option value="">(使)</option>
</select>
<div className="flex gap-3 mt-6 pt-4 border-t border-[#111827]/5 justify-end">
<Button variant="ghost" onClick={() => setTtsOpen(false)} className="rounded-xl font-black text-[#6b7280]"></Button>
<Button onClick={confirmGenerateTts} className="rounded-xl bg-[#A64452] hover:bg-[#8e3845] text-white font-black border-none shadow-md"></Button>
</div>
</div>
</DialogContent>
</Dialog>
<DeleteConfirm
open={deleteOpen}
onOpenChange={setDeleteOpen}
+68 -68
View File
@@ -40,82 +40,82 @@ export default function UserManagement() {
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="space-y-6 md:space-y-8 pb-10">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6 px-2">
<div>
<h1 className="text-3xl md:text-4xl font-black tracking-tight text-[#4A3A2C] flex items-center gap-4">
<Users className="w-8 h-8 text-[#D28F4F] animate-bounce" />
<h1 className="text-3xl md:text-4xl font-black tracking-tight text-[#111827] flex items-center gap-4">
<Users className="w-8 h-8 text-[#111827] animate-bounce" />
</h1>
<p className="text-[#8C7E6C] font-medium mt-2 text-sm md:text-base"></p>
<p className="text-[#6b7280] font-medium mt-2 text-sm md:text-base"></p>
</div>
<div className="flex items-center gap-3 bg-white/60 backdrop-blur-md rounded-2xl p-1.5 border border-[#D28F4F]/10 shadow-sm">
<div className="flex items-center gap-3 bg-white/60 backdrop-blur-md rounded-2xl p-1.5 border border-[#111827]/10 shadow-sm">
{vipTabs.map(tab => (
<button key={tab.value} onClick={() => { setVipFilter(tab.value); setPage(1); }}
className={`px-5 py-2.5 rounded-xl text-sm font-black transition-all ${vipFilter === tab.value ? 'bg-gradient-to-r from-[#D28F4F] to-[#A64452] text-white shadow-lg shadow-[#D28F4F]/20' : 'text-[#8C7E6C] hover:text-[#4A3A2C] hover:bg-white/60'}`}>
className={`px-5 py-2.5 rounded-xl text-sm font-black transition-all ${vipFilter === tab.value ? 'bg-gradient-to-r from-[#111827] to-[#A64452] text-white shadow-lg shadow-[#111827]/20' : 'text-[#6b7280] hover:text-[#111827] hover:bg-white/60'}`}>
{tab.value === 1 && <Crown className="w-3.5 h-3.5 inline mr-1.5 -mt-0.5" />}{tab.label}
</button>
))}
</div>
</div>
<Card className="glass-card warm-noise border-none shadow-glass rounded-[1.5rem] md:rounded-[2.5rem] overflow-hidden min-h-[500px] relative z-10">
<CardHeader className="p-6 md:p-10 border-b border-[#4A3A2C]/5 flex flex-col md:flex-row md:items-center justify-between gap-6 bg-[#FAF5E6]/40">
<Card className="glass-card border-none shadow-glass rounded-[1.5rem] md:rounded-[2.5rem] overflow-hidden min-h-[500px] relative z-10">
<CardHeader className="p-6 md:p-10 border-b border-[#111827]/5 flex flex-col md:flex-row md:items-center justify-between gap-6 bg-[#f9fafb]/40">
<div className="flex items-center gap-4">
<div className="w-1.5 h-6 md:h-8 bg-[#D28F4F] rounded-full shadow-[0_0_12px_#D28F4F]" />
<CardTitle className="text-xl md:text-2xl font-black tracking-tight text-[#4A3A2C]"> <span className="text-[#8C7E6C] text-sm font-bold ml-2"> {total} </span></CardTitle>
<div className="w-1.5 h-6 md:h-8 bg-[#111827] rounded-full shadow-[0_0_12px_#111827]" />
<CardTitle className="text-xl md:text-2xl font-black tracking-tight text-[#111827]"> <span className="text-[#6b7280] text-sm font-bold ml-2"> {total} </span></CardTitle>
</div>
<div className="relative group w-full max-w-sm">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-[#8C7E6C] group-focus-within:text-[#D28F4F] transition-colors" />
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-[#6b7280] group-focus-within:text-[#111827] transition-colors" />
<Input placeholder="搜索昵称 / 手机号 / 账号..." value={searchName} onChange={(e) => setSearchName(e.target.value)}
className="pl-12 h-10 md:h-12 rounded-xl md:rounded-2xl border-none bg-[#FAF5E6]/80 focus:bg-white shadow-inner transition-all font-bold text-[#4A3A2C]" />
className="pl-12 h-10 md:h-12 rounded-xl md:rounded-2xl border-none bg-[#f9fafb]/80 focus:bg-white shadow-inner transition-all font-bold text-[#111827]" />
</div>
</CardHeader>
<CardContent className="p-0">
<div className="overflow-x-auto overflow-y-hidden">
<Table className="min-w-[900px]">
<TableHeader className="bg-[#FAF5E6]/50">
<TableRow className="hover:bg-transparent border-[#4A3A2C]/5">
<TableHead className="pl-10 py-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#8C7E6C]"></TableHead>
<TableHead className="py-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#8C7E6C] text-center"></TableHead>
<TableHead className="py-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#8C7E6C] text-center"></TableHead>
<TableHead className="py-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#8C7E6C] text-center"></TableHead>
<TableHead className="py-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#8C7E6C] text-center"></TableHead>
<TableHead className="py-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#8C7E6C] text-center">()</TableHead>
<TableHead className="py-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#8C7E6C] text-center"></TableHead>
<TableHead className="text-right pr-10 py-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#8C7E6C]"></TableHead>
<TableHeader className="bg-[#f9fafb]/50">
<TableRow className="hover:bg-transparent border-[#111827]/5">
<TableHead className="pl-10 py-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#6b7280]"></TableHead>
<TableHead className="py-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#6b7280] text-center"></TableHead>
<TableHead className="py-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#6b7280] text-center"></TableHead>
<TableHead className="py-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#6b7280] text-center"></TableHead>
<TableHead className="py-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#6b7280] text-center"></TableHead>
<TableHead className="py-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#6b7280] text-center">()</TableHead>
<TableHead className="py-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#6b7280] text-center"></TableHead>
<TableHead className="text-right pr-10 py-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#6b7280]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow><TableCell colSpan={8} className="h-96 text-center text-[#8C7E6C] font-black uppercase"><Users className="w-12 h-12 mx-auto animate-spin-slow mb-4 text-[#D28F4F]/50" />...</TableCell></TableRow>
<TableRow><TableCell colSpan={8} className="h-96 text-center text-[#6b7280] font-black uppercase"><Users className="w-12 h-12 mx-auto animate-spin-slow mb-4 text-[#111827]/50" />...</TableCell></TableRow>
) : data.length === 0 ? (
<TableRow><TableCell colSpan={8} className="h-96 text-center text-[#8C7E6C] text-sm font-black tracking-widest uppercase"></TableCell></TableRow>
<TableRow><TableCell colSpan={8} className="h-96 text-center text-[#6b7280] text-sm font-black tracking-widest uppercase"></TableCell></TableRow>
) : data.map((item) => (
<TableRow key={item.id} className="group border-[#4A3A2C]/5 hover:bg-[#FAF5E6]/80 transition-all duration-300 md:hover:scale-[1.002] relative">
<TableRow key={item.id} className="group border-[#111827]/5 hover:bg-[#f9fafb]/80 transition-all duration-300 md:hover:scale-[1.002] relative">
<TableCell className="pl-10 py-5">
<div className="flex items-center gap-4">
<div className="relative">
<div className="w-12 h-12 rounded-2xl bg-gradient-to-br from-[#D28F4F]/20 to-[#A64452]/20 flex items-center justify-center text-lg font-black text-[#D28F4F] overflow-hidden border border-[#FAF5E6] shadow-sm">
<div className="w-12 h-12 rounded-2xl bg-gradient-to-br from-[#111827]/20 to-[#A64452]/20 flex items-center justify-center text-lg font-black text-[#111827] overflow-hidden border border-[#f9fafb] shadow-sm">
{item.avatarUrl ? <img src={item.avatarUrl} className="w-full h-full object-cover" /> : (item.name?.[0] || '👤')}
</div>
{item.isVip === 1 && <Crown className="absolute -top-1 -right-1 w-4 h-4 text-amber-500 fill-amber-400 drop-shadow" />}
{item.isVip === 1 && <Crown className="absolute -top-1 -right-1 w-4 h-4 text-gray-800 fill-gray-700 drop-shadow" />}
</div>
<div>
<p className="font-black text-[#4A3A2C] text-sm group-hover:text-[#D28F4F] transition-colors">{item.name || '匿名用户'}</p>
<p className="text-[10px] text-[#8C7E6C]/60 font-bold mt-0.5">{item.phone || item.account || '-'}</p>
<p className="font-black text-[#111827] text-sm group-hover:text-[#111827] transition-colors">{item.name || '匿名用户'}</p>
<p className="text-[10px] text-[#6b7280]/60 font-bold mt-0.5">{item.phone || item.account || '-'}</p>
</div>
</div>
</TableCell>
<TableCell className="py-5 text-center">
{item.isVip === 1
? <div className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-amber-50 text-amber-700 border border-amber-200 shadow-sm"><Crown className="w-3 h-3 fill-amber-500" /><span className="text-[10px] font-black uppercase">VIP</span></div>
: <span className="text-[11px] font-black text-[#8C7E6C]/40 uppercase"></span>}
? <div className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-amber-50 text-amber-700 border border-amber-200 shadow-sm"><Crown className="w-3 h-3 fill-gray-800" /><span className="text-[10px] font-black uppercase">VIP</span></div>
: <span className="text-[11px] font-black text-[#6b7280]/40 uppercase"></span>}
</TableCell>
<TableCell className="py-5 text-center"><div className="inline-flex items-center gap-1.5 text-sm font-black text-[#4A3A2C]"><Headphones className="w-3.5 h-3.5 text-[#D28F4F]/60" />{item.subscribeCount}</div></TableCell>
<TableCell className="py-5 text-center"><span className="text-sm font-black text-[#4A3A2C]">{item.listenCount}</span></TableCell>
<TableCell className="py-5 text-center"><div className="inline-flex items-center gap-1 text-sm font-black text-[#4A3A2C]"><Heart className="w-3.5 h-3.5 text-rose-400/60" />{item.favoriteCount}</div></TableCell>
<TableCell className="py-5 text-center"><span className="text-sm font-black text-[#D28F4F]">¥{toYuan(item.totalSpent)}</span></TableCell>
<TableCell className="py-5 text-center"><span className="text-[11px] font-bold text-[#8C7E6C]">{fmt(item.createdAt)}</span></TableCell>
<TableCell className="py-5 text-center"><div className="inline-flex items-center gap-1.5 text-sm font-black text-[#111827]"><Headphones className="w-3.5 h-3.5 text-[#111827]/60" />{item.subscribeCount}</div></TableCell>
<TableCell className="py-5 text-center"><span className="text-sm font-black text-[#111827]">{item.listenCount}</span></TableCell>
<TableCell className="py-5 text-center"><div className="inline-flex items-center gap-1 text-sm font-black text-[#111827]"><Heart className="w-3.5 h-3.5 text-rose-400/60" />{item.favoriteCount}</div></TableCell>
<TableCell className="py-5 text-center"><span className="text-sm font-black text-[#111827]">¥{toYuan(item.totalSpent)}</span></TableCell>
<TableCell className="py-5 text-center"><span className="text-[11px] font-bold text-[#6b7280]">{fmt(item.createdAt)}</span></TableCell>
<TableCell className="text-right pr-10 py-5">
<Button variant="ghost" size="icon" onClick={() => { setSelectedUser(item); setDetailOpen(true); }}
className="w-10 h-10 rounded-xl hover:bg-white hover:text-[#D28F4F] transition-all border border-[#4A3A2C]/5 shadow-sm hover:shadow-md opacity-0 group-hover:opacity-100 translate-x-4 group-hover:translate-x-0">
className="w-10 h-10 rounded-xl hover:bg-white hover:text-[#111827] transition-all border border-[#111827]/5 shadow-sm hover:shadow-md opacity-0 group-hover:opacity-100 translate-x-4 group-hover:translate-x-0">
<Eye className="w-4 h-4" />
</Button>
</TableCell>
@@ -128,17 +128,17 @@ export default function UserManagement() {
<AnimatePresence>
{total > pageSize && (
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: 10 }}
className="p-6 md:p-10 border-t border-[#4A3A2C]/5 flex items-center justify-center bg-[#FAF5E6]/40 backdrop-blur-md">
className="p-6 md:p-10 border-t border-[#111827]/5 flex items-center justify-center bg-[#f9fafb]/40 backdrop-blur-md">
<div className="flex items-center gap-4 md:gap-6">
<Button variant="ghost" onClick={() => setPage(p => Math.max(1, p - 1))} disabled={page === 1}
className="rounded-xl md:rounded-2xl px-4 md:px-8 h-10 md:h-12 font-black uppercase tracking-widest text-[10px] md:text-[11px] hover:bg-white text-[#8C7E6C] hover:text-[#D28F4F]">
className="rounded-xl md:rounded-2xl px-4 md:px-8 h-10 md:h-12 font-black uppercase tracking-widest text-[10px] md:text-[11px] hover:bg-white text-[#6b7280] hover:text-[#111827]">
<ChevronLeft className="w-4 h-4 mr-1" />
</Button>
<div className="px-4 md:px-8 py-2 md:py-2.5 bg-white/60 backdrop-blur-md rounded-full shadow-inner border border-[#D28F4F]/10 text-[10px] md:text-[11px] font-black uppercase tracking-[0.2em] text-[#8C7E6C] whitespace-nowrap">
<span className="text-[#D28F4F]">{page}</span> / {Math.ceil(total / pageSize)}
<div className="px-4 md:px-8 py-2 md:py-2.5 bg-white/60 backdrop-blur-md rounded-full shadow-inner border border-[#111827]/10 text-[10px] md:text-[11px] font-black uppercase tracking-[0.2em] text-[#6b7280] whitespace-nowrap">
<span className="text-[#111827]">{page}</span> / {Math.ceil(total / pageSize)}
</div>
<Button variant="ghost" onClick={() => setPage(p => p + 1)} disabled={data.length < pageSize}
className="rounded-xl md:rounded-2xl px-4 md:px-8 h-10 md:h-12 font-black uppercase tracking-widest text-[10px] md:text-[11px] hover:bg-white text-[#8C7E6C] hover:text-[#D28F4F]">
className="rounded-xl md:rounded-2xl px-4 md:px-8 h-10 md:h-12 font-black uppercase tracking-widest text-[10px] md:text-[11px] hover:bg-white text-[#6b7280] hover:text-[#111827]">
<ChevronRight className="w-4 h-4 ml-1" />
</Button>
</div>
@@ -147,13 +147,13 @@ export default function UserManagement() {
</AnimatePresence>
</Card>
<Dialog open={detailOpen} onOpenChange={setDetailOpen}>
<DialogContent className="max-w-[95vw] sm:max-w-[600px] glass-card warm-noise border-none rounded-[2rem] md:rounded-[3rem] p-0 overflow-hidden shadow-2xl">
<div className="bg-gradient-to-r from-[#D28F4F] to-[#A64452] p-8 md:p-12 text-white relative">
<DialogContent className="max-w-[95vw] sm:max-w-[600px] glass-card border-none rounded-[2rem] md:rounded-[3rem] p-0 overflow-hidden shadow-2xl">
<div className="bg-gradient-to-r from-[#111827] to-[#A64452] p-8 md:p-12 text-white relative">
<div className="flex items-center gap-6">
<div className="w-20 h-20 rounded-[1.5rem] bg-white shadow-2xl flex items-center justify-center text-3xl overflow-hidden shrink-0">
{selectedUser?.avatarUrl
? <img src={selectedUser.avatarUrl} className="w-full h-full object-cover" />
: <span className="text-[#D28F4F] font-black">{selectedUser?.nickName?.[0] || '👤'}</span>}
: <span className="text-[#111827] font-black">{selectedUser?.nickName?.[0] || '👤'}</span>}
</div>
<div>
<DialogTitle className="text-2xl md:text-3xl font-black tracking-tight flex items-center gap-2">
@@ -166,50 +166,50 @@ export default function UserManagement() {
</div>
</div>
</div>
<div className="p-8 md:p-10 space-y-6 bg-[#FAF5E6]/60">
<div className="p-8 md:p-10 space-y-6 bg-[#f9fafb]/60">
<div className="grid grid-cols-3 gap-4">
<div className="rounded-2xl bg-white p-4 text-center shadow-sm border border-[#4A3A2C]/5">
<div className="rounded-2xl bg-white p-4 text-center shadow-sm border border-[#111827]/5">
<Headphones className="w-5 h-5 mx-auto mb-2 text-blue-600" />
<p className="text-lg font-black text-[#4A3A2C]">{selectedUser?.subscribeCount}</p>
<p className="text-[10px] font-bold text-[#8C7E6C] uppercase tracking-wider mt-1"></p>
<p className="text-lg font-black text-[#111827]">{selectedUser?.subscribeCount}</p>
<p className="text-[10px] font-bold text-[#6b7280] uppercase tracking-wider mt-1"></p>
</div>
<div className="rounded-2xl bg-white p-4 text-center shadow-sm border border-[#4A3A2C]/5">
<div className="rounded-2xl bg-white p-4 text-center shadow-sm border border-[#111827]/5">
<Heart className="w-5 h-5 mx-auto mb-2 text-rose-600" />
<p className="text-lg font-black text-[#4A3A2C]">{selectedUser?.favoriteCount}</p>
<p className="text-[10px] font-bold text-[#8C7E6C] uppercase tracking-wider mt-1"></p>
<p className="text-lg font-black text-[#111827]">{selectedUser?.favoriteCount}</p>
<p className="text-[10px] font-bold text-[#6b7280] uppercase tracking-wider mt-1"></p>
</div>
<div className="rounded-2xl bg-white p-4 text-center shadow-sm border border-[#4A3A2C]/5">
<div className="rounded-2xl bg-white p-4 text-center shadow-sm border border-[#111827]/5">
<ShoppingBag className="w-5 h-5 mx-auto mb-2 text-amber-600" />
<p className="text-lg font-black text-[#4A3A2C]">¥{toYuan(selectedUser?.totalSpent || 0)}</p>
<p className="text-[10px] font-bold text-[#8C7E6C] uppercase tracking-wider mt-1"></p>
<p className="text-lg font-black text-[#111827]">¥{toYuan(selectedUser?.totalSpent || 0)}</p>
<p className="text-[10px] font-bold text-[#6b7280] uppercase tracking-wider mt-1"></p>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="rounded-2xl bg-white p-4 flex items-center gap-3 shadow-sm border border-[#4A3A2C]/5">
<Headphones className="w-4 h-4 text-[#D28F4F]" />
<div><p className="text-sm font-black text-[#4A3A2C]">{selectedUser?.listenCount}</p><p className="text-[10px] font-bold text-[#8C7E6C]"></p></div>
<div className="rounded-2xl bg-white p-4 flex items-center gap-3 shadow-sm border border-[#111827]/5">
<Headphones className="w-4 h-4 text-[#111827]" />
<div><p className="text-sm font-black text-[#111827]">{selectedUser?.listenCount}</p><p className="text-[10px] font-bold text-[#6b7280]"></p></div>
</div>
<div className="rounded-2xl bg-white p-4 flex items-center gap-3 shadow-sm border border-[#4A3A2C]/5">
<ShoppingBag className="w-4 h-4 text-[#D28F4F]" />
<div><p className="text-sm font-black text-[#4A3A2C]">{selectedUser?.orderCount}</p><p className="text-[10px] font-bold text-[#8C7E6C]"></p></div>
<div className="rounded-2xl bg-white p-4 flex items-center gap-3 shadow-sm border border-[#111827]/5">
<ShoppingBag className="w-4 h-4 text-[#111827]" />
<div><p className="text-sm font-black text-[#111827]">{selectedUser?.orderCount}</p><p className="text-[10px] font-bold text-[#6b7280]"></p></div>
</div>
</div>
<div className="rounded-2xl bg-white p-5 space-y-3 shadow-sm border border-[#4A3A2C]/5">
<div className="rounded-2xl bg-white p-5 space-y-3 shadow-sm border border-[#111827]/5">
<div className="flex items-center gap-3 text-sm">
<Clock className="w-4 h-4 text-[#8C7E6C]" /><span className="text-[#8C7E6C] font-bold"></span>
<span className="ml-auto text-[#4A3A2C] font-black">{fmt(selectedUser?.createdAt ?? null)}</span>
<Clock className="w-4 h-4 text-[#6b7280]" /><span className="text-[#6b7280] font-bold"></span>
<span className="ml-auto text-[#111827] font-black">{fmt(selectedUser?.createdAt ?? null)}</span>
</div>
<div className="flex items-center gap-3 text-sm">
<Clock className="w-4 h-4 text-[#8C7E6C]" /><span className="text-[#8C7E6C] font-bold"></span>
<span className="ml-auto text-[#4A3A2C] font-black">{fmt(selectedUser?.lastLoginAt ?? null)}</span>
<Clock className="w-4 h-4 text-[#6b7280]" /><span className="text-[#6b7280] font-bold"></span>
<span className="ml-auto text-[#111827] font-black">{fmt(selectedUser?.lastLoginAt ?? null)}</span>
</div>
<div className="flex items-center gap-3 text-sm">
<MapPin className="w-4 h-4 text-[#8C7E6C]" /><span className="text-[#8C7E6C] font-bold"> IP</span>
<span className="ml-auto text-[#4A3A2C] font-black font-mono text-xs">{selectedUser?.lastLoginIp || '-'}</span>
<MapPin className="w-4 h-4 text-[#6b7280]" /><span className="text-[#6b7280] font-bold"> IP</span>
<span className="ml-auto text-[#111827] font-black font-mono text-xs">{selectedUser?.lastLoginIp || '-'}</span>
</div>
{selectedUser?.isVip === 1 && (
<div className="flex items-center gap-3 text-sm">
<Crown className="w-4 h-4 text-amber-500" /><span className="text-[#8C7E6C] font-bold">VIP </span>
<Crown className="w-4 h-4 text-gray-800" /><span className="text-[#6b7280] font-bold">VIP </span>
<span className="ml-auto text-amber-600 font-black">{fmt(selectedUser?.vipExpireAt ?? null)}</span>
</div>
)}
+42 -42
View File
@@ -84,20 +84,20 @@ export default function VipConfig() {
{/* Header Section */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6 px-2">
<div>
<h1 className="text-3xl md:text-4xl font-black tracking-tight text-[#4A3A2C] flex items-center gap-4">
<h1 className="text-3xl md:text-4xl font-black tracking-tight text-[#111827] flex items-center gap-4">
VIP
<div className="w-8 h-8 md:w-10 md:h-10 rounded-xl md:rounded-2xl bg-gradient-to-br from-amber-400 to-orange-600 flex items-center justify-center shadow-lg shadow-orange-500/20">
<div className="w-8 h-8 md:w-10 md:h-10 rounded-xl md:rounded-2xl bg-gradient-to-br from-gray-700 to-orange-600 flex items-center justify-center shadow-lg shadow-orange-500/20">
<Crown className="w-5 h-5 md:w-6 md:h-6 text-white" />
</div>
</h1>
<p className="text-[#8C7E6C] font-medium mt-2 italic text-sm md:text-base"></p>
<p className="text-[#6b7280] font-medium mt-2 italic text-sm md:text-base"></p>
</div>
<div className="flex items-center gap-3 overflow-x-auto pb-2 md:pb-0">
<Button
variant="ghost"
onClick={fetchData}
disabled={loading}
className="rounded-2xl h-12 md:h-14 px-4 md:px-6 font-black text-[#8C7E6C] hover:bg-white/60 transition-all border border-[#4A3A2C]/5 whitespace-nowrap"
className="rounded-2xl h-12 md:h-14 px-4 md:px-6 font-black text-[#6b7280] hover:bg-white/60 transition-all border border-[#111827]/5 whitespace-nowrap"
>
<RefreshCw className={`w-4 h-4 md:w-5 md:h-5 mr-2 md:mr-3 ${loading ? 'animate-spin' : ''}`} />
@@ -105,7 +105,7 @@ export default function VipConfig() {
<Button
onClick={handleSubmit}
disabled={saving}
className="rounded-[1.2rem] md:rounded-[1.5rem] h-12 md:h-14 px-6 md:px-10 font-black shadow-xl shadow-[#D28F4F]/20 hover:scale-105 transition-all bg-gradient-to-r from-amber-500 to-[#D28F4F] border-none group whitespace-nowrap"
className="rounded-[1.2rem] md:rounded-[1.5rem] h-12 md:h-14 px-6 md:px-10 font-black shadow-xl shadow-[#111827]/20 hover:scale-105 transition-all bg-gradient-to-r from-gray-800 to-[#111827] border-none group whitespace-nowrap"
>
{saving ? (
<RefreshCw className="w-4 h-4 md:w-5 md:h-5 mr-2 animate-spin" />
@@ -120,62 +120,62 @@ export default function VipConfig() {
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 md:gap-10">
{/* Form Section */}
<div className="lg:col-span-8 space-y-6 md:space-y-10 order-2 lg:order-1">
<Card className="glass-card warm-noise border-none shadow-glass rounded-[2rem] md:rounded-[3rem] overflow-hidden relative z-10">
<CardHeader className="p-6 md:p-12 border-b border-[#4A3A2C]/5 bg-[#FAF5E6]/40">
<Card className="glass-card border-none shadow-glass rounded-[2rem] md:rounded-[3rem] overflow-hidden relative z-10">
<CardHeader className="p-6 md:p-12 border-b border-[#111827]/5 bg-[#f9fafb]/40">
<div className="flex items-center gap-4">
<div className="p-3 md:p-4 rounded-[1.2rem] md:rounded-[1.5rem] bg-amber-500/10">
<div className="p-3 md:p-4 rounded-[1.2rem] md:rounded-[1.5rem] bg-gray-800/10">
<Coins className="w-6 h-6 md:w-8 md:h-8 text-amber-600" />
</div>
<div>
<CardTitle className="text-xl md:text-2xl font-black tracking-tight text-[#4A3A2C]"></CardTitle>
<p className="text-[#8C7E6C] text-[10px] md:text-sm font-bold opacity-60 mt-1 uppercase tracking-widest">Pricing Strategy</p>
<CardTitle className="text-xl md:text-2xl font-black tracking-tight text-[#111827]"></CardTitle>
<p className="text-[#6b7280] text-[10px] md:text-sm font-bold opacity-60 mt-1 uppercase tracking-widest">Pricing Strategy</p>
</div>
</div>
</CardHeader>
<CardContent className="p-6 md:p-12 space-y-8 md:space-y-10 bg-[#FAF5E6]/20">
<CardContent className="p-6 md:p-12 space-y-8 md:space-y-10 bg-[#f9fafb]/20">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 md:gap-10">
<div className="space-y-4">
<Label className="text-[10px] md:text-xs uppercase font-black tracking-[0.2em] text-[#8C7E6C] ml-2 flex items-center gap-2">
<Label className="text-[10px] md:text-xs uppercase font-black tracking-[0.2em] text-[#6b7280] ml-2 flex items-center gap-2">
()
<div className="w-1.5 h-1.5 rounded-full bg-amber-500" />
<div className="w-1.5 h-1.5 rounded-full bg-gray-800" />
</Label>
<div className="relative group">
<div className="absolute left-6 top-1/2 -translate-y-1/2 text-xl md:text-2xl font-black text-[#D28F4F]">¥</div>
<div className="absolute left-6 top-1/2 -translate-y-1/2 text-xl md:text-2xl font-black text-[#111827]">¥</div>
<Input
type="number"
step="0.01"
placeholder="0.00"
value={toYuan(formData.price)}
onChange={(e) => setFormData({ ...formData, price: toCents(e.target.value) })}
className="h-16 md:h-20 rounded-[1.5rem] md:rounded-[2rem] border-none bg-white shadow-inner font-black text-[#4A3A2C] focus:ring-8 ring-amber-500/5 transition-all pl-12 text-2xl md:text-3xl"
className="h-16 md:h-20 rounded-[1.5rem] md:rounded-[2rem] border-none bg-white shadow-inner font-black text-[#111827] focus:ring-8 ring-gray-800/5 transition-all pl-12 text-2xl md:text-3xl"
/>
</div>
<p className="text-[10px] text-[#8C7E6C] font-bold px-4 italic leading-relaxed">VIP会员的原始标准定价</p>
<p className="text-[10px] text-[#6b7280] font-bold px-4 italic leading-relaxed">VIP会员的原始标准定价</p>
</div>
<div className="space-y-4">
<Label className="text-[10px] md:text-xs uppercase font-black tracking-[0.2em] text-[#8C7E6C] ml-2 flex items-center gap-2">
<Label className="text-[10px] md:text-xs uppercase font-black tracking-[0.2em] text-[#6b7280] ml-2 flex items-center gap-2">
()
<Sparkles className="w-3 h-3 text-amber-500 animate-pulse" />
<Sparkles className="w-3 h-3 text-gray-800 animate-pulse" />
</Label>
<div className="relative group">
<div className="absolute left-6 top-1/2 -translate-y-1/2 text-xl md:text-2xl font-black text-rose-500">¥</div>
<div className="absolute left-6 top-1/2 -translate-y-1/2 text-xl md:text-2xl font-black text-gray-900">¥</div>
<Input
type="number"
step="0.01"
placeholder="0.00"
value={toYuan(formData.discountedPrice)}
onChange={(e) => setFormData({ ...formData, discountedPrice: toCents(e.target.value) })}
className="h-16 md:h-20 rounded-[1.5rem] md:rounded-[2rem] border-none bg-white shadow-inner font-black text-rose-600 focus:ring-8 ring-rose-500/5 transition-all pl-12 text-2xl md:text-3xl"
className="h-16 md:h-20 rounded-[1.5rem] md:rounded-[2rem] border-none bg-white shadow-inner font-black text-rose-600 focus:ring-8 ring-gray-900/5 transition-all pl-12 text-2xl md:text-3xl"
/>
</div>
<p className="text-[10px] text-[#8C7E6C] font-bold px-4 italic leading-relaxed"></p>
<p className="text-[10px] text-[#6b7280] font-bold px-4 italic leading-relaxed"></p>
</div>
</div>
<div className="space-y-4">
<Label className="text-[10px] md:text-xs uppercase font-black tracking-[0.2em] text-[#8C7E6C] ml-2"></Label>
<Label className="text-[10px] md:text-xs uppercase font-black tracking-[0.2em] text-[#6b7280] ml-2"></Label>
<textarea
className="w-full min-h-[150px] md:min-h-[180px] rounded-[1.5rem] md:rounded-[2.5rem] border-none bg-white shadow-inner p-6 md:p-10 font-bold text-[#4A3A2C] focus:ring-8 ring-amber-500/5 transition-all outline-none resize-none placeholder:text-[#8C7E6C]/30 text-base md:text-lg leading-relaxed"
className="w-full min-h-[150px] md:min-h-[180px] rounded-[1.5rem] md:rounded-[2.5rem] border-none bg-white shadow-inner p-6 md:p-10 font-bold text-[#111827] focus:ring-8 ring-gray-800/5 transition-all outline-none resize-none placeholder:text-[#6b7280]/30 text-base md:text-lg leading-relaxed"
placeholder="描述VIP持卡人的专属权益,例如:去广告、高清音质、专属勋章等..."
value={formData.remark}
onChange={(e) => setFormData({ ...formData, remark: e.target.value })}
@@ -185,22 +185,22 @@ export default function VipConfig() {
</Card>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 md:gap-8">
<motion.div whileHover={{ y: -5 }} className="glass-card warm-noise p-6 md:p-8 rounded-[1.5rem] md:rounded-[2.5rem] border-none flex items-start gap-4 md:gap-6 shadow-sm">
<motion.div whileHover={{ y: -5 }} className="glass-card p-6 md:p-8 rounded-[1.5rem] md:rounded-[2.5rem] border-none flex items-start gap-4 md:gap-6 shadow-sm">
<div className="w-10 h-10 md:w-14 md:h-14 rounded-xl md:rounded-2xl bg-emerald-500/10 flex items-center justify-center flex-shrink-0">
<ShieldCheck className="w-5 h-5 md:w-7 md:h-7 text-emerald-600" />
</div>
<div>
<h3 className="text-base md:text-lg font-black text-[#4A3A2C]"></h3>
<p className="text-xs md:text-sm text-[#8C7E6C] font-medium mt-1 leading-relaxed">VIP订单的资金流向透明且安全</p>
<h3 className="text-base md:text-lg font-black text-[#111827]"></h3>
<p className="text-xs md:text-sm text-[#6b7280] font-medium mt-1 leading-relaxed">VIP订单的资金流向透明且安全</p>
</div>
</motion.div>
<motion.div whileHover={{ y: -5 }} className="glass-card warm-noise p-6 md:p-8 rounded-[1.5rem] md:rounded-[2.5rem] border-none flex items-start gap-4 md:gap-6 shadow-sm">
<div className="w-10 h-10 md:w-14 md:h-14 rounded-xl md:rounded-2xl bg-[#D28F4F]/10 flex items-center justify-center flex-shrink-0">
<Zap className="w-5 h-5 md:w-7 md:h-7 text-[#D28F4F]" />
<motion.div whileHover={{ y: -5 }} className="glass-card p-6 md:p-8 rounded-[1.5rem] md:rounded-[2.5rem] border-none flex items-start gap-4 md:gap-6 shadow-sm">
<div className="w-10 h-10 md:w-14 md:h-14 rounded-xl md:rounded-2xl bg-[#111827]/10 flex items-center justify-center flex-shrink-0">
<Zap className="w-5 h-5 md:w-7 md:h-7 text-[#111827]" />
</div>
<div>
<h3 className="text-base md:text-lg font-black text-[#4A3A2C]"></h3>
<p className="text-xs md:text-sm text-[#8C7E6C] font-medium mt-1 leading-relaxed">VIP开通方案</p>
<h3 className="text-base md:text-lg font-black text-[#111827]"></h3>
<p className="text-xs md:text-sm text-[#6b7280] font-medium mt-1 leading-relaxed">VIP开通方案</p>
</div>
</motion.div>
</div>
@@ -208,34 +208,34 @@ export default function VipConfig() {
{/* Preview Section */}
<div className="lg:col-span-4 space-y-10 order-1 lg:order-2">
<Card className="glass-card warm-noise border-none shadow-glass rounded-[2rem] md:rounded-[3rem] overflow-hidden sticky top-8">
<div className="h-2 bg-gradient-to-r from-amber-400 via-[#D28F4F] to-rose-500 w-full" />
<Card className="glass-card border-none shadow-glass rounded-[2rem] md:rounded-[3rem] overflow-hidden sticky top-8">
<div className="h-2 bg-gradient-to-r from-gray-700 via-[#111827] to-gray-900 w-full" />
<CardHeader className="p-6 md:p-10 text-center">
<div className="w-12 h-12 md:w-20 md:h-20 bg-amber-500/10 rounded-full flex items-center justify-center mx-auto mb-4 md:mb-6">
<div className="w-12 h-12 md:w-20 md:h-20 bg-gray-800/10 rounded-full flex items-center justify-center mx-auto mb-4 md:mb-6">
<Crown className="w-6 h-6 md:w-10 md:h-10 text-amber-600" />
</div>
<CardTitle className="text-xl md:text-2xl font-black text-[#4A3A2C]"></CardTitle>
<p className="text-[10px] md:text-xs font-black text-[#8C7E6C]/60 uppercase tracking-widest mt-2">MP Interface Mockup</p>
<CardTitle className="text-xl md:text-2xl font-black text-[#111827]"></CardTitle>
<p className="text-[10px] md:text-xs font-black text-[#6b7280]/60 uppercase tracking-widest mt-2">MP Interface Mockup</p>
</CardHeader>
<CardContent className="p-6 md:p-10 pt-0">
<div className="bg-[#1A1A1A] rounded-[2.5rem] md:rounded-[3.5rem] p-4 md:p-6 shadow-2xl relative overflow-hidden aspect-[9/16] border-8 border-[#2A2A2A] mx-auto max-w-[300px] lg:max-w-none">
<div className="absolute top-0 left-0 w-full h-40 bg-gradient-to-b from-amber-500/20 to-transparent" />
<div className="absolute top-0 left-0 w-full h-40 bg-gradient-to-b from-gray-800/20 to-transparent" />
<div className="relative z-10 space-y-6 md:space-y-8">
<div className="flex items-center justify-center pt-6 md:pt-10">
<div className="w-16 h-16 md:w-20 md:h-20 rounded-full border-4 border-amber-500/30 p-1">
<div className="w-16 h-16 md:w-20 md:h-20 rounded-full border-4 border-gray-800/30 p-1">
<div className="w-full h-full rounded-full bg-slate-700 animate-pulse" />
</div>
</div>
<div className="text-center space-y-1 md:space-y-2">
<h4 className="text-white text-lg md:text-xl font-black">VIP</h4>
<p className="text-amber-500/80 text-[10px] md:text-xs font-bold tracking-widest uppercase">Premium Membership</p>
<p className="text-gray-800/80 text-[10px] md:text-xs font-bold tracking-widest uppercase">Premium Membership</p>
</div>
<div className="bg-white/5 backdrop-blur-md rounded-[1.5rem] md:rounded-3xl p-6 md:p-8 border border-white/10 text-center space-y-3 md:space-y-4">
<div className="flex items-end justify-center gap-1">
<span className="text-amber-500 text-sm md:text-lg font-black pb-1">¥</span>
<span className="text-gray-800 text-sm md:text-lg font-black pb-1">¥</span>
<span className="text-white text-3xl md:text-5xl font-black">{(formData.discountedPrice || formData.price || 0) / 100}</span>
</div>
<div className="text-white/40 text-[10px] md:text-xs font-bold line-through italic"> ¥{formData.price / 100}</div>
@@ -243,12 +243,12 @@ export default function VipConfig() {
<div className="space-y-3 md:space-y-4">
<div className="h-2 md:h-3 w-full bg-white/5 rounded-full overflow-hidden">
<div className="h-full bg-amber-500 w-2/3" />
<div className="h-full bg-gray-800 w-2/3" />
</div>
<p className="text-white/40 text-[9px] md:text-[10px] font-bold text-center italic line-clamp-2"> {formData.remark || "暂无备注详情"} </p>
</div>
<div className="bg-gradient-to-r from-amber-500 to-orange-600 h-12 md:h-16 rounded-xl md:rounded-2xl flex items-center justify-center shadow-lg shadow-orange-500/20">
<div className="bg-gradient-to-r from-gray-800 to-orange-600 h-12 md:h-16 rounded-xl md:rounded-2xl flex items-center justify-center shadow-lg shadow-orange-500/20">
<span className="text-white font-black tracking-widest text-[10px] md:text-sm uppercase"></span>
</div>
</div>
+267
View File
@@ -0,0 +1,267 @@
import { useState, useEffect, useRef } from 'react';
import {
getVoiceListApi,
saveVoiceApi,
updateVoiceApi,
deleteVoiceApi,
setDefaultVoiceApi
} from '@/api/radio.ts';
import type {RadioVoice, VoiceFormData} from '@/types/radio.ts';
import { DeleteConfirm } from '@/components/DeleteConfirm.tsx';
import { FileUploader } from '@/components/FileUploader.tsx';
import { Button } from '@/components/ui/button.tsx';
import { Input } from '@/components/ui/input.tsx';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table.tsx';
import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog.tsx';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card.tsx';
import { Label } from '@/components/ui/label.tsx';
import {
Plus, Edit, Trash2, Search, Disc3, Mic2, Star, Play, Pause
} from 'lucide-react';
import { toast } from 'sonner';
import { motion } from 'framer-motion';
export default function Voice() {
const [data, setData] = useState<RadioVoice[]>([]);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(false);
const [page, setPage] = useState(1);
const [pageSize] = useState(10);
const [searchName, setSearchName] = useState('');
const [debouncedSearch, setDebouncedSearch] = useState('');
const [open, setOpen] = useState(false);
const [isEdit, setIsEdit] = useState(false);
const [formData, setFormData] = useState<VoiceFormData>({
id: '', name: '', speakerId: '', description: '', gender: 'neutral', icon: '', isDefault: 0, audioId: '', sort: 0, status: 1
});
const [deleteOpen, setDeleteOpen] = useState(false);
const [deleteId, setDeleteId] = useState<string | null>(null);
const [playingId, setPlayingId] = useState<string | null>(null);
const audioRef = useRef<HTMLAudioElement | null>(null);
const fetchData = async () => {
setLoading(true);
try {
const res = await getVoiceListApi({ current: page, pageSize, name: debouncedSearch, status: 0 });
setData(res.list || []);
setTotal(res.total || 0);
} catch (e) {
console.error(e);
} finally {
setLoading(false);
}
};
useEffect(() => { fetchData(); }, [page, pageSize, debouncedSearch]);
useEffect(() => {
const timer = setTimeout(() => { setDebouncedSearch(searchName); setPage(1); }, 500);
return () => clearTimeout(timer);
}, [searchName]);
const handleOpenAdd = () => {
setIsEdit(false);
setFormData({ id: '', name: '', speakerId: '', description: '', gender: 'neutral', icon: '', isDefault: 0, audioId: '', sort: 0, status: 1 });
setOpen(true);
};
const handleOpenEdit = (record: RadioVoice) => {
setIsEdit(true);
setFormData({ ...record });
setOpen(true);
};
const confirmDelete = async () => {
if (!deleteId) return;
try {
await deleteVoiceApi({ ids: [deleteId] });
toast.success('删除成功');
fetchData();
} catch (e) {
console.error(e);
} finally {
setDeleteOpen(false);
setDeleteId(null);
}
};
const handleSetDefault = async (id: string | number) => {
try {
await setDefaultVoiceApi({ id });
toast.success('设置默认音色成功');
fetchData();
} catch (e) {
console.error(e);
}
};
const handleSubmit = async () => {
if (!formData.name || !formData.speakerId) return toast.error('请填写音色名称和Speaker ID');
const submitData = { ...formData };
if (submitData.sort === "" || submitData.sort === undefined || submitData.sort === null) submitData.sort = 0;
try {
if (isEdit) {
await updateVoiceApi(submitData);
toast.success('更新成功');
} else {
await saveVoiceApi(submitData);
toast.success('创建成功');
}
setOpen(false);
fetchData();
} catch (e) {
console.error(e);
}
};
const togglePlay = (record: RadioVoice) => {
// Handle both simple URL string and object containing URL
const audioUrl = (record as any).audio?.url || record.audioId;
if (!audioUrl) return toast.error('该音色未提供演示音频');
if (playingId === record.id) {
audioRef.current?.pause();
setPlayingId(null);
} else {
if (audioRef.current) {
audioRef.current.src = audioUrl;
audioRef.current.play();
setPlayingId(record.id);
}
}
};
return (
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="space-y-6 md:space-y-8 pb-10">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6 px-2">
<div>
<h1 className="text-3xl md:text-4xl font-black tracking-tight text-[#111827] flex items-center gap-4"><div className="w-3 h-3 rounded-full bg-[#111827] shadow-sm animate-pulse" /></h1>
<p className="text-[#6b7280] font-medium mt-2 text-sm md:text-base"></p>
</div>
<Button onClick={handleOpenAdd} className="rounded-[1.5rem] h-14 px-8 font-black shadow-xl bg-gradient-to-r from-[#111827] to-[#A64452] border-none group">
<Plus className="w-5 h-5 mr-3 group-hover:rotate-90 transition-transform" />
</Button>
</div>
<Card className="glass-card border-none shadow-glass rounded-[2.5rem] overflow-hidden min-h-[600px] relative z-10">
<CardHeader className="p-10 border-b border-[#111827]/5 flex flex-col md:flex-row md:items-center justify-between gap-6 bg-[#f9fafb]/40">
<div className="flex items-center gap-4">
<div className="p-3 rounded-2xl bg-[#111827]/10"><Mic2 className="w-6 h-6 text-[#111827]" /></div>
<CardTitle className="text-2xl font-black text-[#111827]"></CardTitle>
</div>
<div className="relative group w-full max-w-sm">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-[#6b7280]" />
<Input placeholder="搜索音色名称..." value={searchName} onChange={(e) => setSearchName(e.target.value)} className="pl-12 h-12 rounded-2xl border-none bg-[#f9fafb]/80 focus:bg-white transition-all font-bold text-[#111827]" />
</div>
</CardHeader>
<CardContent className="p-0">
<div className="overflow-x-auto">
<Table className="min-w-[800px]">
<TableHeader className="bg-[#f9fafb]/50">
<TableRow className="border-[#111827]/5">
<TableHead className="pl-10 py-6 text-[10px] font-black uppercase text-[#6b7280]">/</TableHead>
<TableHead className="py-6 text-[10px] font-black uppercase text-[#6b7280]">Speaker ID</TableHead>
<TableHead className="py-6 text-[10px] font-black uppercase text-[#6b7280]"></TableHead>
<TableHead className="py-6 text-[10px] font-black uppercase text-[#6b7280]">/</TableHead>
<TableHead className="text-right pr-10 py-6 text-[10px] font-black uppercase text-[#6b7280]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? <TableRow><TableCell colSpan={6} className="h-96 text-center text-[#6b7280] font-black"><Disc3 className="w-12 h-12 mx-auto animate-spin-slow mb-4" />...</TableCell></TableRow>
: data.length === 0 ? <TableRow><TableCell colSpan={6} className="h-96 text-center text-[#6b7280] font-black"></TableCell></TableRow>
: data.map((item) => (
<TableRow key={item.id} className="group border-[#111827]/5 hover:bg-[#f9fafb]/80 transition-all duration-300">
<TableCell className="pl-10 py-6">
<div className="flex items-center gap-4">
<img src={item.icon || '/favicon.jpg'} alt="icon" className="w-10 h-10 rounded-full object-cover border-2 border-white shadow-sm" />
<div>
<p className="font-black text-[#111827] text-base">{item.name}</p>
<p className="text-[10px] font-bold text-[#6b7280]/50 mt-0.5">{item.gender === 'male' ? '男声' : item.gender === 'female' ? '女声' : '中性'}</p>
</div>
</div>
</TableCell>
<TableCell className="py-6 text-[#6b7280] font-mono text-xs">{item.speakerId}</TableCell>
<TableCell className="py-6 max-w-[200px]"><p className="text-[12px] font-bold text-[#6b7280] truncate">{item.description || '-'}</p></TableCell>
<TableCell className="py-6">
<div className="flex flex-col gap-2">
<span className={`w-fit px-3 py-1 rounded-full text-[10px] font-black border ${item.status === 1 ? 'bg-emerald-50 text-emerald-700 border-emerald-200' : 'bg-rose-50 text-rose-600 border-rose-200'}`}>
{item.status === 1 ? '启用' : '隐藏'}
</span>
{item.isDefault === 1 && <span className="w-fit flex items-center gap-1 px-3 py-1 bg-yellow-100 text-yellow-700 text-[10px] font-black rounded-full border border-yellow-300"><Star className="w-3 h-3"/></span>}
</div>
</TableCell>
<TableCell className="text-right pr-10 py-6">
<div className="flex items-center justify-end gap-3 opacity-0 group-hover:opacity-100 transition-all">
{item.isDefault === 0 && <Button variant="outline" size="sm" onClick={() => handleSetDefault(item.id)} className="h-10 rounded-xl text-xs font-bold text-yellow-600 border-yellow-200 hover:bg-yellow-50"></Button>}
<Button variant="ghost" size="icon" onClick={() => togglePlay(item)} className={`w-10 h-10 rounded-xl shadow-sm ${playingId === item.id ? 'bg-indigo-50 text-indigo-600' : 'hover:bg-white hover:text-indigo-600'}`}>
{playingId === item.id ? <Pause className="w-5 h-5" /> : <Play className="w-5 h-5" />}
</Button>
<Button variant="ghost" size="icon" onClick={() => handleOpenEdit(item)} className="w-10 h-10 rounded-xl hover:bg-white hover:text-[#111827] shadow-sm"><Edit className="w-5 h-5" /></Button>
<Button variant="ghost" size="icon" onClick={() => { setDeleteId(item.id); setDeleteOpen(true); }} className="w-10 h-10 rounded-xl hover:bg-rose-50 hover:text-rose-600 shadow-sm"><Trash2 className="w-5 h-5" /></Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</CardContent>
<div className="p-6 md:p-10 border-t border-[#111827]/5 flex justify-between gap-6 bg-[#f9fafb]/40">
<span className="px-4 py-1.5 bg-white shadow-sm rounded-full text-[11px] font-black text-[#111827]"> {total} </span>
<div className="flex gap-4">
<Button variant="ghost" onClick={() => setPage(p => Math.max(1, p-1))} disabled={page === 1} className="rounded-2xl h-11 px-6 font-black text-[10px] text-[#6b7280] hover:text-[#111827]"></Button>
<Button variant="ghost" onClick={() => setPage(p => p+1)} disabled={data.length < pageSize} className="rounded-2xl h-11 px-6 font-black text-[10px] text-[#6b7280] hover:text-[#111827]"></Button>
</div>
</div>
</Card>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="max-w-[800px] glass-card border-none rounded-[3rem] p-0 overflow-hidden shadow-2xl">
<div className="bg-gradient-to-r from-[#111827] to-[#A64452] p-10 text-white relative">
<DialogTitle className="text-3xl font-black">{isEdit ? '编辑音色' : '新增音色'}</DialogTitle>
<p className="text-white/60 text-xs font-black uppercase mt-2">Voice Configuration</p>
</div>
<div className="p-10 space-y-8 max-h-[70vh] overflow-y-auto custom-scrollbar bg-[#f9fafb]/60 backdrop-blur-3xl">
<div className="grid grid-cols-2 gap-6">
<div className="space-y-3">
<Label className="text-[11px] uppercase font-black text-[#6b7280] ml-1"></Label>
<Input value={formData.name} onChange={(e) => setFormData({...formData, name: e.target.value})} className="h-14 rounded-2xl border-none bg-white shadow-sm font-bold text-[#111827] px-6" placeholder="输入名称..." />
</div>
<div className="space-y-3">
<Label className="text-[11px] uppercase font-black text-[#6b7280] ml-1">Speaker ID</Label>
<Input value={formData.speakerId} onChange={(e) => setFormData({...formData, speakerId: e.target.value})} className="h-14 rounded-2xl border-none bg-white shadow-sm font-bold text-[#111827] px-6" placeholder="第三方音色标识ID" />
</div>
<div className="space-y-3">
<Label className="text-[11px] uppercase font-black text-[#6b7280] ml-1"></Label>
<select className="w-full h-14 rounded-2xl bg-white shadow-sm px-6 font-bold text-[#111827] border-none outline-none appearance-none cursor-pointer" value={formData.gender} onChange={e => setFormData({...formData, gender: e.target.value})}>
<option value="neutral"></option>
<option value="male"></option>
<option value="female"></option>
</select>
</div>
<div className="space-y-3">
<Label className="text-[11px] uppercase font-black text-[#6b7280] ml-1"></Label>
<select className="w-full h-14 rounded-2xl bg-white shadow-sm px-6 font-bold text-[#111827] border-none outline-none appearance-none cursor-pointer" value={formData.status} onChange={e => setFormData({...formData, status: parseInt(e.target.value)})}>
<option value={1}></option>
<option value={0}></option>
</select>
</div>
</div>
<div className="grid grid-cols-2 gap-6">
<FileUploader label="上传音色演示音频 (可选)" accept="audio/*" value={formData.audioId} onChange={(id) => setFormData({...formData, audioId: id})} />
<FileUploader label="上传音色形象图标 (可选)" accept="image/*" value={formData.icon} onChange={(id) => setFormData({...formData, icon: id})} initialPreview={formData.icon?.startsWith('http') ? formData.icon : ''} />
</div>
<div className="space-y-3">
<Label className="text-[11px] uppercase font-black text-[#6b7280] ml-1"></Label>
<textarea className="w-full min-h-[120px] rounded-2xl border-none bg-white shadow-sm p-6 font-bold text-[#111827] outline-none resize-none" placeholder="描述此音色的情感、口音及适用场景..." value={formData.description} onChange={(e) => setFormData({...formData, description: e.target.value})} />
</div>
</div>
<div className="p-8 bg-white/40 backdrop-blur-xl flex gap-4 justify-between items-center border-t border-[#111827]/5">
<Button variant="ghost" onClick={() => setOpen(false)} className="rounded-2xl h-14 px-10 font-black text-[#6b7280]"></Button>
<Button onClick={handleSubmit} className="rounded-2xl h-14 px-12 font-black shadow-2xl bg-gradient-to-r from-[#111827] to-[#A64452] border-none text-white">{isEdit ? '保存变更' : '提交音色'}</Button>
</div>
</DialogContent>
</Dialog>
<DeleteConfirm open={deleteOpen} onOpenChange={setDeleteOpen} onConfirm={confirmDelete} />
<audio ref={audioRef} onEnded={() => setPlayingId(null)} className="hidden" />
</motion.div>
);
}
+50 -50
View File
@@ -134,64 +134,64 @@ export default function Oss() {
>
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
<div>
<h1 className="text-4xl font-black tracking-tight text-[#4A3A2C] flex items-center gap-4">
<h1 className="text-4xl font-black tracking-tight text-[#111827] flex items-center gap-4">
<div className="w-3 h-3 rounded-full bg-[#D28F4F] shadow-[0_0_20px_rgba(210,143,79,0.5)] animate-pulse" />
<div className="w-3 h-3 rounded-full bg-[#111827] shadow-[0_0_20px_rgba(210,143,79,0.5)] animate-pulse" />
</h1>
<p className="text-[#8C7E6C] font-medium mt-2"></p>
<p className="text-[#6b7280] font-medium mt-2"></p>
</div>
<Button
onClick={() => setOpen(true)}
className="rounded-[1.5rem] h-14 px-8 font-black shadow-xl shadow-[#D28F4F]/20 hover:scale-105 transition-all bg-gradient-to-r from-[#D28F4F] to-[#A64452] border-none group"
className="rounded-[1.5rem] h-14 px-8 font-black shadow-xl shadow-[#111827]/20 hover:scale-105 transition-all bg-gradient-to-r from-[#111827] to-[#A64452] border-none group"
>
<CloudUpload className="w-5 h-5 mr-3 group-hover:-translate-y-1 transition-transform" />
</Button>
</div>
<Card className="glass-card warm-noise border-none shadow-glass rounded-[2.5rem] overflow-hidden min-h-[600px] relative z-10">
<CardHeader className="p-10 border-b border-[#4A3A2C]/5 flex flex-col sm:flex-row sm:items-center justify-between gap-6 bg-[#FAF5E6]/40">
<Card className="glass-card border-none shadow-glass rounded-[2.5rem] overflow-hidden min-h-[600px] relative z-10">
<CardHeader className="p-10 border-b border-[#111827]/5 flex flex-col sm:flex-row sm:items-center justify-between gap-6 bg-[#f9fafb]/40">
<div className="flex items-center gap-4">
<div className="p-3 rounded-2xl bg-[#D28F4F]/10">
<HardDrive className="w-6 h-6 text-[#D28F4F]" />
<div className="p-3 rounded-2xl bg-[#111827]/10">
<HardDrive className="w-6 h-6 text-[#111827]" />
</div>
<CardTitle className="text-2xl font-black tracking-tight text-[#4A3A2C]"></CardTitle>
<CardTitle className="text-2xl font-black tracking-tight text-[#111827]"></CardTitle>
</div>
<div className="relative group w-full max-w-sm">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-[#8C7E6C] group-focus-within:text-[#D28F4F] transition-colors" />
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-[#6b7280] group-focus-within:text-[#111827] transition-colors" />
<Input
placeholder="搜索资源名称、关键词..."
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
className="pl-12 h-12 rounded-2xl border-none bg-[#FAF5E6]/80 focus:bg-white shadow-inner transition-all font-bold text-[#4A3A2C]"
className="pl-12 h-12 rounded-2xl border-none bg-[#f9fafb]/80 focus:bg-white shadow-inner transition-all font-bold text-[#111827]"
/>
</div>
</CardHeader>
<CardContent className="p-0">
<div className="overflow-x-auto">
<Table>
<TableHeader className="bg-[#FAF5E6]/50">
<TableRow className="hover:bg-transparent border-[#4A3A2C]/5">
<TableHead className="w-[120px] pl-10 py-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#8C7E6C]"></TableHead>
<TableHead className="py-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#8C7E6C]"></TableHead>
<TableHead className="py-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#8C7E6C]"></TableHead>
<TableHead className="py-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#8C7E6C]"> (URL)</TableHead>
<TableHead className="text-right pr-10 py-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#8C7E6C]"></TableHead>
<TableHeader className="bg-[#f9fafb]/50">
<TableRow className="hover:bg-transparent border-[#111827]/5">
<TableHead className="w-[120px] pl-10 py-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#6b7280]"></TableHead>
<TableHead className="py-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#6b7280]"></TableHead>
<TableHead className="py-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#6b7280]"></TableHead>
<TableHead className="py-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#6b7280]"> (URL)</TableHead>
<TableHead className="text-right pr-10 py-6 text-[10px] font-black uppercase tracking-[0.2em] text-[#6b7280]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading && data.length === 0 ? (
<TableRow><TableCell colSpan={5} className="h-96 text-center text-[#8C7E6C] font-black uppercase"><Disc3 className="w-12 h-12 mx-auto animate-spin-slow mb-4 text-[#D28F4F]/50" />...</TableCell></TableRow>
<TableRow><TableCell colSpan={5} className="h-96 text-center text-[#6b7280] font-black uppercase"><Disc3 className="w-12 h-12 mx-auto animate-spin-slow mb-4 text-[#111827]/50" />...</TableCell></TableRow>
) : data.length === 0 ? (
<TableRow><TableCell colSpan={5} className="h-96 text-center text-[#8C7E6C] text-sm font-black tracking-widest uppercase"></TableCell></TableRow>
<TableRow><TableCell colSpan={5} className="h-96 text-center text-[#6b7280] text-sm font-black tracking-widest uppercase"></TableCell></TableRow>
) : (
data.map((item) => (
<TableRow
key={item.ID}
className="group border-[#4A3A2C]/5 hover:bg-[#FAF5E6]/80 transition-all duration-300 transform hover:scale-[1.001]"
className="group border-[#111827]/5 hover:bg-[#f9fafb]/80 transition-all duration-300 transform hover:scale-[1.001]"
>
<TableCell className="pl-10 py-6">
<div className="w-16 h-16 rounded-2xl bg-white shadow-md flex items-center justify-center border border-[#4A3A2C]/5 p-1.5 overflow-hidden group-hover:rotate-6 transition-transform">
<div className="w-16 h-16 rounded-2xl bg-white shadow-md flex items-center justify-center border border-[#111827]/5 p-1.5 overflow-hidden group-hover:rotate-6 transition-transform">
{['jpg', 'png', 'jpeg', 'gif', 'webp'].includes(item.suffix?.toLowerCase()) ? (
<img src={item.url} alt="" className="w-full h-full object-cover rounded-xl" />
) : (
@@ -200,20 +200,20 @@ export default function Oss() {
</div>
</TableCell>
<TableCell className="py-6">
<p className="font-black text-[#4A3A2C] group-hover:text-[#D28F4F] transition-colors text-sm tracking-tight truncate max-w-[200px]" title={item.name}>{item.name}</p>
<p className="text-[10px] font-bold text-[#8C7E6C]/50 mt-1 uppercase tracking-tighter">REF: {item.ID}</p>
<p className="font-black text-[#111827] group-hover:text-[#111827] transition-colors text-sm tracking-tight truncate max-w-[200px]" title={item.name}>{item.name}</p>
<p className="text-[10px] font-bold text-[#6b7280]/50 mt-1 uppercase tracking-tighter">REF: {item.ID}</p>
</TableCell>
<TableCell className="py-6">
<span className="px-4 py-1.5 bg-[#FAF5E6] rounded-full text-[10px] font-black text-[#D28F4F] uppercase tracking-widest border border-[#D28F4F]/10 shadow-sm">
<span className="px-4 py-1.5 bg-[#f9fafb] rounded-full text-[10px] font-black text-[#111827] uppercase tracking-widest border border-[#111827]/10 shadow-sm">
.{item.suffix}
</span>
</TableCell>
<TableCell className="py-6">
<div className="flex items-center gap-3 max-w-[300px] overflow-hidden group/link">
<p className="text-[11px] font-medium text-[#8C7E6C] truncate italic cursor-alias hover:text-[#D28F4F] transition-colors" onClick={() => handleCopyUrl(item.url)}>
<p className="text-[11px] font-medium text-[#6b7280] truncate italic cursor-alias hover:text-[#111827] transition-colors" onClick={() => handleCopyUrl(item.url)}>
{item.url}
</p>
<ExternalLink className="w-3 h-3 text-[#F2EDE4] group-hover/link:text-[#D28F4F] transition-colors opacity-0 group-hover/link:opacity-100" />
<ExternalLink className="w-3 h-3 text-[#F2EDE4] group-hover/link:text-[#111827] transition-colors opacity-0 group-hover/link:opacity-100" />
</div>
</TableCell>
<TableCell className="text-right pr-10 py-6">
@@ -222,7 +222,7 @@ export default function Oss() {
variant="ghost"
size="icon"
onClick={() => handleCopyUrl(item.url)}
className="w-12 h-12 rounded-[1.2rem] hover:bg-white hover:text-[#D28F4F] transition-all border border-[#4A3A2C]/5 shadow-sm hover:shadow-md"
className="w-12 h-12 rounded-[1.2rem] hover:bg-white hover:text-[#111827] transition-all border border-[#111827]/5 shadow-sm hover:shadow-md"
>
<Copy className="w-4.5 h-4.5" />
</Button>
@@ -230,7 +230,7 @@ export default function Oss() {
variant="ghost"
size="icon"
onClick={() => handleDeleteClick(item.ID)}
className="w-12 h-12 rounded-[1.2rem] hover:bg-rose-50 hover:text-rose-600 transition-all border border-[#4A3A2C]/5 shadow-sm"
className="w-12 h-12 rounded-[1.2rem] hover:bg-rose-50 hover:text-rose-600 transition-all border border-[#111827]/5 shadow-sm"
>
<Trash2 className="w-4.5 h-4.5" />
</Button>
@@ -243,17 +243,17 @@ export default function Oss() {
</Table>
</div>
</CardContent>
<div className="p-10 border-t border-[#4A3A2C]/5 flex items-center justify-between bg-[#FAF5E6]/40 backdrop-blur-md">
<div className="p-10 border-t border-[#111827]/5 flex items-center justify-between bg-[#f9fafb]/40 backdrop-blur-md">
<div className="flex items-center gap-3">
<span className="text-[10px] font-black text-[#8C7E6C] uppercase tracking-widest">:</span>
<span className="px-4 py-1.5 bg-white shadow-sm rounded-full text-[11px] font-black text-[#D28F4F]"> {total} </span>
<span className="text-[10px] font-black text-[#6b7280] uppercase tracking-widest">:</span>
<span className="px-4 py-1.5 bg-white shadow-sm rounded-full text-[11px] font-black text-[#111827]"> {total} </span>
</div>
<div className="flex items-center gap-4">
<Button
variant="ghost"
onClick={() => setPage(p => Math.max(1, p - 1))}
disabled={page === 1}
className="rounded-2xl h-11 px-6 font-black uppercase tracking-widest text-[10px] hover:bg-white text-[#8C7E6C] hover:text-[#D28F4F]"
className="rounded-2xl h-11 px-6 font-black uppercase tracking-widest text-[10px] hover:bg-white text-[#6b7280] hover:text-[#111827]"
>
</Button>
@@ -261,7 +261,7 @@ export default function Oss() {
variant="ghost"
onClick={() => setPage(p => p + 1)}
disabled={data.length < pageSize}
className="rounded-2xl h-11 px-6 font-black uppercase tracking-widest text-[10px] hover:bg-white text-[#8C7E6C] hover:text-[#D28F4F]"
className="rounded-2xl h-11 px-6 font-black uppercase tracking-widest text-[10px] hover:bg-white text-[#6b7280] hover:text-[#111827]"
>
</Button>
@@ -270,24 +270,24 @@ export default function Oss() {
</Card>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="sm:max-w-[550px] glass-card warm-noise border-none rounded-[3.5rem] p-0 overflow-hidden shadow-2xl">
<div className="bg-gradient-to-r from-[#D28F4F] to-[#A64452] p-12 text-white relative">
<DialogContent className="sm:max-w-[550px] glass-card border-none rounded-[3.5rem] p-0 overflow-hidden shadow-2xl">
<div className="bg-gradient-to-r from-[#111827] to-[#A64452] p-12 text-white relative">
<DialogTitle className="text-3xl font-black tracking-tight"></DialogTitle>
<p className="text-white/60 text-xs font-black uppercase tracking-[0.3em] mt-3">Cloud Asset Deployment</p>
<CloudUpload className="absolute right-12 top-1/2 -translate-y-1/2 w-20 h-20 text-white/10 animate-bounce" />
</div>
<div className="p-12 space-y-10 bg-[#FAF5E6]/60 backdrop-blur-3xl">
<div className="p-12 space-y-10 bg-[#f9fafb]/60 backdrop-blur-3xl">
<div className="space-y-4">
<Label className="text-[11px] uppercase font-black tracking-widest text-[#8C7E6C] ml-1"></Label>
<Label className="text-[11px] uppercase font-black tracking-widest text-[#6b7280] ml-1"></Label>
<div className="relative group">
<Input
type="file"
onChange={handleFileChange}
className="h-32 rounded-[2.5rem] border-2 border-dashed border-[#4A3A2C]/10 bg-white shadow-inner hover:bg-white hover:border-[#D28F4F]/50 transition-all cursor-pointer text-transparent file:hidden"
className="h-32 rounded-[2.5rem] border-2 border-dashed border-[#111827]/10 bg-white shadow-inner hover:bg-white hover:border-[#111827]/50 transition-all cursor-pointer text-transparent file:hidden"
/>
<div className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none">
<CloudUpload className="w-10 h-10 text-[#8C7E6C]/30 group-hover:text-[#D28F4F] transition-colors" />
<p className="text-[11px] font-black text-[#8C7E6C]/50 mt-3 uppercase tracking-tighter group-hover:text-[#D28F4F] transition-colors">
<CloudUpload className="w-10 h-10 text-[#6b7280]/30 group-hover:text-[#111827] transition-colors" />
<p className="text-[11px] font-black text-[#6b7280]/50 mt-3 uppercase tracking-tighter group-hover:text-[#111827] transition-colors">
{file ? file.name : '点击或拖拽文件至此区域进行部署'}
</p>
</div>
@@ -298,22 +298,22 @@ export default function Oss() {
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
className="p-6 rounded-[2rem] bg-white shadow-md border border-[#D28F4F]/10 flex items-center justify-between"
className="p-6 rounded-[2rem] bg-white shadow-md border border-[#111827]/10 flex items-center justify-between"
>
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-2xl bg-[#D28F4F]/5 flex items-center justify-center">
{file.type.startsWith('image/') ? <ImageIcon className="w-6 h-6 text-[#D28F4F]" /> : <FileIcon className="w-6 h-6 text-[#D28F4F]" />}
<div className="w-12 h-12 rounded-2xl bg-[#111827]/5 flex items-center justify-center">
{file.type.startsWith('image/') ? <ImageIcon className="w-6 h-6 text-[#111827]" /> : <FileIcon className="w-6 h-6 text-[#111827]" />}
</div>
<div className="overflow-hidden max-w-[240px]">
<p className="text-sm font-black text-[#4A3A2C] truncate">{file.name}</p>
<p className="text-[10px] font-bold text-[#8C7E6C]/50 uppercase mt-0.5">{(file.size / 1024).toFixed(2)} KB</p>
<p className="text-sm font-black text-[#111827] truncate">{file.name}</p>
<p className="text-[10px] font-bold text-[#6b7280]/50 uppercase mt-0.5">{(file.size / 1024).toFixed(2)} KB</p>
</div>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => setFile(null)}
className="h-10 w-10 p-0 rounded-full hover:bg-rose-50 hover:text-rose-500 font-bold text-lg"
className="h-10 w-10 p-0 rounded-full hover:bg-rose-50 hover:text-gray-900 font-bold text-lg"
>
×
</Button>
@@ -322,18 +322,18 @@ export default function Oss() {
</AnimatePresence>
</div>
</div>
<div className="p-10 bg-white/40 backdrop-blur-xl flex justify-between items-center border-t border-[#4A3A2C]/5 font-black">
<div className="p-10 bg-white/40 backdrop-blur-xl flex justify-between items-center border-t border-[#111827]/5 font-black">
<Button
variant="ghost"
onClick={() => setOpen(false)}
className="rounded-[1.5rem] h-14 px-10 uppercase tracking-[0.2em] text-xs hover:bg-white text-[#8C7E6C]"
className="rounded-[1.5rem] h-14 px-10 uppercase tracking-[0.2em] text-xs hover:bg-white text-[#6b7280]"
>
</Button>
<Button
onClick={handleUpload}
disabled={uploading || !file}
className="rounded-[1.5rem] h-14 px-12 shadow-2xl shadow-[#D28F4F]/30 hover:scale-105 transition-all bg-gradient-to-r from-[#D28F4F] to-[#A64452] border-none disabled:opacity-50"
className="rounded-[1.5rem] h-14 px-12 shadow-2xl shadow-[#111827]/30 hover:scale-105 transition-all bg-gradient-to-r from-[#111827] to-[#A64452] border-none disabled:opacity-50"
>
{uploading ? '部署中...' : '开始上线资源'}
</Button>
+5
View File
@@ -7,6 +7,7 @@ import Channel from '../pages/Radio/Channel';
import Program from '../pages/Radio/Program';
import VipConfig from '../pages/Radio/VipConfig';
import UserManagement from '../pages/Radio/User';
import VoiceManagement from '../pages/Radio/Voice';
import Oss from '../pages/System/Oss';
const router = createBrowserRouter([
@@ -46,6 +47,10 @@ const router = createBrowserRouter([
path: 'radio/user',
element: <UserManagement />,
},
{
path: 'radio/voice',
element: <VoiceManagement />,
},
],
},
{
+34
View File
@@ -148,3 +148,37 @@ export interface ChannelFormData {
sort: number;
status: number;
}
/** 音色 */
export interface RadioVoice extends BaseModel {
name: string;
speakerId: string;
description: string;
gender: 'male' | 'female' | 'neutral';
icon: string;
isDefault: number;
audioId: string;
sort: number;
status: number;
}
/** 音色查询参数 */
export interface VoiceListParams extends PageParams {
name?: string;
keyword?: string;
status?: number;
}
/** 音色表单数据 */
export interface VoiceFormData {
id?: string;
name: string;
speakerId: string;
description: string;
gender: string;
icon: string;
isDefault: number;
audioId: string;
sort: number | string;
status: number;
}