refactor: 页面重构
This commit is contained in:
+16
-2
@@ -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');
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user