This commit is contained in:
539943419 committed 2024-03-12 23:11:05 +08:00
commit 97a4391120
20 files changed
+3709

No files matched your search

+51
View File
@@ -0,0 +1,51 @@
<script setup>
import uploadFileMiao from "./components/uploadFileMiao.vue";
import downloadFileMiao from "./components/downloadFileMiao.vue";
import settingMiao from "./components/setting.vue";
</script>
<template>
<div id="windows">
<n-notification-provider placement="bottom-right" :max="3">
<n-message-provider>
<n-card content-style="padding: 0; margin:0;" id="theCard">
<n-tabs type="line" size="large" :tabs-padding="20" pane-style="padding: 20px;">
<n-tab-pane name="上传文件">
<uploadFileMiao style />
</n-tab-pane>
<n-tab-pane name="下载文件">
<downloadFileMiao style/>
</n-tab-pane>
<n-tab-pane name="设置">
<settingMiao style="max-width:100%" />
</n-tab-pane>
</n-tabs>
</n-card>
</n-message-provider>
</n-notification-provider>
</div>
</template>
<style scoped>
#windows {
width: 100%;
height: 100%;
padding: 0%;
display: flex;
position: relative;
justify-content: center;
align-items: center;
}
#theCard {
width: 50%;
height: 80%;
}
@media screen and (max-width:768px) {
#theCard {
width: 100vw;
height: 100%;
}
}
</style>
+86
View File
@@ -0,0 +1,86 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition:
color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 276 B

