From af18085f400444797a6d6c38bf92fd315f1bf5d0 Mon Sep 17 00:00:00 2001 From: chunyu <12914@example.com> Date: Tue, 15 Sep 2026 15:26:51 +0800 Subject: [PATCH] =?UTF-8?q?feat(C-04)=EF=BC=9AAI=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E6=94=BE=E5=A4=A7=E9=A1=B5+=E6=90=9C=E7=B4=A2=E7=BB=93?= =?UTF-8?q?=E6=9E=9C=E8=B7=B3=E8=BD=AC=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增/utility/image-upscaler页(2x/4x上传放大、积分扣费402余额不足分支、blob下载);Search页响应体剥层对齐(成功码10000)、q截断100、结果URL对齐后端现行路由(/post、/utility、/course-learn、/open-api-detail)。 --- src/pages/ImageUpscaler/ImageUpscaler.css | 87 +++++++++++++ src/pages/ImageUpscaler/ImageUpscaler.tsx | 145 ++++++++++++++++++++++ src/pages/Search/Search.tsx | 45 ++++--- 3 files changed, 258 insertions(+), 19 deletions(-) create mode 100644 src/pages/ImageUpscaler/ImageUpscaler.css create mode 100644 src/pages/ImageUpscaler/ImageUpscaler.tsx diff --git a/src/pages/ImageUpscaler/ImageUpscaler.css b/src/pages/ImageUpscaler/ImageUpscaler.css new file mode 100644 index 0000000..f5780ad --- /dev/null +++ b/src/pages/ImageUpscaler/ImageUpscaler.css @@ -0,0 +1,87 @@ +.upscaler-page { + max-width: 1080px; + margin: 0 auto; + padding: 32px 20px 64px; +} + +.upscaler-inner { + display: flex; + flex-direction: column; + gap: 8px; +} + +.upscaler-title { + font-size: 26px; + font-weight: 800; + margin: 0; +} + +.upscaler-desc { + color: #64748b; + margin: 0 0 16px; +} + +.upscaler-balance { + color: #059669; + font-weight: 700; +} + +.upscaler-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 16px; +} + +.upscaler-card { + background: #fff; + border: 1px solid #e2e8f0; + border-radius: 14px; + padding: 18px; + display: flex; + flex-direction: column; + gap: 12px; +} + +.upscaler-card h3 { + margin: 0; + font-size: 15px; +} + +.upscaler-preview { + width: 100%; + border-radius: 10px; + border: 1px solid #e2e8f0; + margin-top: 8px; +} + +.upscaler-actions { + margin-top: 8px; +} + +.upscaler-loading { + display: flex; + align-items: center; + gap: 8px; + color: #64748b; +} + +.upscaler-empty { + color: #94a3b8; + font-size: 13px; +} + +.upscaler-download { + display: inline-block; + margin-top: 10px; +} + +@media (max-width: 900px) { + .upscaler-grid { + grid-template-columns: 1fr; + } +} + +[data-theme="dark"] .upscaler-card { + background: #1e293b; + border-color: #334155; +} diff --git a/src/pages/ImageUpscaler/ImageUpscaler.tsx b/src/pages/ImageUpscaler/ImageUpscaler.tsx new file mode 100644 index 0000000..bc166b0 --- /dev/null +++ b/src/pages/ImageUpscaler/ImageUpscaler.tsx @@ -0,0 +1,145 @@ +import "./ImageUpscaler.css"; +import React, { useRef, useState } from "react"; +import { Button, Radio, Spin } from "antd"; +import { message } from "@/utils/message"; +import { + ExpandOutlined, + UploadOutlined, + DownloadOutlined, +} from "@ant-design/icons"; +import { useRecordHistory } from "@/hooks/useRecordHistory"; +import { api_request } from "@/utils/request"; + +const COST_TIPS: Record = { 2: 2, 4: 2 }; + +const ImageUpscaler: React.FC = () => { + useRecordHistory({ + type: "tool", + title: "AI 图片放大", + description: "2x/4x 无损放大,扣积分", + category: "AI 工具", + link: "/utility/image-upscaler", + }); + + const fileRef = useRef(null); + const [file, setFile] = useState(null); + const [preview, setPreview] = useState(""); + const [scale, setScale] = useState<2 | 4>(2); + const [balance, setBalance] = useState(null); + const [running, setRunning] = useState(false); + const [resultUrl, setResultUrl] = useState(""); + const [resultName, setResultName] = useState(""); + + const pick = (f: File | undefined) => { + if (!f) return; + if (!f.type.startsWith("image/")) { + message.error("请上传图片文件"); + return; + } + if (f.size > 10 * 1024 * 1024) { + message.error("图片过大(>10MB)"); + return; + } + setFile(f); + setPreview(URL.createObjectURL(f)); + setResultUrl(""); + }; + + const run = async () => { + if (!file || running) return; + setRunning(true); + try { + const fd = new FormData(); + fd.append("file", file); + fd.append("scale", String(scale)); + const res: any = await api_request.aitool.upscale(fd); + // 后端返回文件流(axios blob):兼容 request 封装的多种返回形态 + const blob: Blob | undefined = res?.data instanceof Blob ? res.data : res instanceof Blob ? res : undefined; + if (blob) { + const url = URL.createObjectURL(blob); + setResultUrl(url); + setResultName(`upscaled_${scale}x.png`); + // header 余额(axios response headers 小写) + const headers = res?.headers || {}; + const bal = headers["x-aitool-balance"]; + if (bal !== undefined) setBalance(Number(bal)); + message.success(`放大成功,已扣 ${COST_TIPS[scale]} 积分`); + } else { + message.success("放大成功"); + } + } catch (e: any) { + const status = e?.response?.status; + const data = e?.response?.data; + if (status === 402) { + message.error(`积分不足(需 ${data?.cost ?? COST_TIPS[scale]} 积分),先去签到赚积分`); + } else { + const msg = typeof data === "string" ? data : data?.error || "处理失败,积分已退回"; + message.error(msg); + } + } finally { + setRunning(false); + } + }; + + return ( +
+
+

+ AI 图片放大 +

+

+ 本地高清重采样 2x / 4x · 每次扣 {COST_TIPS[scale]} 积分 · 失败自动退回 + {balance !== null && · 余额 {balance} 积分} +

+
+
+

1 · 上传图片

+ pick(e.target.files?.[0])} + /> + + {preview && 原图预览} +
+
+

