This commit is contained in:
2026-07-16 17:54:03 +08:00
parent 9ba28f2f7c
commit 4806ca6bc0
9 changed files with 454 additions and 93 deletions
+2
View File
@@ -21,6 +21,8 @@ env/
.idea/
.claude/
uploads/*
nn/dataset/toy/*
nn/dataset/benchmark/*
nn/models/*.pth
+41
View File
@@ -2,7 +2,11 @@ package main
import (
"context"
"encoding/base64"
"fmt"
"os"
"path/filepath"
"time"
)
// App struct
@@ -36,3 +40,40 @@ func (a *App) Register() string {
println("1111")
return ""
}
func (a *App) UploadImage(data []byte, filename string) ImageResult {
uploadsDir := "./uploads"
err := os.MkdirAll(uploadsDir, 0755)
if err != nil {
return ImageResult{Code: 1, Message: "failed", Data: fmt.Sprintf("创建目录失败: %v", err)}
}
ext := filepath.Ext(filename)
newFilename := fmt.Sprintf("%d%s", time.Now().UnixMilli(), ext)
filePath := filepath.Join(uploadsDir, newFilename)
err = os.WriteFile(filePath, data, 0644)
if err != nil {
return ImageResult{Code: 1, Message: "failed", Data: fmt.Sprintf("保存文件失败: %v", err)}
}
return ImageResult{Code: 0, Message: "success", Data: newFilename}
}
func (a *App) GetImage(filename string) ImageResult {
filePath := filepath.Join("./uploads", filename)
fmt.Println("filePath", filePath)
data, err := os.ReadFile(filePath)
if err != nil {
return ImageResult{Code: 1, Message: "failed", Data: fmt.Sprintf("打开文件路径失败: %v", err)}
}
ext := filepath.Ext(filename)
mimeType := "image/jpeg"
if ext == ".png" {
mimeType = "image/png"
}
// app.go - GetImage
return ImageResult{Code: 0, Message: "success", Data: fmt.Sprintf("data:image/%s;base64,%s", mimeType, base64.StdEncoding.EncodeToString(data))}
// return ImageResult{Code: 1, Message: "failed", Data: data}
}
+133 -74
View File
@@ -1,4 +1,71 @@
import { useState, useEffect, useRef } from 'preact/hooks'
const Main = () => {
const fileInputRef = useRef<HTMLInputElement>(null)
const [fileName, setFileName] = useState<string>('')
const [fileSrc, setFileSrc] = useState<string>('')
const [step, setStep] = useState(0)
const handleUploadClick = () => {
fileInputRef.current?.click()
}
const handleFileChange = (e: Event) => {
const target = e.target as HTMLInputElement
const file = target.files?.[0]
if (file) {
const reader = new FileReader()
reader.onload = async function() {
const arrayBuffer = reader.result as ArrayBuffer
const uint8Array = new Uint8Array(arrayBuffer)
const result = await (window as any).go.main.App.UploadImage(
Array.from(uint8Array),
file.name
)
if (result.code === 0) {
setFileName(result.data)
const imgResult = await (window as any).go.main.App.GetImage(result.data)
if (imgResult.code === 0) {
setFileSrc(imgResult.data)
setStep(1)
}
}
}
reader.readAsArrayBuffer(file)
}
}
const resetUpload = () => {
if (fileInputRef.current) {
fileInputRef.current.value = ''
}
setFileSrc('')
}
const handleRemovePhoto = (e: Event) => {
e.stopPropagation()
setStep(0)
resetUpload()
}
const handleDetect = () => {
setStep(2)
setTimeout(() => {
setStep(3)
}, 1000)
}
const handleTryOther = () => {
setStep(0)
resetUpload()
}
return (
<main class="app-main">
<div class="main-container">
@@ -11,100 +78,92 @@ const Main = () => {
</p>
<i></i>
</div>
<div class="upload">
<div class={`upload${!(step == 0 || step == 1 || step == 2) ? ' hidden' : ''}`}>
<div
id="dropZone"
class="upload-zone"
style={{borderStyle: step != 2 ? 'dashed' : 'solid'}}
onClick={handleUploadClick}
>
{fileSrc.length > 0 && step == 1 ?
<div id="previewContent">
<img src={fileSrc} id="previewImage" />
<button onClick={handleRemovePhoto}>
</button>
</div> : null
}
{fileSrc.length == 0 && step == 0 ?
<>
<div id="uploadContent">
<svg class="w-16 h-16 mx-auto mb-4 text-secondary animate-pulse-slow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 16.5V9.75m0 0l3 3m-3-3l-3 3M6.75 19.5a4.5 4.5 0 01-1.41-8.775 5.25 5.25 0 0110.233-2.33 3 3 0 013.758 3.848A3.752 3.752 0 0118 19.5H6.75z" />
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 16.5V9.75m0 0l3 3m-3-3l-3 3M6.75 19.5a4.5 4.5 0 01-1.41-8.775 5.25 5.25 0 0110.233-2.33 3 3 0 013.758 3.848A3.752 3.752 0 0118 19.5H6.75z"
/>
</svg>
<p class="font-heading font-semibold text-xl text-text mb-2"></p>
<p class="text-sm text-text/50">JPGPNGJPEG2MB</p>
<p></p>
<p>JPGPNGJPEG2MB</p>
</div>
{/* <div id="previewContent" class="hidden">
<img id="previewImage" class="max-h-64 mx-auto rounded-2xl mb-4 object-cover" alt="Cat preview" />
<button id="removeBtn" class="text-red-500 hover:text-red-600 font-medium transition-colors duration-200 cursor-pointer">
Remove photo
</button>
<input
type="file"
ref={fileInputRef}
class="hidden"
accept="image/*"
onChange={handleFileChange}
/>
</> : null
}
{fileSrc.length > 0 && step == 2 ?
<>
<div class="zone-loading">
<div class="loading-spinner" />
<p>...</p>
<p>~</p>
</div>
<input type="file" id="fileInput" class="hidden" accept="image/*" /> */}
</> : null
}
</div>
{/* <button
id="detectBtn"
class="hidden w-full mt-4 bg-cta hover:bg-orange-600 text-white font-heading font-semibold text-lg py-4 rounded-2xl transition-colors duration-200 cursor-pointer flex items-center justify-center gap-3"
>
<svg class="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09zM18.259 8.715L18 9.75l-.259-1.035a3.375 3.375 0 00-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 002.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 002.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 00-2.456 2.456z" />
{fileSrc.length > 0 && step == 1 ?
<button id="detectBtn" onClick={handleDetect}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z"
/>
</svg>
Detect Breed
</button> */}
</button> : null
}
</div>
{/* <div id="loadingState" class="hidden max-w-2xl mx-auto mb-12">
<div class="bg-white rounded-3xl border border-border p-8 text-center">
<div class="w-16 h-16 mx-auto mb-4 border-4 border-primary border-t-transparent rounded-full animate-spin"></div>
<p class="font-heading font-semibold text-lg text-text mb-2">Analyzing your cat...</p>
<p class="text-text/60">Our AI is identifying the breed</p>
</div>
</div> */}
{/* <div id="resultsSection" class="hidden">
<div class="flex items-center justify-center gap-3 mb-6">
<svg class="w-6 h-6 text-primary" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
<div class={`result${step != 3 ? ' hidden' : ''}`}>
<div class="result-complete">
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" />
</svg>
<h2 class="font-heading font-bold text-2xl text-text">Detection Complete!</h2>
<h2></h2>
</div>
<div id="primaryResult" class="bg-white rounded-3xl border border-border p-6 md:p-8 mb-6">
<div class="flex flex-col md:flex-row gap-6 md:gap-8">
<div class="flex-shrink-0">
<img id="resultImage" class="w-full md:w-48 h-48 rounded-2xl object-cover" alt="Cat photo" />
</div>
<div class="flex-grow">
<div class="flex items-start justify-between mb-4">
<div class="result-main">
<img />
<div class="result-word">
<div>
<h3 id="breedName" class="font-heading font-bold text-2xl md:text-3xl text-text mb-1"></h3>
<p id="scientificName" class="text-text/60 font-medium"></p>
<h3></h3>
<div>
<div class="probability-bar">
<div />
</div>
<div class="bg-primary/10 px-4 py-2 rounded-xl">
<p class="font-heading font-bold text-xl text-primary"><span id="confidence">0</span>%</p>
<p class="text-xs text-text/60">confidence</p>
<span> 78%</span>
</div>
</div>
<div class="mb-4">
<div class="h-3 bg-border rounded-full overflow-hidden">
<div id="confidenceBar" class="confidence-bar h-full bg-gradient-to-r from-primary to-secondary rounded-full" style="width: 0%"></div>
<h4></h4>
<p> </p>
</div>
</div>
<p id="breedDescription" class="text-text/80 leading-relaxed mb-4"></p>
<div id="breedTraits" class="flex flex-wrap gap-2">
</div>
</div>
</div>
</div>
<div class="mb-6">
<h4 class="font-heading font-semibold text-lg text-text mb-4">Other possibilities</h4>
<div id="otherResults" class="grid grid-cols-1 md:grid-cols-2 gap-4">
</div>
</div>
<div class="text-center">
<button
id="tryAgainBtn"
class="bg-primary hover:bg-blue-600 text-white font-heading font-semibold px-8 py-3 rounded-2xl transition-colors duration-200 cursor-pointer"
>
Try Another Photo
<button onClick={handleTryOther}>
</button>
</div>
</div> */}
{/* <div id="featuresSection" class="grid grid-cols-1 md:grid-cols-3 gap-6 mt-16">
<div class="bg-white rounded-2xl border border-border p-6 text-center cursor-pointer hover:shadow-lg transition-shadow duration-200">
+225 -7
View File
@@ -19,6 +19,9 @@ body
:root
--primary: #3B82F6
--title: #1e293b
--text:#5f6874
--textsub: #878d95
#app
height: 100vh
@@ -96,21 +99,21 @@ body
display: flex
flex-direction: column
text-align: center
margin-bottom: 2rem
margin-bottom: 1.25rem
> h1
font-family: "ZcoolHappy"
font-weight: 700
font-size: 2.75rem
margin-bottom: 0.75rem
color: #323232
margin-bottom: 0.5rem
color: var(--title)
> span
color: var(--primary)
> p
text-align: center
color: #5D5D5D
color: var(--text)
font-size: 1.0625rem
margin-bottom: 1rem
max-width: 42rem
@@ -130,18 +133,233 @@ body
border-radius: 0.2rem
.upload
width: 36rem
width: 32rem
margin: 0 auto 3rem auto
.upload-zone
display: flex
flex-direction: column
align-items: center
justify-content: center
height: 17rem
background-color: white
border-radius: 1.5rem
border-width: 2px
border: 2px dashed #c6c6c6
padding: 3rem
border-style: dashed
border-color: #e2e8f0
border-style: dashed
text-align: center
cursor: pointer
#uploadContent
> svg
width: 4rem
height: 4rem
margin: 0 auto
margin-bottom: 1rem
color: var(--primary)
> p
&:nth-child(2)
font-weight: 600
color: var(--title)
font-size: 1.25rem
margin-bottom: 0.5rem
&:nth-child(3)
font-size: 0.875rem
color: var(--textsub)
#previewContent
margin-top: 0.5rem
> img
max-height: 10.5rem
margin-left: auto
margin-right: auto
margin-bottom: 0.25rem
border-radius: 1rem
object-fit: cover
> button
padding: 0.5rem 1.1rem
color: #FD6089
font-size: 0.925rem
font-weight: normal
text-decoration-line: underline
cursor: pointer
&:hover
color: #DC2626
.zone-loading
padding: 2rem
text-align: center
background-color: #FFFFFF
border-radius: 1.5rem
.loading-spinner
width: 4rem
height: 4rem
margin: 0 auto 1rem auto
border-width: 4px
border-color: var(--primary)
border-top-color: transparent
border-radius: 4rem
animation: spin 1s linear infinite
@keyframes spin
to
transform: rotate(360deg)
> p
&:nth-child(2)
font-weight: 600
font-size: 1.25rem
color: var(--title)
margin-bottom: 0.5rem
&:nth-child(3)
font-size: 1rem
color: var(--textsub)
>button#detectBtn
width: 12rem
height: 3.5rem
margin: 1rem auto 0 auto
background-color: var(--primary)
color: white
font-weight: 600
font-size: 1.125rem
border-radius: 0.75rem
transition: background-color 200ms, color 200ms
cursor: pointer
display: flex
align-items: center
justify-content: center
gap: 0.5rem
> svg
width: 1.5rem
height: 1.5rem
.result
width: 35rem
margin: 0 auto
.result-complete
display: flex
align-items: center
justify-content: center
gap: 0.75rem
margin-bottom: 1.5rem
> svg
width: 1.5rem
height: 1.5rem
color: var(--primary)
> h2
font-family: "ZcoolHappy"
font-weight: 700
font-size: 1.5rem
color: var(--title)
.result-main
display: flex
min-height: 10rem
padding: 1rem 1rem
margin-bottom: 1.5rem
background-color: #ffffff
border-radius: 1rem
border: 1px solid var(--color-border)
gap: 2rem
> img
width: 8rem
height: 8rem
background-color: #333
border-radius: 0.5rem
.result-word
display: flex
flex-direction: column
justify-content: flex-start
align-items: flex-start
width: calc(100% - 10rem)
> div
display: flex
justify-content: space-between
align-items: center
width: 100%
padding-bottom: 1rem
margin-bottom: 1rem
border-bottom: 1px solid #c6c6c6
> h3
display: flex
width: 40%
color: #333
font-size: 1.25rem
text-align: left
font-weight: bold
> div
display: flex
justify-content: flex-end
align-items: center
width: 60%
gap: 0.5rem
.probability-bar
display: flex
width: 50%
height: 0.5rem
background-color: #e2e8f0
border-radius: 0.125rem
> div
width: 80%
height: 100%
background-color: #5ea3f9
border-radius: 0.125rem
> span
display: flex
color: #5ea3f9
font-size: 1rem
font-weight: bold
> span
color: var(--title)
// font-size: 0.725rem
> h4
color: var(--title)
text-align: left
font-size: 1rem
font-weight: bold
margin-bottom: 0.5rem
> p
color: var(--text)
text-align: left
font-size: 0.875rem
> button
width: 9rem
height: 3rem
background-color: var(--primary)
color: white
font-weight: 600
font-size: 1.125rem
border-radius: 0.5rem
transition: background-color 200ms, color 200ms
cursor: pointer
.app-footer
display: flex
justify-content: center
+1 -1
View File
@@ -1,7 +1,7 @@
.app-dashboard
display: flex
height: 648px
padding: 7rem 1rem 4rem 1rem
padding: 7rem 1.5rem 4rem 1.5rem
.dashboard-sidebar
display: flex
+5
View File
@@ -1,8 +1,13 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
import {main} from '../models';
export function GetImage(arg1:string):Promise<main.ImageResult>;
export function Greet(arg1:string):Promise<string>;
export function Login():Promise<string>;
export function Register():Promise<string>;
export function UploadImage(arg1:Array<number>,arg2:string):Promise<main.ImageResult>;
+8
View File
@@ -2,6 +2,10 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export function GetImage(arg1) {
return window['go']['main']['App']['GetImage'](arg1);
}
export function Greet(arg1) {
return window['go']['main']['App']['Greet'](arg1);
}
@@ -13,3 +17,7 @@ export function Login() {
export function Register() {
return window['go']['main']['App']['Register']();
}
export function UploadImage(arg1, arg2) {
return window['go']['main']['App']['UploadImage'](arg1, arg2);
}
+21
View File
@@ -0,0 +1,21 @@
export namespace main {
export class ImageResult {
code: number;
message: string;
data?: string;
static createFrom(source: any = {}) {
return new ImageResult(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.code = source["code"];
this.message = source["message"];
this.data = source["data"];
}
}
}
+7
View File
@@ -0,0 +1,7 @@
package main
type ImageResult struct {
Code int `json:"code"`
Message string `json:"message"`
Data string `json:"data,omitempty"` // 成功时返回文件路径
}