+35
View File
@@ -0,0 +1,35 @@
@import './base.css';
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
font-weight: normal;
}
a,
.green {
text-decoration: none;
color: hsla(160, 100%, 37%, 1);
transition: 0.4s;
padding: 3px;
}
@media (hover: hover) {
a:hover {
background-color: hsla(160, 100%, 37%, 0.2);
}
}
@media (min-width: 1024px) {
body {
display: flex;
place-items: center;
}
#app {
display: grid;
grid-template-columns: 1fr 1fr;
padding: 0 2rem;
}
}
+238
View File
@@ -0,0 +1,238 @@
<template>
<div style="width:auto">
<!-- <p>downloadProcess:{{ downloadProcess }}%</p>
<button @click="downloadFile">下载</button>-->
<n-input
v-model:value="downloadFileID"
type="textarea"
placeholder="于此处粘贴输入取件码&#13;&#10;多行也能正确识别"
:autosize="{
minRows: 3,
maxRows: 5
}"
/>
<n-flex justify="end" style="margin: 10px 0;position:relative">
<n-button type="error" @click="downloadFileID = null">清空输入</n-button>
<n-button type="primary" @click="praseInput">开始下载</n-button>
</n-flex>
<n-data-table
:columns="columns"
:data="downloadQuene"
:max-height="250"
:bordered="false"
:single-line="false"
/>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
import aesjs from "aes-js";
import { Base64 } from "js-base64";
import { saveAs } from "file-saver";
import { useSettingStore } from "@/pinia/store";
import { sm4 } from "sm-crypto";
var downloadFileID = ref();
const setting = useSettingStore();
// 服务端端口
const serverPort = setting.serverPort;
var baseUrl =
location.protocol + "//" + location.hostname + ":" + serverPort + "/";
// console.log(baseUrl);
const blob2Uint8Array = function(blob) {
return new Promise((resolve, reject) => {
let reader = new FileReader();
reader.readAsArrayBuffer(blob);
reader.onload = function() {
let buffer = reader.result;
let uint8array = new Uint8Array(buffer);
resolve(uint8array);
};
reader.onerror = function() {
reject(reader.error);
};
});
};
const decrypt = (key, data) => {
return blob2Uint8Array(data).then(data => {
// 无加密
if(key.length === 0){
return data
}
// SM4
if (key.length >= 34) {
key = key.substring(3);
// console.log([...data])
var decryptedBytes = new Uint8Array(
sm4.decrypt([...data], key, { output: "array" })
);
return decryptedBytes;
} else {
// AES
key = aesjs.utils.utf8.toBytes(key);
var aesCtr = new aesjs.ModeOfOperation.ctr(
key,
new aesjs.Counter(5)
);
var decryptedBytes = aesCtr.decrypt(data);
return decryptedBytes;
}
});
};
const decryptString = (key, str) => {
// 无加密
if(key.length === 0){
return sm4.decrypt(str, 'miaospringmiaospringmiaospringao');
}
// SM4
// console.log(key,key.length)
if (key.length >= 34) {
key = key.substring(3);
return sm4.decrypt(str, key);
}
// AES
var data = new Uint8Array(
Base64.decode(str)
.split(",")
.map(n => parseInt(n))
);
key = aesjs.utils.utf8.toBytes(key);
var aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(5));
var decryptedBytes = aesCtr.decrypt(data);
var decryptedStr = aesjs.utils.utf8.fromBytes(decryptedBytes);
return decryptedStr;
};
const downloadFromUint8Array = (data, fileName) => {
let uint8Array = data;
let blob = new Blob([uint8Array], { type: "text/plain" });
// 使用file-saver.js
saveAs(blob, fileName);
//let url = URL.createObjectURL(blob);
//let a = document.createElement("a");
//a.href = url;
//a.download = fileName;
//a.click();
//URL.revokeObjectURL(url);
};
// 刷新展示的下载进度
const updateDownloadQuene = (code, key, value) => {
for (let index in downloadQuene.value) {
if (downloadQuene.value[index].code == code) {
downloadQuene.value[index][key] = value;
}
}
};
const praseInput = () => {
let codeArray = downloadFileID.value.split("\n");
for (let code of codeArray) {
if (
code === "" ||
downloadQuene.value.filter(m => m.code == code).length !== 0
)
continue;
// console.log(code);
let dp = {
code: code,
fileName: "有的事总是不得而知",
downloadProcess: "连接中。。。"
};
downloadQuene.value.push(dp);
downloadFile(code);
}
};
const downloadFile = code => {
var id = code.substring(0, 8);
var key = code.substring(8);
var fileName;
const params = {
fileName: id
};
const options = {
method: "GET"
};
fetch(baseUrl + "file/download?" + new URLSearchParams(params), options)
.then(res => {
fileName = res.headers.get("File-Name");
fileName = decryptString(key, fileName);
updateDownloadQuene(code, "fileName", fileName);
const total = +res.headers.get("Content-Length");
let loaded = 0;
const clone = res.clone();
const reader = res.body.getReader();
// 感觉怪怪的
const read = async () => {
const { done, value } = await reader.read();
if (done) {
// console.log(clone)
return clone;
}
loaded += value.length;
const percent = (loaded / total) * 100;
// downloadProcess['downloadProcess'] = `下载进度:${percent.toFixed(2)}%`;
updateDownloadQuene(
code,
"downloadProcess",
`下载进度:${percent.toFixed(2)}%`
);
// downloadProcess.value = percent.toFixed(2);
return read();
};
return read();
})
.then(res => {
var data = res.blob();
return data;
})
.then(data => {
// downloadProcess['downloadProcess'] = '解密中'
updateDownloadQuene(code, "downloadProcess", `解密中`);
decrypt(key, data).then(data => {
updateDownloadQuene(code, "downloadProcess", `已完成`);
downloadFromUint8Array(data, fileName);
});
})
.catch(err => {
updateDownloadQuene(
code,
"fileName",
`出错,可能是文件过期或下载次数用完`
);
updateDownloadQuene(code, "downloadProcess", `出错`);
console.error(err);
});
};
// 下载队列
const downloadQuene = ref([]);
const columns = [
{
title: "取件码",
key: "code",
width: 100
},
{
title: "文件名",
key: "fileName",
width: 100
},
{
title: "下载状态",
key: "downloadProcess",
width: 150
}
];
// 进度条进度
const progressPercentage = ref(60);
</script>
<style>
</style>
+82
View File
@@ -0,0 +1,82 @@
<template>
<div>
<n-list hoverable>
<n-list-item>
<p style="font-size:larger;position:absolute;left:5px;">选择加密算法:</p>
<br />
<!-- <br /> -->
<!-- <br /> -->
<n-flex justify="end">
<span v-for="f in fArray" style="margin:6px 5px -4px 0;">
<n-button
:type="f == activeFuction ? 'primary' : 'default'"
@click="setActiveF(f)"
>{{ f }}</n-button>
</span>
</n-flex>
</n-list-item>
<n-list-item>
<p style="font-size:larger;position:absolute;left:5px;">选择密钥组成:</p>
<br />
<n-flex justify="end">
<span v-for="k in keyButtons" style="margin:5px">
<n-button
:type="activeKeyButtons.includes(k) && activeFuction !== '不加密' ? 'primary' : 'default'"
:disabled="activeFuction === '不加密'"
@click="setActiveK(k)"
>{{ k }}</n-button>
</span>
</n-flex justify="end">
</n-list-item>
</n-list>
</div>
<div style="width: 100vw;height:100%"></div>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { useSettingStore } from "@/pinia/store";
import { useMessage, useNotification } from "naive-ui";
const setting = useSettingStore();
const message = useMessage();
// const notification = useNotification();
const fArray = ref(["AES128", "AES256", "SM4", "不加密"]);
const activeFuction = ref(setting.activeFuction);
const keyButtons = ref(["a-z", "A-Z", "0-9"]);
const activeKeyButtons = ref(setting.activeKeyMap);
console.log(
activeFuction.value,
activeKeyButtons.value,
activeKeyButtons.value.includes("A-Z")
);
function setActiveF(f) {
activeFuction.value = f;
setting.activeFuction = f;
localStorage.setItem("activeFuction", f);
}
function setActiveK(k) {
if (activeKeyButtons.value.includes(k)) {
if (activeKeyButtons.value.length <= 1) {
return message.info(`至少选择1项密钥组成`);
}
activeKeyButtons.value = activeKeyButtons.value.filter(v => v != k);
} else {
activeKeyButtons.value.push(k);
}
setting.activeKeyMap = activeKeyButtons.value;
localStorage.setItem(
"activeKeyMap",
JSON.stringify(activeKeyButtons.value)
);
console.log(
activeFuction.value,
activeKeyButtons.value,
activeKeyButtons.value.includes("A-Z")
);
}
</script>
<style>
</style>
+267
View File
@@ -0,0 +1,267 @@
<template>
<div>
<n-upload
ref="uploadRef"
:default-upload="false"
multiple
:custom-request="customRequest"
@before-upload="beforeUpload"
>
<n-upload-dragger>
<!-- <div style="margin-bottom: 12px">
<n-icon size="48" :depth="3"></n-icon>
</div>-->
<n-text style="font-size: 16px">点击此处或者拖动文件到该区域来上传</n-text>
<n-p depth="3" style="margin: 8px 0 0 0">
最大支持200MB大小文件,其实只是懒得做分片上传而已
<br />虽然理论上没有人有能力审查你上传的东西
<br />反正没人用写这么多干什么。。。
</n-p>
</n-upload-dragger>
</n-upload>
<n-flex justify="end" style="margin: 10px 0;position:relative">
<n-input-number v-model:value="downloadCredit" placeholder="可下载次数,默认无限" clearable :min="1" :max="2147483647" style="width:max-content"/>
<n-button type="primary" @click="clickUpload">开始上传</n-button>
</n-flex>
<n-data-table
:columns="columns"
:data="file_Code_Map"
:row-props="rowProps"
:max-height="250"
:bordered="false"
:single-line="false"
/>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
import aesjs from "aes-js";
import { Base64 } from "js-base64";
import { useMessage, useNotification } from "naive-ui";
import useClipboard from "vue-clipboard3";
import { useSettingStore } from "@/pinia/store";
import { sm4 } from 'sm-crypto'
const setting = useSettingStore();
const { toClipboard } = useClipboard();
const message = useMessage();
const notification = useNotification();
// 目前使用加密
const activeFuction = setting.activeFuction;
// 启用的密钥组成
const activeKeyButtons = setting.activeKeyMap;
// 服务端端口
const serverPort = setting.serverPort;
// console.log(activeFuction,activeKeyButtons)
var charMap = ""
for(let i of setting.activeKeyMap){
if(i === 'A-Z'){
charMap = charMap + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
}else if(i === "a-z"){
charMap = charMap + 'abcdefghijklmnopqrstuvwxyz'
}else if(i === '0-9'){
charMap = charMap + '0123456789'
}
}
// 生成密钥
const generateKey = function() {
if(setting.activeFuction === "不加密"){
return ''
}
var returnStr = "",
length = setting.activeFuction === "AES128" ? 16 : 32
// charMap = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
for (var i = 0; i < length; i++) {
var index = Math.round(Math.random() * charMap.length);
returnStr += charMap.substring(index, index + 1);
}
return returnStr;
};
// 上传文件用
const uploadRef = ref(null);
// 分享码可用次数
const downloadCredit = ref(null);
var file_Code_Map = ref([]);
var uploadProcess = ref(0);
const baseUrl =
location.protocol + "//" + location.hostname + ":" + serverPort + "/";
const blob2Uint8Array = function(blob) {
return new Promise((resolve, reject) => {
let reader = new FileReader();
reader.readAsArrayBuffer(blob);
reader.onload = function() {
let buffer = reader.result;
let uint8array = new Uint8Array(buffer);
resolve(uint8array);
};
reader.onerror = function() {
reject(reader.error);
};
});
};
// 包含AES128和AES256
const AES = (key,data)=>{
key = aesjs.utils.utf8.toBytes(key);
var aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(5));
var encryptedBytes = aesCtr.encrypt(data);
return encryptedBytes;
}
const SM4 = (key,data)=>{
// console.log(key)
var encryptedBytes = sm4.encrypt(data,key,{padding: 'pkcs#7',output:'array'});
// console.log(encryptedBytes, sm4.decrypt(encryptedBytes, key,{output:'array'}))
return new Uint8Array(encryptedBytes);
}
const encrypt = (key, data, fileName) => {
return blob2Uint8Array(data).then(data => {
let encrypted_data = null
if(setting.activeFuction === "不加密"){
encrypted_data = data
}else if(setting.activeFuction != "SM4"){
message.info(`开始加密${fileName}`);
encrypted_data = AES(key,data)
message.info(`${fileName}加密完成`);
}else{
message.info(`开始加密${fileName}`);
encrypted_data = SM4(key,data)
message.info(`${fileName}加密完成`);
}
return encrypted_data
});
};
const encryptString = (key, str) => {
// console.log(key)
if(setting.activeFuction === "不加密"){
return sm4.encrypt(str, 'miaospringmiaospringmiaospringao')
}else if(setting.activeFuction !== "SM4"){
key = aesjs.utils.utf8.toBytes(key);
var data = aesjs.utils.utf8.toBytes(str);
var aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(5));
var encryptedBytes = aesCtr.encrypt(data);
return Base64.encode(encryptedBytes);
}else{
return sm4.encrypt(str, key)
}
};
const clickUpload = function(){
// for(let i of uploadRef.value)console.log(i)
// console.log(uploadRef.value)
message.info(`开始上传`);
uploadRef.value.submit();
}
const beforeUpload = async function(data) {
// console.log(data.file.file.size)
if (data.file.file.size > 200 * 1024 * 1024) {
message.error(`${data.file.file.name}的大小超过限制,无法上传`);
return false;
}
return true;
};
const customRequest = ({
file,
data,
headers,
withCredentials,
action,
onFinish,
onError,
onProgress
}) => {
// console.log(file);
file = file.file;
var fileName = file.name;
const key = generateKey();
const encryptedFileName = encryptString(key, fileName);
var f = new Blob([file]);
encrypt(key, f, fileName).then(data => {
const params = {
fileName: encryptedFileName,
downloadCredit:downloadCredit.value == null ? -1 : downloadCredit.value
};
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener("progress", function(event) {
const percent = (event.loaded / event.total) * 100;
uploadProcess.value = percent.toFixed(2);
onProgress({ percent: Math.ceil(percent) });
});
onProgress({ percent: 1 });
xhr.open(
"POST",
baseUrl + "file/upload?" + new URLSearchParams(params)
);
xhr.send(data);
xhr.onreadystatechange = () => {
if (xhr.readyState === 4 && xhr.status === 200) {
let id = xhr.responseText;
let code = id + key
if(setting.activeFuction === "SM4"){
code = id + 'SM4' + key
}
file_Code_Map.value.push({
code: code,
fileName: fileName
});
notification["success"]({
content: "上传完成",
meta: `${fileName} 已成功上传`,
duration: 2500,
keepAliveOnHover: true
});
onFinish();
} else {
console.error(xhr.statusText);
onError();
}
};
});
};
const columns = [
{
title: "文件名",
key: "fileName",
width: 150
},
{
title: "取件码(点击复制)",
key: "code"
}
];
// 记录是否复制过,第一次复制的时候提醒可以连续复制
var copyedArray = [];
const rowProps = row => {
return {
style: "cursor: pointer;",
onClick: () => {
toClipboard(`${copyedArray.join('\n')}\n${row.code}`.trim()).then(() => {
message.info(`已复制 ${row.fileName} 的取件码至剪切板`);
if (copyedArray.length == 0) {
message.info(
`可以试试点击其他取件码,也许这个玩意会自动处理剪切板内容`
);
}
copyedArray.push(row.code)
});
}
};
};
</script>
<style>
</style>
+21
View File
@@ -0,0 +1,21 @@
import "./assets/main.css";
import 'vfonts/Lato.css'
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import naive from "naive-ui";
import { createPinia } from "pinia";
// const naive = create({
// components: [NButton,NUpload,NP,NText]
// });
const pinia = createPinia()
const app = createApp(App);
app.use(router);
app.use(pinia);
app.use(naive);
app.mount("#app");
+30
View File
@@ -0,0 +1,30 @@
import { defineStore } from "pinia";
var activeFuctionLocal = localStorage.getItem('activeFuction')
if (activeFuctionLocal == null){
activeFuctionLocal = 'AES128'
localStorage.setItem('activeFuction',activeFuctionLocal)
}
// localStorage.removeItem('activeKeyMap')
var activeKeyMap = localStorage.getItem('activeKeyMap')
if (activeKeyMap == null ){
activeKeyMap = JSON.stringify(['A-Z','0-9'])
localStorage.setItem('activeKeyMap',activeKeyMap)
}
export const useSettingStore = defineStore("setting", {
state: () => ({
activeFuction: activeFuctionLocal,
activeKeyMap:JSON.parse(activeKeyMap),
serverPort:"17058"
}),
getters: {
// double: state => state.count * 2
},
actions: {
// increment() {
// this.count++;
// }
}
});
+23
View File
@@ -0,0 +1,23 @@
import { createRouter, createWebHistory } from 'vue-router'
// import HomeView from '../views/HomeView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
// {
// path: '/',
// name: 'home',
// component: HomeView
// },
// {
// path: '/about',
// name: 'about',
// // route level code-splitting
// // this generates a separate chunk (About.[hash].js) for this route
// // which is lazy-loaded when the route is visited.
// component: () => import('../views/AboutView.vue')
// }
]
})
export default router