├── .eslintrc.json
├── .gitignore
├── .node-version
├── .prettierrc
├── LICENSE
├── README.md
├── components.json
├── next.config.ts
├── package.json
├── pnpm-lock.yaml
├── postcss.config.mjs
├── public
├── file.svg
├── globe.svg
├── icon.png
├── next.svg
├── og.png
├── vercel.svg
└── window.svg
├── src
├── app
│ ├── favicon.ico
│ ├── globals.css
│ ├── layout.tsx
│ └── page.tsx
├── components
│ ├── ImageCompareResult.tsx
│ ├── ImageUploader.tsx
│ ├── ProcessingError.tsx
│ ├── ProcessingLoader.tsx
│ ├── icons
│ │ ├── github.tsx
│ │ ├── sparkles.tsx
│ │ └── x.tsx
│ ├── images
│ │ ├── homepage-image-1.tsx
│ │ └── homepage-image-2.tsx
│ └── ui
│ │ ├── button.tsx
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── logo.tsx
│ │ ├── select.tsx
│ │ ├── spinner.tsx
│ │ ├── toast.tsx
│ │ └── toaster.tsx
├── fonts
│ ├── DingTalk_JinBuTi.ttf
│ └── DingTalk_Sans.ttf
├── hooks
│ └── use-toast.ts
└── lib
│ ├── backgroundRemoval.ts
│ └── utils.ts
├── tailwind.config.ts
└── tsconfig.json
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["next/core-web-vitals", "next/typescript"]
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.*
7 | .yarn/*
8 | !.yarn/patches
9 | !.yarn/plugins
10 | !.yarn/releases
11 | !.yarn/versions
12 |
13 | # testing
14 | /coverage
15 |
16 | # next.js
17 | /.next/
18 | /out/
19 |
20 | # production
21 | /build
22 |
23 | # misc
24 | .DS_Store
25 | *.pem
26 |
27 | # debug
28 | npm-debug.log*
29 | yarn-debug.log*
30 | yarn-error.log*
31 |
32 | # env files (can opt-in for committing if needed)
33 | .env*
34 |
35 | # vercel
36 | .vercel
37 |
38 | # typescript
39 | *.tsbuildinfo
40 | next-env.d.ts
41 |
--------------------------------------------------------------------------------
/.node-version:
--------------------------------------------------------------------------------
1 | 20.12.1
2 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": ["prettier-plugin-tailwindcss"]
3 | }
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 hellokaton
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # remove-bg
2 |
3 | remove-bg 是一款AI赋能的现代化Web应用,基于 Next.js 构建,致力于提供极致简单、快速精准的在线图片背景移除服务。
4 |
5 | ## 📸 预览
6 |
7 | 
8 |
9 | 预览地址: https://rmbg.hellokaton.me
10 |
11 | ## ✨ 核心功能
12 |
13 | - 🚀 **一键操作**:上传图片即可自动移除背景,简单高效。
14 | - 🖼️ **效果对比**:提供处理前后图片对比,直观查看效果。
15 | - 💾 **轻松下载**:方便下载处理后的无背景图片。
16 | - 📱 **响应式设计**:适配桌面和移动设备,随时随地使用。
17 |
18 | ## 🛠️ 技术栈
19 |
20 | - **框架**: Next.js 15.3 (App Router)
21 | - **开发语言**: TypeScript
22 | - **样式**: Tailwind CSS
23 | - **核心处理**: @imgly/background-removal
24 | - **交互**:
25 | - react-compare-slider
26 | - react-dropzone
27 |
28 | ## 🚀 快速开始
29 |
30 | 1. 克隆仓库:
31 |
32 | ```bash
33 | git clone https://github.com/hellokaton/remove-bg.git
34 | ```
35 |
36 | 2. 安装依赖:
37 |
38 | ```bash
39 | pnpm install
40 | ```
41 |
42 | 3. 启动开发服务器:
43 |
44 | ```bash
45 | pnpm dev
46 | ```
47 |
48 | ## 💡 使用指南
49 |
50 | 1. 点击或拖拽上传您的图片。
51 | 2. 等待应用自动处理图片背景。
52 | 3. 在对比视图中查看移除背景后的效果。
53 | 4. 点击下载按钮保存处理后的图片。
54 |
55 | ## 🤝 贡献指南
56 |
57 | 欢迎贡献!请随时提交 Pull Request。
58 |
59 | ## 📝 许可证
60 |
61 | [MIT](LICENSE)
62 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "src/app/globals.css",
9 | "baseColor": "neutral",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | },
20 | "iconLibrary": "lucide"
21 | }
--------------------------------------------------------------------------------
/next.config.ts:
--------------------------------------------------------------------------------
1 | import type { NextConfig } from "next";
2 |
3 | const nextConfig: NextConfig = {
4 | async headers() {
5 | return [
6 | {
7 | source: "/api/:path*",
8 | headers: [
9 | { key: "Access-Control-Allow-Credentials", value: "true" },
10 | { key: "Access-Control-Allow-Origin", value: "*" },
11 | {
12 | key: "Access-Control-Allow-Methods",
13 | value: "GET,OPTIONS,PATCH,DELETE,POST,PUT",
14 | },
15 | {
16 | key: "Access-Control-Allow-Headers",
17 | value:
18 | "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version",
19 | },
20 | ],
21 | },
22 | {
23 | source: "/(.*)",
24 | headers: [
25 | {
26 | key: "Cross-Origin-Opener-Policy",
27 | value: "same-origin",
28 | },
29 | {
30 | key: "Cross-Origin-Embedder-Policy",
31 | value: "require-corp",
32 | },
33 | ],
34 | },
35 | ];
36 | },
37 | };
38 |
39 | export default nextConfig;
40 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "remove-bg",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint",
10 | "update:all": "pnpm update --interactive --latest"
11 | },
12 | "dependencies": {
13 | "@imgly/background-removal": "^1.6.0",
14 | "@radix-ui/react-label": "^2.1.6",
15 | "@radix-ui/react-select": "^2.1.2",
16 | "@radix-ui/react-slot": "^1.1.0",
17 | "@radix-ui/react-toast": "^1.2.2",
18 | "@tailwindcss/typography": "^0.5.16",
19 | "class-variance-authority": "^0.7.0",
20 | "clsx": "^2.1.1",
21 | "dedent": "^1.5.3",
22 | "lucide-react": "^0.503.0",
23 | "nanoid": "^5.1.5",
24 | "next": "15.3.1",
25 | "next-plausible": "^3.12.4",
26 | "react": "19.1.0",
27 | "react-compare-slider": "^3.1.0",
28 | "react-dom": "19.1.0",
29 | "react-dropzone": "^14.3.5",
30 | "sonner": "^2.0.3",
31 | "tailwind-merge": "^2.5.4",
32 | "tailwindcss-animate": "^1.0.7"
33 | },
34 | "devDependencies": {
35 | "@types/node": "^22.15.3",
36 | "@types/react": "^19.1.2",
37 | "@types/react-dom": "^19.1.2",
38 | "eslint": "9.25.1",
39 | "eslint-config-next": "15.3.1",
40 | "postcss": "^8",
41 | "prettier": "^3.3.3",
42 | "prettier-plugin-tailwindcss": "^0.6.8",
43 | "tailwindcss": "^3.4.1",
44 | "typescript": "^5"
45 | },
46 | "engines": {
47 | "node": "22.x"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/public/file.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/globe.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hellokaton/remove-bg/7a26368b03f76680fa43c0fd426dbbb795463ed3/public/icon.png
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/og.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hellokaton/remove-bg/7a26368b03f76680fa43c0fd426dbbb795463ed3/public/og.png
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/window.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hellokaton/remove-bg/7a26368b03f76680fa43c0fd426dbbb795463ed3/src/app/favicon.ico
--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | body {
6 | font-family: Arial, Helvetica, sans-serif;
7 | }
8 |
9 | @layer base {
10 | :root {
11 | --background: 37 27% 94%;
12 | --foreground: 0 0% 3.9%;
13 | --card: 0 0% 100%;
14 | --card-foreground: 0 0% 3.9%;
15 | --popover: 0 0% 100%;
16 | --popover-foreground: 0 0% 3.9%;
17 | --primary: 0 2.44% 24.12%;
18 | --primary-foreground: 0 0% 98%;
19 | --secondary: 0 0% 96.1%;
20 | --secondary-foreground: 0 0% 9%;
21 | --muted: 0 0% 96.1%;
22 | --muted-foreground: 0 0% 45.1%;
23 | --accent: 0 0% 96.1%;
24 | --accent-foreground: 0 0% 9%;
25 | --destructive: 0 84.2% 60.2%;
26 | --destructive-foreground: 0 0% 98%;
27 | --border: 0 0% 89.8%;
28 | --input: 0 0% 89.8%;
29 | --ring: 0 0% 3.9%;
30 | --chart-1: 12 76% 61%;
31 | --chart-2: 173 58% 39%;
32 | --chart-3: 197 37% 24%;
33 | --chart-4: 43 74% 66%;
34 | --chart-5: 27 87% 67%;
35 | --radius: 0.5rem;
36 | }
37 | }
38 |
39 | @layer base {
40 | * {
41 | @apply border-border;
42 | }
43 | body {
44 | @apply bg-background text-foreground;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import GithubIcon from "@/components/icons/github";
2 | import XIcon from "@/components/icons/x";
3 | import Logo from "@/components/ui/logo";
4 | import type { Metadata } from "next";
5 | import "./globals.css";
6 | import { Toaster as RadixToaster } from "@/components/ui/toaster";
7 | import { Toaster } from "sonner";
8 | import Link from "next/link";
9 |
10 | import { Geist, Geist_Mono } from "next/font/google";
11 | import localFont from "next/font/local";
12 | import { cn } from "@/lib/utils";
13 |
14 | const geistSans = Geist({
15 | variable: "--font-geist-sans",
16 | subsets: ["latin"],
17 | });
18 |
19 | const geistMono = Geist_Mono({
20 | variable: "--font-geist-mono",
21 | subsets: ["latin"],
22 | });
23 |
24 | const dingTalkFont = localFont({
25 | src: "../fonts/DingTalk_JinBuTi.ttf",
26 | variable: "--font-dingtalk",
27 | });
28 |
29 | export const metadata: Metadata = {
30 | title: "智能抠图 | 一键移除图片背景",
31 | description: "上传图片,立即获得背景移除效果,免费高效的AI抠图工具!",
32 | openGraph: {
33 | images: "https://rmbg.hellokaton.me/og.png",
34 | },
35 | };
36 |
37 | export default function RootLayout({
38 | children,
39 | }: Readonly<{
40 | children: React.ReactNode;
41 | }>) {
42 | return (
43 |
44 |
52 |
57 |
58 | {children}
59 |
60 |
61 |
96 |
97 |
98 | );
99 | }
100 |
--------------------------------------------------------------------------------
/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useState } from "react";
4 | import { toast } from "sonner";
5 | import HomepageImage1 from "@/components/images/homepage-image-1";
6 | import HomepageImage2 from "@/components/images/homepage-image-2";
7 | import { ImageUploader } from "@/components/ImageUploader";
8 | import { ProcessingLoader } from "@/components/ProcessingLoader";
9 | import { ImageCompareResult } from "@/components/ImageCompareResult";
10 | import { ProcessingError } from "@/components/ProcessingError";
11 | import { fileToDataUrl, processImageBackground } from "@/lib/backgroundRemoval";
12 |
13 | export default function Home() {
14 | const [selectedImage, setSelectedImage] = useState(null);
15 | const [originalImageDataUrl, setOriginalImageDataUrl] = useState<
16 | string | null
17 | >(null);
18 | const [processedImage, setProcessedImage] = useState(null);
19 | const [isLoading, setIsLoading] = useState(false);
20 |
21 | // 清理URL对象,防止内存泄漏
22 | useEffect(() => {
23 | return () => {
24 | if (processedImage && processedImage.startsWith("blob:")) {
25 | URL.revokeObjectURL(processedImage);
26 | }
27 | };
28 | }, [processedImage]);
29 |
30 | const handleFileChange = async (file: File | null) => {
31 | try {
32 | // 清理之前的状态
33 | setIsLoading(false);
34 | if (processedImage && processedImage.startsWith("blob:")) {
35 | URL.revokeObjectURL(processedImage);
36 | setProcessedImage(null);
37 | }
38 | setOriginalImageDataUrl(null);
39 |
40 | if (!file) {
41 | setSelectedImage(null);
42 | return;
43 | }
44 |
45 | // 将文件转换为 Data URL (不会过期)
46 | const dataUrl = await fileToDataUrl(file);
47 | setSelectedImage(file);
48 | setOriginalImageDataUrl(dataUrl);
49 | setIsLoading(true);
50 |
51 | try {
52 | // 处理图片
53 | const processedUrl = await processImageBackground(
54 | file,
55 | );
56 | setProcessedImage(processedUrl);
57 | } catch (e) {
58 | console.error("背景移除失败:", e);
59 | toast.error("背景移除失败,请检查图片或稍后再试。");
60 | } finally {
61 | setIsLoading(false);
62 | }
63 | } catch (err) {
64 | console.error("处理图片时出错:", err);
65 | toast.error("处理图片时出错,请重试。");
66 | setIsLoading(false);
67 | }
68 | };
69 |
70 | const resetSelection = () => {
71 | handleFileChange(null);
72 | };
73 |
74 | return (
75 |
76 |
77 |
78 | 智能移除图片背景
79 |
一键完成
80 |
81 |
82 | 上传一张图片,即可移除背景并下载。
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 | {!originalImageDataUrl && !isLoading ? (
94 |
95 | ) : (
96 | <>
97 | {/* 加载中 */}
98 | {isLoading &&
}
99 |
100 | {/* 处理结果 */}
101 | {!isLoading && processedImage && originalImageDataUrl && (
102 |
108 | )}
109 |
110 | {/* 处理失败 */}
111 | {!isLoading && !processedImage && originalImageDataUrl && (
112 |
116 | )}
117 | >
118 | )}
119 |
120 |
121 |
122 | );
123 | }
124 |
--------------------------------------------------------------------------------
/src/components/ImageCompareResult.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 | import { Download } from "lucide-react";
3 | import {
4 | ReactCompareSlider,
5 | ReactCompareSliderImage,
6 | } from "react-compare-slider";
7 |
8 | interface ImageCompareResultProps {
9 | originalImage: string;
10 | processedImage: string;
11 | fileName?: string;
12 | onReset: () => void;
13 | }
14 |
15 | export function ImageCompareResult({
16 | originalImage,
17 | processedImage,
18 | fileName,
19 | onReset,
20 | }: ImageCompareResultProps) {
21 | const handleDownload = () => {
22 | if (processedImage) {
23 | const link = document.createElement("a");
24 | link.href = processedImage;
25 | link.download = `bg-removed-${fileName || "image"}.png`;
26 | document.body.appendChild(link);
27 | link.click();
28 | document.body.removeChild(link);
29 | }
30 | };
31 |
32 | return (
33 |
34 |
35 |
47 | }
48 | itemTwo={
49 |
59 | }
60 | className="rounded-lg"
61 | style={{
62 | height: "100%",
63 | width: "100%",
64 | }}
65 | position={50}
66 | />
67 |
68 |
69 |
72 |
79 |
80 |
81 | );
82 | }
83 |
--------------------------------------------------------------------------------
/src/components/ImageUploader.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 | import { SparklesIcon } from "lucide-react";
3 | import { useDropzone } from "react-dropzone";
4 |
5 | interface ImageUploaderProps {
6 | onImageSelected: (file: File | null) => void;
7 | }
8 |
9 | export function ImageUploader({ onImageSelected }: ImageUploaderProps) {
10 | const { getRootProps, getInputProps, isDragActive, open } = useDropzone({
11 | accept: {
12 | "image/*": [],
13 | },
14 | multiple: false,
15 | onDrop: (acceptedFiles) => {
16 | if (acceptedFiles && acceptedFiles[0]) {
17 | onImageSelected(acceptedFiles[0]);
18 | }
19 | },
20 | });
21 |
22 | return (
23 |
29 |
30 |
40 |
或拖放图片至此处
41 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/src/components/ProcessingError.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 |
3 | interface ProcessingErrorProps {
4 | imageUrl: string;
5 | onReset: () => void;
6 | }
7 |
8 | export function ProcessingError({ imageUrl, onReset }: ProcessingErrorProps) {
9 | return (
10 |
11 |
处理失败
12 |
13 |

18 |
19 |
20 |
23 |
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/src/components/ProcessingLoader.tsx:
--------------------------------------------------------------------------------
1 |
2 | export function ProcessingLoader() {
3 | return (
4 |
5 |
8 |
9 | 移除背景中...
10 |
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/icons/github.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentProps } from "react";
2 |
3 | export default function GithubIcon(props: ComponentProps<"svg">) {
4 | return (
5 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/icons/sparkles.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentProps } from "react";
2 |
3 | export default function SparklesIcon(props: ComponentProps<"svg">) {
4 | return (
5 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/src/components/icons/x.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentProps } from "react";
2 |
3 | export default function XIcon(props: ComponentProps<"svg">) {
4 | return (
5 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/images/homepage-image-1.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentProps } from "react";
2 |
3 | export default function HomepageImage1(props: ComponentProps<"svg">) {
4 | return (
5 |
1293 | );
1294 | }
1295 |
--------------------------------------------------------------------------------
/src/components/images/homepage-image-2.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentProps } from "react";
2 |
3 | export default function HomepageImage2(props: ComponentProps<"svg">) {
4 | return (
5 |
275 | );
276 | }
277 |
--------------------------------------------------------------------------------
/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Slot } from "@radix-ui/react-slot";
3 | import { cva, type VariantProps } from "class-variance-authority";
4 |
5 | import { cn } from "@/lib/utils";
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
16 | outline:
17 | "border text-base border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
18 | "outline-active":
19 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
20 | secondary:
21 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
22 | ghost: "hover:bg-accent hover:text-accent-foreground",
23 | link: "text-primary underline-offset-4 hover:underline",
24 | },
25 | size: {
26 | default: "h-9 px-4 py-2",
27 | sm: "h-8 rounded-md px-3 text-xs",
28 | lg: "h-10 rounded-md px-8",
29 | icon: "h-9 w-9",
30 | },
31 | },
32 | defaultVariants: {
33 | variant: "default",
34 | size: "default",
35 | },
36 | },
37 | );
38 |
39 | export interface ButtonProps
40 | extends React.ButtonHTMLAttributes,
41 | VariantProps {
42 | asChild?: boolean;
43 | }
44 |
45 | const Button = React.forwardRef(
46 | ({ className, variant, size, asChild = false, ...props }, ref) => {
47 | const Comp = asChild ? Slot : "button";
48 | return (
49 |
54 | );
55 | },
56 | );
57 | Button.displayName = "Button";
58 |
59 | export { Button, buttonVariants };
60 |
--------------------------------------------------------------------------------
/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Input = React.forwardRef>(
6 | ({ className, type, ...props }, ref) => {
7 | return (
8 |
17 | )
18 | }
19 | )
20 | Input.displayName = "Input"
21 |
22 | export { Input }
23 |
--------------------------------------------------------------------------------
/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as LabelPrimitive from "@radix-ui/react-label";
5 | import { cva, type VariantProps } from "class-variance-authority";
6 |
7 | import { cn } from "@/lib/utils";
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
11 | );
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ));
24 | Label.displayName = LabelPrimitive.Root.displayName;
25 |
26 | export { Label };
27 |
--------------------------------------------------------------------------------
/src/components/ui/logo.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 |
3 | export default function Logo(props: React.ComponentProps<"div">) {
4 | return (
5 |
6 |
13 | remove.bg
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SelectPrimitive from "@radix-ui/react-select"
5 | import { Check, ChevronDown, ChevronUp } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Select = SelectPrimitive.Root
10 |
11 | const SelectGroup = SelectPrimitive.Group
12 |
13 | const SelectValue = SelectPrimitive.Value
14 |
15 | const SelectTrigger = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, children, ...props }, ref) => (
19 | span]:line-clamp-1",
23 | className
24 | )}
25 | {...props}
26 | >
27 | {children}
28 |
29 |
30 |
31 |
32 | ))
33 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
34 |
35 | const SelectScrollUpButton = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 |
48 |
49 | ))
50 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
51 |
52 | const SelectScrollDownButton = React.forwardRef<
53 | React.ElementRef,
54 | React.ComponentPropsWithoutRef
55 | >(({ className, ...props }, ref) => (
56 |
64 |
65 |
66 | ))
67 | SelectScrollDownButton.displayName =
68 | SelectPrimitive.ScrollDownButton.displayName
69 |
70 | const SelectContent = React.forwardRef<
71 | React.ElementRef,
72 | React.ComponentPropsWithoutRef
73 | >(({ className, children, position = "popper", ...props }, ref) => (
74 |
75 |
86 |
87 |
94 | {children}
95 |
96 |
97 |
98 |
99 | ))
100 | SelectContent.displayName = SelectPrimitive.Content.displayName
101 |
102 | const SelectLabel = React.forwardRef<
103 | React.ElementRef,
104 | React.ComponentPropsWithoutRef
105 | >(({ className, ...props }, ref) => (
106 |
111 | ))
112 | SelectLabel.displayName = SelectPrimitive.Label.displayName
113 |
114 | const SelectItem = React.forwardRef<
115 | React.ElementRef,
116 | React.ComponentPropsWithoutRef
117 | >(({ className, children, ...props }, ref) => (
118 |
126 |
127 |
128 |
129 |
130 |
131 | {children}
132 |
133 | ))
134 | SelectItem.displayName = SelectPrimitive.Item.displayName
135 |
136 | const SelectSeparator = React.forwardRef<
137 | React.ElementRef,
138 | React.ComponentPropsWithoutRef
139 | >(({ className, ...props }, ref) => (
140 |
145 | ))
146 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName
147 |
148 | export {
149 | Select,
150 | SelectGroup,
151 | SelectValue,
152 | SelectTrigger,
153 | SelectContent,
154 | SelectLabel,
155 | SelectItem,
156 | SelectSeparator,
157 | SelectScrollUpButton,
158 | SelectScrollDownButton,
159 | }
160 |
--------------------------------------------------------------------------------
/src/components/ui/spinner.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from "react";
2 |
3 | export default function Spinner({
4 | loading = true,
5 | children,
6 | className = "",
7 | }: {
8 | loading?: boolean;
9 | children?: ReactNode;
10 | className?: string;
11 | }) {
12 | if (!loading) return children;
13 |
14 | const spinner = (
15 | <>
16 |
28 |
29 | {Array.from(Array(8).keys()).map((i) => (
30 |
38 | ))}
39 |
40 | >
41 | );
42 |
43 | if (!children) return spinner;
44 |
45 | return (
46 |
47 | {children}
48 |
49 |
50 | {spinner}
51 |
52 |
53 | );
54 | }
55 |
--------------------------------------------------------------------------------
/src/components/ui/toast.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ToastPrimitives from "@radix-ui/react-toast"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 | import { X } from "lucide-react"
7 |
8 | import { cn } from "@/lib/utils"
9 |
10 | const ToastProvider = ToastPrimitives.Provider
11 |
12 | const ToastViewport = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, ...props }, ref) => (
16 |
24 | ))
25 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName
26 |
27 | const toastVariants = cva(
28 | "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
29 | {
30 | variants: {
31 | variant: {
32 | default: "border bg-background text-foreground",
33 | destructive:
34 | "destructive group border-destructive bg-destructive text-destructive-foreground",
35 | },
36 | },
37 | defaultVariants: {
38 | variant: "default",
39 | },
40 | }
41 | )
42 |
43 | const Toast = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef &
46 | VariantProps
47 | >(({ className, variant, ...props }, ref) => {
48 | return (
49 |
54 | )
55 | })
56 | Toast.displayName = ToastPrimitives.Root.displayName
57 |
58 | const ToastAction = React.forwardRef<
59 | React.ElementRef,
60 | React.ComponentPropsWithoutRef
61 | >(({ className, ...props }, ref) => (
62 |
70 | ))
71 | ToastAction.displayName = ToastPrimitives.Action.displayName
72 |
73 | const ToastClose = React.forwardRef<
74 | React.ElementRef,
75 | React.ComponentPropsWithoutRef
76 | >(({ className, ...props }, ref) => (
77 |
86 |
87 |
88 | ))
89 | ToastClose.displayName = ToastPrimitives.Close.displayName
90 |
91 | const ToastTitle = React.forwardRef<
92 | React.ElementRef,
93 | React.ComponentPropsWithoutRef
94 | >(({ className, ...props }, ref) => (
95 |
100 | ))
101 | ToastTitle.displayName = ToastPrimitives.Title.displayName
102 |
103 | const ToastDescription = React.forwardRef<
104 | React.ElementRef,
105 | React.ComponentPropsWithoutRef
106 | >(({ className, ...props }, ref) => (
107 |
112 | ))
113 | ToastDescription.displayName = ToastPrimitives.Description.displayName
114 |
115 | type ToastProps = React.ComponentPropsWithoutRef
116 |
117 | type ToastActionElement = React.ReactElement
118 |
119 | export {
120 | type ToastProps,
121 | type ToastActionElement,
122 | ToastProvider,
123 | ToastViewport,
124 | Toast,
125 | ToastTitle,
126 | ToastDescription,
127 | ToastClose,
128 | ToastAction,
129 | }
130 |
--------------------------------------------------------------------------------
/src/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useToast } from "@/hooks/use-toast"
4 | import {
5 | Toast,
6 | ToastClose,
7 | ToastDescription,
8 | ToastProvider,
9 | ToastTitle,
10 | ToastViewport,
11 | } from "@/components/ui/toast"
12 |
13 | export function Toaster() {
14 | const { toasts } = useToast()
15 |
16 | return (
17 |
18 | {toasts.map(function ({ id, title, description, action, ...props }) {
19 | return (
20 |
21 |
22 | {title && {title}}
23 | {description && (
24 | {description}
25 | )}
26 |
27 | {action}
28 |
29 |
30 | )
31 | })}
32 |
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/src/fonts/DingTalk_JinBuTi.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hellokaton/remove-bg/7a26368b03f76680fa43c0fd426dbbb795463ed3/src/fonts/DingTalk_JinBuTi.ttf
--------------------------------------------------------------------------------
/src/fonts/DingTalk_Sans.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hellokaton/remove-bg/7a26368b03f76680fa43c0fd426dbbb795463ed3/src/fonts/DingTalk_Sans.ttf
--------------------------------------------------------------------------------
/src/hooks/use-toast.ts:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | // Inspired by react-hot-toast library
4 | import * as React from "react";
5 |
6 | import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
7 |
8 | const TOAST_LIMIT = 1;
9 | const TOAST_REMOVE_DELAY = 1000000;
10 |
11 | type ToasterToast = ToastProps & {
12 | id: string;
13 | title?: React.ReactNode;
14 | description?: React.ReactNode;
15 | action?: ToastActionElement;
16 | };
17 |
18 | type ActionTypes = {
19 | ADD_TOAST: "ADD_TOAST";
20 | UPDATE_TOAST: "UPDATE_TOAST";
21 | DISMISS_TOAST: "DISMISS_TOAST";
22 | REMOVE_TOAST: "REMOVE_TOAST";
23 | };
24 |
25 | let count = 0;
26 |
27 | function genId() {
28 | count = (count + 1) % Number.MAX_SAFE_INTEGER;
29 | return count.toString();
30 | }
31 |
32 | type Action =
33 | | {
34 | type: ActionTypes["ADD_TOAST"];
35 | toast: ToasterToast;
36 | }
37 | | {
38 | type: ActionTypes["UPDATE_TOAST"];
39 | toast: Partial;
40 | }
41 | | {
42 | type: ActionTypes["DISMISS_TOAST"];
43 | toastId?: ToasterToast["id"];
44 | }
45 | | {
46 | type: ActionTypes["REMOVE_TOAST"];
47 | toastId?: ToasterToast["id"];
48 | };
49 |
50 | interface State {
51 | toasts: ToasterToast[];
52 | }
53 |
54 | const toastTimeouts = new Map>();
55 |
56 | const addToRemoveQueue = (toastId: string) => {
57 | if (toastTimeouts.has(toastId)) {
58 | return;
59 | }
60 |
61 | const timeout = setTimeout(() => {
62 | toastTimeouts.delete(toastId);
63 | dispatch({
64 | type: "REMOVE_TOAST",
65 | toastId: toastId,
66 | });
67 | }, TOAST_REMOVE_DELAY);
68 |
69 | toastTimeouts.set(toastId, timeout);
70 | };
71 |
72 | export const reducer = (state: State, action: Action): State => {
73 | switch (action.type) {
74 | case "ADD_TOAST":
75 | return {
76 | ...state,
77 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
78 | };
79 |
80 | case "UPDATE_TOAST":
81 | return {
82 | ...state,
83 | toasts: state.toasts.map((t) =>
84 | t.id === action.toast.id ? { ...t, ...action.toast } : t,
85 | ),
86 | };
87 |
88 | case "DISMISS_TOAST": {
89 | const { toastId } = action;
90 |
91 | // ! Side effects ! - This could be extracted into a dismissToast() action,
92 | // but I'll keep it here for simplicity
93 | if (toastId) {
94 | addToRemoveQueue(toastId);
95 | } else {
96 | state.toasts.forEach((toast) => {
97 | addToRemoveQueue(toast.id);
98 | });
99 | }
100 |
101 | return {
102 | ...state,
103 | toasts: state.toasts.map((t) =>
104 | t.id === toastId || toastId === undefined
105 | ? {
106 | ...t,
107 | open: false,
108 | }
109 | : t,
110 | ),
111 | };
112 | }
113 | case "REMOVE_TOAST":
114 | if (action.toastId === undefined) {
115 | return {
116 | ...state,
117 | toasts: [],
118 | };
119 | }
120 | return {
121 | ...state,
122 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
123 | };
124 | }
125 | };
126 |
127 | const listeners: Array<(state: State) => void> = [];
128 |
129 | let memoryState: State = { toasts: [] };
130 |
131 | function dispatch(action: Action) {
132 | memoryState = reducer(memoryState, action);
133 | listeners.forEach((listener) => {
134 | listener(memoryState);
135 | });
136 | }
137 |
138 | type Toast = Omit;
139 |
140 | function toast({ ...props }: Toast) {
141 | const id = genId();
142 |
143 | const update = (props: ToasterToast) =>
144 | dispatch({
145 | type: "UPDATE_TOAST",
146 | toast: { ...props, id },
147 | });
148 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
149 |
150 | dispatch({
151 | type: "ADD_TOAST",
152 | toast: {
153 | ...props,
154 | id,
155 | open: true,
156 | onOpenChange: (open) => {
157 | if (!open) dismiss();
158 | },
159 | },
160 | });
161 |
162 | return {
163 | id: id,
164 | dismiss,
165 | update,
166 | };
167 | }
168 |
169 | function useToast() {
170 | const [state, setState] = React.useState(memoryState);
171 |
172 | React.useEffect(() => {
173 | listeners.push(setState);
174 | return () => {
175 | const index = listeners.indexOf(setState);
176 | if (index > -1) {
177 | listeners.splice(index, 1);
178 | }
179 | };
180 | }, [state]);
181 |
182 | return {
183 | ...state,
184 | toast,
185 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
186 | };
187 | }
188 |
189 | export { useToast, toast };
190 |
--------------------------------------------------------------------------------
/src/lib/backgroundRemoval.ts:
--------------------------------------------------------------------------------
1 | import { removeBackground } from "@imgly/background-removal";
2 |
3 | // 将文件转换为 Data URL
4 | export const fileToDataUrl = (file: File): Promise => {
5 | return new Promise((resolve, reject) => {
6 | const reader = new FileReader();
7 | reader.onload = () => {
8 | if (typeof reader.result === "string") {
9 | resolve(reader.result);
10 | } else {
11 | reject(new Error("Failed to convert file to data URL"));
12 | }
13 | };
14 | reader.onerror = () => reject(reader.error);
15 | reader.readAsDataURL(file);
16 | });
17 | };
18 |
19 | // 处理背景移除并返回处理后的URL
20 | export const processImageBackground = async (file: File): Promise => {
21 | // 配置背景移除选项
22 | const config = {
23 | debug: true,
24 | progress: (key: string, current: number, total: number) => {
25 | // 显示进度
26 | const percentage = Math.round((current / total) * 100);
27 | console.log(`处理进度: ${key} ${percentage}%`);
28 | },
29 | };
30 |
31 | // 调用背景移除库
32 | const processedBlob = await removeBackground(file, config);
33 |
34 | // 创建结果URL
35 | return URL.createObjectURL(processedBlob);
36 | };
37 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 | import tailwindcssAnimate from "tailwindcss-animate";
3 |
4 | export default {
5 | darkMode: ["class"],
6 | content: [
7 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
8 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
9 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
10 | ],
11 | theme: {
12 | extend: {
13 | fontFamily: {
14 | dingtalk: ["var(--font-dingtalk)"],
15 | },
16 | borderRadius: {
17 | lg: "var(--radius)",
18 | md: "calc(var(--radius) - 2px)",
19 | sm: "calc(var(--radius) - 4px)",
20 | },
21 | colors: {
22 | gray: {
23 | "100": "#F4F1EC",
24 | "200": "#FAF8F5",
25 | "250": "#E1E1E1",
26 | "300": "#B7B7B7",
27 | "500": "#9A9A9A",
28 | "900": "#3F3C3C",
29 | },
30 | background: "hsl(var(--background))",
31 | foreground: "hsl(var(--foreground))",
32 | card: {
33 | DEFAULT: "hsl(var(--card))",
34 | foreground: "hsl(var(--card-foreground))",
35 | },
36 | popover: {
37 | DEFAULT: "hsl(var(--popover))",
38 | foreground: "hsl(var(--popover-foreground))",
39 | },
40 | primary: {
41 | DEFAULT: "hsl(var(--primary))",
42 | foreground: "hsl(var(--primary-foreground))",
43 | },
44 | secondary: {
45 | DEFAULT: "hsl(var(--secondary))",
46 | foreground: "hsl(var(--secondary-foreground))",
47 | },
48 | muted: {
49 | DEFAULT: "hsl(var(--muted))",
50 | foreground: "hsl(var(--muted-foreground))",
51 | },
52 | accent: {
53 | DEFAULT: "hsl(var(--accent))",
54 | foreground: "hsl(var(--accent-foreground))",
55 | },
56 | destructive: {
57 | DEFAULT: "hsl(var(--destructive))",
58 | foreground: "hsl(var(--destructive-foreground))",
59 | },
60 | border: "hsl(var(--border))",
61 | input: "hsl(var(--input))",
62 | ring: "hsl(var(--ring))",
63 | chart: {
64 | "1": "hsl(var(--chart-1))",
65 | "2": "hsl(var(--chart-2))",
66 | "3": "hsl(var(--chart-3))",
67 | "4": "hsl(var(--chart-4))",
68 | "5": "hsl(var(--chart-5))",
69 | },
70 | },
71 | },
72 | },
73 | plugins: [tailwindcssAnimate, require("@tailwindcss/typography")],
74 | } satisfies Config;
75 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2017",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./src/*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------