Compare commits

..

10 Commits

34 changed files with 3954 additions and 2837 deletions

View File

@@ -9,6 +9,7 @@ from app.models.work import Work
from app.models.order import Order, OrderStatus from app.models.order import Order, OrderStatus
from app.models.download import DownloadRecord from app.models.download import DownloadRecord
from app.schemas.order import OrderCreate, OrderResponse, PaymentResponse from app.schemas.order import OrderCreate, OrderResponse, PaymentResponse
from app.services.download_tracker import record_download
router = APIRouter(prefix="/orders", tags=["订单"]) router = APIRouter(prefix="/orders", tags=["订单"])
@@ -148,17 +149,9 @@ def pay_order(
order.status = OrderStatus.PAID order.status = OrderStatus.PAID
order.paid_at = datetime.now() order.paid_at = datetime.now()
# 创建下载记录
download_record = DownloadRecord(
user_id=current_user.id,
work_id=order.work_id,
order_id=order.id
)
db.add(download_record)
# 增加作品下载量
work = db.query(Work).filter(Work.id == order.work_id).first() work = db.query(Work).filter(Work.id == order.work_id).first()
work.downloads += 1 if work:
record_download(db, order, work, current_user)
db.commit() db.commit()

View File

@@ -17,6 +17,7 @@ from app.models.download import DownloadRecord
from app.ysm_sdk import create_payment, query_order, PaymentNotify from app.ysm_sdk import create_payment, query_order, PaymentNotify
from app.api.orders import get_current_user from app.api.orders import get_current_user
from app.models.user import User from app.models.user import User
from app.services.download_tracker import record_download
router = APIRouter(prefix="/payment", tags=["支付"]) router = APIRouter(prefix="/payment", tags=["支付"])
@@ -138,19 +139,11 @@ async def payment_notify(
order.status = OrderStatus.PAID order.status = OrderStatus.PAID
order.payment_method = "ysm_wechat" # 易收米微信支付 order.payment_method = "ysm_wechat" # 易收米微信支付
order.paid_at = datetime.now() order.paid_at = datetime.now()
# 创建下载记录
download_record = DownloadRecord(
user_id=order.user_id,
work_id=order.work_id,
order_id=order.id
)
db.add(download_record)
# 更新作品下载次数
work = db.query(Work).filter(Work.id == order.work_id).first() work = db.query(Work).filter(Work.id == order.work_id).first()
if work: buyer = db.query(User).filter(User.id == order.user_id).first()
work.downloads += 1 if work and buyer:
record_download(db, order, work, buyer)
db.commit() db.commit()
@@ -203,19 +196,10 @@ async def query_order_status(
order.status = OrderStatus.PAID order.status = OrderStatus.PAID
order.payment_method = "ysm_wechat" order.payment_method = "ysm_wechat"
order.paid_at = datetime.now() order.paid_at = datetime.now()
# 创建下载记录
download_record = DownloadRecord(
user_id=order.user_id,
work_id=order.work_id,
order_id=order.id
)
db.add(download_record)
# 更新作品下载次数
work = db.query(Work).filter(Work.id == order.work_id).first() work = db.query(Work).filter(Work.id == order.work_id).first()
if work: if work:
work.downloads += 1 record_download(db, order, work, current_user)
db.commit() db.commit()

View File

@@ -59,6 +59,15 @@ def _user_designer_aliases(user: User) -> list[str]:
return aliases return aliases
def _generate_fallback_title(category: str, designer_name: str, unique_id: str) -> str:
category_name = str(category or "设计素材").strip() or "设计素材"
designer = str(designer_name or "").strip()
suffix = unique_id.replace("-", "")[:6]
if designer:
return f"{category_name}_{designer}_{suffix}"
return f"{category_name}_自动生成_{suffix}"
def generate_thumbnail(image_path: str, thumb_path: str, size=(400, 400)): def generate_thumbnail(image_path: str, thumb_path: str, size=(400, 400)):
"""生成缩略图 - 修复透明 PNG 问题""" """生成缩略图 - 修复透明 PNG 问题"""
with Image.open(image_path) as img: with Image.open(image_path) as img:
@@ -115,10 +124,11 @@ def add_watermark(image_path: str, watermarked_path: str, watermark_text: str =
@router.post("", summary="上传作品") @router.post("", summary="上传作品")
async def upload_work( async def upload_work(
file: UploadFile = File(..., description="作品图片文件"), file: UploadFile = File(..., description="作品图片文件"),
title: str = Form(..., description="作品标题"), title: Optional[str] = Form(None, description="作品标题"),
description: Optional[str] = Form(None, description="作品描述"), description: Optional[str] = Form(None, description="作品描述"),
category: str = Form(..., description="作品分类"), category: str = Form(..., description="作品分类"),
tags: Optional[str] = Form(None, description="标签,逗号分隔"), tags: Optional[str] = Form(None, description="标签,逗号分隔"),
designer_name: Optional[str] = Form(None, description="归属设计师"),
price: float = Form(..., ge=0, description="作品价格"), price: float = Form(..., ge=0, description="作品价格"),
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
db: Session = Depends(get_db) db: Session = Depends(get_db)
@@ -189,15 +199,17 @@ async def upload_work(
tags_str = ",".join(tags_list) # 转成逗号分隔的字符串 tags_str = ",".join(tags_list) # 转成逗号分隔的字符串
else: else:
tags_str = None tags_str = None
resolved_designer = str(designer_name or "").strip() or current_user.nickname or current_user.phone
resolved_title = str(title or "").strip() or _generate_fallback_title(category, resolved_designer, unique_id)
# 创建数据库记录 - 修复所有字段问题 # 创建数据库记录 - 修复所有字段问题
work = Work( work = Work(
title=title, title=resolved_title,
description=description or "", description=description or "",
category=category, category=category,
tags=tags_str, # 修复:使用字符串而不是列表 tags=tags_str, # 修复:使用字符串而不是列表
price=price, price=price,
designer=current_user.nickname or current_user.phone, designer=resolved_designer,
original_image=f"/uploads/original/{timestamp}/{original_filename}", original_image=f"/uploads/original/{timestamp}/{original_filename}",
thumbnail_image=f"/uploads/thumbnail/{timestamp}/{thumb_filename}", thumbnail_image=f"/uploads/thumbnail/{timestamp}/{thumb_filename}",
watermarked_image=f"/uploads/watermarked/{timestamp}/{watermarked_filename}" watermarked_image=f"/uploads/watermarked/{timestamp}/{watermarked_filename}"

View File

@@ -1,10 +1,11 @@
from fastapi import APIRouter, Depends, HTTPException, status, Query, Header from fastapi import APIRouter, Depends, HTTPException, status, Query, Header, BackgroundTasks
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlalchemy import desc from sqlalchemy import desc
from typing import List from typing import List
import os import os
import mimetypes import mimetypes
from urllib.parse import urlparse
from app.core.database import get_db from app.core.database import get_db
from app.models.work import Work from app.models.work import Work
from app.models.order import Order, OrderStatus from app.models.order import Order, OrderStatus
@@ -12,9 +13,27 @@ from app.models.user import User
from app.core.security import decode_access_token from app.core.security import decode_access_token
from app.core.config import settings from app.core.config import settings
from app.schemas.work import WorkResponse, WorkListResponse from app.schemas.work import WorkResponse, WorkListResponse
from app.services.download_tracker import notify_download
router = APIRouter(prefix="/works", tags=["作品"]) router = APIRouter(prefix="/works", tags=["作品"])
def resolve_upload_file_path(image_path: str) -> str:
"""兼容 /uploads 相对路径和完整 URL 两种历史存储格式。"""
if not image_path:
return ""
normalized = image_path.strip()
if normalized.startswith(("http://", "https://")):
normalized = urlparse(normalized).path or ""
normalized = normalized.lstrip("/")
if normalized.startswith("uploads/"):
normalized = normalized[len("uploads/"):]
return os.path.join(settings.UPLOAD_DIR, normalized)
@router.get("", response_model=WorkListResponse, summary="获取作品列表") @router.get("", response_model=WorkListResponse, summary="获取作品列表")
def get_works( def get_works(
page: int = Query(1, ge=1), page: int = Query(1, ge=1),
@@ -108,6 +127,7 @@ def get_current_user(authorization: str = Header(None), db: Session = Depends(ge
@router.get("/{work_id}/download", summary="下载作品原图") @router.get("/{work_id}/download", summary="下载作品原图")
def download_work( def download_work(
work_id: int, work_id: int,
background_tasks: BackgroundTasks,
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
@@ -139,10 +159,7 @@ def download_work(
) )
# 构建原图文件路径 # 构建原图文件路径
relative_path = work.original_image.lstrip("/") full_path = resolve_upload_file_path(work.original_image)
if relative_path.startswith("uploads/"):
relative_path = relative_path[len("uploads/"):]
full_path = os.path.join(settings.UPLOAD_DIR, relative_path)
# 检查文件是否存在 # 检查文件是否存在
if not os.path.exists(full_path): if not os.path.exists(full_path):
@@ -150,6 +167,8 @@ def download_work(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail="文件不存在" detail="文件不存在"
) )
background_tasks.add_task(notify_download, work, current_user, paid_order)
# 返回文件 # 返回文件
filename = os.path.basename(full_path) filename = os.path.basename(full_path)

View File

@@ -42,6 +42,7 @@ class Settings(BaseSettings):
YSM_NOTIFY_URL: str = "https://tuhui.cloud/api/payment/notify" # 支付回调地址 YSM_NOTIFY_URL: str = "https://tuhui.cloud/api/payment/notify" # 支付回调地址
YSM_CALLBACK_URL: str = "https://tuhui.cloud/payment/success" # 支付成功跳转地址 YSM_CALLBACK_URL: str = "https://tuhui.cloud/payment/success" # 支付成功跳转地址
YSM_NOPAY_URL: str = "https://tuhui.cloud/payment/cancel" # 未支付跳转地址 YSM_NOPAY_URL: str = "https://tuhui.cloud/payment/cancel" # 未支付跳转地址
WECOM_BOT_WEBHOOK: str = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=cc88bdef-a13f-4d7e-bdb6-ee51b68b8205"
class Config: class Config:
env_file = ".env" env_file = ".env"

View File

@@ -1,6 +1,7 @@
from fastapi import FastAPI, Request, Response from fastapi import FastAPI, Request, Response
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from sqlalchemy import inspect, text
from app.core.config import settings from app.core.config import settings
from app.core.database import Base, engine from app.core.database import Base, engine
from app.api import auth, works, orders, payment, upload from app.api import auth, works, orders, payment, upload
@@ -8,6 +9,25 @@ from app.api import auth, works, orders, payment, upload
# 创建数据库表 # 创建数据库表
Base.metadata.create_all(bind=engine) Base.metadata.create_all(bind=engine)
def ensure_runtime_schema():
inspector = inspect(engine)
columns = {col["name"] for col in inspector.get_columns("download_records")}
statements = []
if "designer_name" not in columns:
statements.append("ALTER TABLE download_records ADD COLUMN designer_name VARCHAR(100)")
if "work_title" not in columns:
statements.append("ALTER TABLE download_records ADD COLUMN work_title VARCHAR(255)")
if "amount" not in columns:
statements.append("ALTER TABLE download_records ADD COLUMN amount FLOAT DEFAULT 0")
if statements:
with engine.begin() as conn:
for sql in statements:
conn.execute(text(sql))
ensure_runtime_schema()
# 创建 FastAPI 应用 # 创建 FastAPI 应用
app = FastAPI( app = FastAPI(
title=settings.PROJECT_NAME, title=settings.PROJECT_NAME,

View File

@@ -1,4 +1,4 @@
from sqlalchemy import Column, Integer, DateTime, ForeignKey from sqlalchemy import Column, Integer, DateTime, ForeignKey, String, Float
from sqlalchemy.sql import func from sqlalchemy.sql import func
from app.core.database import Base from app.core.database import Base
@@ -9,5 +9,8 @@ class DownloadRecord(Base):
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True) user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
work_id = Column(Integer, ForeignKey("works.id"), nullable=False, index=True) work_id = Column(Integer, ForeignKey("works.id"), nullable=False, index=True)
order_id = Column(Integer, ForeignKey("orders.id"), nullable=False, index=True) order_id = Column(Integer, ForeignKey("orders.id"), nullable=False, index=True)
designer_name = Column(String(100), nullable=True, index=True)
work_title = Column(String(255), nullable=True)
amount = Column(Float, default=0.0)
downloaded_at = Column(DateTime(timezone=True), server_default=func.now()) downloaded_at = Column(DateTime(timezone=True), server_default=func.now())

View File

@@ -0,0 +1,46 @@
from datetime import datetime
from sqlalchemy.orm import Session
from app.models.download import DownloadRecord
from app.models.order import Order
from app.models.user import User
from app.models.work import Work
from app.services.wecom_bot import send_wecom_text
def _build_download_notice(work: Work, buyer: User, order: Order) -> str:
return "\n".join(
[
"【图绘下载通知】",
f"作品ID{work.id}",
f"作品标题:{work.title}",
f"设计师:{work.designer or '-'}",
f"购买用户:{buyer.nickname or buyer.phone or buyer.id}",
f"金额:{order.amount}",
f"下载时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
f"作品链接https://tuhui.cloud/detail/{work.id}",
]
)
def record_download(db: Session, order: Order, work: Work, buyer: User) -> DownloadRecord:
existing = db.query(DownloadRecord).filter(DownloadRecord.order_id == order.id).first()
if existing:
return existing
record = DownloadRecord(
user_id=buyer.id,
work_id=work.id,
order_id=order.id,
designer_name=work.designer or "",
work_title=work.title or "",
amount=float(order.amount or 0.0),
)
db.add(record)
work.downloads = int(work.downloads or 0) + 1
return record
def notify_download(work: Work, buyer: User, order: Order):
send_wecom_text(_build_download_notice(work, buyer, order))

View File

@@ -0,0 +1,32 @@
import logging
import json
from urllib import request
from app.core.config import settings
logger = logging.getLogger(__name__)
def send_wecom_text(content: str) -> bool:
text = str(content or "").strip()
webhook = str(getattr(settings, "WECOM_BOT_WEBHOOK", "") or "").strip()
if not text or not webhook:
return False
try:
payload = json.dumps({"msgtype": "text", "text": {"content": text[:3500]}}).encode("utf-8")
req = request.Request(
webhook,
data=payload,
headers={"Content-Type": "application/json"},
method="POST",
)
with request.urlopen(req, timeout=10) as resp:
body = resp.read().decode("utf-8", errors="ignore")
data = json.loads(body or "{}")
ok = int(data.get("errcode", -1)) == 0
if not ok:
logger.warning(f"WeCom bot send failed: {data}")
return ok
except Exception as e:
logger.warning(f"WeCom bot send exception: {e}")
return False

View File

@@ -1,15 +1,13 @@
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700;800&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body { body {
font-family: 'Noto Sans SC', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-family: 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif;
background: #f5f5f5; color: var(--ink);
color: #333; }
#root,
.App {
min-height: 100vh;
} }
.app { .app {
@@ -25,32 +23,17 @@ body {
width: 100%; width: 100%;
} }
/* Scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
/* Ant Design Overrides */
.ant-btn-primary { .ant-btn-primary {
background: #ff5a5a; background: linear-gradient(135deg, var(--brand) 0%, var(--brand-strong) 100%);
border-color: #ff5a5a; border-color: transparent;
box-shadow: none;
} }
.ant-btn-primary:hover { .ant-btn-primary:hover {
background: #ff7070 !important; background: linear-gradient(135deg, #3c97ba 0%, #236b88 100%) !important;
border-color: #ff7070 !important; border-color: transparent !important;
}
.ant-tag {
border-radius: 999px;
} }

View File

@@ -21,7 +21,7 @@
} }
.close-icon:hover { .close-icon:hover {
color: #ff5a5a; color: var(--brand);
} }
.auth-modal-content { .auth-modal-content {
@@ -62,7 +62,7 @@
.auth-input:hover, .auth-input:hover,
.auth-input:focus, .auth-input:focus,
.auth-input.ant-input-affix-wrapper-focused { .auth-input.ant-input-affix-wrapper-focused {
border-color: #ff5a5a; border-color: var(--brand);
box-shadow: 0 0 0 2px rgba(255, 90, 90, 0.1); box-shadow: 0 0 0 2px rgba(255, 90, 90, 0.1);
} }
@@ -82,12 +82,12 @@
} }
.form-options .ant-checkbox-checked .ant-checkbox-inner { .form-options .ant-checkbox-checked .ant-checkbox-inner {
background-color: #ff5a5a; background-color: var(--brand);
border-color: #ff5a5a; border-color: var(--brand);
} }
.forgot-link { .forgot-link {
color: #ff5a5a; color: var(--brand);
font-size: 14px; font-size: 14px;
} }
@@ -100,7 +100,7 @@
border-radius: 24px; border-radius: 24px;
font-size: 16px; font-size: 16px;
font-weight: 500; font-weight: 500;
background: linear-gradient(135deg, #ff5a5a 0%, #ff8080 100%); background: linear-gradient(135deg, var(--brand) 0%, var(--brand-strong) 100%);
border: none; border: none;
box-shadow: 0 4px 12px rgba(255, 90, 90, 0.3); box-shadow: 0 4px 12px rgba(255, 90, 90, 0.3);
transition: all 0.3s; transition: all 0.3s;
@@ -179,7 +179,7 @@
} }
.switch-link { .switch-link {
color: #ff5a5a; color: var(--brand);
font-weight: 500; font-weight: 500;
margin-left: 4px; margin-left: 4px;
cursor: pointer; cursor: pointer;
@@ -205,8 +205,8 @@
font-size: 14px; font-size: 14px;
white-space: nowrap; white-space: nowrap;
padding: 0 16px; padding: 0 16px;
border-color: #ff5a5a; border-color: var(--brand);
color: #ff5a5a; color: var(--brand);
} }
.verify-btn:hover:not(:disabled) { .verify-btn:hover:not(:disabled) {
@@ -226,7 +226,7 @@
} }
.agreement-checkbox a { .agreement-checkbox a {
color: #ff5a5a; color: var(--brand);
} }
.agreement-checkbox a:hover { .agreement-checkbox a:hover {
@@ -234,8 +234,8 @@
} }
.agreement-checkbox .ant-checkbox-checked .ant-checkbox-inner { .agreement-checkbox .ant-checkbox-checked .ant-checkbox-inner {
background-color: #ff5a5a; background-color: var(--brand);
border-color: #ff5a5a; border-color: var(--brand);
} }
/* Animation */ /* Animation */

View File

@@ -1,128 +1,228 @@
.categories { .categories {
padding: 30px 20px; padding: 20px;
max-width: 1200px; }
margin: 0 auto;
} .categories-panel {
max-width: 1400px;
.categories-container { margin: 0 auto;
position: relative; padding: 30px;
} border-radius: 32px;
background: rgba(255, 255, 255, 0.9);
.categories-scroll { border: 1px solid rgba(233, 221, 212, 0.92);
display: flex; box-shadow: 0 22px 48px rgba(66, 42, 26, 0.08);
gap: 16px; }
overflow-x: auto;
padding: 10px 0; .categories-header {
scrollbar-width: none; display: flex;
-ms-overflow-style: none; justify-content: space-between;
} gap: 24px;
align-items: flex-end;
.categories-scroll::-webkit-scrollbar { margin-bottom: 24px;
display: none; }
}
.categories-copy {
.category-card { max-width: 760px;
flex-shrink: 0; }
width: 170px;
border-radius: 12px; .categories-kicker {
overflow: hidden; display: inline-flex;
background: white; margin-bottom: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); padding: 7px 12px;
cursor: pointer; border-radius: 999px;
transition: all 0.3s ease; background: rgba(239, 106, 91, 0.1);
animation: fadeInUp 0.6s ease forwards; color: var(--brand-strong);
animation-delay: var(--delay); font-size: 12px;
opacity: 0; font-weight: 700;
} letter-spacing: 0.08em;
}
@keyframes fadeInUp {
from { .categories-copy h2 {
opacity: 0; margin: 0;
transform: translateY(20px); color: #221d18;
} font-size: clamp(26px, 4vw, 36px);
to { line-height: 1.15;
opacity: 1; }
transform: translateY(0);
} .categories-copy p {
} margin: 12px 0 0;
color: #766c61;
.category-card:hover { line-height: 1.85;
transform: translateY(-8px); }
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15);
} .categories-actions {
display: flex;
.category-image { align-items: center;
height: 170px; gap: 14px;
position: relative; flex-wrap: wrap;
display: flex; justify-content: flex-end;
align-items: center; }
justify-content: center;
} .categories-summary {
padding: 10px 14px;
.category-overlay { border-radius: 999px;
position: absolute; background: #fff8f2;
top: 0; border: 1px solid rgba(236, 223, 213, 0.94);
left: 0; color: #63574b;
right: 0; font-size: 13px;
bottom: 0; font-weight: 600;
background: rgba(0, 0, 0, 0.1); }
display: flex;
align-items: center; .categories-controls {
justify-content: center; display: flex;
} gap: 10px;
}
.category-icon {
font-size: 80px; .scroll-btn.ant-btn {
color: white; width: 42px;
font-weight: 700; height: 42px;
text-shadow: 2px 2px 10px rgba(0, 0, 0, 0.3); border-radius: 16px;
} border-color: rgba(224, 208, 197, 0.95);
background: #fffaf5;
.category-info { color: #66594d;
padding: 12px 16px; }
display: flex;
justify-content: space-between; .scroll-btn.ant-btn:hover {
align-items: center; color: var(--brand) !important;
} border-color: rgba(239, 106, 91, 0.4) !important;
background: var(--brand-soft) !important;
.category-name { }
font-size: 16px;
font-weight: 600; .categories-scroll {
color: #333; display: grid;
} grid-auto-flow: column;
grid-auto-columns: minmax(250px, 1fr);
.category-count { gap: 16px;
font-size: 12px; overflow-x: auto;
color: #999; padding-bottom: 4px;
} scrollbar-width: none;
}
.scroll-btn {
position: absolute; .categories-scroll::-webkit-scrollbar {
top: 50%; display: none;
transform: translateY(-50%); }
z-index: 10;
background: white; .category-card {
border: none; border: 1px solid rgba(233, 221, 212, 0.92);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); border-radius: 24px;
width: 40px; overflow: hidden;
height: 40px; background: linear-gradient(180deg, #fffdfb, #f8f2eb);
} text-align: left;
cursor: pointer;
.scroll-btn:hover { padding: 0;
background: #ff5a5a !important; transition: transform 0.24s ease, box-shadow 0.24s ease, border-color 0.24s ease;
color: white !important; animation: fadeIn 0.6s ease-out;
} animation-delay: var(--delay);
animation-fill-mode: both;
.scroll-btn-right { }
right: -10px;
} .category-card:hover {
transform: translateY(-6px);
@media (max-width: 768px) { border-color: rgba(239, 106, 91, 0.28);
.category-card { box-shadow: 0 18px 36px rgba(66, 42, 26, 0.12);
width: 160px; }
}
.category-card-top {
.category-image { min-height: 140px;
height: 100px; padding: 20px;
} display: flex;
} justify-content: space-between;
align-items: flex-start;
}
.category-icon-wrap {
width: 54px;
height: 54px;
border-radius: 18px;
background: rgba(255, 255, 255, 0.2);
border: 1px solid rgba(255, 255, 255, 0.24);
display: grid;
place-items: center;
color: #fff;
font-size: 24px;
backdrop-filter: blur(10px);
}
.category-pill {
padding: 7px 12px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.18);
border: 1px solid rgba(255, 255, 255, 0.24);
color: #fff;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.04em;
}
.category-body {
padding: 18px 18px 20px;
display: flex;
flex-direction: column;
gap: 16px;
}
.category-name {
margin: 0 0 8px;
color: #281f18;
font-size: 20px;
}
.category-description {
margin: 0;
color: #7b6f63;
line-height: 1.7;
font-size: 14px;
}
.category-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding-top: 14px;
border-top: 1px solid rgba(237, 227, 217, 0.95);
}
.category-count {
color: #67594d;
font-size: 13px;
font-weight: 600;
}
.category-link {
display: inline-flex;
align-items: center;
gap: 6px;
color: var(--brand);
font-size: 13px;
font-weight: 700;
}
@media (max-width: 900px) {
.categories-header {
flex-direction: column;
align-items: flex-start;
}
.categories-actions {
justify-content: flex-start;
}
}
@media (max-width: 768px) {
.categories {
padding: 12px;
}
.categories-panel {
padding: 20px 18px;
border-radius: 26px;
}
.categories-scroll {
grid-auto-columns: minmax(220px, 1fr);
}
.category-card-top {
min-height: 120px;
}
}

View File

@@ -1,67 +1,163 @@
import { useRef } from 'react'; import { useRef } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Card, Button } from 'antd'; import {
import { RightOutlined } from '@ant-design/icons'; RightOutlined,
import './Categories.css'; LeftOutlined,
AppstoreOutlined,
const Categories = () => { NotificationOutlined,
const scrollRef = useRef(null); PlayCircleOutlined,
const navigate = useNavigate(); CompassOutlined,
GiftOutlined,
const categories = [ FileTextOutlined,
{ name: '活动', count: '408131', gradient: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)' }, RiseOutlined,
{ name: '中式', count: '105545', gradient: 'linear-gradient(135deg, #c94b4b 0%, #4b134f 100%)' }, CoffeeOutlined,
{ name: '直播', count: '35429', gradient: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)' }, } from '@ant-design/icons';
{ name: '旅游', count: '97826', gradient: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)' }, import { Button } from 'antd';
{ name: '周年庆', count: '15868', gradient: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)' }, import './Categories.css';
{ name: '长图', count: '130856', gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' },
{ name: '价值点', count: '214448', gradient: 'linear-gradient(135deg, #0c3483 0%, #a2b6df 100%)' }, const categories = [
{ name: '酒吧', count: '36397', gradient: 'linear-gradient(135deg, #ff6b35 0%, #ff8c42 100%)' }, {
]; name: '活动',
count: '408131',
const scroll = (direction) => { description: '促销、开业、发布会等高频营销场景素材。',
if (scrollRef.current) { tag: '热点',
const scrollAmount = direction === 'left' ? -300 : 300; gradient: 'linear-gradient(135deg, #8ec4d8 0%, #2f87a8 100%)',
scrollRef.current.scrollBy({ left: scrollAmount, behavior: 'smooth' }); icon: <NotificationOutlined />,
} },
}; {
name: '中式',
const handleCategoryClick = (categoryName) => { count: '105545',
navigate(`/category/${categoryName}`); description: '国风、红金、传统节令与典雅视觉方向。',
}; tag: '质感',
gradient: 'linear-gradient(135deg, #7d2f2f 0%, #d98f5f 100%)',
return ( icon: <AppstoreOutlined />,
<section className="categories"> },
<div className="categories-container"> {
<div className="categories-scroll" ref={scrollRef}> name: '直播',
{categories.map((cat, index) => ( count: '35429',
<div description: '口播封面、预告海报和直播场景包装。',
key={cat.name} tag: '转化',
className="category-card" gradient: 'linear-gradient(135deg, #4a8ee8 0%, #57d3e2 100%)',
style={{ '--delay': `${index * 0.1}s` }} icon: <PlayCircleOutlined />,
onClick={() => handleCategoryClick(cat.name)} },
> {
<div className="category-image" style={{ background: cat.gradient }}> name: '旅游',
<div className="category-overlay"> count: '97826',
<span className="category-icon">{cat.name.charAt(0)}</span> description: '景区、民宿、出行路线和旅行氛围图。',
</div> tag: '场景',
</div> gradient: 'linear-gradient(135deg, #2f8f73 0%, #7bdcb5 100%)',
<div className="category-info"> icon: <CompassOutlined />,
<span className="category-name">{cat.name}</span> },
<span className="category-count">{cat.count} </span> {
</div> name: '周年庆',
</div> count: '15868',
))} description: '品牌纪念日、店庆和阶段性节点专题。',
</div> tag: '品牌',
<Button gradient: 'linear-gradient(135deg, #e17baa 0%, #f7c46c 100%)',
className="scroll-btn scroll-btn-right" icon: <GiftOutlined />,
shape="circle" },
icon={<RightOutlined />} {
onClick={() => scroll('right')} name: '长图',
/> count: '130856',
</div> description: '卖点拆解、流程展示和信息编排型视觉。',
</section> tag: '信息流',
); gradient: 'linear-gradient(135deg, #5767c8 0%, #8c77dc 100%)',
}; icon: <FileTextOutlined />,
},
export default Categories; {
name: '价值点',
count: '214448',
description: '优势提炼、卖点表达和产品力沟通模板。',
tag: '卖点',
gradient: 'linear-gradient(135deg, #2b5775 0%, #6da4c9 100%)',
icon: <RiseOutlined />,
},
{
name: '酒吧',
count: '36397',
description: '夜场、派对、餐酒主题和情绪化海报。',
tag: '氛围',
gradient: 'linear-gradient(135deg, #8c4b2e 0%, #d78c52 100%)',
icon: <CoffeeOutlined />,
},
];
const Categories = () => {
const scrollRef = useRef(null);
const navigate = useNavigate();
const scroll = (direction) => {
if (scrollRef.current) {
const scrollAmount = direction === 'left' ? -320 : 320;
scrollRef.current.scrollBy({ left: scrollAmount, behavior: 'smooth' });
}
};
return (
<section className="categories">
<div className="categories-panel">
<div className="categories-header">
<div className="categories-copy">
<span className="categories-kicker">快速浏览</span>
<h2>把高频需求先整理好用户会更容易继续点下去</h2>
<p>
分类区不只是导航更是首页的第二层承接让用户在首屏之后立刻看到更清楚的行业和场景入口
</p>
</div>
<div className="categories-actions">
<span className="categories-summary">{categories.length} 个主分类</span>
<div className="categories-controls">
<Button
className="scroll-btn"
shape="circle"
icon={<LeftOutlined />}
onClick={() => scroll('left')}
/>
<Button
className="scroll-btn"
shape="circle"
icon={<RightOutlined />}
onClick={() => scroll('right')}
/>
</div>
</div>
</div>
<div className="categories-scroll" ref={scrollRef}>
{categories.map((cat, index) => (
<button
key={cat.name}
type="button"
className="category-card"
style={{ '--delay': `${index * 0.06}s` }}
onClick={() => navigate(`/category/${cat.name}`)}
>
<div className="category-card-top" style={{ background: cat.gradient }}>
<div className="category-icon-wrap">{cat.icon}</div>
<span className="category-pill">{cat.tag}</span>
</div>
<div className="category-body">
<div>
<h3 className="category-name">{cat.name}</h3>
<p className="category-description">{cat.description}</p>
</div>
<div className="category-footer">
<span className="category-count">{cat.count} </span>
<span className="category-link">
查看分类
<RightOutlined />
</span>
</div>
</div>
</button>
))}
</div>
</div>
</section>
);
};
export default Categories;

View File

@@ -1,144 +1,194 @@
.designers-section { .designers-section {
padding: 30px 20px 50px; padding: 18px 20px 56px;
max-width: 1200px; }
margin: 0 auto;
} .designers-panel {
max-width: 1400px;
.designers-section .section-header { margin: 0 auto;
margin-bottom: 20px; }
}
.designers-header {
.designers-section .section-title { display: flex;
font-size: 20px; justify-content: space-between;
font-weight: 600; gap: 24px;
color: #333; align-items: flex-end;
margin: 0; margin-bottom: 24px;
position: relative; }
padding-left: 12px;
} .designers-copy {
max-width: 760px;
.designers-section .section-title::before { }
content: '';
position: absolute; .designers-kicker {
left: 0; display: inline-flex;
top: 50%; margin-bottom: 12px;
transform: translateY(-50%); padding: 7px 12px;
width: 4px; border-radius: 999px;
height: 20px; background: rgba(239, 106, 91, 0.1);
background: #ff5a5a; color: var(--brand-strong);
border-radius: 2px; font-size: 12px;
} font-weight: 700;
letter-spacing: 0.08em;
.designers-grid { }
margin: 0 !important;
} .designers-copy h2 {
margin: 0;
.designer-card { color: #221d18;
text-align: center; font-size: clamp(26px, 4vw, 36px);
border-radius: 12px; line-height: 1.15;
padding: 20px 12px; }
cursor: pointer;
transition: all 0.3s ease; .designers-copy p {
animation: fadeInUp 0.5s ease forwards; margin: 12px 0 0;
animation-delay: var(--delay); color: #756b61;
opacity: 0; line-height: 1.85;
} }
@keyframes fadeInUp { .designers-summary {
from { padding: 10px 14px;
opacity: 0; border-radius: 999px;
transform: translateY(20px); background: rgba(255, 255, 255, 0.82);
} border: 1px solid rgba(236, 223, 213, 0.94);
to { color: #63574b;
opacity: 1; font-size: 13px;
transform: translateY(0); font-weight: 600;
} }
}
.designers-grid {
.designer-card:hover { margin: 0 !important;
transform: translateY(-8px); }
box-shadow: 0 16px 32px rgba(0, 0, 0, 0.1);
} .designer-card.ant-card {
position: relative;
.designer-card .ant-card-body { overflow: hidden;
padding: 0; border-radius: 26px;
} border: 1px solid rgba(233, 221, 212, 0.94);
background: rgba(255, 255, 255, 0.94);
.designer-avatar { box-shadow: 0 20px 42px rgba(66, 42, 26, 0.08);
width: 80px; animation: fadeIn 0.6s ease-out;
height: 80px; animation-delay: var(--delay);
border-radius: 50%; animation-fill-mode: both;
margin: 0 auto 12px; transition: transform 0.24s ease, box-shadow 0.24s ease;
display: flex; }
align-items: center;
justify-content: center; .designer-card.ant-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); transform: translateY(-6px);
} box-shadow: 0 26px 48px rgba(66, 42, 26, 0.12);
}
.avatar-emoji {
font-size: 36px; .designer-card .ant-card-body {
} padding: 0;
}
.designer-name {
font-size: 14px; .designer-card-top {
font-weight: 600; height: 92px;
color: #333; position: relative;
margin: 0 0 4px; }
overflow: hidden;
text-overflow: ellipsis; .designer-badge {
white-space: nowrap; position: absolute;
} top: 16px;
right: 16px;
.designer-desc { width: 36px;
font-size: 12px; height: 36px;
color: #999; border-radius: 14px;
margin: 0 0 8px; background: rgba(255, 255, 255, 0.2);
} border: 1px solid rgba(255, 255, 255, 0.26);
color: #fff;
.designer-works { display: grid;
font-size: 24px; place-items: center;
font-weight: 700; font-size: 18px;
color: #ff5a5a; }
line-height: 1.2;
} .designer-avatar {
width: 82px;
.designer-fans { height: 82px;
font-size: 12px; border-radius: 28px;
color: #999; margin: -40px auto 0;
margin: 8px 0 12px; border: 5px solid rgba(255, 255, 255, 0.95);
} display: grid;
place-items: center;
.fans-count { position: relative;
color: #333; z-index: 1;
font-weight: 500; color: #fff;
} box-shadow: 0 14px 28px rgba(66, 42, 26, 0.18);
}
.follow-btn {
border-radius: 20px; .avatar-text {
border-color: #e8e8e8; font-size: 28px;
color: #666; font-weight: 800;
font-size: 12px; letter-spacing: 0.04em;
height: 28px; }
padding: 0 16px;
} .designer-content {
padding: 18px 18px 20px;
.follow-btn:hover { text-align: center;
color: #ff5a5a !important; }
border-color: #ff5a5a !important;
background: #fff5f5 !important; .designer-name {
} margin: 10px 0 6px;
color: #261f19;
@media (max-width: 768px) { font-size: 18px;
.designer-avatar { }
width: 60px;
height: 60px; .designer-specialty {
} margin: 0;
color: #83776b;
.avatar-emoji { font-size: 13px;
font-size: 28px; }
}
.designer-stats {
.designer-works { display: grid;
font-size: 20px; grid-template-columns: repeat(2, minmax(0, 1fr));
} gap: 12px;
} margin: 18px 0;
}
.designer-stat {
padding: 14px 10px;
border-radius: 18px;
background: #fff8f2;
border: 1px solid rgba(238, 226, 216, 0.94);
display: flex;
flex-direction: column;
gap: 4px;
}
.designer-stat strong {
color: #2b241d;
font-size: 22px;
}
.designer-stat span {
color: #908377;
font-size: 12px;
}
.follow-btn.ant-btn {
width: 100%;
height: 44px;
border-radius: 16px;
border-color: rgba(224, 208, 197, 0.95);
color: #5f5449;
font-weight: 700;
background: #fffaf5;
}
.follow-btn.ant-btn:hover {
color: var(--brand) !important;
border-color: rgba(239, 106, 91, 0.4) !important;
background: var(--brand-soft) !important;
}
@media (max-width: 900px) {
.designers-header {
flex-direction: column;
align-items: flex-start;
}
}
@media (max-width: 768px) {
.designers-section {
padding: 12px 12px 40px;
}
}

View File

@@ -1,72 +1,88 @@
import { Card, Button, Avatar, Row, Col } from 'antd'; import { Card, Button, Row, Col } from 'antd';
import { PlusOutlined, UserOutlined } from '@ant-design/icons'; import { PlusOutlined, CrownOutlined, StarOutlined, FireOutlined } from '@ant-design/icons';
import './Designers.css'; import './Designers.css';
const Designers = () => { const designers = [
const designers = [ { id: 1, name: '顾九思', works: 223, fans: 546, specialty: '活动 / 医美', badge: <CrownOutlined /> },
{ id: 1, name: '顾九思', works: 223, fans: 546, avatar: '🎨' }, { id: 2, name: '下辈子别做设计', works: 380, fans: 1099, specialty: '电商 / 长图', badge: <FireOutlined /> },
{ id: 2, name: '下辈子别做设计', works: 380, fans: 1099, avatar: '🖼️' }, { id: 3, name: 'h突然的', works: 537, fans: 574, specialty: '直播 / 海报', badge: <StarOutlined /> },
{ id: 3, name: 'h突然的', works: 537, fans: 574, avatar: '✨' }, { id: 4, name: '赤木流歌', works: 300, fans: 735, specialty: '品牌 / 中式', badge: <CrownOutlined /> },
{ id: 4, name: '赤木流歌', works: 300, fans: 735, avatar: '🎭' }, { id: 5, name: '秃头选手', works: 311, fans: 538, specialty: '活动 / 价值点', badge: <FireOutlined /> },
{ id: 5, name: '秃头选手', works: 311, fans: 538, avatar: '🎯' }, { id: 6, name: 'M.A', works: 553, fans: 567, specialty: '节日 / 场景图', badge: <StarOutlined /> },
{ id: 6, name: 'M.A', works: 553, fans: 567, avatar: '🌟' }, { id: 7, name: 'NIMINMIN', works: 305, fans: 1208, specialty: '科技 / 电商', badge: <CrownOutlined /> },
{ id: 7, name: 'NIMINMIN', works: 305, fans: 1208, avatar: '💫' }, { id: 8, name: '星玥设计', works: 402, fans: 576, specialty: '品牌 / 节庆', badge: <StarOutlined /> },
{ id: 8, name: '星玥设计', works: 402, fans: 576, avatar: '⭐' }, { id: 9, name: '一十九', works: 1598, fans: 2457, specialty: '热门 / 专题', badge: <FireOutlined /> },
{ id: 9, name: '一十九', works: 1598, fans: 2457, avatar: '🔥' }, { id: 10, name: 'Jance', works: 341, fans: 3424, specialty: '高端 / 质感', badge: <CrownOutlined /> },
{ id: 10, name: 'Jance', works: 341, fans: 3424, avatar: '💎' }, ];
];
const gradients = [
const getGradient = (index) => { 'linear-gradient(135deg, #2f87a8 0%, #82bfd5 100%)',
const gradients = [ 'linear-gradient(135deg, #4a8ee8 0%, #67d0df 100%)',
'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', 'linear-gradient(135deg, #7d60c9 0%, #bd8eff 100%)',
'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)', 'linear-gradient(135deg, #2f8f73 0%, #86d6b2 100%)',
'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)', 'linear-gradient(135deg, #d96a44 0%, #f2b15d 100%)',
'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)', ];
'linear-gradient(135deg, #fa709a 0%, #fee140 100%)',
'linear-gradient(135deg, #a8edea 0%, #fed6e3 100%)', const getInitials = (name) => {
'linear-gradient(135deg, #d299c2 0%, #fef9d7 100%)', if (!name) {
'linear-gradient(135deg, #89f7fe 0%, #66a6ff 100%)', return '设';
'linear-gradient(135deg, #fddb92 0%, #d1fdff 100%)', }
'linear-gradient(135deg, #ff9a9e 0%, #fecfef 100%)', return name.length <= 2 ? name : name.slice(0, 2);
]; };
return gradients[index % gradients.length];
}; const Designers = () => {
return (
return ( <section className="designers-section">
<section className="designers-section"> <div className="designers-panel">
<div className="section-header"> <div className="designers-header">
<h2 className="section-title">设计师</h2> <div className="designers-copy">
</div> <span className="designers-kicker">活跃供稿人</span>
<h2>让站点不只是有图还要看起来有人在持续更新</h2>
<Row gutter={[16, 16]} className="designers-grid"> <p>
{designers.map((designer, index) => ( 设计师区是站点信任感的一部分把头像擅长方向作品量和粉丝量做得更完整页面会更像一个真实的内容市场
<Col xs={12} sm={8} md={6} lg={4} xl={4} key={designer.id}> </p>
<Card </div>
className="designer-card" <span className="designers-summary">{designers.length} 位精选设计师</span>
style={{ '--delay': `${index * 0.05}s` }} </div>
>
<div className="designer-avatar" style={{ background: getGradient(index) }}> <Row gutter={[18, 18]} className="designers-grid">
<span className="avatar-emoji">{designer.avatar}</span> {designers.map((designer, index) => (
</div> <Col xs={24} sm={12} md={8} lg={6} xl={6} key={designer.id}>
<h3 className="designer-name">{designer.name}</h3> <Card className="designer-card" style={{ '--delay': `${index * 0.05}s` }}>
<p className="designer-desc">/她已上传作品</p> <div className="designer-card-top" style={{ background: gradients[index % gradients.length] }}>
<div className="designer-works">{designer.works}</div> <span className="designer-badge">{designer.badge}</span>
<div className="designer-fans"> </div>
<span className="fans-count">{designer.fans}</span>
<span className="fans-text"> 粉丝</span> <div className="designer-avatar" style={{ background: gradients[index % gradients.length] }}>
</div> <span className="avatar-text">{getInitials(designer.name)}</span>
<Button </div>
className="follow-btn"
icon={<PlusOutlined />} <div className="designer-content">
> <h3 className="designer-name">{designer.name}</h3>
关注 <p className="designer-specialty">{designer.specialty}</p>
</Button>
</Card> <div className="designer-stats">
</Col> <div className="designer-stat">
))} <strong>{designer.works}</strong>
</Row> <span>作品</span>
</section> </div>
); <div className="designer-stat">
}; <strong>{designer.fans}</strong>
<span>粉丝</span>
export default Designers; </div>
</div>
<Button className="follow-btn" icon={<PlusOutlined />}>
关注设计师
</Button>
</div>
</Card>
</Col>
))}
</Row>
</div>
</section>
);
};
export default Designers;

View File

@@ -1,122 +1,234 @@
.festival { .festival {
padding: 20px 20px 30px; padding: 12px 20px 24px;
max-width: 1200px; }
margin: 0 auto;
} .festival-panel {
max-width: 1400px;
.festival-container { margin: 0 auto;
position: relative; padding: 30px;
} border-radius: 32px;
background: linear-gradient(135deg, rgba(255, 250, 244, 0.94), rgba(255, 245, 236, 0.94));
.festival-scroll { border: 1px solid rgba(233, 221, 212, 0.92);
display: flex; box-shadow: 0 22px 48px rgba(66, 42, 26, 0.08);
gap: 12px; }
overflow-x: auto;
padding: 10px 0; .festival-header {
scrollbar-width: none; display: flex;
-ms-overflow-style: none; justify-content: space-between;
} align-items: flex-end;
gap: 24px;
.festival-scroll::-webkit-scrollbar { margin-bottom: 22px;
display: none; }
}
.festival-copy {
.festival-card { max-width: 760px;
flex-shrink: 0; }
width: 150px;
background: white; .festival-kicker {
border-radius: 12px; display: inline-flex;
padding: 16px; margin-bottom: 12px;
display: flex; padding: 7px 12px;
flex-direction: column; border-radius: 999px;
align-items: center; background: rgba(239, 106, 91, 0.1);
gap: 8px; color: var(--brand-strong);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); font-size: 12px;
cursor: pointer; font-weight: 700;
transition: all 0.3s ease; letter-spacing: 0.08em;
animation: slideIn 0.5s ease forwards; }
animation-delay: var(--delay);
opacity: 0; .festival-copy h2 {
transform: translateX(20px); margin: 0;
} color: #221d18;
font-size: clamp(24px, 4vw, 34px);
@keyframes slideIn { line-height: 1.15;
to { }
opacity: 1;
transform: translateX(0); .festival-copy p {
} margin: 12px 0 0;
} color: #756b61;
line-height: 1.85;
.festival-card:hover { }
transform: translateY(-4px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12); .festival-actions {
} display: flex;
align-items: center;
.festival-emoji { gap: 14px;
font-size: 32px; flex-wrap: wrap;
line-height: 1; justify-content: flex-end;
} }
.festival-content { .festival-summary {
text-align: center; padding: 10px 14px;
} border-radius: 999px;
background: rgba(255, 255, 255, 0.8);
.festival-name { border: 1px solid rgba(236, 223, 213, 0.94);
font-size: 16px; color: #63574b;
font-weight: 600; font-size: 13px;
color: #333; font-weight: 600;
margin: 0; }
}
.festival-controls {
.festival-date { display: flex;
font-size: 12px; gap: 10px;
color: #999; }
margin: 4px 0 0;
} .festival-scroll-btn.ant-btn {
width: 42px;
.festival-days { height: 42px;
display: flex; border-radius: 16px;
align-items: baseline; border-color: rgba(224, 208, 197, 0.95);
gap: 2px; background: #fffaf5;
margin-top: 4px; color: #66594d;
} }
.days-number { .festival-scroll-btn.ant-btn:hover {
font-size: 20px; color: var(--brand) !important;
font-weight: 700; border-color: rgba(239, 106, 91, 0.4) !important;
color: #ff5a5a; background: var(--brand-soft) !important;
} }
.days-text { .festival-scroll {
font-size: 12px; display: grid;
color: #999; grid-auto-flow: column;
} grid-auto-columns: minmax(240px, 1fr);
gap: 14px;
.festival-scroll-btn { overflow-x: auto;
position: absolute; scrollbar-width: none;
right: -10px; }
top: 50%;
transform: translateY(-50%); .festival-scroll::-webkit-scrollbar {
z-index: 10; display: none;
background: white; }
border: none;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); .festival-card {
width: 36px; display: grid;
height: 36px; grid-template-columns: 54px minmax(0, 1fr);
} gap: 14px;
padding: 20px;
.festival-scroll-btn:hover { border-radius: 24px;
background: #ff5a5a !important; background: rgba(255, 255, 255, 0.82);
color: white !important; border: 1px solid rgba(233, 221, 212, 0.95);
} animation: fadeIn 0.6s ease-out;
animation-delay: var(--delay);
@media (max-width: 768px) { animation-fill-mode: both;
.festival-card { transition: transform 0.24s ease, box-shadow 0.24s ease;
width: 130px; }
padding: 12px;
} .festival-card:hover {
transform: translateY(-4px);
.festival-emoji { box-shadow: 0 18px 36px rgba(66, 42, 26, 0.1);
font-size: 28px; }
}
} .festival-icon {
width: 54px;
height: 54px;
border-radius: 18px;
display: grid;
place-items: center;
font-size: 24px;
}
.tone-orange .festival-icon {
background: linear-gradient(135deg, #ffe7d6 0%, #ffcfb4 100%);
color: #d96a44;
}
.tone-green .festival-icon {
background: linear-gradient(135deg, #e0f4e8 0%, #c3ead4 100%);
color: #2f8f73;
}
.tone-pink .festival-icon {
background: linear-gradient(135deg, #ffe7f0 0%, #ffd1e4 100%);
color: #cc5f8b;
}
.tone-purple .festival-icon {
background: linear-gradient(135deg, #efe7ff 0%, #dccfff 100%);
color: #7d60c9;
}
.tone-slate .festival-icon {
background: linear-gradient(135deg, #edf1f7 0%, #d8e0ec 100%);
color: #60708a;
}
.tone-red .festival-icon {
background: linear-gradient(135deg, #ffe2df 0%, #ffc4bc 100%);
color: #cf5347;
}
.festival-content {
min-width: 0;
}
.festival-topline {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.festival-name {
margin: 0;
color: #241d17;
font-size: 18px;
}
.festival-days {
padding: 6px 10px;
border-radius: 999px;
background: #fff3ea;
color: var(--brand-strong);
font-size: 12px;
font-weight: 700;
white-space: nowrap;
}
.festival-date {
margin: 8px 0 6px;
color: #7e7266;
font-size: 13px;
}
.festival-note {
color: #a29488;
font-size: 12px;
}
@media (max-width: 900px) {
.festival-header {
flex-direction: column;
align-items: flex-start;
}
.festival-actions {
justify-content: flex-start;
}
}
@media (max-width: 768px) {
.festival {
padding: 12px;
}
.festival-panel {
padding: 20px 18px;
border-radius: 26px;
}
.festival-scroll {
grid-auto-columns: minmax(220px, 1fr);
}
.festival-card {
grid-template-columns: 48px minmax(0, 1fr);
padding: 18px;
}
.festival-icon {
width: 48px;
height: 48px;
border-radius: 16px;
}
}

View File

@@ -1,78 +1,98 @@
import { useRef } from 'react'; import { useRef } from 'react';
import { Button } from 'antd'; import { Button } from 'antd';
import { RightOutlined } from '@ant-design/icons'; import {
RightOutlined,
LeftOutlined,
ThunderboltOutlined,
EnvironmentOutlined,
GiftOutlined,
HeartOutlined,
FireOutlined,
FlagOutlined,
BookOutlined,
CloudOutlined,
StarOutlined,
CalendarOutlined,
} from '@ant-design/icons';
import './Festival.css'; import './Festival.css';
const festivals = [
{ name: '惊蛰', date: '2026-03-05', weekday: '周四', daysLeft: 6, icon: <ThunderboltOutlined />, tone: 'orange' },
{ name: '植树节', date: '2026-03-12', weekday: '周四', daysLeft: 13, icon: <EnvironmentOutlined />, tone: 'green' },
{ name: '春分', date: '2026-03-20', weekday: '周五', daysLeft: 21, icon: <StarOutlined />, tone: 'pink' },
{ name: '愚人节', date: '2026-04-01', weekday: '周三', daysLeft: 33, icon: <GiftOutlined />, tone: 'purple' },
{ name: '清明节', date: '2026-04-05', weekday: '周日', daysLeft: 37, icon: <CloudOutlined />, tone: 'slate' },
{ name: '谷雨', date: '2026-04-20', weekday: '周一', daysLeft: 52, icon: <EnvironmentOutlined />, tone: 'green' },
{ name: '劳动节', date: '2026-05-01', weekday: '周五', daysLeft: 63, icon: <FlagOutlined />, tone: 'orange' },
{ name: '青年节', date: '2026-05-04', weekday: '周一', daysLeft: 66, icon: <FireOutlined />, tone: 'orange' },
{ name: '母亲节', date: '2026-05-10', weekday: '周日', daysLeft: 72, icon: <HeartOutlined />, tone: 'pink' },
{ name: '端午节', date: '2026-06-19', weekday: '周五', daysLeft: 112, icon: <GiftOutlined />, tone: 'green' },
{ name: '教师节', date: '2026-09-10', weekday: '周四', daysLeft: 195, icon: <BookOutlined />, tone: 'slate' },
{ name: '国庆节', date: '2026-10-01', weekday: '周四', daysLeft: 216, icon: <CalendarOutlined />, tone: 'red' },
];
const Festival = () => { const Festival = () => {
const scrollRef = useRef(null); const scrollRef = useRef(null);
const festivals = [
{ name: '惊蛰', date: '2026-03-05', weekday: '周四', daysLeft: 6, emoji: '⚡' },
{ name: '植树节', date: '2026-03-12', weekday: '周四', daysLeft: 13, emoji: '🌳' },
{ name: '春分', date: '2026-03-20', weekday: '周五', daysLeft: 21, emoji: '🌸' },
{ name: '愚人节', date: '2026-04-01', weekday: '周三', daysLeft: 33, emoji: '🤡' },
{ name: '清明节', date: '2026-04-05', weekday: '周日', daysLeft: 37, emoji: '🌿' },
{ name: '谷雨', date: '2026-04-20', weekday: '周一', daysLeft: 52, emoji: '🌾' },
{ name: '劳动节', date: '2026-05-01', weekday: '周五', daysLeft: 63, emoji: '🎉' },
{ name: '青年节', date: '2026-05-04', weekday: '周一', daysLeft: 66, emoji: '🔥' },
{ name: '立夏', date: '2026-05-05', weekday: '周二', daysLeft: 67, emoji: '☀️' },
{ name: '母亲节', date: '2026-05-10', weekday: '周日', daysLeft: 72, emoji: '💐' },
{ name: '小满', date: '2026-05-21', weekday: '周四', daysLeft: 83, emoji: '🌾' },
{ name: '端午节', date: '2026-06-19', weekday: '周五', daysLeft: 112, emoji: '🛶' },
{ name: '夏至', date: '2026-06-21', weekday: '周日', daysLeft: 114, emoji: '🌻' },
{ name: '建党节', date: '2026-07-01', weekday: '周三', daysLeft: 124, emoji: '🚩' },
{ name: '小暑', date: '2026-07-07', weekday: '周二', daysLeft: 130, emoji: '🔥' },
{ name: '建军节', date: '2026-08-01', weekday: '周六', daysLeft: 155, emoji: '🎖️' },
{ name: '立秋', date: '2026-08-07', weekday: '周五', daysLeft: 161, emoji: '🍂' },
{ name: '七夕节', date: '2026-08-25', weekday: '周二', daysLeft: 179, emoji: '💕' },
{ name: '白露', date: '2026-09-07', weekday: '周一', daysLeft: 192, emoji: '💧' },
{ name: '教师节', date: '2026-09-10', weekday: '周四', daysLeft: 195, emoji: '📚' },
{ name: '秋分', date: '2026-09-23', weekday: '周三', daysLeft: 208, emoji: '🍁' },
{ name: '国庆节', date: '2026-10-01', weekday: '周四', daysLeft: 216, emoji: '🇨🇳' },
{ name: '中秋节', date: '2026-10-03', weekday: '周六', daysLeft: 218, emoji: '🥮' },
{ name: '重阳节', date: '2026-10-21', weekday: '周三', daysLeft: 236, emoji: '👴' },
{ name: '立冬', date: '2026-11-07', weekday: '周六', daysLeft: 253, emoji: '❄️' },
{ name: '感恩节', date: '2026-11-26', weekday: '周四', daysLeft: 272, emoji: '🦃' },
{ name: '大雪', date: '2026-12-07', weekday: '周一', daysLeft: 283, emoji: '🌨️' },
{ name: '冬至', date: '2026-12-22', weekday: '周二', daysLeft: 298, emoji: '🥟' },
{ name: '圣诞节', date: '2026-12-25', weekday: '周五', daysLeft: 301, emoji: '🎄' },
];
const scroll = (direction) => { const scroll = (direction) => {
if (scrollRef.current) { if (scrollRef.current) {
const scrollAmount = direction === 'left' ? -300 : 300; const scrollAmount = direction === 'left' ? -320 : 320;
scrollRef.current.scrollBy({ left: scrollAmount, behavior: 'smooth' }); scrollRef.current.scrollBy({ left: scrollAmount, behavior: 'smooth' });
} }
}; };
return ( return (
<section className="festival"> <section className="festival">
<div className="festival-container"> <div className="festival-panel">
<div className="festival-header">
<div className="festival-copy">
<span className="festival-kicker">营销节点</span>
<h2>把时间感也做成页面的一部分</h2>
<p>
这些节点能帮助用户快速想到最近该做什么图比起单纯展示日期更像是一个素材站自己的选题提醒
</p>
</div>
<div className="festival-actions">
<span className="festival-summary">按最近营销节奏排序</span>
<div className="festival-controls">
<Button
className="festival-scroll-btn"
shape="circle"
icon={<LeftOutlined />}
onClick={() => scroll('left')}
/>
<Button
className="festival-scroll-btn"
shape="circle"
icon={<RightOutlined />}
onClick={() => scroll('right')}
/>
</div>
</div>
</div>
<div className="festival-scroll" ref={scrollRef}> <div className="festival-scroll" ref={scrollRef}>
{festivals.map((item, index) => ( {festivals.map((item, index) => (
<div <div
key={item.name} key={item.name}
className="festival-card" className={`festival-card tone-${item.tone}`}
style={{ '--delay': `${index * 0.08}s` }} style={{ '--delay': `${index * 0.05}s` }}
> >
<div className="festival-emoji">{item.emoji}</div> <div className="festival-icon">{item.icon}</div>
<div className="festival-content"> <div className="festival-content">
<h3 className="festival-name">{item.name}</h3> <div className="festival-topline">
<p className="festival-date">{item.date} {item.weekday}</p> <h3 className="festival-name">{item.name}</h3>
</div> <span className="festival-days">{item.daysLeft} 天后</span>
<div className="festival-days"> </div>
<span className="days-number">{item.daysLeft}</span> <p className="festival-date">
<span className="days-text">天后</span> {item.date} · {item.weekday}
</p>
<span className="festival-note">适合提前布局专题与节日视觉</span>
</div> </div>
</div> </div>
))} ))}
</div> </div>
<Button
className="festival-scroll-btn"
shape="circle"
icon={<RightOutlined />}
onClick={() => scroll('right')}
/>
</div> </div>
</section> </section>
); );

View File

@@ -1,159 +1,240 @@
.footer { .footer {
margin-top: auto; padding: 0 20px 24px;
} }
.footer-main { .footer-shell {
background: #f5f5f5; max-width: 1400px;
padding: 40px 0; margin: 0 auto;
} border-radius: 32px;
overflow: hidden;
.footer-container { background:
max-width: 1200px; radial-gradient(circle at top right, rgba(244, 178, 84, 0.14), transparent 26%),
margin: 0 auto; linear-gradient(180deg, rgba(255, 251, 246, 0.96), rgba(250, 243, 236, 0.96));
padding: 0 20px; border: 1px solid rgba(233, 221, 212, 0.92);
} box-shadow: 0 22px 48px rgba(66, 42, 26, 0.08);
}
.footer-links-group {
display: flex; .footer-main {
gap: 24px; display: grid;
margin-bottom: 12px; grid-template-columns: 1.15fr 0.95fr 0.9fr;
} gap: 28px;
padding: 34px 32px 28px;
.footer-link { }
color: #666;
font-size: 14px; .footer-brand {
transition: color 0.2s; display: flex;
} flex-direction: column;
gap: 18px;
.footer-link:hover { }
color: #ff5a5a;
} .footer-logo {
display: inline-flex;
.footer-contact { align-items: center;
margin-bottom: 20px; gap: 12px;
} width: fit-content;
text-decoration: none;
.contact-title { }
font-size: 14px;
color: #333; .footer-logo-icon {
margin: 0 0 12px; width: 46px;
font-weight: 500; height: 28px;
} }
.email-input { .footer-logo strong {
background: white; display: block;
border-radius: 8px; color: #261f19;
max-width: 360px; font-size: 24px;
} }
.email-input .ant-input { .footer-logo span {
color: #666; color: #8b7f73;
} font-size: 12px;
}
.footer-qrcodes {
display: flex; .footer-brand-text {
gap: 24px; margin: 0;
} color: #6f645a;
line-height: 1.9;
.qrcode-item { }
text-align: center;
} .footer-contact-card {
display: grid;
.qrcode-placeholder { grid-template-columns: 44px minmax(0, 1fr);
width: 100px; gap: 14px;
height: 100px; padding: 16px;
background: white; border-radius: 22px;
border-radius: 8px; background: rgba(255, 255, 255, 0.8);
display: flex; border: 1px solid rgba(236, 223, 213, 0.94);
align-items: center; }
justify-content: center;
margin-bottom: 8px; .footer-contact-card .anticon {
border: 1px solid #e8e8e8; width: 44px;
} height: 44px;
border-radius: 16px;
.qrcode-icon { display: grid;
font-size: 48px; place-items: center;
color: #ccc; background: #fff1e9;
} color: var(--brand);
font-size: 18px;
.qrcode-text { }
font-size: 12px;
color: #999; .footer-contact-card span {
} display: block;
color: #8b7f73;
.footer-partners { font-size: 12px;
margin-top: 30px; margin-bottom: 4px;
padding-top: 20px; }
border-top: 1px solid #e8e8e8;
display: flex; .footer-contact-card a {
flex-wrap: wrap; color: #261f19;
gap: 12px 20px; font-size: 16px;
} font-weight: 700;
text-decoration: none;
.partner-link { }
font-size: 12px;
color: #999; .footer-links {
transition: color 0.2s; display: grid;
} grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 18px;
.partner-link:hover { }
color: #ff5a5a;
} .footer-link-group h4 {
margin: 0 0 12px;
.footer-bottom { color: #261f19;
background: #3d3d3d; font-size: 16px;
padding: 20px 0; }
}
.footer-link-group {
.copyright-text { display: flex;
color: #999; flex-direction: column;
font-size: 12px; gap: 10px;
line-height: 1.8; }
margin: 0 0 8px;
} .footer-link {
display: inline-flex;
.email-link { align-items: center;
color: #ff5a5a; gap: 10px;
} padding: 12px 14px;
border-radius: 18px;
.email-link:hover { border: 1px solid rgba(236, 223, 213, 0.94);
text-decoration: underline; background: rgba(255, 255, 255, 0.76);
} color: #5f5449;
cursor: pointer;
.copyright-links { transition: all 0.22s ease;
color: #999; text-align: left;
font-size: 12px; }
margin: 0;
} .footer-link:hover {
border-color: rgba(239, 106, 91, 0.32);
.copyright-links a { color: var(--brand);
color: #999; background: var(--brand-soft);
transition: color 0.2s; }
}
.footer-link .anticon {
.copyright-links a:hover { font-size: 16px;
color: #ff5a5a; }
}
.footer-qr {
@media (max-width: 768px) { display: grid;
.footer-main { gap: 14px;
padding: 30px 0; }
}
.footer-qr-card {
.footer-links-group { padding: 18px;
justify-content: center; border-radius: 22px;
} border: 1px solid rgba(236, 223, 213, 0.94);
background: rgba(255, 255, 255, 0.82);
.footer-qrcodes { display: flex;
justify-content: center; flex-direction: column;
} gap: 10px;
}
.footer-partners {
justify-content: center; .footer-qr-icon {
} width: 52px;
height: 52px;
.copyright-text, border-radius: 18px;
.copyright-links { background: linear-gradient(135deg, #fff1e9 0%, #ffe0d2 100%);
text-align: center; color: var(--brand);
} display: grid;
} place-items: center;
font-size: 24px;
}
.footer-qr-card strong {
color: #261f19;
font-size: 16px;
}
.footer-qr-card span {
color: #83776b;
font-size: 13px;
line-height: 1.7;
}
.footer-bottom {
display: flex;
flex-direction: column;
gap: 10px;
padding: 18px 32px 24px;
border-top: 1px solid rgba(233, 221, 212, 0.92);
}
.copyright-text,
.copyright-links {
margin: 0;
color: #8a7d70;
font-size: 12px;
line-height: 1.8;
}
.email-link {
color: var(--brand);
margin-left: 4px;
}
.copyright-links {
display: flex;
flex-wrap: wrap;
gap: 10px 14px;
}
.copyright-links a {
color: #7c6f63;
}
@media (max-width: 1100px) {
.footer-main {
grid-template-columns: 1fr;
}
.footer-links {
grid-template-columns: 1fr 1fr;
}
}
@media (max-width: 768px) {
.footer {
padding: 0 12px 16px;
}
.footer-shell {
border-radius: 26px;
}
.footer-main {
padding: 22px 18px 18px;
}
.footer-links {
grid-template-columns: 1fr;
}
.footer-bottom {
padding: 16px 18px 20px;
}
.copyright-links {
flex-direction: column;
gap: 4px;
}
}

View File

@@ -1,107 +1,137 @@
import { Row, Col, Input, Button, Divider } from 'antd'; import {
import { MailOutlined, QrcodeOutlined } from '@ant-design/icons'; MailOutlined,
import { useNavigate } from 'react-router-dom'; SafetyCertificateOutlined,
import './Footer.css'; FileTextOutlined,
QuestionCircleOutlined,
const Footer = () => { UploadOutlined,
const navigate = useNavigate(); GlobalOutlined,
MessageOutlined,
const navLinks = [ } from '@ant-design/icons';
{ title: '网站协议', url: '/protocol' }, import { useNavigate } from 'react-router-dom';
{ title: '支付协议', url: '/pay-protocol' }, import './Footer.css';
{ title: '版权声明', url: '/copyright' },
]; const Footer = () => {
const navigate = useNavigate();
const navLinks2 = [
{ title: '帮助中心', url: '/help' }, const productLinks = [
{ title: '供稿必读', url: '/upload-guide' }, { title: '网站协议', url: '/protocol', icon: <FileTextOutlined /> },
]; { title: '支付协议', url: '/pay-protocol', icon: <SafetyCertificateOutlined /> },
{ title: '版权声明', url: '/copyright', icon: <FileTextOutlined /> },
const handleLinkClick = (url) => { ];
navigate(url);
}; const helpLinks = [
{ title: '帮助中心', url: '/help', icon: <QuestionCircleOutlined /> },
return ( { title: '供稿必读', url: '/upload-guide', icon: <UploadOutlined /> },
<footer className="footer"> ];
<div className="footer-main">
<div className="footer-container"> const handleLinkClick = (url) => {
<Row gutter={[48, 24]}> navigate(url);
<Col xs={24} sm={24} md={8} lg={6}> };
<div className="footer-links-group">
{navLinks.map(link => ( return (
<a <footer className="footer">
key={link.title} <div className="footer-shell">
onClick={() => handleLinkClick(link.url)} <div className="footer-main">
className="footer-link" <div className="footer-brand">
> <a href="/" className="footer-logo">
{link.title} <svg viewBox="0 0 40 24" className="footer-logo-icon" aria-hidden="true">
</a> <path
))} d="M20 0c-5.5 0-10 4.5-10 10s4.5 10 10 10c2.8 0 5.3-1.1 7.1-2.9L20 10l7.1-7.1C25.3 1.1 22.8 0 20 0z"
</div> fill="#2f87a8"
<div className="footer-links-group"> />
{navLinks2.map(link => ( <path
<a d="M30 4c-5.5 0-10 4.5-10 10s4.5 10 10 10c5.5 0 10-4.5 10-10S35.5 4 30 4zm0 16c-3.3 0-6-2.7-6-6s2.7-6 6-6 6 2.7 6 6-2.7 6-6 6z"
key={link.title} fill="#2f87a8"
onClick={() => handleLinkClick(link.url)} />
className="footer-link" </svg>
> <div>
{link.title} <strong>图汇</strong>
</a> <span>AiSheji.com</span>
))} </div>
</div> </a>
</Col>
<p className="footer-brand-text">
<Col xs={24} sm={24} md={8} lg={10}> 图汇是一个更偏可成交的素材站支持作品预览支付下载和详情页承接让找图和交付链路更顺
<div className="footer-contact"> </p>
<h4 className="contact-title">客服邮箱</h4>
<div className="contact-email"> <div className="footer-contact-card">
<Input <MailOutlined />
readOnly <div>
value="service@aishej.com 工作日9:00-18:00" <span>客服邮箱</span>
suffix={<MailOutlined />} <a href="mailto:service@aishej.com">service@aishej.com</a>
className="email-input" </div>
/> </div>
</div> </div>
</div>
</Col> <div className="footer-links">
<div className="footer-link-group">
<Col xs={24} sm={24} md={8} lg={8}> <h4>平台说明</h4>
<div className="footer-qrcodes"> {productLinks.map((link) => (
<div className="qrcode-item"> <button
<div className="qrcode-placeholder"> key={link.title}
<QrcodeOutlined className="qrcode-icon" /> type="button"
</div> onClick={() => handleLinkClick(link.url)}
<span className="qrcode-text">移动端网站</span> className="footer-link"
</div> >
<div className="qrcode-item"> {link.icon}
<div className="qrcode-placeholder"> <span>{link.title}</span>
<QrcodeOutlined className="qrcode-icon" /> </button>
</div> ))}
<span className="qrcode-text">微信公众号</span> </div>
</div>
</div> <div className="footer-link-group">
</Col> <h4>帮助与合作</h4>
</Row> {helpLinks.map((link) => (
</div> <button
</div> key={link.title}
type="button"
<div className="footer-bottom"> onClick={() => handleLinkClick(link.url)}
<div className="footer-container"> className="footer-link"
<p className="copyright-text"> >
图汇作为网络服务平台方平台上的作品均由供稿设计师上传并发布若您的权利被侵害请联系客服邮箱 {link.icon}
<a href="mailto:service@aishej.com" className="email-link">service@aishej.com</a> <span>{link.title}</span>
我们将及时为您处理 | 本站法律顾问张明律师 </button>
</p> ))}
<p className="copyright-links"> </div>
<a href="#">京ICP备2024068521号-1</a> | </div>
增值电信业务经营许可证京B2-20240312 |
<a href="#">京公网安备11010802045678号</a> | <div className="footer-qr">
Copyright © 2024-2026 图汇 AiSheji.com <div className="footer-qr-card">
</p> <div className="footer-qr-icon">
</div> <GlobalOutlined />
</div> </div>
</footer> <strong>移动端网站</strong>
); <span>适合手机端快速查找与预览作品</span>
}; </div>
export default Footer; <div className="footer-qr-card">
<div className="footer-qr-icon">
<MessageOutlined />
</div>
<strong>公众号 / 社群</strong>
<span>后续可替换成真实二维码或企微入口</span>
</div>
</div>
</div>
<div className="footer-bottom">
<p className="copyright-text">
图汇作为网络服务平台方平台上的作品均由供稿设计师上传并发布若您的权利被侵害请联系
<a href="mailto:service@aishej.com" className="email-link">
service@aishej.com
</a>
我们将及时处理
</p>
<p className="copyright-links">
<a href="#">京ICP备2024068521号-1</a>
<span>增值电信业务经营许可证京B2-20240312</span>
<a href="#">京公网安备11010802045678号</a>
<span>Copyright © 2024-2026 图汇 AiSheji.com</span>
</p>
</div>
</div>
</footer>
);
};
export default Footer;

View File

@@ -1,9 +1,11 @@
.header { .header {
background: #fff; background: rgba(255, 251, 246, 0.86);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); backdrop-filter: blur(18px);
position: fixed; border-bottom: 1px solid rgba(233, 221, 212, 0.88);
top: 0; box-shadow: 0 14px 34px rgba(61, 40, 24, 0.05);
left: 0; position: fixed;
top: 0;
left: 0;
right: 0; right: 0;
z-index: 1000; z-index: 1000;
} }
@@ -34,17 +36,17 @@
height: 24px; height: 24px;
} }
.logo-text { .logo-text {
font-size: 20px; font-size: 20px;
font-weight: 700; font-weight: 700;
color: #ff5a5a; color: var(--brand);
} }
.logo-domain { .logo-domain {
font-size: 10px; font-size: 10px;
color: #999; color: #8d8074;
margin-left: 4px; margin-left: 4px;
} }
.header-nav { .header-nav {
display: flex; display: flex;
@@ -52,34 +54,34 @@
gap: 24px; gap: 24px;
} }
.nav-item { .nav-item {
color: #333; color: #3b3027;
font-size: 14px; font-size: 14px;
cursor: pointer; cursor: pointer;
transition: color 0.2s; transition: color 0.2s;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 4px; gap: 4px;
position: relative; position: relative;
} }
.nav-item:hover { .nav-item:hover {
color: #ff5a5a; color: var(--brand);
} }
.nav-new { .nav-new {
position: relative; position: relative;
} }
.new-dot { .new-dot {
position: absolute; position: absolute;
top: -2px; top: -2px;
right: -8px; right: -8px;
width: 6px; width: 6px;
height: 6px; height: 6px;
background: #ff5a5a; background: var(--brand);
border-radius: 50%; border-radius: 50%;
} }
.header-search { .header-search {
flex: 1; flex: 1;
@@ -92,12 +94,12 @@
padding: 6px 16px; padding: 6px 16px;
} }
.header-search .ant-input-affix-wrapper:hover, .header-search .ant-input-affix-wrapper:hover,
.header-search .ant-input-affix-wrapper:focus, .header-search .ant-input-affix-wrapper:focus,
.header-search .ant-input-affix-wrapper-focused { .header-search .ant-input-affix-wrapper-focused {
border-color: #ff5a5a; border-color: var(--brand);
box-shadow: none; box-shadow: none;
} }
.header-right { .header-right {
display: flex; display: flex;
@@ -106,44 +108,46 @@
flex-shrink: 0; flex-shrink: 0;
} }
.btn-invite { .btn-invite {
background: #ff5a5a; background: linear-gradient(135deg, var(--brand) 0%, var(--brand-strong) 100%);
border-color: #ff5a5a; border-color: var(--brand);
border-radius: 20px; border-radius: 20px;
padding: 4px 20px; padding: 4px 20px;
height: 36px; height: 36px;
} }
.btn-invite:hover { .btn-invite:hover {
background: #ff7070 !important; background: linear-gradient(135deg, #3c97ba 0%, #236b88 100%) !important;
border-color: #ff7070 !important; border-color: var(--brand) !important;
} }
.btn-register { .btn-register {
border-radius: 20px; border-radius: 20px;
padding: 4px 20px; padding: 4px 20px;
height: 36px; height: 36px;
border-color: #333; border-color: #d9cabc;
color: #333; color: #3b3027;
} background: rgba(255, 255, 255, 0.72);
}
.btn-register:hover {
border-color: #ff5a5a !important; .btn-register:hover {
color: #ff5a5a !important; border-color: var(--brand) !important;
} color: var(--brand) !important;
}
.btn-login { .btn-login {
border-radius: 20px; border-radius: 20px;
padding: 4px 20px; padding: 4px 20px;
height: 36px; height: 36px;
border-color: #333; border-color: #d9cabc;
color: #333; color: #3b3027;
} background: rgba(255, 255, 255, 0.72);
}
.btn-login:hover {
border-color: #ff5a5a !important; .btn-login:hover {
color: #ff5a5a !important; border-color: var(--brand) !important;
} color: var(--brand) !important;
}
@media (max-width: 1024px) { @media (max-width: 1024px) {
.header-nav { .header-nav {

View File

@@ -82,8 +82,8 @@ const Header = () => {
<div className="header-left"> <div className="header-left">
<a href="/" className="logo"> <a href="/" className="logo">
<svg viewBox="0 0 40 24" className="logo-icon"> <svg viewBox="0 0 40 24" className="logo-icon">
<path d="M20 0c-5.5 0-10 4.5-10 10s4.5 10 10 10c2.8 0 5.3-1.1 7.1-2.9L20 10l7.1-7.1C25.3 1.1 22.8 0 20 0z" fill="#ff5a5a"/> <path d="M20 0c-5.5 0-10 4.5-10 10s4.5 10 10 10c2.8 0 5.3-1.1 7.1-2.9L20 10l7.1-7.1C25.3 1.1 22.8 0 20 0z" fill="#2f87a8"/>
<path d="M30 4c-5.5 0-10 4.5-10 10s4.5 10 10 10c5.5 0 10-4.5 10-10S35.5 4 30 4zm0 16c-3.3 0-6-2.7-6-6s2.7-6 6-6 6 2.7 6 6-2.7 6-6 6z" fill="#ff5a5a"/> <path d="M30 4c-5.5 0-10 4.5-10 10s4.5 10 10 10c5.5 0 10-4.5 10-10S35.5 4 30 4zm0 16c-3.3 0-6-2.7-6-6s2.7-6 6-6 6 2.7 6 6-2.7 6-6 6z" fill="#2f87a8"/>
</svg> </svg>
<span className="logo-text">图汇</span> <span className="logo-text">图汇</span>
<span className="logo-domain">DESIGN006.COM</span> <span className="logo-domain">DESIGN006.COM</span>

View File

@@ -1,177 +1,381 @@
/* ========== Hero 区块整体样式 ========== */ .hero {
.hero-section {
position: relative; position: relative;
height: 600px; padding: 32px 20px 24px;
}
.hero-shell {
max-width: 1400px;
margin: 0 auto;
padding: 44px;
border-radius: 36px;
background:
radial-gradient(circle at top left, rgba(239, 106, 91, 0.16), transparent 36%),
radial-gradient(circle at bottom right, rgba(244, 178, 84, 0.22), transparent 32%),
linear-gradient(135deg, #fffaf4 0%, #fff3e7 54%, #fffdf8 100%);
border: 1px solid rgba(225, 205, 186, 0.7);
box-shadow: 0 28px 80px rgba(76, 48, 30, 0.12);
display: grid;
grid-template-columns: minmax(0, 1.2fr) minmax(360px, 0.8fr);
gap: 28px;
overflow: hidden;
position: relative;
}
.hero-shell::before {
content: '';
position: absolute;
inset: 0;
background:
linear-gradient(120deg, rgba(255, 255, 255, 0.55), transparent 35%),
linear-gradient(305deg, rgba(255, 255, 255, 0.42), transparent 30%);
pointer-events: none;
}
.hero-copy,
.hero-stage {
position: relative;
z-index: 1;
}
.hero-copy {
display: flex; display: flex;
align-items: center; flex-direction: column;
justify-content: center; justify-content: center;
overflow: hidden; gap: 22px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
animation: fadeIn 1s ease-out;
} }
/* ========== 动态背景 ========== */ .hero-eyebrow {
.hero-background { display: inline-flex;
position: absolute; align-items: center;
top: 0; width: fit-content;
left: 0; padding: 8px 14px;
right: 0; border-radius: 999px;
bottom: 0; background: rgba(255, 255, 255, 0.82);
overflow: hidden; border: 1px solid rgba(228, 202, 181, 0.9);
} color: #b55d43;
font-size: 13px;
.hero-particle { font-weight: 700;
position: absolute; letter-spacing: 0.08em;
width: 4px;
height: 4px;
background: rgba(255, 255, 255, 0.3);
border-radius: 50%;
animation: float 6s ease-in-out infinite;
}
.hero-particle:nth-child(1) { left: 10%; animation-delay: 0s; }
.hero-particle:nth-child(2) { left: 20%; animation-delay: 1s; }
.hero-particle:nth-child(3) { left: 30%; animation-delay: 2s; }
.hero-particle:nth-child(4) { left: 40%; animation-delay: 3s; }
.hero-particle:nth-child(5) { left: 50%; animation-delay: 4s; }
.hero-particle:nth-child(6) { left: 60%; animation-delay: 5s; }
.hero-particle:nth-child(7) { left: 70%; animation-delay: 6s; }
.hero-particle:nth-child(8) { left: 80%; animation-delay: 7s; }
.hero-particle:nth-child(9) { left: 90%; animation-delay: 8s; }
/* ========== Hero 内容 ========== */
.hero-content {
position: relative;
z-index: 2;
text-align: center;
color: white;
max-width: 800px;
padding: 0 20px;
animation: slideIn 1s ease-out;
} }
.hero-title { .hero-title {
font-size: 56px; margin: 0;
color: #211d18;
font-size: clamp(38px, 6vw, 64px);
line-height: 1.05;
font-weight: 800; font-weight: 800;
margin-bottom: 20px; max-width: 11ch;
line-height: 1.2;
text-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
} }
.hero-subtitle { .hero-title span {
font-size: 20px; display: block;
margin-bottom: 40px; color: var(--brand);
opacity: 0.95;
line-height: 1.6;
} }
/* ========== Hero 按钮 ========== */ .hero-description {
.hero-buttons { max-width: 620px;
margin: 0;
font-size: 17px;
line-height: 1.9;
color: #6f665d;
}
.hero-search-panel {
display: flex; display: flex;
gap: 20px; flex-direction: column;
justify-content: center; gap: 16px;
}
.hero-search-box {
padding: 14px;
border-radius: 26px;
background: rgba(255, 255, 255, 0.92);
border: 1px solid rgba(230, 215, 202, 0.92);
box-shadow: 0 16px 30px rgba(70, 44, 26, 0.08);
}
.hero-search-input.ant-input-affix-wrapper {
height: 62px;
border: none;
box-shadow: none;
padding: 0 10px;
background: transparent;
}
.hero-search-input.ant-input-affix-wrapper input {
font-size: 16px;
color: #2d261f;
}
.hero-search-input.ant-input-affix-wrapper input::placeholder {
color: #a19488;
}
.hero-search-prefix,
.hero-search-camera {
color: #b18a72;
font-size: 18px;
}
.hero-action-row {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap; flex-wrap: wrap;
} }
.btn-hero-primary { .hero-primary-btn.ant-btn {
background: white; height: 48px;
color: #667eea; padding: 0 22px;
padding: 16px 40px; border-radius: 999px;
border-radius: 30px;
font-size: 18px;
font-weight: 700;
border: none; border: none;
cursor: pointer; background: linear-gradient(135deg, var(--brand) 0%, var(--brand-strong) 100%);
transition: all 0.3s ease; box-shadow: 0 14px 30px rgba(216, 78, 63, 0.26);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
}
.btn-hero-primary:hover {
transform: translateY(-3px);
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.3);
}
.btn-hero-secondary {
background: transparent;
color: white;
padding: 16px 40px;
border-radius: 30px;
font-size: 18px;
font-weight: 700; font-weight: 700;
border: 2px solid white; }
.hero-primary-btn.ant-btn:hover {
transform: translateY(-1px);
}
.hero-secondary-btn.ant-btn {
height: 48px;
padding: 0 22px;
border-radius: 999px;
border: 1px solid rgba(182, 149, 120, 0.42);
background: rgba(255, 255, 255, 0.72);
color: #4d3f31;
font-weight: 600;
}
.hero-secondary-btn.ant-btn:hover {
color: var(--brand) !important;
border-color: rgba(239, 106, 91, 0.45) !important;
}
.hero-tags {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
}
.hero-tags-label {
font-size: 13px;
font-weight: 700;
color: #7d7267;
}
.hero-tag.ant-tag {
margin: 0;
padding: 8px 14px;
border-radius: 999px;
border: 1px solid rgba(221, 200, 182, 0.95);
background: rgba(255, 255, 255, 0.8);
color: #4b4036;
cursor: pointer; cursor: pointer;
transition: all 0.3s ease; transition: all 0.2s ease;
} }
.btn-hero-secondary:hover { .hero-tag.ant-tag:hover {
background: rgba(255, 255, 255, 0.1); color: var(--brand);
transform: translateY(-3px); border-color: rgba(239, 106, 91, 0.45);
background: var(--brand-soft);
} }
/* ========== 装饰元素 ========== */ .hero-stats {
.hero-decoration { display: grid;
position: absolute; grid-template-columns: repeat(3, minmax(0, 1fr));
width: 400px; gap: 14px;
height: 400px; max-width: 520px;
border: 2px solid rgba(255, 255, 255, 0.1);
border-radius: 50%;
animation: float 8s ease-in-out infinite;
} }
.hero-decoration-1 { .hero-stat {
top: -100px; padding: 18px 16px;
right: -100px; border-radius: 22px;
background: rgba(255, 255, 255, 0.72);
border: 1px solid rgba(228, 208, 190, 0.88);
display: flex;
flex-direction: column;
gap: 6px;
} }
.hero-decoration-2 { .hero-stat strong {
bottom: -150px; font-size: 28px;
left: -150px; line-height: 1;
width: 300px; color: #211d18;
height: 300px;
animation-delay: 2s;
} }
/* ========== 渐变遮罩 ========== */ .hero-stat span {
.hero-overlay { font-size: 13px;
position: absolute; color: #7d7267;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: radial-gradient(circle at center, transparent 0%, rgba(102, 126, 234, 0.3) 100%);
} }
/* ========== 响应式设计 ========== */ .hero-stage {
@media (max-width: 768px) { display: flex;
.hero-section { flex-direction: column;
height: 500px; gap: 16px;
justify-content: center;
}
.hero-stage-card {
border-radius: 28px;
border: 1px solid rgba(229, 210, 192, 0.9);
background: rgba(255, 255, 255, 0.72);
box-shadow: 0 18px 36px rgba(66, 43, 27, 0.08);
}
.hero-stage-main {
padding: 28px;
min-height: 280px;
display: flex;
flex-direction: column;
justify-content: space-between;
background:
radial-gradient(circle at top right, rgba(244, 178, 84, 0.2), transparent 35%),
linear-gradient(180deg, rgba(255, 255, 255, 0.95), rgba(255, 248, 241, 0.94));
}
.hero-stage-topline {
display: flex;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
}
.hero-stage-label,
.hero-stage-badge {
padding: 8px 12px;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
}
.hero-stage-label {
background: rgba(239, 106, 91, 0.12);
color: var(--brand-strong);
}
.hero-stage-badge {
background: rgba(47, 103, 81, 0.12);
color: #2f6751;
}
.hero-stage-main h2 {
margin: 0;
font-size: 30px;
line-height: 1.2;
color: #211d18;
}
.hero-stage-main p {
margin: 0;
color: #6f665d;
line-height: 1.9;
}
.hero-stage-pills {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.hero-stage-pills span {
padding: 9px 14px;
border-radius: 999px;
background: #fff;
border: 1px solid rgba(229, 210, 192, 0.85);
color: #594c40;
font-size: 13px;
font-weight: 600;
}
.hero-stage-stack {
display: grid;
grid-template-columns: 1fr;
gap: 12px;
}
.hero-stage-mini {
display: grid;
grid-template-columns: 46px minmax(0, 1fr);
gap: 14px;
align-items: start;
padding: 18px 20px;
}
.hero-stage-icon {
width: 46px;
height: 46px;
border-radius: 16px;
background: linear-gradient(135deg, #fff3ed 0%, #ffe2d5 100%);
color: #e35c4e;
display: grid;
place-items: center;
font-size: 20px;
}
.hero-stage-mini h3 {
margin: 0 0 6px;
font-size: 17px;
color: #211d18;
}
.hero-stage-mini p {
margin: 0;
color: #776c60;
line-height: 1.75;
font-size: 14px;
}
@media (max-width: 1200px) {
.hero-shell {
grid-template-columns: 1fr;
padding: 34px;
} }
.hero-title { .hero-title {
font-size: 36px; max-width: none;
}
.hero-subtitle {
font-size: 16px;
}
.hero-buttons {
flex-direction: column;
gap: 15px;
}
.btn-hero-primary,
.btn-hero-secondary {
padding: 14px 30px;
font-size: 16px;
width: 100%;
max-width: 300px;
} }
} }
/* ========== 暗黑模式 ========== */ @media (max-width: 768px) {
@media (prefers-color-scheme: dark) { .hero {
.hero-section { padding: 20px 12px 10px;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); }
.hero-shell {
padding: 24px 18px;
border-radius: 28px;
}
.hero-title {
font-size: 34px;
}
.hero-description {
font-size: 15px;
}
.hero-search-box {
padding: 10px 12px;
}
.hero-search-input.ant-input-affix-wrapper {
height: 52px;
}
.hero-stats {
grid-template-columns: 1fr;
max-width: none;
}
.hero-stage-main {
min-height: auto;
padding: 22px;
}
.hero-stage-main h2 {
font-size: 24px;
} }
} }

View File

@@ -1,76 +1,149 @@
import { Input, Button, Tag } from 'antd'; import { Input, Button, Tag } from 'antd';
import { SearchOutlined, CameraOutlined } from '@ant-design/icons'; import {
import './Hero.css'; SearchOutlined,
CameraOutlined,
const Hero = () => { ArrowRightOutlined,
const recommendTags = ['地产', '医美', '旅游', '汽车', '价值点', '美陈', '电商', '画册', '大寒']; ThunderboltOutlined,
SafetyCertificateOutlined,
return ( ClockCircleOutlined,
<section className="hero"> } from '@ant-design/icons';
<div className="hero-background"> import { useNavigate } from 'react-router-dom';
{/* Winter Scene Illustration */} import './Hero.css';
<div className="hero-scene">
<div className="snowflakes"> const recommendTags = ['活动', '医美', '科技', '包装', '直播', '邀请函'];
{[...Array(20)].map((_, i) => ( const heroStats = [
<div key={i} className="snowflake" style={{ { value: '24h', label: '持续更新' },
left: `${Math.random() * 100}%`, { value: '原图', label: '即时交付' },
animationDelay: `${Math.random() * 5}s`, { value: '在线', label: '支付下载' },
animationDuration: `${3 + Math.random() * 4}s` ];
}}></div> const stageHighlights = [
))} {
</div> icon: <ThunderboltOutlined />,
title: '热门原图',
{/* Left Side - Title */} description: '按场景、风格和行业快速找图,减少反复沟通。',
<div className="hero-title-area"> },
<h1 className="hero-main-title">这个冬天</h1> {
<h2 className="hero-sub-title">相约雪山</h2> icon: <SafetyCertificateOutlined />,
<p className="hero-english">APPOINTMENT AT SNOWMOUNTAIN</p> title: '支付后交付',
</div> description: '下单、支付、下载一条链路走通,用户体验更顺。',
},
{/* Decorative Elements */} {
<div className="hero-decorations"> icon: <ClockCircleOutlined />,
<div className="mountain mountain-1"></div> title: '最新上传',
<div className="mountain mountain-2"></div> description: '设计师持续上新,支持详情页预览和作品编号追踪。',
<div className="tree tree-1">🌲</div> },
<div className="tree tree-2">🌲</div> ];
<div className="tree tree-3">🎄</div>
<div className="snowman"></div> const Hero = () => {
<div className="people people-1">🎿</div> const navigate = useNavigate();
<div className="people people-2">🛷</div>
</div> const scrollToHotWorks = () => {
</div> document.getElementById('home-hot-works')?.scrollIntoView({
</div> behavior: 'smooth',
block: 'start',
<div className="hero-content"> });
<h2 className="hero-slogan">有所想不如有所享</h2> };
<div className="hero-search-box"> return (
<Input <section className="hero">
size="large" <div className="hero-shell">
placeholder="搜索作品或编号" <div className="hero-copy">
className="hero-search-input" <span className="hero-eyebrow">图汇精选素材库</span>
suffix={ <h1 className="hero-title">
<div className="search-actions"> 把灵感整理成
<CameraOutlined className="camera-icon" /> <span>可直接下载的设计资产</span>
<Button type="primary" shape="circle" icon={<SearchOutlined />} className="search-btn" /> </h1>
</div> <p className="hero-description">
} 原图背景电商图活动物料统一归档预览支付下载三步走通
/> 让客户从找图到拿到成品更省心
</div> </p>
<div className="hero-tags"> <div className="hero-search-panel">
<span className="tags-label">推荐搜索</span> <div className="hero-search-box">
{recommendTags.map(tag => ( <Input
<Tag key={tag} className="recommend-tag">{tag}</Tag> size="large"
))} placeholder="搜索作品标题、作品编号或场景关键词"
</div> className="hero-search-input"
prefix={<SearchOutlined className="hero-search-prefix" />}
<div className="hero-designer"> suffix={<CameraOutlined className="hero-search-camera" />}
设计师M.A />
</div> </div>
</div> <div className="hero-action-row">
</section> <Button
); type="primary"
}; size="large"
className="hero-primary-btn"
export default Hero; icon={<ArrowRightOutlined />}
onClick={scrollToHotWorks}
>
浏览热门作品
</Button>
<Button
size="large"
className="hero-secondary-btn"
onClick={() => navigate('/upload-guide')}
>
上传指南
</Button>
</div>
</div>
<div className="hero-tags">
<span className="hero-tags-label">推荐分类</span>
{recommendTags.map((tag) => (
<Tag
key={tag}
className="hero-tag"
onClick={() => navigate(`/category/${tag}`)}
>
{tag}
</Tag>
))}
</div>
<div className="hero-stats">
{heroStats.map((item) => (
<div key={item.label} className="hero-stat">
<strong>{item.value}</strong>
<span>{item.label}</span>
</div>
))}
</div>
</div>
<div className="hero-stage">
<div className="hero-stage-card hero-stage-main">
<div className="hero-stage-topline">
<span className="hero-stage-label">本周趋势</span>
<span className="hero-stage-badge">精选专题</span>
</div>
<h2>更像一个能直接成交的素材站而不是简单图库</h2>
<p>
首页负责承接搜索意图详情页负责转化下载动作让用户一眼知道
这里能找能买能下
</p>
<div className="hero-stage-pills">
<span>高清原图</span>
<span>在线支付</span>
<span>作品详情页</span>
</div>
</div>
<div className="hero-stage-stack">
{stageHighlights.map((item) => (
<div key={item.title} className="hero-stage-card hero-stage-mini">
<div className="hero-stage-icon">{item.icon}</div>
<div>
<h3>{item.title}</h3>
<p>{item.description}</p>
</div>
</div>
))}
</div>
</div>
</div>
</section>
);
};
export default Hero;

View File

@@ -1,275 +1,314 @@
/* ========== 作品区块整体样式 ========== */
.works-section { .works-section {
padding: 60px 20px;
max-width: 1400px; max-width: 1400px;
margin: 0 auto; margin: 0 auto;
animation: fadeIn 0.8s ease-out; padding: 54px 20px;
}
.works-loading {
display: flex;
justify-content: center;
align-items: center;
min-height: 220px;
border-radius: 28px;
background: rgba(255, 255, 255, 0.72);
border: 1px solid rgba(231, 218, 207, 0.85);
} }
/* ========== 区块标题 ========== */
.section-header { .section-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: flex-end;
margin-bottom: 40px; gap: 24px;
padding: 0 10px; margin-bottom: 28px;
}
.section-copy {
max-width: 760px;
}
.section-kicker {
display: inline-flex;
margin-bottom: 12px;
padding: 7px 12px;
border-radius: 999px;
background: rgba(239, 106, 91, 0.1);
color: var(--brand-strong);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.08em;
} }
.section-title { .section-title {
font-size: 32px; margin: 0;
font-weight: 700; font-size: clamp(28px, 4vw, 38px);
color: #2d3436; line-height: 1.1;
position: relative; color: #221d18;
padding-left: 20px;
animation: slideIn 0.6s ease-out;
} }
.section-title::before { .section-description {
content: ''; margin: 12px 0 0;
position: absolute; font-size: 15px;
left: 0; line-height: 1.85;
top: 50%; color: #756b61;
transform: translateY(-50%);
width: 6px;
height: 32px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 3px;
} }
.view-more { .section-summary {
color: #667eea;
font-size: 16px;
font-weight: 600;
text-decoration: none;
transition: all 0.3s ease;
display: flex; display: flex;
align-items: center; flex-wrap: wrap;
gap: 6px; gap: 10px;
padding: 8px 16px; justify-content: flex-end;
border-radius: 20px;
background: rgba(102, 126, 234, 0.1);
} }
.view-more:hover { .section-pill {
background: rgba(102, 126, 234, 0.2); padding: 10px 14px;
transform: translateX(5px); border-radius: 999px;
background: rgba(255, 255, 255, 0.84);
border: 1px solid rgba(232, 220, 209, 0.9);
color: #564a40;
font-size: 13px;
font-weight: 600;
}
.section-pill-accent {
background: linear-gradient(135deg, #e7f5f2 0%, #d8eeea 100%);
color: var(--brand-strong);
} }
/* ========== 作品网格布局 ========== */
.works-grid { .works-grid {
animation: scaleIn 0.8s ease-out; animation: fadeIn 0.6s ease-out;
} }
/* ========== 作品卡片 ========== */ .work-card.ant-card {
.work-card {
border-radius: 16px;
overflow: hidden; overflow: hidden;
border: none; border: 1px solid rgba(233, 221, 212, 0.92);
background: white; border-radius: 24px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); background: rgba(255, 255, 255, 0.94);
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); box-shadow: 0 18px 40px rgba(58, 38, 23, 0.08);
cursor: pointer; transition: transform 0.28s ease, box-shadow 0.28s ease, border-color 0.28s ease;
animation: fadeIn 0.6s ease-out; animation: fadeIn 0.6s ease-out;
animation-delay: var(--delay);
animation-fill-mode: both; animation-fill-mode: both;
} }
.work-card:hover { .work-card.ant-card:hover {
transform: translateY(-10px) scale(1.02); transform: translateY(-8px);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15); border-color: rgba(239, 106, 91, 0.3);
box-shadow: 0 26px 48px rgba(58, 38, 23, 0.14);
}
.work-card .ant-card-body {
padding: 20px;
} }
/* ========== 作品图片容器 ========== */
.work-image { .work-image {
position: relative; position: relative;
width: 100%; height: 290px;
height: 280px;
overflow: hidden; overflow: hidden;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); display: flex;
align-items: center;
justify-content: center;
padding: 8px;
background: #ffffff;
}
.work-image::after {
content: '';
position: absolute;
inset: auto 0 0;
height: 40%;
background: linear-gradient(180deg, transparent, rgba(29, 22, 17, 0.28));
pointer-events: none;
} }
.work-image img { .work-image img {
width: 100%; max-width: 100%;
height: 100%; max-height: 100%;
object-fit: cover; width: auto;
height: auto;
object-fit: contain;
transition: transform 0.5s ease; transition: transform 0.5s ease;
} }
.work-card:hover .work-image img { .work-card:hover .work-image img {
transform: scale(1.1); transform: scale(1.03);
} }
/* ========== 预览遮罩层 ========== */ .work-image-overlay {
.work-preview {
position: absolute; position: absolute;
top: 0; inset: auto 18px 18px 18px;
left: 0; z-index: 2;
right: 0;
bottom: 0;
background: rgba(102, 126, 234, 0.8);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: space-between;
gap: 10px;
padding: 12px 14px;
border-radius: 16px;
background: rgba(28, 24, 20, 0.58);
color: #fff;
font-size: 14px;
font-weight: 600;
opacity: 0; opacity: 0;
transition: all 0.3s ease; transform: translateY(10px);
transition: opacity 0.25s ease, transform 0.25s ease;
} }
.work-card:hover .work-preview { .work-card:hover .work-image-overlay {
opacity: 1; opacity: 1;
transform: translateY(0);
} }
.preview-icon { .work-price-badge {
font-size: 48px; position: absolute;
transform: scale(0.5); top: 16px;
transition: transform 0.3s ease; right: 16px;
z-index: 2;
padding: 8px 12px;
border-radius: 999px;
background: rgba(255, 250, 244, 0.92);
border: 1px solid rgba(233, 218, 207, 0.92);
color: var(--brand-strong);
font-size: 14px;
font-weight: 700;
backdrop-filter: blur(10px);
} }
.work-card:hover .preview-icon {
transform: scale(1);
}
/* ========== 作品信息 ========== */
.work-info { .work-info {
padding: 20px; display: flex;
background: white; flex-direction: column;
gap: 14px;
}
.work-topline {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
}
.work-category.ant-tag {
margin: 0;
padding: 6px 12px;
border-radius: 999px;
border: 1px solid rgba(226, 209, 196, 0.9);
background: #fff9f4;
color: #7b6655;
font-size: 12px;
font-weight: 700;
}
.work-id {
color: #a09083;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.04em;
} }
.work-title { .work-title {
font-size: 16px; margin: 0;
font-weight: 600; color: #241e19;
color: #2d3436; font-size: 18px;
margin-bottom: 12px; line-height: 1.45;
line-height: 1.5; font-weight: 700;
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
transition: color 0.3s ease;
} }
.work-card:hover .work-title {
color: #667eea;
}
/* ========== 作品元数据 ========== */
.work-meta { .work-meta {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
margin-bottom: 12px;
flex-wrap: wrap; flex-wrap: wrap;
} }
.work-designer { .work-designer {
font-size: 13px; font-size: 13px;
color: #636e72; color: #72675d;
font-weight: 500; font-weight: 600;
} }
.work-level { .work-level.ant-tag {
margin: 0;
padding: 4px 10px;
border-radius: 999px;
background: transparent;
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 700;
padding: 3px 10px;
border-radius: 12px;
border: 1px solid;
} }
.work-level-text { .work-level-text {
font-size: 12px; font-size: 12px;
color: #b2bec3; color: #a29488;
} }
/* ========== 作品价格 ========== */ .work-bottom {
.work-price {
font-size: 20px;
font-weight: 700;
color: #ff5a5a;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 4px; justify-content: space-between;
gap: 14px;
padding-top: 6px;
border-top: 1px solid rgba(239, 230, 222, 0.95);
} }
.work-price::before { .work-price {
content: '¥'; color: var(--brand);
font-size: 14px; font-size: 24px;
font-weight: 800;
line-height: 1;
} }
/* ========== 卡片延迟动画 ========== */ .work-cta {
.work-card:nth-child(1) { animation-delay: 0.05s; } display: inline-flex;
.work-card:nth-child(2) { animation-delay: 0.1s; } align-items: center;
.work-card:nth-child(3) { animation-delay: 0.15s; } gap: 6px;
.work-card:nth-child(4) { animation-delay: 0.2s; } color: #6d6258;
.work-card:nth-child(5) { animation-delay: 0.25s; } font-size: 13px;
.work-card:nth-child(6) { animation-delay: 0.3s; } font-weight: 700;
.work-card:nth-child(7) { animation-delay: 0.35s; } }
.work-card:nth-child(8) { animation-delay: 0.4s; }
.work-card:nth-child(9) { animation-delay: 0.45s; } .work-card:hover .work-cta {
color: var(--brand);
}
/* ========== 加载状态 ========== */
.works-section .ant-spin { .works-section .ant-spin {
color: #667eea; color: var(--brand);
} }
.works-section .ant-spin-text { .works-section .ant-spin-text {
color: #636e72; color: #756b61;
font-size: 14px;
margin-top: 10px; margin-top: 10px;
} }
/* ========== 空状态 ========== */ @media (max-width: 900px) {
.works-empty { .section-header {
text-align: center; flex-direction: column;
padding: 60px 20px; align-items: flex-start;
color: #b2bec3; }
.section-summary {
justify-content: flex-start;
}
} }
.works-empty-icon {
font-size: 64px;
margin-bottom: 20px;
opacity: 0.5;
}
.works-empty-text {
font-size: 16px;
}
/* ========== 响应式设计 ========== */
@media (max-width: 768px) { @media (max-width: 768px) {
.works-section { .works-section {
padding: 40px 15px; padding: 42px 12px;
} }
.section-title { .section-title {
font-size: 24px; font-size: 28px;
} }
.work-image { .section-description {
height: 200px;
}
.work-title {
font-size: 14px; font-size: 14px;
} }
.work-price {
font-size: 18px;
}
}
/* ========== 暗黑模式 ========== */ .work-image {
@media (prefers-color-scheme: dark) { height: 250px;
.work-card {
background: #2d2d44;
} }
.work-title { .work-card .ant-card-body {
color: #e0e0e0; padding: 18px;
}
.work-designer {
color: #a0a0a0;
}
.work-card:hover .work-title {
color: #667eea;
} }
} }

View File

@@ -1,120 +1,162 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { Card, Tag, Row, Col, Spin } from 'antd'; import { Card, Tag, Row, Col, Spin } from 'antd';
import { RightOutlined } from '@ant-design/icons'; import { RightOutlined } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { getWorksList } from '../api/works'; import { getWorksList } from '../api/works';
import { API_CONFIG } from '../utils/config'; import { API_CONFIG } from '../utils/config';
import './Works.css'; import './Works.css';
const Works = ({ title, type, categoryFilter }) => { const sectionPresets = {
const navigate = useNavigate(); hot: {
const [works, setWorks] = useState([]); kicker: '精选推荐',
const [loading, setLoading] = useState(true); description: '优先展示更适合首页承接咨询和转化的热门素材,适合直接跳详情页成交。',
badge: '本周热度',
useEffect(() => { },
loadWorks(); new: {
}, [type, categoryFilter]); kicker: '最新上架',
description: '设计师最新上传内容集中展示,适合让站点保持“持续更新”的活跃感。',
const loadWorks = async () => { badge: '持续更新',
setLoading(true); },
const result = await getWorksList(1, 9, categoryFilter || ''); };
setLoading(false);
const Works = ({ title, type, categoryFilter, sectionId }) => {
if (result.success) { const navigate = useNavigate();
setWorks(result.data.items || []); const [works, setWorks] = useState([]);
} const [loading, setLoading] = useState(true);
};
useEffect(() => {
// 使用 Picsum 随机图片(更稳定) loadWorks();
const getImageUrl = (id, width = 400, height = 300) => { }, [type, categoryFilter]);
return `https://picsum.photos/seed/${id}/${width}/${height}`;
}; const loadWorks = async () => {
setLoading(true);
const getLevelColor = (level) => { const result = await getWorksList(1, 9, categoryFilter || '');
const colors = { setLoading(false);
1: '#999',
2: '#74b9ff', if (result.success) {
3: '#00b894', setWorks(result.data.items || []);
4: '#6c5ce7', } else {
5: '#e17055', setWorks([]);
6: '#ff5a5a', }
}; };
return colors[level] || '#999';
}; const getImageUrl = (id, width = 480, height = 340) => {
return `https://picsum.photos/seed/${id}/${width}/${height}`;
// 点击卡片,跳转到详情页 };
const handleCardClick = (work) => {
navigate(`/detail/${work.id}`); const getLevelColor = (level) => {
}; const colors = {
1: '#8b847d',
if (loading) { 2: '#5f9fd2',
return ( 3: '#2f8f73',
<section className="works-section"> 4: '#9a5be0',
<div style={{ textAlign: 'center', padding: '40px 0' }}> 5: '#d46b40',
<Spin size="large" tip="加载中..." /> 6: '#e0564a',
</div> };
</section> return colors[level] || '#8b847d';
); };
}
const handleCardClick = (work) => {
return ( navigate(`/detail/${work.id}`);
<section className="works-section"> };
<div className="section-header">
<h2 className="section-title">{title}</h2> const sectionMeta = categoryFilter
<a href="#" className="view-more"> ? {
查看更多 <RightOutlined /> kicker: '分类浏览',
</a> description: `当前正在浏览「${categoryFilter}」分类下的作品,卡片会优先承接下载和详情页转化。`,
</div> badge: `${categoryFilter} 分类`,
}
<Row gutter={[16, 16]} className="works-grid"> : sectionPresets[type] || sectionPresets.hot;
{works.map((work, index) => (
<Col xs={12} sm={8} md={8} lg={8} xl={8} key={work.id}> if (loading) {
<Card return (
className="work-card" <section className="works-section" id={sectionId}>
hoverable <div className="works-loading">
style={{ '--delay': `${index * 0.05}s` }} <Spin size="large" tip="加载作品中..." />
onClick={() => handleCardClick(work)} </div>
cover={ </section>
<div className="work-image"> );
<img }
src={work.thumbnail_image ? `${API_CONFIG.baseURL}${work.thumbnail_image}` : getImageUrl(work.id)}
alt={work.title} return (
loading="lazy" <section className="works-section" id={sectionId}>
onError={(e) => { <div className="section-header">
e.target.src = getImageUrl(work.id); <div className="section-copy">
}} <span className="section-kicker">{sectionMeta.kicker}</span>
/> <h2 className="section-title">{title}</h2>
<div className="work-preview"> <p className="section-description">{sectionMeta.description}</p>
<span className="preview-icon">🔍</span> </div>
</div> <div className="section-summary">
</div> <span className="section-pill">{works.length} 张展示</span>
} <span className="section-pill section-pill-accent">{sectionMeta.badge}</span>
> </div>
<div className="work-info"> </div>
<h3 className="work-title">{work.title}</h3>
<div className="work-meta"> <Row gutter={[20, 20]} className="works-grid">
<span className="work-designer">{work.designer}</span> {works.map((work, index) => (
<Tag <Col xs={24} sm={12} lg={8} key={work.id}>
className="work-level" <Card
style={{ className="work-card"
color: getLevelColor(work.level), hoverable
borderColor: getLevelColor(work.level) style={{ '--delay': `${index * 0.05}s` }}
}} onClick={() => handleCardClick(work)}
> cover={
Lv.{work.level} <div className="work-image">
</Tag> <img
<span className="work-level-text">{work.level_text}</span> src={
</div> work.watermarked_image
<div className="work-price" style={{ marginTop: 8, color: '#ff5a5a', fontWeight: 'bold', fontSize: 16 }}> ? `${API_CONFIG.baseURL}${work.watermarked_image}`
¥{work.price} : work.thumbnail_image
</div> ? `${API_CONFIG.baseURL}${work.thumbnail_image}`
</div> : getImageUrl(work.id)
</Card> }
</Col> alt={work.title}
))} loading="lazy"
</Row> onError={(e) => {
</section> e.target.src = getImageUrl(work.id);
); }}
}; />
<div className="work-image-overlay">
export default Works; <span>查看详情</span>
<RightOutlined />
</div>
<div className="work-price-badge">¥{work.price}</div>
</div>
}
>
<div className="work-info">
<div className="work-topline">
<Tag className="work-category">{work.category || '设计素材'}</Tag>
<span className="work-id">#{work.id}</span>
</div>
<h3 className="work-title">{work.title}</h3>
<div className="work-meta">
<span className="work-designer">{work.designer}</span>
<Tag
className="work-level"
style={{
color: getLevelColor(work.level),
borderColor: getLevelColor(work.level),
}}
>
Lv.{work.level}
</Tag>
<span className="work-level-text">{work.level_text}</span>
</div>
<div className="work-bottom">
<div className="work-price">¥{work.price}</div>
<span className="work-cta">
进入详情
<RightOutlined />
</span>
</div>
</div>
</Card>
</Col>
))}
</Row>
</section>
);
};
export default Works;

View File

@@ -1,44 +1,65 @@
/* ========== 全局样式重置 ========== */ :root {
--brand: #2f87a8;
--brand-strong: #1f5f78;
--brand-soft: #e7f3f8;
--ink: #241d17;
--text: #5f5449;
--muted: #8c8073;
--surface: rgba(255, 255, 255, 0.94);
--surface-soft: #fffaf5;
--line: rgba(233, 221, 212, 0.95);
--shadow: 0 22px 48px rgba(66, 42, 26, 0.08);
}
* { * {
margin: 0; margin: 0;
padding: 0; padding: 0;
box-sizing: border-box; box-sizing: border-box;
} }
html {
scroll-behavior: smooth;
}
body { body {
font-family: 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', Arial, sans-serif; font-family: 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', Arial, sans-serif;
font-size: 16px; font-size: 16px;
line-height: 1.6; line-height: 1.6;
color: #2d3436; color: var(--ink);
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); background:
radial-gradient(circle at top left, rgba(47, 135, 168, 0.12), transparent 22%),
radial-gradient(circle at top right, rgba(135, 173, 193, 0.16), transparent 20%),
linear-gradient(180deg, #f8fbfd 0%, #eef4f7 100%);
min-height: 100vh; min-height: 100vh;
} }
/* ========== 滚动条美化 ========== */ a {
color: inherit;
}
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 8px; width: 8px;
height: 8px; height: 8px;
} }
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
background: #f1f1f1; background: #f4ede6;
border-radius: 4px; border-radius: 999px;
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: linear-gradient(180deg, #2f87a8 0%, #1f5f78 100%);
border-radius: 4px; border-radius: 999px;
} }
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%); background: linear-gradient(180deg, #297997 0%, #184e62 100%);
} }
/* ========== 通用动画 ========== */
@keyframes fadeIn { @keyframes fadeIn {
from { from {
opacity: 0; opacity: 0;
transform: translateY(20px); transform: translateY(16px);
} }
to { to {
opacity: 1; opacity: 1;
@@ -49,7 +70,7 @@ body {
@keyframes slideIn { @keyframes slideIn {
from { from {
opacity: 0; opacity: 0;
transform: translateX(-30px); transform: translateX(-24px);
} }
to { to {
opacity: 1; opacity: 1;
@@ -60,187 +81,10 @@ body {
@keyframes scaleIn { @keyframes scaleIn {
from { from {
opacity: 0; opacity: 0;
transform: scale(0.9); transform: scale(0.96);
} }
to { to {
opacity: 1; opacity: 1;
transform: scale(1); transform: scale(1);
} }
} }
@keyframes float {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-10px);
}
}
@keyframes shimmer {
0% {
background-position: -1000px 0;
}
100% {
background-position: 1000px 0;
}
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
/* ========== 通用类 ========== */
.fade-in {
animation: fadeIn 0.6s ease-out;
}
.slide-in {
animation: slideIn 0.6s ease-out;
}
.scale-in {
animation: scaleIn 0.6s ease-out;
}
.float {
animation: float 3s ease-in-out infinite;
}
/* ========== 按钮样式 ========== */
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 12px 30px;
border-radius: 25px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
}
.btn-primary:active {
transform: translateY(0);
}
/* ========== 卡片阴影 ========== */
.card-shadow {
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
.card-shadow:hover {
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.15);
transform: translateY(-5px);
}
/* ========== 渐变背景 ========== */
.gradient-bg {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.gradient-text {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* ========== 玻璃拟态 ========== */
.glass {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
/* ========== 加载动画 ========== */
.loading-shimmer {
background: linear-gradient(
90deg,
#f0f0f0 0%,
#e0e0e0 20%,
#f0f0f0 40%,
#f0f0f0 100%
);
background-size: 1000px 100%;
animation: shimmer 2s infinite;
}
/* ========== 标签样式 ========== */
.tag-gradient {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
}
/* ========== 输入框美化 ========== */
.input-modern {
width: 100%;
padding: 12px 20px;
border: 2px solid #e0e0e0;
border-radius: 12px;
font-size: 16px;
transition: all 0.3s ease;
background: white;
}
.input-modern:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
/* ========== 分割线 ========== */
.divider-gradient {
height: 2px;
border: none;
background: linear-gradient(90deg, #667eea 0%, #764ba2 50%, #667eea 100%);
background-size: 200% 100%;
animation: shimmer 3s infinite;
}
/* ========== 响应式断点 ========== */
@media (max-width: 768px) {
body {
font-size: 14px;
}
.btn-primary {
padding: 10px 20px;
font-size: 14px;
}
}
/* ========== 暗黑模式支持 ========== */
@media (prefers-color-scheme: dark) {
body {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
color: #e0e0e0;
}
.input-modern {
background: #2d2d44;
border-color: #404060;
color: #e0e0e0;
}
.input-modern:focus {
border-color: #667eea;
}
}

View File

@@ -1,121 +1,154 @@
.category-detail-page { .category-detail-page {
min-height: 100vh; min-height: 100vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} background:
radial-gradient(circle at top left, rgba(239, 106, 91, 0.08), transparent 24%),
.category-detail-hero { linear-gradient(180deg, #fffdf9 0%, #f8f2eb 100%);
height: 350px; }
position: relative;
display: flex; .category-detail-hero {
align-items: center; position: relative;
justify-content: center; min-height: 360px;
color: white; margin: 88px 20px 0;
} border-radius: 34px;
overflow: hidden;
.category-detail-overlay { box-shadow: 0 28px 64px rgba(66, 42, 26, 0.14);
position: absolute; }
top: 0;
left: 0; .category-detail-overlay {
right: 0; min-height: 360px;
bottom: 0; padding: 28px 32px 34px;
background: rgba(0, 0, 0, 0.3); background:
display: flex; linear-gradient(120deg, rgba(16, 14, 12, 0.18), transparent 36%),
flex-direction: column; linear-gradient(180deg, rgba(32, 23, 17, 0.1), rgba(32, 23, 17, 0.24));
align-items: center; display: flex;
justify-content: center; flex-direction: column;
padding: 20px; justify-content: center;
} color: #fff;
}
.back-btn {
position: absolute; .back-btn.ant-btn {
top: 90px; position: absolute;
left: 40px; top: 24px;
background: rgba(255, 255, 255, 0.9); left: 24px;
border: none; height: 42px;
color: #333; padding: 0 18px;
font-weight: 500; border-radius: 16px;
height: 40px; border: none;
padding: 0 20px; background: rgba(255, 255, 255, 0.18);
transition: all 0.3s ease; color: #fff;
} backdrop-filter: blur(12px);
}
.back-btn:hover {
background: white !important; .back-btn.ant-btn:hover {
color: #ff5a5a !important; color: #fff !important;
transform: translateX(-5px); background: rgba(255, 255, 255, 0.24) !important;
} }
.category-detail-content { .category-detail-content {
text-align: center; max-width: 840px;
animation: fadeInUp 0.8s ease; }
}
.category-detail-kicker {
.category-detail-title { display: inline-flex;
font-size: 56px; margin-bottom: 14px;
font-weight: 700; padding: 8px 12px;
margin: 0; border-radius: 999px;
text-shadow: 2px 2px 20px rgba(0, 0, 0, 0.3); background: rgba(255, 255, 255, 0.18);
animation: scaleIn 0.6s ease; border: 1px solid rgba(255, 255, 255, 0.22);
} font-size: 12px;
font-weight: 700;
.category-detail-count { letter-spacing: 0.08em;
font-size: 20px; }
margin-top: 16px;
opacity: 0.95; .category-detail-title {
text-shadow: 1px 1px 10px rgba(0, 0, 0, 0.2); margin: 0;
} font-size: clamp(40px, 6vw, 64px);
line-height: 1.05;
.category-detail-works { }
flex: 1;
padding: 40px 0; .category-detail-desc {
background: #f8f9fa; max-width: 680px;
} margin: 14px 0 0;
font-size: 16px;
@keyframes fadeInUp { line-height: 1.85;
from { color: rgba(255, 255, 255, 0.9);
opacity: 0; }
transform: translateY(30px);
} .category-detail-stats {
to { display: grid;
opacity: 1; grid-template-columns: repeat(3, minmax(0, 1fr));
transform: translateY(0); gap: 14px;
} margin-top: 24px;
} }
@keyframes scaleIn { .category-stat {
from { display: grid;
opacity: 0; grid-template-columns: 44px minmax(0, 1fr);
transform: scale(0.8); gap: 12px;
} padding: 16px;
to { border-radius: 20px;
opacity: 1; background: rgba(255, 255, 255, 0.16);
transform: scale(1); border: 1px solid rgba(255, 255, 255, 0.18);
} backdrop-filter: blur(10px);
} }
@media (max-width: 768px) { .category-stat .anticon {
.category-detail-hero { width: 44px;
height: 250px; height: 44px;
} border-radius: 16px;
display: grid;
.back-btn { place-items: center;
top: 70px; background: rgba(255, 255, 255, 0.18);
left: 20px; font-size: 18px;
height: 36px; }
padding: 0 16px;
font-size: 14px; .category-stat strong {
} display: block;
font-size: 22px;
.category-detail-title { }
font-size: 36px;
} .category-stat span {
display: block;
.category-detail-count { margin-top: 4px;
font-size: 16px; font-size: 12px;
} color: rgba(255, 255, 255, 0.78);
}
.category-detail-works {
padding: 30px 0; .category-detail-works {
} flex: 1;
} padding: 26px 0 18px;
}
@media (max-width: 900px) {
.category-detail-stats {
grid-template-columns: 1fr;
}
}
@media (max-width: 768px) {
.category-detail-hero {
margin: 82px 12px 0;
min-height: 320px;
border-radius: 28px;
}
.category-detail-overlay {
min-height: 320px;
padding: 24px 18px 24px;
}
.back-btn.ant-btn {
top: 18px;
left: 18px;
}
.category-detail-title {
font-size: 38px;
}
.category-detail-desc {
font-size: 14px;
}
}

View File

@@ -1,56 +1,113 @@
import { useParams, useNavigate } from 'react-router-dom'; import { useParams, useNavigate } from 'react-router-dom';
import { Button } from 'antd'; import { Button } from 'antd';
import { LeftOutlined } from '@ant-design/icons'; import { LeftOutlined, AppstoreOutlined, FireOutlined, EyeOutlined } from '@ant-design/icons';
import Header from '../components/Header'; import Header from '../components/Header';
import Footer from '../components/Footer'; import Footer from '../components/Footer';
import Works from '../components/Works'; import Works from '../components/Works';
import './CategoryDetail.css'; import './CategoryDetail.css';
const CategoryDetail = () => { const CategoryDetail = () => {
const { name } = useParams(); const { name } = useParams();
const navigate = useNavigate(); const navigate = useNavigate();
// 分类数据映射 const categoryData = {
const categoryData = { 文化墙: {
'文化墙': { count: '6572', gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' }, count: '6572',
'周年庆': { count: '15868', gradient: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)' }, gradient: 'linear-gradient(135deg, #d9755d 0%, #f2bb7c 100%)',
'直播': { count: '35429', gradient: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)' }, description: '适合企业形象、品牌理念和空间场景布置的长图与信息展示素材。',
'包装': { count: '9533', gradient: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)' }, },
'科技': { count: '44921', gradient: 'linear-gradient(135deg, #0c3483 0%, #a2b6df 100%)' }, 周年庆: {
'活动': { count: '408131', gradient: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)' }, count: '15868',
'医美': { count: '223068', gradient: 'linear-gradient(135deg, #a8edea 0%, #fed6e3 100%)' }, gradient: 'linear-gradient(135deg, #e17baa 0%, #f7c46c 100%)',
'邀请函': { count: '15361', gradient: 'linear-gradient(135deg, #d299c2 0%, #fef9d7 100%)' }, description: '围绕纪念日、店庆、品牌里程碑等主题的高频视觉物料。',
}; },
直播: {
const category = categoryData[name]; count: '35429',
gradient: 'linear-gradient(135deg, #4a8ee8 0%, #57d3e2 100%)',
return ( description: '直播预告、封面、流程页和互动氛围图的整套素材集合。',
<div className="category-detail-page"> },
<Header /> 包装: {
count: '9533',
<div className="category-detail-hero" style={{ background: category?.gradient }}> gradient: 'linear-gradient(135deg, #2f8f73 0%, #86d6b2 100%)',
<div className="category-detail-overlay"> description: '包装延展、产品展示和品牌包装视觉的常用内容。',
<Button },
icon={<LeftOutlined />} 科技: {
className="back-btn" count: '44921',
onClick={() => navigate('/')} gradient: 'linear-gradient(135deg, #4356a8 0%, #7eb0da 100%)',
> description: '偏未来感、信息感和产品感表达的科技类视觉素材。',
返回首页 },
</Button> 活动: {
<div className="category-detail-content"> count: '408131',
<h1 className="category-detail-title">{name}</h1> gradient: 'linear-gradient(135deg, #8ec4d8 0%, #2f87a8 100%)',
<p className="category-detail-count"> {category?.count} 张作品</p> description: '最适合首页承接的热门分类之一,覆盖促销、开业和节点营销素材。',
</div> },
</div> 医美: {
</div> count: '223068',
gradient: 'linear-gradient(135deg, #f0a6b4 0%, #f7d6df 100%)',
<div className="category-detail-works"> description: '轻医美、抗衰、项目卖点和机构宣传类视觉内容。',
<Works categoryFilter={name} /> },
</div> 邀请函: {
count: '15361',
<Footer /> gradient: 'linear-gradient(135deg, #a87ab8 0%, #ead9f3 100%)',
</div> description: '会议、婚礼、活动和品牌邀约相关的正式视觉模板。',
); },
}; };
export default CategoryDetail; const category = categoryData[name] || {
count: '0',
gradient: 'linear-gradient(135deg, #d9755d 0%, #f2bb7c 100%)',
description: '当前分类正在持续补充中,可以先浏览已上线的作品内容。',
};
return (
<div className="category-detail-page">
<Header />
<div className="category-detail-hero" style={{ background: category.gradient }}>
<div className="category-detail-overlay">
<Button icon={<LeftOutlined />} className="back-btn" onClick={() => navigate('/')}>
返回首页
</Button>
<div className="category-detail-content">
<span className="category-detail-kicker">分类精选</span>
<h1 className="category-detail-title">{name}</h1>
<p className="category-detail-desc">{category.description}</p>
<div className="category-detail-stats">
<div className="category-stat">
<AppstoreOutlined />
<div>
<strong>{category.count}</strong>
<span>作品总量</span>
</div>
</div>
<div className="category-stat">
<FireOutlined />
<div>
<strong>高频</strong>
<span>适合首页承接</span>
</div>
</div>
<div className="category-stat">
<EyeOutlined />
<div>
<strong>详情页</strong>
<span>支持预览与下载</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div className="category-detail-works">
<Works categoryFilter={name} title={`${name} 分类作品`} />
</div>
<Footer />
</div>
);
};
export default CategoryDetail;

View File

@@ -57,7 +57,7 @@
display: inline-block; display: inline-block;
width: 4px; width: 4px;
height: 14px; height: 14px;
background: #ff5a5a; background: var(--brand);
margin-right: 8px; margin-right: 8px;
vertical-align: middle; vertical-align: middle;
} }
@@ -106,7 +106,7 @@
.highlight-box { .highlight-box {
background: #fff5f5; background: #fff5f5;
border-left: 4px solid #ff5a5a; border-left: 4px solid var(--brand);
padding: 20px; padding: 20px;
margin: 24px 0; margin: 24px 0;
border-radius: 8px; border-radius: 8px;
@@ -115,7 +115,7 @@
.highlight-box h3 { .highlight-box h3 {
font-size: 16px; font-size: 16px;
font-weight: 600; font-weight: 600;
color: #ff5a5a; color: var(--brand);
margin-bottom: 12px; margin-bottom: 12px;
} }

View File

@@ -107,7 +107,7 @@ const Help = () => {
<div className="protocol-content help-content"> <div className="protocol-content help-content">
<h1> <h1>
<QuestionCircleOutlined style={{ marginRight: '12px', color: '#ff5a5a' }} /> <QuestionCircleOutlined style={{ marginRight: '12px', color: 'var(--brand)' }} />
帮助中心 帮助中心
</h1> </h1>

View File

@@ -6,19 +6,19 @@ import Works from '../components/Works';
import Designers from '../components/Designers'; import Designers from '../components/Designers';
import Footer from '../components/Footer'; import Footer from '../components/Footer';
function Home() { function Home() {
return ( return (
<div className="home-page" style={{ paddingTop: '70px' }}> <div className="home-page" style={{ paddingTop: '70px' }}>
<Header /> <Header />
<Hero /> <Hero />
<Categories /> <Categories />
<Festival /> <Festival />
<Works title="热门推荐" type="hot" /> <Works title="热门推荐" type="hot" sectionId="home-hot-works" />
<Works title="最新上传" type="new" /> <Works title="最新上传" type="new" sectionId="home-new-works" />
<Designers /> <Designers />
<Footer /> <Footer />
</div> </div>
); );
} }
export default Home; export default Home;

View File

@@ -50,12 +50,12 @@
} }
.protocol-sidebar a:hover { .protocol-sidebar a:hover {
color: #ff5a5a; color: var(--brand);
background: #fff5f5; background: #fff5f5;
} }
.protocol-sidebar a.active { .protocol-sidebar a.active {
color: #ff5a5a; color: var(--brand);
background: #fff5f5; background: #fff5f5;
font-weight: 500; font-weight: 500;
} }
@@ -74,7 +74,7 @@
color: #333; color: #333;
margin-bottom: 24px; margin-bottom: 24px;
padding-bottom: 16px; padding-bottom: 16px;
border-bottom: 2px solid #ff5a5a; border-bottom: 2px solid var(--brand);
} }
.protocol-content h2 { .protocol-content h2 {
@@ -119,7 +119,7 @@
border-radius: 4px; border-radius: 4px;
font-family: 'Courier New', monospace; font-family: 'Courier New', monospace;
font-size: 13px; font-size: 13px;
color: #ff5a5a; color: var(--brand);
} }
.level-table { .level-table {

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,23 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom'; import { useParams, useNavigate } from 'react-router-dom';
import { Button, Tag, message, Input, Modal, Spin } from 'antd'; import { Button, Tag, message, Input, Modal, Spin } from 'antd';
import { HeartOutlined, HeartFilled, DownloadOutlined, ShareAltOutlined, EyeOutlined, PlusOutlined, SearchOutlined } from '@ant-design/icons'; import {
import { QRCodeSVG } from 'qrcode.react'; HeartOutlined,
import { getWorkDetail } from '../api/works'; HeartFilled,
import { createOrder } from '../api/orders'; DownloadOutlined,
import { createPayment, queryPaymentStatus } from '../api/payment'; ShareAltOutlined,
import { downloadWork } from '../api/download'; EyeOutlined,
import { isLoggedIn } from '../api/auth'; PlusOutlined,
import { API_CONFIG } from '../utils/config'; SearchOutlined,
CheckCircleFilled,
} from '@ant-design/icons';
import { QRCodeSVG } from 'qrcode.react';
import { getWorkDetail } from '../api/works';
import { createOrder } from '../api/orders';
import { createPayment, queryPaymentStatus } from '../api/payment';
import { downloadWork } from '../api/download';
import { isLoggedIn } from '../api/auth';
import { API_CONFIG } from '../utils/config';
import Header from '../components/Header'; import Header from '../components/Header';
import Footer from '../components/Footer'; import Footer from '../components/Footer';
import './WorkDetail.css'; import './WorkDetail.css';
@@ -34,7 +43,7 @@ const parseTags = (rawTags) => {
const parsed = JSON.parse(trimmed); const parsed = JSON.parse(trimmed);
return Array.isArray(parsed) ? parsed.filter(Boolean) : []; return Array.isArray(parsed) ? parsed.filter(Boolean) : [];
} catch { } catch {
// 回退到逗号分隔解析,避免详情页白屏 // 兼容历史逗号分隔格式,避免详情页白屏
} }
} }
@@ -50,452 +59,507 @@ const getDownloadFilename = (work) => {
const ext = extMatch ? extMatch[0] : '.jpg'; const ext = extMatch ? extMatch[0] : '.jpg';
return `${work?.title || 'work'}${ext}`; return `${work?.title || 'work'}${ext}`;
}; };
const WorkDetail = () => { const WorkDetail = () => {
const { id } = useParams(); const { id } = useParams();
const navigate = useNavigate(); const navigate = useNavigate();
const [collected, setCollected] = useState(false); const [collected, setCollected] = useState(false);
const [followed, setFollowed] = useState(false); const [followed, setFollowed] = useState(false);
const [downloadModalOpen, setDownloadModalOpen] = useState(false); const [downloadModalOpen, setDownloadModalOpen] = useState(false);
const [workData, setWorkData] = useState(null); const [workData, setWorkData] = useState(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [purchasing, setPurchasing] = useState(false); const [purchasing, setPurchasing] = useState(false);
const [paymentUrl, setPaymentUrl] = useState(''); const [paymentUrl, setPaymentUrl] = useState('');
const [orderNumber, setOrderNumber] = useState(''); const [orderNumber, setOrderNumber] = useState('');
const [checkingPayment, setCheckingPayment] = useState(false); const [checkingPayment, setCheckingPayment] = useState(false);
// 加载作品详情 useEffect(() => {
useEffect(() => { loadWorkDetail();
loadWorkDetail(); }, [id]);
}, [id]);
const loadWorkDetail = async () => {
const loadWorkDetail = async () => { setLoading(true);
setLoading(true); const result = await getWorkDetail(id);
const result = await getWorkDetail(id); setLoading(false);
setLoading(false);
if (result.success) {
if (result.success) { setWorkData(result.data);
setWorkData(result.data); } else {
} else { message.error('加载作品详情失败');
message.error('加载作品详情失败'); }
} };
};
const getImageUrl = (workId) => {
// 使用 Picsum 图片作为后备 return `https://picsum.photos/seed/${workId}/800/1200`;
const getImageUrl = (workId) => { };
return `https://picsum.photos/seed/${workId}/800/1200`;
}; const getRelatedImageUrl = (workId) => {
return `https://picsum.photos/seed/related${workId}/400/600`;
const getRelatedImageUrl = (workId) => { };
return `https://picsum.photos/seed/related${workId}/400/600`;
}; const relatedWorks = [
{
const relatedWorks = [ id: '20',
{ title: '相关设计作品 1',
id: '20', image: getRelatedImageUrl(20),
title: '相关设计作品1', designer: 'cestbon',
image: getRelatedImageUrl(20), level: 1,
designer: 'cestbon', levelName: '设计爱好者',
level: 1, },
levelName: '设计爱好者' {
}, id: '21',
{ title: '相关设计作品 2',
id: '21', image: getRelatedImageUrl(21),
title: '相关设计作品2', designer: '六十六号屯',
image: getRelatedImageUrl(21), level: 4,
designer: '六十六号屯', levelName: '资深设计师',
level: 4, },
levelName: '资深设计师' {
}, id: '22',
{ title: '相关设计作品 3',
id: '22', image: getRelatedImageUrl(22),
title: '相关设计作品3', designer: '扶摇',
image: getRelatedImageUrl(22), level: 3,
designer: '扶摇', levelName: '设计师',
level: 3, },
levelName: '设计师' ];
}
]; const handleCollect = () => {
setCollected(!collected);
const handleCollect = () => { message.success(collected ? '已取消收藏' : '收藏成功');
setCollected(!collected); };
message.success(collected ? '已取消收藏' : '收藏成功');
}; const handleFollow = () => {
setFollowed(!followed);
const handleFollow = () => { message.success(followed ? '已取消关注' : '关注成功');
setFollowed(!followed); };
message.success(followed ? '已取消关注' : '关注成功');
}; const handleDownload = async () => {
if (!isLoggedIn()) {
const handleDownload = async () => { message.warning('请先登录');
// 检查是否登录 return;
if (!isLoggedIn()) { }
message.warning('请先登录');
return;
}
// 尝试直接下载
const result = await downloadWork(id, getDownloadFilename(workData)); const result = await downloadWork(id, getDownloadFilename(workData));
// 如果需要购买,显示购买确认弹窗 if (!result.success && result.needPurchase) {
if (!result.success && result.needPurchase) { setDownloadModalOpen(true);
setDownloadModalOpen(true); }
} };
};
const confirmDownload = async () => {
const confirmDownload = async () => { setPurchasing(true);
setPurchasing(true);
const orderResult = await createOrder(id);
// 1. 创建订单 if (!orderResult.success) {
const orderResult = await createOrder(id); setPurchasing(false);
if (!orderResult.success) { message.error(orderResult.message);
setPurchasing(false); return;
message.error(orderResult.message); }
return;
} const order = orderResult.data;
setOrderNumber(order.order_no);
const order = orderResult.data;
setOrderNumber(order.order_no); const paymentResult = await createPayment(order.id);
setPurchasing(false);
// 2. 创建支付链接
const paymentResult = await createPayment(order.id); if (paymentResult.success) {
setPurchasing(false); setPaymentUrl(paymentResult.data.pay_url);
message.success('请扫描二维码完成支付');
if (paymentResult.success) { startPaymentStatusCheck(order.order_no);
setPaymentUrl(paymentResult.data.pay_url); } else {
message.success('请扫描二维码完成支付'); message.error(paymentResult.message);
// 开始轮询支付状态 setDownloadModalOpen(false);
startPaymentStatusCheck(order.order_no); }
} else { };
message.error(paymentResult.message);
setDownloadModalOpen(false); const startPaymentStatusCheck = (orderNum) => {
} setCheckingPayment(true);
}; const checkInterval = setInterval(async () => {
const result = await queryPaymentStatus(orderNum);
// 轮询检查支付状态 if (result.success) {
const startPaymentStatusCheck = (orderNum) => { if (result.data.local_status === 'paid' || result.data.remote_status === 'SUCCESS') {
setCheckingPayment(true); clearInterval(checkInterval);
const checkInterval = setInterval(async () => { setCheckingPayment(false);
const result = await queryPaymentStatus(orderNum); setDownloadModalOpen(false);
if (result.success) { setPaymentUrl('');
// 检查本地订单状态是否为已支付 message.success('支付成功,开始下载');
if (result.data.local_status === 'paid' || result.data.remote_status === 'SUCCESS') {
clearInterval(checkInterval);
setCheckingPayment(false);
setDownloadModalOpen(false);
setPaymentUrl('');
message.success('支付成功!开始下载...');
// 重新尝试下载
await downloadWork(id, getDownloadFilename(workData)); await downloadWork(id, getDownloadFilename(workData));
} }
} }
}, 3000); // 每3秒检查一次 }, 3000);
// 5分钟后停止检查 setTimeout(() => {
setTimeout(() => { clearInterval(checkInterval);
clearInterval(checkInterval); setCheckingPayment(false);
setCheckingPayment(false); }, 300000);
}, 300000); };
};
const handleModalClose = () => {
const handleModalClose = () => { setDownloadModalOpen(false);
setDownloadModalOpen(false); setPaymentUrl('');
setPaymentUrl(''); setOrderNumber('');
setOrderNumber(''); setCheckingPayment(false);
setCheckingPayment(false); };
};
const generateWorkNumber = (workId) => {
// 生成作品编号:半年前日期 + 当前时分秒 + 作品ID const now = new Date();
const generateWorkNumber = (workId) => { const sixMonthsAgo = new Date(now.getTime() - 180 * 24 * 60 * 60 * 1000);
const now = new Date(); const datePart = sixMonthsAgo.toISOString().slice(0, 10).replace(/-/g, '');
const sixMonthsAgo = new Date(now.getTime() - 180 * 24 * 60 * 60 * 1000); // 半年前 const timePart = now.toTimeString().slice(0, 8).replace(/:/g, '');
const datePart = sixMonthsAgo.toISOString().slice(0, 10).replace(/-/g, ''); // YYYYMMDD return `${datePart}${timePart}${workId}`;
const timePart = now.toTimeString().slice(0, 8).replace(/:/g, ''); // HHMMSS };
return `${datePart}${timePart}${workId}`;
}; const handleShare = () => {
navigator.clipboard.writeText(window.location.href);
const handleShare = () => { message.success('链接已复制到剪贴板');
navigator.clipboard.writeText(window.location.href); };
message.success('链接已复制到剪贴板');
}; const copyWorkId = () => {
const fullWorkNumber = generateWorkNumber(workData?.id || 0);
const copyWorkId = () => { navigator.clipboard.writeText(fullWorkNumber);
const fullWorkNumber = generateWorkNumber(workData?.id || 0); message.success('编号已复制');
navigator.clipboard.writeText(fullWorkNumber); };
message.success('编号已复制');
}; if (loading) {
return (
if (loading) { <div className="work-detail-page">
return ( <Header />
<div className="work-detail-page"> <div className="work-loading">
<Header /> <Spin size="large" tip="加载中..." />
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '60vh' }}> </div>
<Spin size="large" tip="加载中..." /> <Footer />
</div> </div>
<Footer /> );
</div> }
);
} if (!workData) {
return (
if (!workData) { <div className="work-detail-page">
return ( <Header />
<div className="work-detail-page"> <div className="work-loading">
<Header /> <p>作品不存在</p>
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '60vh' }}> </div>
<p>作品不存在</p> <Footer />
</div> </div>
<Footer /> );
</div> }
);
}
// 解析标签
const tags = parseTags(workData.tags); const tags = parseTags(workData.tags);
const detailFacts = [
return ( { label: '分类', value: workData.category || '设计素材' },
<div className="work-detail-page"> { label: '设计师', value: workData.designer || '-' },
<Header /> { label: '等级', value: `Lv.${workData.level} ${workData.level_text}` },
{ label: '价格', value: `¥${workData.price}` },
<div className="work-detail-container"> ];
{/* 面包屑导航 */} const servicePromises = [
<div className="breadcrumb"> '支付成功后自动下载原图',
<a onClick={() => navigate('/')}>首页</a> '详情页支持预览与编号复制',
<span> / </span> '订单状态与支付状态可追踪',
<span className="current">{workData.title}</span> ];
</div>
return (
<div className="work-detail-content"> <div className="work-detail-page">
{/* 左侧:作品展示 */} <Header />
<div className="work-main">
<div className="work-image-wrapper"> <div className="work-detail-container">
<img <div className="breadcrumb">
src={workData.watermarked_image ? `${API_CONFIG.baseURL}${workData.watermarked_image}` : getImageUrl(id)} <a onClick={() => navigate('/')}>首页</a>
alt={workData.title} <span> / </span>
className="work-image" {workData.category ? (
/> <>
<a onClick={() => navigate(`/category/${workData.category}`)}>{workData.category}</a>
{/* 相关搜索标签 */} <span> / </span>
<div className="related-tags"> </>
<h4>相关搜索</h4> ) : null}
<div className="tags-list"> <span className="current">{workData.title}</span>
{tags.map((tag, index) => ( </div>
<Tag key={index} className="work-tag">{tag}</Tag>
))} <div className="work-detail-heading">
</div> <div className="detail-heading-copy">
</div> <span className="detail-kicker">
{workData.category || '设计素材'} · 支付后即时交付
{/* 猜你喜欢 */} </span>
<div className="related-works"> <h1 className="work-title">{workData.title}</h1>
<h3>猜你喜欢</h3> <p className="work-subtitle">
<div className="related-works-grid"> 适合活动海报电商主图背景素材等快速落地场景先看预览再决定是否下载原图
{relatedWorks.map(work => ( </p>
<div key={work.id} className="related-work-card" onClick={() => navigate(`/detail/${work.id}`)}> </div>
<div className="related-work-image"> <div className="detail-heading-stats">
<img src={work.image} alt={work.title} /> <div className="heading-stat">
<div className="related-work-overlay"> <strong>{workData.views}</strong>
<Button type="primary" size="small" icon={<DownloadOutlined />}> <span>浏览量</span>
源文件下载 </div>
</Button> <div className="heading-stat">
</div> <strong>{workData.collects}</strong>
</div> <span>收藏量</span>
<div className="related-work-info"> </div>
<h4>{work.title}</h4> <div className="heading-stat">
<div className="related-work-designer"> <strong>Lv.{workData.level}</strong>
<span className="designer-level">Lv.{work.level}</span> <span>{workData.level_text}</span>
<span className="designer-level-name">{work.levelName}</span> </div>
<p className="designer-name">{work.designer}</p> </div>
</div> </div>
</div>
</div> <div className="work-detail-content">
))} <div className="work-main">
</div> <div className="work-preview-card">
</div> <div className="work-preview-topline">
</div> <span className="preview-badge">水印预览</span>
</div> <span className="preview-badge subtle">可购买原图下载</span>
</div>
{/* 右侧:作品信息 */}
<div className="work-sidebar"> <div className="work-image-shell">
{/* 编号和复制 */} <img
<div className="work-id-section"> src={workData.watermarked_image ? `${API_CONFIG.baseURL}${workData.watermarked_image}` : getImageUrl(id)}
<span className="work-id">{generateWorkNumber(workData.id)}</span> alt={workData.title}
<Button type="link" size="small" onClick={copyWorkId}>复制</Button> className="work-image"
</div> />
</div>
{/* 作品标题 */}
<h1 className="work-title">{workData.title}</h1> <div className="preview-benefits">
{servicePromises.map((item) => (
{/* 作品信息 */} <div key={item} className="preview-benefit">
<div className="work-info-list"> <CheckCircleFilled />
<div className="work-info-item"> <span>{item}</span>
<span className="info-label">分类</span> </div>
<span className="info-value">{workData.category}</span> ))}
</div> </div>
<div className="work-info-item"> </div>
<span className="info-label">设计师</span>
<span className="info-value">{workData.designer}</span> <div className="detail-panel related-tags">
</div> <div className="panel-header">
<div className="work-info-item"> <h3>相关搜索</h3>
<span className="info-label">等级</span> <span>帮助用户继续筛选相似素材</span>
<span className="info-value">Lv.{workData.level} {workData.level_text}</span> </div>
</div> <div className="tags-list">
<div className="work-info-item"> {tags.length ? (
<span className="info-label">价格</span> tags.map((tag, index) => (
<span className="info-value" style={{ color: '#ff5a5a', fontWeight: 'bold' }}>¥{workData.price}</span> <Tag key={index} className="work-tag">
</div> {tag}
</div> </Tag>
))
{/* 操作按钮 */} ) : (
<div className="work-actions"> <span className="empty-hint">暂无标签后续上传时可补充关键词</span>
<Button )}
type="primary" </div>
size="large" </div>
block
icon={<DownloadOutlined />} <div className="detail-panel related-works">
onClick={handleDownload} <div className="panel-header">
className="download-btn" <h3>猜你喜欢</h3>
> <span>保持详情页浏览节奏减少单页跳出</span>
我要下载 </div>
</Button> <div className="related-works-grid">
<Button {relatedWorks.map((work) => (
size="large" <div key={work.id} className="related-work-card" onClick={() => navigate(`/detail/${work.id}`)}>
icon={collected ? <HeartFilled /> : <HeartOutlined />} <div className="related-work-image">
onClick={handleCollect} <img src={work.image} alt={work.title} />
className={`collect-btn ${collected ? 'collected' : ''}`} <div className="related-work-overlay">
/> <Button type="primary" size="small" icon={<DownloadOutlined />}>
<Button 查看详情
size="large" </Button>
icon={<ShareAltOutlined />} </div>
onClick={handleShare} </div>
className="share-btn" <div className="related-work-info">
/> <h4>{work.title}</h4>
</div> <div className="related-work-designer">
<span className="designer-level">Lv.{work.level}</span>
{/* 设计师信息 */} <span className="designer-level-name">{work.levelName}</span>
<div className="designer-card"> <p className="designer-name">{work.designer}</p>
<div className="designer-header"> </div>
<div className="designer-avatar"> </div>
<img src={`https://api.dicebear.com/7.x/avataaars/svg?seed=${workData.designer}`} alt={workData.designer} /> </div>
</div> ))}
<div className="designer-info"> </div>
<div className="designer-name-level"> </div>
<span className="designer-level-badge">Lv.{workData.level}</span> </div>
<span className="designer-level-text">{workData.level_text}</span>
</div> <div className="work-sidebar">
<h3 className="designer-name">{workData.designer}</h3> <div className="price-card">
</div> <div className="work-id-section">
</div> <span className="work-id">{generateWorkNumber(workData.id)}</span>
<Button type="link" size="small" onClick={copyWorkId}>
<div className="designer-stats"> 复制编号
<div className="stat-item"> </Button>
<span className="stat-value">-</span> </div>
<span className="stat-label">作品</span>
</div> <div className="price-block">
<div className="stat-item"> <span className="price-label">当前下载价</span>
<span className="stat-value">-</span> <div className="price-value">¥{workData.price}</div>
<span className="stat-label">粉丝</span> </div>
</div>
</div> <div className="work-info-list">
{detailFacts.map((item) => (
<Button <div key={item.label} className="work-info-item">
type="primary" <span className="info-label">{item.label}</span>
block <span className={`info-value ${item.label === '价格' ? 'highlight' : ''}`}>
icon={<PlusOutlined />} {item.value}
onClick={handleFollow} </span>
className={`follow-btn ${followed ? 'followed' : ''}`} </div>
> ))}
{followed ? '已关注' : '关注'} </div>
</Button>
</div> <div className="work-actions">
<Button
{/* 搜索画板 */} type="primary"
<div className="board-search"> size="large"
<h4>搜索画板</h4> block
<Input icon={<DownloadOutlined />}
placeholder="搜索画板" onClick={handleDownload}
prefix={<SearchOutlined />} className="download-btn"
className="board-search-input" >
/> 立即下载
</div> </Button>
<Button
{/* 浏览统计 */} size="large"
<div className="work-stats"> icon={collected ? <HeartFilled /> : <HeartOutlined />}
<div className="stat-item"> onClick={handleCollect}
<EyeOutlined /> className={`icon-btn ${collected ? 'collected' : ''}`}
<span>{workData.views} 浏览</span> />
</div> <Button
<div className="stat-item"> size="large"
<HeartOutlined /> icon={<ShareAltOutlined />}
<span>{workData.collects} 收藏</span> onClick={handleShare}
</div> className="icon-btn"
</div> />
</div> </div>
</div> </div>
</div>
<div className="detail-panel designer-card">
{/* 购买确认弹窗 */} <div className="designer-header">
<Modal <div className="designer-avatar">
title={paymentUrl ? "扫码支付" : "购买作品"} <img
open={downloadModalOpen} src={`https://api.dicebear.com/7.x/avataaars/svg?seed=${workData.designer}`}
onOk={paymentUrl ? null : confirmDownload} alt={workData.designer}
onCancel={handleModalClose} />
okText="确定购买" </div>
cancelText={paymentUrl ? "关闭" : "取消"} <div className="designer-info">
centered <div className="designer-name-level">
confirmLoading={purchasing} <span className="designer-level-badge">Lv.{workData.level}</span>
footer={paymentUrl ? [ <span className="designer-level-text">{workData.level_text}</span>
<Button key="close" onClick={handleModalClose}> </div>
关闭 <h3 className="designer-name">{workData.designer}</h3>
</Button> </div>
] : undefined} </div>
width={paymentUrl ? 450 : 416}
> <div className="designer-stats">
{!paymentUrl ? ( <div className="stat-item">
<div> <span className="stat-value">-</span>
<p>作品{workData.title}</p> <span className="stat-label">作品</span>
<p>价格<span style={{ color: '#ff5a5a', fontSize: '18px', fontWeight: 'bold' }}>¥{workData.price}</span></p> </div>
<p style={{ color: '#999', fontSize: '12px' }}>点击确定后将生成支付二维码</p> <div className="stat-item">
</div> <span className="stat-value">-</span>
) : ( <span className="stat-label">粉丝</span>
<div style={{ textAlign: 'center', padding: '20px 0' }}> </div>
<div style={{ </div>
background: '#fff',
padding: '20px', <Button
borderRadius: '8px', type="primary"
boxShadow: '0 2px 8px rgba(0,0,0,0.1)', block
display: 'inline-block' icon={<PlusOutlined />}
}}> onClick={handleFollow}
<QRCodeSVG className={`follow-btn ${followed ? 'followed' : ''}`}
value={paymentUrl} >
size={200} {followed ? '已关注' : '关注设计师'}
level="H" </Button>
includeMargin={true} </div>
/>
</div> <div className="detail-panel board-search">
<p style={{ marginTop: '20px', fontSize: '16px', fontWeight: 'bold' }}> <div className="panel-header compact">
订单金额<span style={{ color: '#ff5a5a' }}>¥{workData.price}</span> <h3>搜索画板</h3>
</p> <span>继续搜同类风格</span>
<p style={{ color: '#666', fontSize: '14px' }}> </div>
请使用微信或支付宝扫码支付 <Input
</p> placeholder="搜索画板关键词"
{checkingPayment && ( prefix={<SearchOutlined />}
<div style={{ marginTop: '15px' }}> className="board-search-input"
<Spin size="small" /> />
<span style={{ marginLeft: '10px', color: '#1890ff' }}>等待支付中...</span> </div>
</div>
)} <div className="detail-panel work-stats">
<p style={{ color: '#999', fontSize: '12px', marginTop: '15px' }}> <div className="panel-header compact">
订单号{orderNumber} <h3>作品动态</h3>
</p> <span>帮助用户判断热度</span>
<p style={{ color: '#999', fontSize: '12px' }}> </div>
支付完成后将自动开始下载 <div className="stats-grid">
</p> <div className="stat-row">
</div> <EyeOutlined />
)} <span>{workData.views} 次浏览</span>
</Modal> </div>
<div className="stat-row">
<Footer /> <HeartOutlined />
</div> <span>{workData.collects} 次收藏</span>
); </div>
}; </div>
</div>
export default WorkDetail; </div>
</div>
</div>
<Modal
title={paymentUrl ? '扫码支付' : '购买作品'}
open={downloadModalOpen}
onOk={paymentUrl ? null : confirmDownload}
onCancel={handleModalClose}
okText="确定购买"
cancelText={paymentUrl ? '关闭' : '取消'}
centered
confirmLoading={purchasing}
footer={
paymentUrl
? [
<Button key="close" onClick={handleModalClose}>
关闭
</Button>,
]
: undefined
}
width={paymentUrl ? 450 : 416}
>
{!paymentUrl ? (
<div>
<p>作品{workData.title}</p>
<p>
价格
<span style={{ color: 'var(--brand)', fontSize: '18px', fontWeight: 'bold' }}>
¥{workData.price}
</span>
</p>
<p style={{ color: '#999', fontSize: '12px' }}>点击确定后将生成支付二维码</p>
</div>
) : (
<div style={{ textAlign: 'center', padding: '20px 0' }}>
<div
style={{
background: '#fff',
padding: '20px',
borderRadius: '8px',
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
display: 'inline-block',
}}
>
<QRCodeSVG value={paymentUrl} size={200} level="H" includeMargin />
</div>
<p style={{ marginTop: '20px', fontSize: '16px', fontWeight: 'bold' }}>
订单金额<span style={{ color: 'var(--brand)' }}>¥{workData.price}</span>
</p>
<p style={{ color: '#666', fontSize: '14px' }}>请使用微信或支付宝扫码支付</p>
{checkingPayment && (
<div style={{ marginTop: '15px' }}>
<Spin size="small" />
<span style={{ marginLeft: '10px', color: '#1890ff' }}>等待支付中...</span>
</div>
)}
<p style={{ color: '#999', fontSize: '12px', marginTop: '15px' }}>订单号{orderNumber}</p>
<p style={{ color: '#999', fontSize: '12px' }}>支付完成后将自动开始下载</p>
</div>
)}
</Modal>
<Footer />
</div>
);
};
export default WorkDetail;