Skip to content

Commit

Permalink
Merge pull request #67 from besscroft/dev
Browse files Browse the repository at this point in the history
v0.8.4
  • Loading branch information
besscroft authored Jun 15, 2024
2 parents 77d6af9 + 234af6f commit d8ee7a6
Show file tree
Hide file tree
Showing 5 changed files with 288 additions and 48 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ PicImpact 是一个摄影师专用的摄影作品展示网站,基于 Next.js
- 响应式设计,在 PC 和移动端都有不错的体验,支持暗黑模式。
- 图片存储兼容 S3 API、Cloudflare R2、AList API。
- 图片支持绑定标签,并且可通过标签进行交互,筛选标签下所有图片。
- 上传图片时会生成 0.3 倍率的压缩图片,以提供加载优化。
- 支持批量自动化上传,上传图片时会生成 0.3 倍率的压缩图片,以提供加载优化。
- 图片版权信息展示和维护功能,支持外链跳转。
- 后台有图片数据统计、图片上传、图片维护、相册管理、系统设置和存储配置功能。
- 基于 SSR 的混合渲染,采用状态机制,提供良好的使用体验。
Expand Down
43 changes: 32 additions & 11 deletions components/MasonryItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import {
} from '~/components/ui/Dialog'
import { useButtonStore } from '~/app/providers/button-store-Providers'
import { CopyrightType, DataProps, ImageType } from '~/types'
import { Image, Tabs, Tab, Card, CardHeader, CardBody, CardFooter, Table, TableHeader, TableColumn, TableBody, TableRow, TableCell, Button, Chip, Link, Avatar } from '@nextui-org/react'
import { Aperture, Camera, Image as ImageIcon, Languages, CalendarDays, X, SunMedium, MoonStar, Copyright } from 'lucide-react'
import { Image, Tabs, Tab, Card, CardHeader, CardBody, CardFooter, Button, Chip, Link, Avatar } from '@nextui-org/react'
import { Aperture, Camera, Image as ImageIcon, Languages, CalendarDays, X, SunMedium, MoonStar, Copyright, Crosshair, Timer, CircleGauge } from 'lucide-react'
import * as React from 'react'
import { useTheme } from 'next-themes'
import { useRouter } from 'next-nprogress-bar'
Expand Down Expand Up @@ -89,15 +89,36 @@ export default function MasonryItem() {
}
>
<div className="flex flex-col space-y-2">
<Card className="py-4" shadow="sm">
<CardHeader className="pb-0 pt-2 px-4 flex-col items-start">
<div className="flex items-center space-x-1">
<Camera size={20}/>
<p className="text-tiny uppercase font-bold select-none">相机</p>
</div>
<h4 className="font-bold text-large">{MasonryViewData?.exif?.model || 'N&A'}</h4>
</CardHeader>
</Card>
{MasonryViewData?.exif?.model && MasonryViewData?.exif?.f_number
&& MasonryViewData?.exif?.exposure_time && MasonryViewData?.exif?.focal_length
&& MasonryViewData?.exif?.iso_speed_rating &&
<Card className="py-2" shadow="sm">
<CardHeader className="pb-0 pt-2 px-2 flex-col items-start space-y-2">
<div className="flex items-center justify-center space-x-1 w-full">
<Camera size={20}/>
<p className="text-tiny uppercase font-bold select-none items-center justify-center">{MasonryViewData?.exif?.model}</p>
</div>
<div className="grid grid-cols-4 gap-4 items-center justify-center w-full">
<div className="flex flex-col items-center justify-center w-full">
<Aperture size={20}/>
<p className="text-tiny uppercase font-bold select-none">{MasonryViewData?.exif?.f_number}</p>
</div>
<div className="flex flex-col items-center justify-center w-full">
<Timer size={20}/>
<p className="text-tiny uppercase font-bold select-none">{MasonryViewData?.exif?.exposure_time}</p>
</div>
<div className="flex flex-col items-center justify-center w-full">
<Crosshair size={20}/>
<p className="text-tiny uppercase font-bold select-none">{MasonryViewData?.exif?.focal_length}</p>
</div>
<div className="flex flex-col items-center justify-center w-full">
<CircleGauge size={20}/>
<p className="text-tiny uppercase font-bold select-none">ISO {MasonryViewData?.exif?.iso_speed_rating}</p>
</div>
</div>
</CardHeader>
</Card>
}
<Card className="py-4" shadow="sm">
<CardHeader className="pb-0 pt-2 px-4 flex-col items-start">
<div className="flex items-center space-x-1">
Expand Down
227 changes: 191 additions & 36 deletions components/admin/upload/FileUpload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import { Button, Select, SelectItem, Input, Divider, Card, CardBody, CardHeader,
import ExifReader from 'exifreader'
import Compressor from 'compressorjs'
import { TagInput } from '@douyinfe/semi-ui'
import { Select as ShadcnSelect, SelectContent, SelectItem as ShadcnSelectItem, SelectTrigger, SelectValue } from '~/components/ui/Select'
import { useButtonStore } from '~/app/providers/button-store-Providers'
import { CircleHelp } from 'lucide-react'
import FileUploadHelpSheet from '~/components/admin/upload/FileUploadHelpSheet'

export default function FileUpload() {
const [alistStorage, setAlistStorage] = useState([])
Expand All @@ -29,6 +33,10 @@ export default function FileUpload() {
const [lon, setLon] = useState('')
const [detail, setDetail] = useState('')
const [imageLabels, setImageLabels] = useState([] as string[])
const [mode, setMode] = useState('singleton')
const { setUploadHelp } = useButtonStore(
(state) => state,
)

const { data, isLoading } = useSWR('/api/v1/get-tags', fetcher)

Expand Down Expand Up @@ -216,7 +224,104 @@ export default function FileUpload() {
}).then((res) => res.json())
}

async function uploadPreviewImage(option: any, type: string) {
async function autoSubmit(file: any, url: string, previewUrl: string) {
try {
if (mode === 'multiple') {
const tagArray = Array.from(tag)
if (tagArray.length === 0 || tagArray[0] === '') {
toast.warning('请先选择相册!')
return
}
const tags = await ExifReader.load(file)
const exifObj = {
make: '',
model: '',
bits: '',
data_time: '',
exposure_time: '',
f_number: '',
exposure_program: '',
iso_speed_rating: '',
focal_length: '',
lens_specification: '',
lens_model: '',
exposure_mode: '',
cfa_pattern: '',
color_space: '',
white_balance: '',
} as ExifType
exifObj.make = tags?.Make?.description
exifObj.model = tags?.Model?.description
exifObj.bits = tags?.['Bits Per Sample']?.description
exifObj.data_time = tags?.DateTime?.description
exifObj.exposure_time = tags?.ExposureTime?.description
exifObj.f_number = tags?.FNumber?.description
exifObj.exposure_program = tags?.ExposureProgram?.description
exifObj.iso_speed_rating = tags?.ISOSpeedRatings?.description
exifObj.focal_length = tags?.FocalLength?.description
exifObj.lens_specification = tags?.LensSpecification?.description
exifObj.lens_model = tags?.LensModel?.description
exifObj.exposure_mode = tags?.ExposureMode?.description
exifObj.cfa_pattern = tags?.CFAPattern?.description
exifObj.color_space = tags?.ColorSpace?.description
exifObj.white_balance = tags?.WhiteBalance?.description
if (tags?.GPSLatitude?.description) {
setLat(tags?.GPSLatitude?.description)
} else {
setLat('')
}
if (tags?.GPSLongitude?.description) {
setLon(tags?.GPSLongitude?.description)
} else {
setLon('')
}
try {
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = async () => {
const data = {
tag: tagArray[0],
url: url,
title: '',
preview_url: previewUrl,
exif: exifObj,
labels: [],
detail: '',
width: Number(img.width),
height: Number(img.height),
lat: '',
lon: '',
} as ImageType
const res = await fetch('/api/v1/image-add', {
headers: {
'Content-Type': 'application/json',
},
method: 'post',
// @ts-ignore
body: JSON.stringify(data),
}).then(res => res.json())
console.log(res)
if (res?.code === 200) {
toast.success('保存成功!')
} else {
toast.error('保存失败!')
}
};
// @ts-ignore
img.src = e.target.result;
};
reader.readAsDataURL(file);
} catch (e) {
console.log(e)
}
}
} catch (e) {
console.log(e)
}
}

async function uploadPreviewImage(option: any, type: string, url: string) {
new Compressor(option.file, {
quality: 0.3,
checkOrientation: false,
Expand All @@ -228,6 +333,7 @@ export default function FileUpload() {
toast.success('预览图片上传成功!')
option.onSuccess(option.file)
setPreviewUrl(res?.data)
await autoSubmit(option.file, url, res?.data)
} else {
toast.error('预览图片上传失败!')
}
Expand All @@ -240,6 +346,7 @@ export default function FileUpload() {
toast.success('预览图片上传成功!')
option.onSuccess(option.file)
setPreviewUrl(res?.data)
await autoSubmit(option.file, url, res?.data)
} else {
toast.error('预览图片上传失败!')
}
Expand All @@ -259,44 +366,52 @@ export default function FileUpload() {
toast.success('图片上传成功,尝试生成预览图片并上传!')
try {
if (tagArray[0] === '/') {
await uploadPreviewImage(option, '/preview')
await uploadPreviewImage(option, '/preview', res?.data)
} else {
await uploadPreviewImage(option, tagArray[0] + '/preview')
await uploadPreviewImage(option, tagArray[0] + '/preview', res?.data)
}
} catch (e) {
console.log(e)
option.onSuccess(option.file)
}
await loadExif(option.file)
setUrl(res?.data)
if (mode === 'singleton') {
await loadExif(option.file)
setUrl(res?.data)
}
} else {
option.onError(option.file)
toast.error('图片上传失败!')
}
}

async function onRemoveFile() {
setStorageSelect(false)
setStorage(new Set([] as string[]))
setTag(new Set([] as string[]))
setAlistMountPath(new Set([] as string[]))
setExif({} as ExifType)
setUrl('')
setTitle('')
setDetail('')
setWidth(0)
setHeight(0)
setLat('')
setLon('')
setPreviewUrl('')
setImageLabels([])
}

const props: UploadProps = {
listType: "picture",
name: 'file',
multiple: false,
maxCount: 1,
multiple: mode === 'multiple',
maxCount: mode === 'singleton' ? 1 : 5,
customRequest: (file) => onRequestUpload(file),
beforeUpload: async (file) => await onBeforeUpload(file),
onRemove: (file) => {
setStorageSelect(false)
setStorage(new Set([] as string[]))
setTag(new Set([] as string[]))
setAlistMountPath(new Set([] as string[]))
setExif({} as ExifType)
setUrl('')
setTitle('')
setDetail('')
setWidth(0)
setHeight(0)
setLat('')
setLon('')
setPreviewUrl('')
setImageLabels([])
onRemove: async (file) => {
if (mode === 'singleton') {
await onRemoveFile()
}
}
}

Expand All @@ -305,20 +420,59 @@ export default function FileUpload() {
<Card shadow="sm">
<CardHeader className="justify-between">
<div className="flex gap-5">
<div className="flex flex-col gap-1 items-start justify-center">
<h4 className="text-small font-semibold leading-none text-default-600 select-none">上传</h4>
</div>
<ShadcnSelect
value={mode}
onValueChange={(value: string) => {
setMode(value)
}}
>
<SelectTrigger>
<SelectValue placeholder="请选择上传方式,默认单文件上传"/>
</SelectTrigger>
<SelectContent>
<ShadcnSelectItem key="singleton" value="singleton">
单文件上传
</ShadcnSelectItem>
<ShadcnSelectItem key="multiple" value="multiple">
多文件上传
</ShadcnSelectItem>
</SelectContent>
</ShadcnSelect>
</div>
<div className="flex items-center space-x-1">
<Button
isIconOnly
size="sm"
color="warning"
aria-label="帮助"
onClick={() => setUploadHelp(true)}
>
<CircleHelp/>
</Button>
{mode === 'singleton'
? <Button
color="primary"
radius="full"
size="sm"
isLoading={loading}
onClick={() => submit()}
aria-label="提交"
>
提交
</Button>
:
<Button
color="primary"
radius="full"
size="sm"
isLoading={loading}
onClick={() => onRemoveFile()}
aria-label="重置"
>
重置
</Button>
}
</div>
<Button
color="primary"
radius="full"
size="sm"
isLoading={loading}
onClick={() => submit()}
aria-label="提交"
>
提交
</Button>
</CardHeader>
</Card>
<Card shadow="sm" className="flex-1">
Expand Down Expand Up @@ -416,7 +570,7 @@ export default function FileUpload() {
</CardBody>
<CardFooter>
{
url && url !== '' &&
url && url !== '' && mode === 'singleton' &&
<div className="w-full mt-2 space-y-2">
<Input
size="sm"
Expand Down Expand Up @@ -509,6 +663,7 @@ export default function FileUpload() {
}
</CardFooter>
</Card>
<FileUploadHelpSheet />
</div>
)
}
Loading

0 comments on commit d8ee7a6

Please sign in to comment.