Compare commits
10 Commits
2e796bb55f
...
770c303a00
| Author | SHA1 | Date | |
|---|---|---|---|
| 770c303a00 | |||
| cce27b7281 | |||
| 02ac19f063 | |||
| beae751f25 | |||
| 147fc58409 | |||
| 5ff85debdc | |||
| 3d91cd68a1 | |||
| 15dac7f876 | |||
| 045ae62373 | |||
| b0c9c193aa |
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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}"
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
46
backend/app/services/download_tracker.py
Normal file
46
backend/app/services/download_tracker.py
Normal 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))
|
||||||
32
backend/app/services/wecom_bot.py
Normal file
32
backend/app/services/wecom_bot.py
Normal 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
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 */
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user