From a7491ebcec47d6fc9c6e1e360fe9f5edb64a21ed Mon Sep 17 00:00:00 2001 From: atom Date: Fri, 3 Feb 2023 15:32:07 +0800 Subject: [PATCH] 1.init commit. --- .env | 1 + .env.development | 1 + .gitignore | 5 + LICENSE | 201 +++++++++++++++++++++++++++ README.md | 41 ++++++ functions/rest/[[path]].ts | 21 +++ functions/rest/router.ts | 3 + functions/rest/routes/index.ts | 141 +++++++++++++++++++ functions/rest/type.ts | 57 ++++++++ functions/rest/utils.ts | 48 +++++++ index.html | 14 ++ package.json | 34 +++++ postcss.config.js | 6 + public/favicon.ico | Bin 0 -> 16958 bytes src/App.vue | 66 +++++++++ src/app.css | 11 ++ src/assets/pic.png | Bin 0 -> 3134 bytes src/assets/picx-logo.png | Bin 0 -> 4180 bytes src/components/ImageBox.vue | 108 +++++++++++++++ src/components/ImageItem.vue | 82 +++++++++++ src/components/LoadingOverlay.vue | 16 +++ src/components/ResultList.vue | 84 ++++++++++++ src/env.d.ts | 16 +++ src/main.ts | 8 ++ src/plugins/router.ts | 21 +++ src/utils/format-bytes.ts | 11 ++ src/utils/request.ts | 25 ++++ src/utils/types.ts | 41 ++++++ src/views/ManageImages.vue | 124 +++++++++++++++++ src/views/UploadImages.vue | 221 ++++++++++++++++++++++++++++++ tailwind.config.js | 8 ++ tsconfig.json | 27 ++++ vite.config.ts | 11 ++ 33 files changed, 1453 insertions(+) create mode 100644 .env create mode 100644 .env.development create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 functions/rest/[[path]].ts create mode 100644 functions/rest/router.ts create mode 100644 functions/rest/routes/index.ts create mode 100644 functions/rest/type.ts create mode 100644 functions/rest/utils.ts create mode 100644 index.html create mode 100644 package.json create mode 100644 postcss.config.js create mode 100644 public/favicon.ico create mode 100644 src/App.vue create mode 100644 src/app.css create mode 100644 src/assets/pic.png create mode 100644 src/assets/picx-logo.png create mode 100644 src/components/ImageBox.vue create mode 100644 src/components/ImageItem.vue create mode 100644 src/components/LoadingOverlay.vue create mode 100644 src/components/ResultList.vue create mode 100644 src/env.d.ts create mode 100644 src/main.ts create mode 100644 src/plugins/router.ts create mode 100644 src/utils/format-bytes.ts create mode 100644 src/utils/request.ts create mode 100644 src/utils/types.ts create mode 100644 src/views/ManageImages.vue create mode 100644 src/views/UploadImages.vue create mode 100644 tailwind.config.js create mode 100644 tsconfig.json create mode 100644 vite.config.ts 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 0000000000000000000000000000000000000000..f558effdef8dda06a47e4320ad200c943213f607 GIT binary patch literal 16958 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 + +
+ +
+
+ +
+ {{ appName }} +
+
+ +
+ + +
+ +
+ + +
+
+
+ +
+ +
+ + +
+
+
+ + + 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 0000000000000000000000000000000000000000..9f9e8ce82317a62a30c010580cbe3dbcbf7d3e45 GIT binary patch literal 3134 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 C^uFZ) literal 0 HcmV?d00001 diff --git a/src/assets/picx-logo.png b/src/assets/picx-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..ed9205a640c3f677850e069a10ca7e6a0b696428 GIT binary patch literal 4180 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 +
+ + + +
+
+
+
+ + {{ name }} + +
+
+ +
+
+ + {{ formatBytes(size) }} + + + {{ new Date(uploadedAt).toLocaleDateString() }} + + +
+
+ +
+ +
+ + 链接 +
+
+ + + + +
+
+
+
+ + + 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" + } +})