2 · 放大倍数

+ setScale(e.target.value)}> + 2x(扣 2 积分) + 4x(扣 2 积分) + +
+ +
+ {running && ( +
+ 正在放大,请稍候… +
+ )} +
+
+

3 · 预览与下载

+ {resultUrl ? ( + + ) : ( +

放大完成后在此预览与下载

+ )} +
+
+
+
+ ); +}; + +export default ImageUpscaler; diff --git a/src/pages/Search/Search.tsx b/src/pages/Search/Search.tsx index b84ec47..98a1db0 100644 --- a/src/pages/Search/Search.tsx +++ b/src/pages/Search/Search.tsx @@ -96,9 +96,11 @@ const Search: React.FC = () => { useEffect(() => { const fetchHotKeywords = async () => { try { - const res = await api_request.search.hotKeywords(); - if (res.data?.code === 0) { - setHotKeywords(res.data.data); + const res: any = await api_request.search.hotKeywords(); + // axios 拦截器已剥掉一层:res 即响应体 {data, code},成功码 10000 + const body = res?.data?.code !== undefined ? res.data : res; + if (body?.code === 10000 && Array.isArray(body?.data)) { + setHotKeywords(body.data); } } catch { setHotKeywords(["React Hooks", "RESTful API", "TypeScript", "Docker部署", "GraphQL", "微服务架构", "CI/CD", "性能优化"]); @@ -118,16 +120,18 @@ const Search: React.FC = () => { try { const filterToUse = searchFilter !== undefined ? searchFilter : activeFilter; const sortToUse = searchSort !== undefined ? searchSort : sort; - const res = await api_request.search.global({ - q: q.trim(), + const res: any = await api_request.search.global({ + q: q.trim().slice(0, 100), type: filterToUse === 'all' ? undefined : filterToUse, page: pageNum, page_size: pageSize, sort: sortToUse, }); - if (res.data?.code === 0) { - setResults(res.data.data.results || []); - setTotal(res.data.data.total || 0); + // axios 拦截器已剥掉一层:res 即响应体 {data, code},成功码 10000 + const body = res?.data?.code !== undefined ? res.data : res; + if (body?.code === 10000 && body?.data) { + setResults(body.data.results || []); + setTotal(body.data.total || 0); setCurrentPage(pageNum); } } catch { @@ -174,9 +178,11 @@ const Search: React.FC = () => { return; } try { - const res = await api_request.search.suggestions({ q: keyword.trim(), limit: 8 }); - if (res.data?.code === 0) { - setSuggestions(res.data.data || []); + const res: any = await api_request.search.suggestions({ q: keyword.trim(), limit: 8 }); + // axios 拦截器已剥掉一层:res 即响应体 {data, code},成功码 10000 + const body = res?.data?.code !== undefined ? res.data : res; + if (body?.code === 10000) { + setSuggestions(body?.data || []); } } catch { setSuggestions([]); @@ -225,19 +231,20 @@ const Search: React.FC = () => { const getResultUrl = (item: SearchResult) => { if (item.url) return item.url; - if (item.type === '文章') return `/articles/${item.id}`; - if (item.type === '工具') return `/tools/${item.id}`; - if (item.type === '课程') return `/courses/${item.id}`; + if (item.type === '文章') return `/post/${item.id}`; + if (item.type === '工具') return '/utility'; + if (item.type === '课程') return `/course-learn?id=${item.id}`; + if (item.type === 'API') return `/open-api-detail/${item.id}`; return '/search'; }; const categories = [ - { name: t('search.typeApi'), icon: , gradient: "linear-gradient(135deg, #667eea, #764ba2)", path: "/api" }, + { name: t('search.typeApi'), icon: , gradient: "linear-gradient(135deg, #667eea, #764ba2)", path: "/open-api" }, { name: t('search.typeArticle'), icon: , gradient: "linear-gradient(135deg, #f093fb, #f5576c)", path: "/articles" }, - { name: t('search.typeCourse'), icon: , gradient: "linear-gradient(135deg, #4facfe, #00f2fe)", path: "/courses" }, - { name: t('search.typeTool'), icon: , gradient: "linear-gradient(135deg, #43e97b, #38f9d7)", path: "/api" }, - { name: t('search.typeFrontend'), icon: , gradient: "linear-gradient(135deg, #fa709a, #fee140)", path: "/frontend" }, - { name: t('search.typeBackend'), icon: , gradient: "linear-gradient(135deg, #a18cd1, #fbc2eb)", path: "/backend" }, + { name: t('search.typeCourse'), icon: , gradient: "linear-gradient(135deg, #4facfe, #00f2fe)", path: "/learn" }, + { name: t('search.typeTool'), icon: , gradient: "linear-gradient(135deg, #43e97b, #38f9d7)", path: "/utility" }, + { name: t('search.typeFrontend'), icon: , gradient: "linear-gradient(135deg, #fa709a, #fee140)", path: "/open-api" }, + { name: t('search.typeBackend'), icon: , gradient: "linear-gradient(135deg, #a18cd1, #fbc2eb)", path: "/learn" }, ]; const filterTabs = [