commit a7491ebcec47d6fc9c6e1e360fe9f5edb64a21ed Author: atom Date: Fri Feb 3 15:32:07 2023 +0800 1.init commit. diff --git a/.env b/.env new file mode 100644 index 0000000..78f16b9 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +VITE_APP_API_URL = . \ No newline at end of file diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..bf7f098 --- /dev/null +++ b/.env.development @@ -0,0 +1 @@ +VITE_APP_API_URL = https://picx.s1s.workers.dev diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e6183de --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules +package-lock.json +.idea +yarn.lock +dist diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..bb7c7df --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# roim-picx + +### 预览地址 +[roim-picx](https://pigx.tuqu.me) +> 该系统仅作为预览使用,每天有使用限额,请勿大量上传图片。不要使用该图床的地址作为生产使用,因为要定时删除。 + +### 一款基于Cloudflare的Worker、R2、Pages实现的图床应用,具有以下特点: +* 10GB的免费存储空间 +* 每月300W次的不计流量的图片访问,每天10W的限制。 +* 每月100W次的图片上传次数 +* 不需要自己购买服务器,克隆代码后部署CloudFlare即可使用。 +* 独立部署不需要担心被第三方删除数据。 + +### 已实现功能 +* 图片批量上传 +* 图片列表查询 +* 图片删除 +* 目录创建 +* 按目录查询 +* 链接地址点击复制 + +### TODO +* 简单的身份认证功能,进入管理页面需要授权 +* 上传时支持选择目录。 +* 提供删除图片的访问链接 +* 管理页面支持分页加载图片 + +### 使用教程 + +### 图床截图 +![Upload](https://oss.tuqu.me/roim/blog/5.png) +![Preview](https://oss.tuqu.me/roim/blog/1.png) +![HTML](https://oss.tuqu.me/roim/blog/2.png) +![Markdown](https://oss.tuqu.me/roim/blog/3.png) +![Link](https://oss.tuqu.me/roim/blog/4.png) +![Manage](https://oss.tuqu.me/roim/blog/6.png) + +### 项目参考来源 +[1. cfworker-kv-image-hosting](https://github.com/realByg/cfworker-kv-image-hosting) + +[2. HikariSearch](https://github.com/mixmoe/HikariSearch) diff --git a/functions/rest/[[path]].ts b/functions/rest/[[path]].ts new file mode 100644 index 0000000..557e39a --- /dev/null +++ b/functions/rest/[[path]].ts @@ -0,0 +1,21 @@ +import { error } from 'itty-router-extras'; + +export interface Env { + BASE_URL: string + XK: KVNamespace + PICX: R2Bucket +} + +export const onRequest: PagesFunction = async (context : EventContext) => { + const { router } = await import('./router').then( + async (module) => (await import('./routes'), module) + ); + + try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const response: Response | undefined = await router.handle(context.request, context.env); + return response ?? error(404, 'not found'); + } catch (err) { + return error(500, (err as Error).message); + } +}; diff --git a/functions/rest/router.ts b/functions/rest/router.ts new file mode 100644 index 0000000..8084704 --- /dev/null +++ b/functions/rest/router.ts @@ -0,0 +1,3 @@ +import { ThrowableRouter } from 'itty-router-extras'; + +export const router = ThrowableRouter({ base: '/rest' }); diff --git a/functions/rest/routes/index.ts b/functions/rest/routes/index.ts new file mode 100644 index 0000000..117751f --- /dev/null +++ b/functions/rest/routes/index.ts @@ -0,0 +1,141 @@ +import { router } from '../router'; +import { Env } from '../[[path]]' +import { json } from 'itty-router-extras'; +import { Ok, Fail, Build, ImgItem, ImgList, ImgReq, Folder } from "../type"; +import { checkFileType, getFileName, parseRange } from '../utils' +import { R2ListOptions } from "@cloudflare/workers-types"; + +// list image +router.post('/list', async (req : Request, env : Env) => { + const data = await req.json() as ImgReq + if (!data.limit) { + data.limit = 10 + } + if (data.limit > 100) { + data.limit = 100 + } + if (!data.delimiter) { + data.delimiter = "/" + } + let include = undefined + if (data.delimiter != "/") { + include = data.delimiter + } + // console.log(include) + const options = { + limit: data.limit, + cursor: data.cursor, + delimiter: data.delimiter, + prefix: include + } + const list = await env.PICX.list(options) + // console.log(list) + const truncated = list.truncated ? list.truncated : false + const cursor = list.cursor + const objs = list.objects + const urls = objs.map(it => { + return { + url: `${env.BASE_URL}/rest/${it.key}`, + key: it.key, + size: it.size + } + }) + return json(Ok({ + list: urls, + next: truncated, + cursor: cursor, + prefixes: list.delimitedPrefixes + })) +}) + +// batch upload file +router.post('/upload', async (req: Request, env : Env) => { + const files = await req.formData() + const images = files.getAll("files") + const errs = [] + const urls = Array() + for (let item of images) { + const fileType = item.type + if (!checkFileType(fileType)) { + errs.push(`${fileType} not support.`) + continue + } + const time = new Date().getTime() + const filename = await getFileName(fileType, time) + const header = new Headers() + header.set("content-type", fileType) + header.set("content-length", `${item.size}`) + const object = await env.PICX.put(filename, item.stream(), { + httpMetadata: header, + }) as R2Object + if (object || object.key) { + urls.push({ + key: object.key, + size: object.size, + url: `${env.BASE_URL}/rest/${object.key}`, + filename: item.name + }) + } + } + return json(Build(urls, errs.toString())) +}) + +// 创建目录 +router.post("/folder", async (req: Request, env: Env) => { + try { + const data = await req.json() as Folder + const regx = /^[A-Za-z_]+$/ + if (!regx.test(data.name)) { + return json(Fail("Folder name error")) + } + await env.PICX.put(data.name + '/', "") + return json(Ok("Success")) + } catch (e) { + return json(Fail("Create folder fail")) + } +}) + +// delete image +router.delete("/", async (req : Request, env: Env) => { + const params = await req.json() + // console.log(params) + const keys = params.keys; + if (!keys || keys.length < 1) { + return json(Fail("not delete keys")) + } + const arr = keys.split(',') + try { + for (let it of arr) { + if(it && it.length) { + await env.PICX.delete(it) + } + } + } catch (e) { + console.log(`img delete error:${e.message}`,) + } + return json(Ok(keys)) +}) + +// image detail +router.get("/:id+", async (req : Request, env : Env) => { + let id = req.params.id + const range = parseRange(req.headers.get('range')) + const object = await env.PICX.get(id, { + range, + onlyIf: req.headers, + }) + if (object == null) { + return json(Fail("object not found")) + } + const headers = new Headers() + object.writeHttpMetadata(headers) + headers.set('etag', object.httpEtag) + if (range) { + headers.set("content-range", `bytes ${range.offset}-${range.end}/${object.size}`) + } + const status = object.body ? (range ? 206 : 200) : 304 + return new Response(object.body, { + headers, + status + }) +}) diff --git a/functions/rest/type.ts b/functions/rest/type.ts new file mode 100644 index 0000000..bd8d991 --- /dev/null +++ b/functions/rest/type.ts @@ -0,0 +1,57 @@ +export interface Result { + code: number + msg: string + data?: any +} + +export interface ImgItem { + key : string + url : string + size: number + filename ?: string +} + +export interface ImgList { + next: boolean + cursor ?: string + list : Array + prefixes ?: Array +} + +export interface ImgReq { + limit: number, + cursor ?: string + delimiter ?: string +} + +// 文件夹名称 +export interface Folder { + name: string +} + +export function Fail(msg : string) : Result { + return { + code: StatusCode.ERROR, + msg: msg, + data: null + } +} +export function Ok(data : any) : Result { + return { + code: StatusCode.OK, + msg: "ok", + data: data + } +} +export function Build(data : any, msg: string) : Result { + return { + code: StatusCode.OK, + msg: msg, + data: data + } +} +const StatusCode = { + OK: 200, + ERROR: 500 +} +export default StatusCode diff --git a/functions/rest/utils.ts b/functions/rest/utils.ts new file mode 100644 index 0000000..81b5f38 --- /dev/null +++ b/functions/rest/utils.ts @@ -0,0 +1,48 @@ +const supportFiles = [{type:'image/png',ext:'png'},{type:'image/jpeg',ext:'jpeg'},{type:'image/gif',ext:'gif'},{type:'image/webp',ext:'webp'},{type:'image/jpg',ext:'jpg'},{type:'image/x-icon',ext:'ico'},{type:'application/x-ico',ext:'ico'},{type:'image/vnd.microsoft.icon',ext:'ico'}] +const supportFile = 'image/png,image/jpeg,image/gif,image/webp,image/jpg,image/x-icon,application/x-ico,image/vnd.microsoft.icon' + +// 字符串编码 +export function randomString(value: number) { + let baseStr = 'Aa0Bb1Cc2Dd3Ee4Ff5Gg6Hh7Ii8Jj9KkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz'; + const chars = baseStr.split(''); + let maxPos = baseStr.length; + const uuid = []; + let q = value; + for(;q > 0;) { + let mod = q % maxPos; + q = (q - mod) / maxPos; + uuid.push(chars[mod]); + } + return uuid.join(''); +} + +// 解析range +export function parseRange(encoded: string | null): undefined | { offset: number, end: number, length: number } { + if (encoded === null) { + return + } + const parts = encoded.split("bytes=")[1]?.split("-") ?? [] + if (parts.length !== 2) { + throw new Error('Not supported to skip specifying the beginning/ending byte at this time') + } + return { + offset: Number(parts[0]), + end: Number(parts[1]), + length: Number(parts[1]) + 1 - Number(parts[0]), + } +} + +// 检查文件类是否支持 +export function checkFileType(val : string) : boolean { + return supportFile.indexOf(val) > -1 +} + +// 获取文件名 +export async function getFileName(val : string, time : number) : Promise { + const types = supportFiles.filter(it => it.type === val) + if (!types || types.length < 1) { + return val + } + const rand = Math.floor(Math.random() * 100000) + return randomString(time + rand).concat(`.${types[0].ext}`) +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..2fd0198 --- /dev/null +++ b/index.html @@ -0,0 +1,14 @@ + + + + + + + Loading + + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..6e79c3d --- /dev/null +++ b/package.json @@ -0,0 +1,34 @@ +{ + "name": "roim-picx", + "version": "1.0.1", + "scripts": { + "dev": "vite", + "build": "vite build", + "dev:worker": "wrangler pages dev ./public --kv=XK --r2=PICX", + "preview": "vite preview" + }, + "dependencies": { + "@fortawesome/fontawesome-svg-core": "^6.1.0", + "@fortawesome/free-brands-svg-icons": "^6.1.0", + "@fortawesome/free-regular-svg-icons": "^6.1.0", + "@fortawesome/free-solid-svg-icons": "^6.1.0", + "@fortawesome/vue-fontawesome": "^3.0.0-5", + "axios": "^0.25.0", + "copy-to-clipboard": "^3.3.1", + "element-plus": "^2.2.23", + "itty-router": "^3.0.11", + "itty-router-extras": "^0.4.2", + "vue": "^3.2.47", + "vue-router": "^4.1.6" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20230115.0", + "@vitejs/plugin-vue": "^3.2.0", + "autoprefixer": "^10.4.2", + "postcss": "^8.4.5", + "tailwindcss": "^3.2.4", + "typescript": "^4.4.4", + "vite": "^3.2.5", + "wrangler": "^2.9.0" + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..33ad091 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..f558eff Binary files /dev/null and b/public/favicon.ico differ diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..71f8e5c --- /dev/null +++ b/src/App.vue @@ -0,0 +1,66 @@ + + + diff --git a/src/app.css b/src/app.css new file mode 100644 index 0000000..57b53d3 --- /dev/null +++ b/src/app.css @@ -0,0 +1,11 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + @apply scroll-smooth text-base bg-gray-100 select-none overflow-x-hidden; +} + +.area-disabled { + @apply !opacity-60 active:!pointer-events-none !cursor-not-allowed; +} diff --git a/src/assets/pic.png b/src/assets/pic.png new file mode 100644 index 0000000..9f9e8ce Binary files /dev/null and b/src/assets/pic.png differ diff --git a/src/assets/picx-logo.png b/src/assets/picx-logo.png new file mode 100644 index 0000000..ed9205a Binary files /dev/null and b/src/assets/picx-logo.png differ diff --git a/src/components/ImageBox.vue b/src/components/ImageBox.vue new file mode 100644 index 0000000..ff84908 --- /dev/null +++ b/src/components/ImageBox.vue @@ -0,0 +1,108 @@ + + + diff --git a/src/components/ImageItem.vue b/src/components/ImageItem.vue new file mode 100644 index 0000000..439b4d3 --- /dev/null +++ b/src/components/ImageItem.vue @@ -0,0 +1,82 @@ + + + + + diff --git a/src/components/LoadingOverlay.vue b/src/components/LoadingOverlay.vue new file mode 100644 index 0000000..67226fb --- /dev/null +++ b/src/components/LoadingOverlay.vue @@ -0,0 +1,16 @@ + + + diff --git a/src/components/ResultList.vue b/src/components/ResultList.vue new file mode 100644 index 0000000..57fd9cd --- /dev/null +++ b/src/components/ResultList.vue @@ -0,0 +1,84 @@ + + + + + diff --git a/src/env.d.ts b/src/env.d.ts new file mode 100644 index 0000000..59f7ec6 --- /dev/null +++ b/src/env.d.ts @@ -0,0 +1,16 @@ +/// + +declare module '*.vue' { + import { DefineComponent } from 'vue' + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types + const component: DefineComponent<{}, {}, any> + export default component +} + +interface ImportMetaEnv { + readonly VITE_APP_API_URL: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..534fcb9 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,8 @@ +import { createApp } from 'vue' +import App from './App.vue' +import router from './plugins/router' +import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' +import './app.css' +import 'element-plus/dist/index.css' + +createApp(App).use(router).component('font-awesome-icon', FontAwesomeIcon).mount('#app') diff --git a/src/plugins/router.ts b/src/plugins/router.ts new file mode 100644 index 0000000..901ce16 --- /dev/null +++ b/src/plugins/router.ts @@ -0,0 +1,21 @@ +import { createRouter, createWebHistory } from 'vue-router' + +const router = createRouter({ + history: createWebHistory(), + routes: [ + { + path: '/', + component: () => import('../views/ManageImages.vue') + }, + { + path: '/up', + component: () => import('../views/UploadImages.vue') + }, + { + path: '/:path(.*)', + redirect: '/' + } + ] +}) + +export default router diff --git a/src/utils/format-bytes.ts b/src/utils/format-bytes.ts new file mode 100644 index 0000000..9cb391e --- /dev/null +++ b/src/utils/format-bytes.ts @@ -0,0 +1,11 @@ +const k = 1024 +const dm = 2 < 0 ? 0 : 2 +const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] + +const formatBytes = (bytes: number) => { + if (bytes === 0) return '0 B' + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i] +} + +export default formatBytes diff --git a/src/utils/request.ts b/src/utils/request.ts new file mode 100644 index 0000000..4c80776 --- /dev/null +++ b/src/utils/request.ts @@ -0,0 +1,25 @@ +import axios from 'axios' +import { ElNotification as elNotify } from 'element-plus' +import {ImgList, ImgDel, ImgReq, Folder, ImgItem} from "./types"; + +const request = axios.create({ + baseURL: import.meta.env.VITE_APP_API_URL, + timeout: 200000 +}) + +request.interceptors.response.use( + (response) => response.data.data, + (error) => { + elNotify({ + message: error?.response?.data || String(error), + duration: 0, + type: 'error' + }) + return Promise.reject(error) + } +) + +export const requestListImages = (data : ImgReq): Promise => request.post('/rest/list', data) +export const requestUploadImages = (data: FormData) : Promise => request.post('/rest/upload', data) +export const createFolder = (data: Folder) => request.post('/rest/folder', data) +export const requestDeleteImage = (data: ImgDel) => request.delete('/rest', { data }) diff --git a/src/utils/types.ts b/src/utils/types.ts new file mode 100644 index 0000000..cc34dfc --- /dev/null +++ b/src/utils/types.ts @@ -0,0 +1,41 @@ +export type ConvertedImage = { + file: File + tmpSrc: string +} + +export type UploadedImage = { + src: string + size: number + name: string + uploadedAt: number + expiresAt: number +} + +export interface ImgItem { + key : string + url : string + size: number + + filename ?: string +} + +export interface ImgList { + next: boolean + cursor ?: string + list : Array + + prefixes ?: Array +} + +export interface ImgDel { + keys : string +} + +export interface ImgReq { + cursor?: string + delimiter?: string + limit: Number +} +export interface Folder { + name: string +} diff --git a/src/views/ManageImages.vue b/src/views/ManageImages.vue new file mode 100644 index 0000000..385fee8 --- /dev/null +++ b/src/views/ManageImages.vue @@ -0,0 +1,124 @@ + + + diff --git a/src/views/UploadImages.vue b/src/views/UploadImages.vue new file mode 100644 index 0000000..031dd79 --- /dev/null +++ b/src/views/UploadImages.vue @@ -0,0 +1,221 @@ + + + + + diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..912d5c7 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,8 @@ +module.exports = { + content: ['./index.html', './src/**/*.vue'], + theme: { + extend: {} + }, + plugins: [], + important: true +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..349932e --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "esnext", + "useDefineForClassFields": true, + "module": "esnext", + "moduleResolution": "node", + "strict": true, + "jsx": "preserve", + "sourceMap": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "lib": [ + "esnext", + "dom" + ] + }, + "types": [ + "@cloudflare/workers-types" + ], + "include": [ + "src/**/*.ts", + "src/**/*.d.ts", + "src/**/*.tsx", + "src/**/*.vue" + ], + "exclude": ["functions/**/*"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..5bdb430 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [vue()], + server: { + port: 8990, + host: "0.0.0.0" + } +})