Compare commits

..

10 Commits

34 changed files with 3954 additions and 2837 deletions

View File

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

View File

@@ -17,6 +17,7 @@ from app.models.download import DownloadRecord
from app.ysm_sdk import create_payment, query_order, PaymentNotify from app.ysm_sdk import create_payment, query_order, PaymentNotify
from app.api.orders import get_current_user from app.api.orders import get_current_user
from app.models.user import User from app.models.user import User
from app.services.download_tracker import record_download
router = APIRouter(prefix="/payment", tags=["支付"]) router = APIRouter(prefix="/payment", tags=["支付"])
@@ -139,18 +140,10 @@ async def payment_notify(
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()
@@ -204,18 +197,9 @@ async def query_order_status(
order.payment_method = "ysm_wechat" order.payment_method = "ysm_wechat"
order.paid_at = datetime.now() order.paid_at = datetime.now()
# 创建下载记录
download_record = DownloadRecord(
user_id=order.user_id,
work_id=order.work_id,
order_id=order.id
)
db.add(download_record)
# 更新作品下载次数
work = db.query(Work).filter(Work.id == order.work_id).first() work = db.query(Work).filter(Work.id == order.work_id).first()
if work: if work:
work.downloads += 1 record_download(db, order, work, current_user)
db.commit() db.commit()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,72 +1,67 @@
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-scroll" ref={scrollRef}> <div className="festival-header">
{festivals.map((item, index) => ( <div className="festival-copy">
<div <span className="festival-kicker">营销节点</span>
key={item.name} <h2>把时间感也做成页面的一部分</h2>
className="festival-card" <p>
style={{ '--delay': `${index * 0.08}s` }} 这些节点能帮助用户快速想到最近该做什么图比起单纯展示日期更像是一个素材站自己的选题提醒
> </p>
<div className="festival-emoji">{item.emoji}</div>
<div className="festival-content">
<h3 className="festival-name">{item.name}</h3>
<p className="festival-date">{item.date} {item.weekday}</p>
</div>
<div className="festival-days">
<span className="days-number">{item.daysLeft}</span>
<span className="days-text">天后</span>
</div>
</div>
))}
</div> </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 <Button
className="festival-scroll-btn" className="festival-scroll-btn"
shape="circle" shape="circle"
@@ -74,6 +69,31 @@ const Festival = () => {
onClick={() => scroll('right')} onClick={() => scroll('right')}
/> />
</div> </div>
</div>
</div>
<div className="festival-scroll" ref={scrollRef}>
{festivals.map((item, index) => (
<div
key={item.name}
className={`festival-card tone-${item.tone}`}
style={{ '--delay': `${index * 0.05}s` }}
>
<div className="festival-icon">{item.icon}</div>
<div className="festival-content">
<div className="festival-topline">
<h3 className="festival-name">{item.name}</h3>
<span className="festival-days">{item.daysLeft} 天后</span>
</div>
<p className="festival-date">
{item.date} · {item.weekday}
</p>
<span className="festival-note">适合提前布局专题与节日视觉</span>
</div>
</div>
))}
</div>
</div>
</section> </section>
); );
}; };

View File

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

View File

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

View File

@@ -1,6 +1,8 @@
.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);
border-bottom: 1px solid rgba(233, 221, 212, 0.88);
box-shadow: 0 14px 34px rgba(61, 40, 24, 0.05);
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
@@ -37,12 +39,12 @@
.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;
} }
@@ -53,7 +55,7 @@
} }
.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;
@@ -64,7 +66,7 @@
} }
.nav-item:hover { .nav-item:hover {
color: #ff5a5a; color: var(--brand);
} }
.nav-new { .nav-new {
@@ -77,7 +79,7 @@
right: -8px; right: -8px;
width: 6px; width: 6px;
height: 6px; height: 6px;
background: #ff5a5a; background: var(--brand);
border-radius: 50%; border-radius: 50%;
} }
@@ -95,7 +97,7 @@
.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;
} }
@@ -107,42 +109,44 @@
} }
.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 { .btn-register:hover {
border-color: #ff5a5a !important; border-color: var(--brand) !important;
color: #ff5a5a !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 { .btn-login:hover {
border-color: #ff5a5a !important; border-color: var(--brand) !important;
color: #ff5a5a !important; color: var(--brand) !important;
} }
@media (max-width: 1024px) { @media (max-width: 1024px) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,8 +13,8 @@ function Home() {
<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>

View File

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

View File

@@ -1,128 +1,279 @@
.work-detail-page { .work-detail-page {
min-height: 100vh; min-height: 100vh;
background: #f5f5f5;
padding-top: 70px; padding-top: 70px;
background:
radial-gradient(circle at top left, rgba(239, 106, 91, 0.08), transparent 28%),
radial-gradient(circle at top right, rgba(244, 178, 84, 0.12), transparent 24%),
linear-gradient(180deg, #fffdf9 0%, #f8f2eb 100%);
} }
.work-detail-container { .work-detail-container {
max-width: 1400px; max-width: 1400px;
margin: 0 auto; margin: 0 auto;
padding: 20px; padding: 18px 20px 56px;
}
.work-loading {
display: flex;
justify-content: center;
align-items: center;
min-height: 60vh;
} }
/* 面包屑 */
.breadcrumb { .breadcrumb {
padding: 16px 0; padding: 14px 0 22px;
font-size: 14px; font-size: 14px;
color: #999; color: #8e8175;
} }
.breadcrumb a { .breadcrumb a {
color: #666; color: #716457;
cursor: pointer; cursor: pointer;
transition: color 0.3s; transition: color 0.2s ease;
} }
.breadcrumb a:hover { .breadcrumb a:hover {
color: #ff5a5a; color: var(--brand);
} }
.breadcrumb .current { .breadcrumb .current {
color: #333; color: #30271f;
} }
/* 主要内容区 */ .work-detail-heading {
.work-detail-content {
display: flex; display: flex;
justify-content: space-between;
gap: 28px;
padding: 28px 30px;
border-radius: 32px;
background:
radial-gradient(circle at right top, rgba(244, 178, 84, 0.18), transparent 28%),
linear-gradient(135deg, rgba(255, 250, 244, 0.94), rgba(255, 244, 233, 0.96));
border: 1px solid rgba(233, 219, 207, 0.92);
box-shadow: 0 22px 50px rgba(69, 44, 26, 0.08);
margin-bottom: 24px;
}
.detail-heading-copy {
max-width: 760px;
}
.detail-kicker {
display: inline-flex;
margin-bottom: 12px;
padding: 8px 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;
}
.work-title {
margin: 0;
font-size: clamp(30px, 4vw, 42px);
line-height: 1.15;
color: #241d17;
}
.work-subtitle {
margin: 14px 0 0;
color: #72675d;
line-height: 1.85;
font-size: 16px;
}
.detail-heading-stats {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
min-width: 360px;
}
.heading-stat {
padding: 18px 16px;
border-radius: 22px;
background: rgba(255, 255, 255, 0.78);
border: 1px solid rgba(231, 219, 209, 0.9);
display: flex;
flex-direction: column;
gap: 6px;
justify-content: center;
}
.heading-stat strong {
font-size: 24px;
color: #241d17;
}
.heading-stat span {
font-size: 13px;
color: #837668;
}
.work-detail-content {
display: grid;
grid-template-columns: minmax(0, 1fr) 380px;
gap: 24px; gap: 24px;
align-items: flex-start; align-items: start;
} }
/* 左侧作品展示 */
.work-main { .work-main {
flex: 1; display: flex;
background: white; flex-direction: column;
border-radius: 12px; gap: 24px;
overflow: hidden;
} }
.work-image-wrapper { .work-preview-card,
padding: 24px; .detail-panel,
.price-card {
border-radius: 28px;
background: rgba(255, 255, 255, 0.94);
border: 1px solid rgba(234, 222, 212, 0.95);
box-shadow: 0 22px 48px rgba(66, 42, 26, 0.08);
}
.work-preview-card {
padding: 22px;
}
.work-preview-topline {
display: flex;
justify-content: space-between;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 16px;
}
.preview-badge {
padding: 8px 12px;
border-radius: 999px;
background: var(--brand-soft);
color: var(--brand-strong);
font-size: 12px;
font-weight: 700;
}
.preview-badge.subtle {
background: #f7f1ea;
color: #6b5f53;
}
.work-image-shell {
position: relative;
border-radius: 24px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
background: #ffffff;
border: 1px solid rgba(232, 223, 214, 0.95);
}
.work-image-shell::after {
display: none;
} }
.work-image { .work-image {
width: 100%; width: 100%;
height: auto; height: auto;
display: block; display: block;
border-radius: 8px; object-fit: contain;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
} }
/* 相关标签 */ .preview-benefits {
.related-tags { display: grid;
margin-top: 32px; grid-template-columns: repeat(3, minmax(0, 1fr));
padding-top: 24px; gap: 12px;
border-top: 1px solid #f0f0f0; margin-top: 18px;
} }
.related-tags h4 { .preview-benefit {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 14px 16px;
border-radius: 20px;
background: #fffaf5;
border: 1px solid rgba(236, 225, 215, 0.92);
color: #5f5449;
line-height: 1.7;
font-size: 14px;
}
.preview-benefit .anticon {
color: var(--brand);
font-size: 16px; font-size: 16px;
font-weight: 600; margin-top: 2px;
}
.detail-panel {
padding: 22px;
}
.panel-header {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 12px;
margin-bottom: 16px; margin-bottom: 16px;
color: #333; }
.panel-header.compact {
align-items: center;
}
.panel-header h3 {
margin: 0;
color: #2c241d;
font-size: 20px;
}
.panel-header span {
color: #8c8073;
font-size: 13px;
} }
.tags-list { .tags-list {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 8px; gap: 10px;
} }
.work-tag { .work-tag.ant-tag {
padding: 6px 16px; margin: 0;
background: #f5f5f5; padding: 8px 14px;
border: 1px solid #e8e8e8; border-radius: 999px;
border-radius: 20px; background: #fff6ef;
cursor: pointer; border: 1px solid rgba(236, 214, 196, 0.95);
transition: all 0.3s; color: #6d5b4c;
} }
.work-tag:hover { .empty-hint {
background: #fff0f0; color: #9a8d81;
border-color: #ff5a5a; font-size: 14px;
color: #ff5a5a;
}
/* 猜你喜欢 */
.related-works {
margin-top: 32px;
padding-top: 24px;
border-top: 1px solid #f0f0f0;
}
.related-works h3 {
font-size: 18px;
font-weight: 600;
margin-bottom: 20px;
color: #333;
} }
.related-works-grid { .related-works-grid {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 16px; gap: 16px;
} }
.related-work-card { .related-work-card {
cursor: pointer; cursor: pointer;
border-radius: 8px; border-radius: 22px;
overflow: hidden; overflow: hidden;
transition: transform 0.3s; background: linear-gradient(180deg, #fffdfb, #f8f2eb);
background: #fafafa; border: 1px solid rgba(237, 226, 216, 0.95);
transition: transform 0.24s ease, box-shadow 0.24s ease;
} }
.related-work-card:hover { .related-work-card:hover {
transform: translateY(-4px); transform: translateY(-6px);
box-shadow: 0 18px 34px rgba(64, 42, 28, 0.12);
} }
.related-work-image { .related-work-image {
@@ -130,30 +281,26 @@
width: 100%; width: 100%;
padding-top: 150%; padding-top: 150%;
overflow: hidden; overflow: hidden;
background: #f0f0f0; background: linear-gradient(135deg, #edf6fb 0%, #dcecf4 100%);
} }
.related-work-image img { .related-work-image img {
position: absolute; position: absolute;
top: 0; inset: 12px;
left: 0; width: calc(100% - 24px);
width: 100%; height: calc(100% - 24px);
height: 100%; object-fit: contain;
object-fit: cover;
} }
.related-work-overlay { .related-work-overlay {
position: absolute; position: absolute;
top: 0; inset: 0;
left: 0; background: rgba(21, 18, 15, 0.45);
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
opacity: 0; opacity: 0;
transition: opacity 0.3s; transition: opacity 0.24s ease;
} }
.related-work-card:hover .related-work-overlay { .related-work-card:hover .related-work-overlay {
@@ -161,26 +308,22 @@
} }
.related-work-info { .related-work-info {
padding: 12px; padding: 14px;
} }
.related-work-info h4 { .related-work-info h4 {
font-size: 14px; margin: 0 0 8px;
font-weight: 500; color: #2d241d;
margin-bottom: 8px; font-size: 15px;
color: #333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} }
.related-work-designer { .related-work-designer {
color: #8a7d72;
font-size: 12px; font-size: 12px;
color: #999;
} }
.designer-level { .designer-level {
color: #ff5a5a; color: var(--brand);
margin-right: 4px; margin-right: 4px;
} }
@@ -189,133 +332,137 @@
} }
.designer-name { .designer-name {
margin-top: 4px; margin: 4px 0 0;
color: #666; color: #65594d;
} }
/* 右侧信息栏 */
.work-sidebar { .work-sidebar {
width: 380px;
background: white;
border-radius: 12px;
padding: 24px;
position: sticky; position: sticky;
top: 80px; top: 86px;
display: flex;
flex-direction: column;
gap: 18px;
}
.price-card {
padding: 22px;
} }
/* 作品编号 */
.work-id-section { .work-id-section {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 12px; gap: 12px;
background: #f5f5f5; padding: 12px 14px;
border-radius: 8px; border-radius: 18px;
margin-bottom: 16px; background: #faf5ef;
border: 1px solid rgba(235, 224, 214, 0.94);
} }
.work-id { .work-id {
color: #66594d;
font-size: 14px; font-size: 14px;
color: #666; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-family: monospace;
} }
/* 作品标题 */ .price-block {
.work-title { margin: 18px 0 10px;
font-size: 24px; }
font-weight: 600;
color: #333; .price-label {
margin-bottom: 20px; display: block;
line-height: 1.4; color: #8a7c70;
font-size: 13px;
margin-bottom: 6px;
}
.price-value {
color: var(--brand);
font-size: 40px;
line-height: 1;
font-weight: 800;
} }
/* 作品信息列表 */
.work-info-list { .work-info-list {
border-top: 1px solid #f0f0f0; display: flex;
border-bottom: 1px solid #f0f0f0; flex-direction: column;
gap: 10px;
padding: 16px 0; padding: 16px 0;
margin-bottom: 20px; border-top: 1px solid rgba(239, 229, 220, 0.95);
border-bottom: 1px solid rgba(239, 229, 220, 0.95);
} }
.work-info-item { .work-info-item {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
padding: 8px 0; gap: 18px;
font-size: 14px;
} }
.info-label { .info-label {
color: #999; color: #95887c;
font-size: 14px;
} }
.info-value { .info-value {
color: #333; color: #2d241d;
font-weight: 500; font-size: 14px;
font-weight: 600;
text-align: right;
}
.info-value.highlight {
color: var(--brand);
} }
/* 操作按钮 */
.work-actions { .work-actions {
display: grid; display: grid;
grid-template-columns: 1fr auto auto; grid-template-columns: 1fr auto auto;
gap: 12px; gap: 12px;
margin-bottom: 24px; margin-top: 18px;
} }
.download-btn { .download-btn.ant-btn {
background: linear-gradient(135deg, #ff5a5a 0%, #ff8080 100%); height: 50px;
border: none; border: none;
height: 44px; border-radius: 18px;
font-size: 16px; background: linear-gradient(135deg, var(--brand) 0%, var(--brand-strong) 100%);
font-weight: 500; box-shadow: 0 16px 30px rgba(216, 78, 63, 0.24);
box-shadow: 0 4px 12px rgba(255, 90, 90, 0.3); font-weight: 700;
} }
.download-btn:hover { .icon-btn.ant-btn {
background: linear-gradient(135deg, #ff7070 0%, #ff9090 100%) !important; width: 50px;
height: 50px;
border-radius: 18px;
border-color: rgba(224, 208, 197, 0.95);
color: #65594d;
background: #fffaf5;
} }
.collect-btn, .icon-btn.ant-btn:hover,
.share-btn { .icon-btn.ant-btn.collected {
height: 44px; color: var(--brand);
width: 44px; border-color: rgba(239, 106, 91, 0.4);
padding: 0;
border-color: #e8e8e8;
color: #666;
} }
.collect-btn.collected {
color: #ff5a5a;
border-color: #ff5a5a;
}
.collect-btn:hover,
.share-btn:hover {
color: #ff5a5a;
border-color: #ff5a5a;
}
/* 设计师卡片 */
.designer-card { .designer-card {
padding: 20px; background: linear-gradient(180deg, #fffdfb, #f8f2eb);
background: #fafafa;
border-radius: 12px;
margin-bottom: 24px;
} }
.designer-header { .designer-header {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 14px;
margin-bottom: 16px; margin-bottom: 18px;
} }
.designer-avatar { .designer-avatar {
width: 56px; width: 60px;
height: 56px; height: 60px;
border-radius: 50%; border-radius: 20px;
overflow: hidden; overflow: hidden;
border: 2px solid #fff; background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); border: 1px solid rgba(233, 221, 212, 0.95);
} }
.designer-avatar img { .designer-avatar img {
@@ -325,150 +472,162 @@
} }
.designer-info { .designer-info {
flex: 1; min-width: 0;
} }
.designer-name-level { .designer-name-level {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 8px;
margin-bottom: 4px; margin-bottom: 6px;
} }
.designer-level-badge { .designer-level-badge {
background: #ff5a5a; padding: 4px 10px;
color: white; border-radius: 999px;
padding: 2px 8px; background: var(--brand);
border-radius: 12px; color: #fff;
font-size: 12px; font-size: 12px;
font-weight: 700;
} }
.designer-level-text { .designer-level-text {
color: #8c8073;
font-size: 12px; font-size: 12px;
color: #999;
} }
.designer-card .designer-name { .designer-card .designer-name {
font-size: 16px;
font-weight: 600;
color: #333;
margin: 0; margin: 0;
font-size: 18px;
font-weight: 700;
color: #2b241d;
} }
.designer-stats { .designer-stats {
display: flex; display: grid;
justify-content: space-around; grid-template-columns: repeat(2, minmax(0, 1fr));
padding: 16px 0; gap: 12px;
border-top: 1px solid #e8e8e8;
border-bottom: 1px solid #e8e8e8;
margin-bottom: 16px; margin-bottom: 16px;
} }
.designer-stats .stat-item { .designer-stats .stat-item {
padding: 14px;
border-radius: 18px;
background: rgba(255, 255, 255, 0.72);
border: 1px solid rgba(236, 225, 216, 0.95);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center;
gap: 4px; gap: 4px;
align-items: center;
} }
.stat-value { .stat-value {
font-size: 20px; color: #2d241d;
font-weight: 600; font-size: 22px;
color: #333; font-weight: 700;
} }
.stat-label { .stat-label {
color: #918578;
font-size: 12px; font-size: 12px;
color: #999;
} }
.follow-btn { .follow-btn.ant-btn {
height: 40px; height: 46px;
background: #ff5a5a; border-radius: 16px;
border: none; border: none;
font-weight: 700;
} }
.follow-btn:hover { .follow-btn.ant-btn.followed {
background: #ff7070 !important; background: #ebe3db;
color: #66594d;
} }
.follow-btn.followed { .board-search-input.ant-input-affix-wrapper {
background: #e8e8e8; height: 44px;
color: #666; border-radius: 16px;
border-color: rgba(226, 211, 200, 0.95);
} }
.follow-btn.followed:hover { .board-search-input.ant-input-affix-wrapper:hover,
background: #d0d0d0 !important; .board-search-input.ant-input-affix-wrapper-focused {
border-color: rgba(239, 106, 91, 0.4);
box-shadow: none;
} }
/* 搜索画板 */ .stats-grid {
.board-search { display: grid;
margin-bottom: 24px; gap: 12px;
} }
.board-search h4 { .stat-row {
font-size: 14px;
font-weight: 600;
margin-bottom: 12px;
color: #333;
}
.board-search-input {
height: 40px;
border-radius: 20px;
}
/* 浏览统计 */
.work-stats {
display: flex;
justify-content: space-around;
padding-top: 16px;
border-top: 1px solid #f0f0f0;
}
.work-stats .stat-item {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 10px;
font-size: 14px; padding: 14px 16px;
color: #999; border-radius: 18px;
background: #fff8f2;
border: 1px solid rgba(237, 225, 215, 0.95);
color: #66594d;
} }
.work-stats .stat-item .anticon { .stat-row .anticon {
font-size: 16px; color: var(--brand);
} }
/* 响应式 */
@media (max-width: 1200px) { @media (max-width: 1200px) {
.work-detail-content { .work-detail-heading {
flex-direction: column; flex-direction: column;
} }
.work-sidebar { .detail-heading-stats {
width: 100%; min-width: 0;
position: static;
} }
.related-works-grid { .work-detail-content {
grid-template-columns: repeat(2, 1fr); grid-template-columns: 1fr;
}
.work-sidebar {
position: static;
}
}
@media (max-width: 900px) {
.preview-benefits,
.related-works-grid,
.detail-heading-stats {
grid-template-columns: 1fr;
} }
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.work-detail-container { .work-detail-container {
padding: 12px; padding: 12px 12px 40px;
} }
.related-works-grid { .work-detail-heading,
grid-template-columns: 1fr; .work-preview-card,
.detail-panel,
.price-card {
border-radius: 24px;
padding: 18px;
}
.work-title {
font-size: 28px;
}
.work-subtitle {
font-size: 14px;
} }
.work-actions { .work-actions {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.collect-btn, .icon-btn.ant-btn {
.share-btn {
width: 100%; width: 100%;
} }
} }

View File

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