This commit is contained in:
Lan
2024-10-06 14:00:28 +08:00
parent eb59b429b8
commit f653c67c77
10 changed files with 555 additions and 92 deletions
+1
View File
@@ -0,0 +1 @@
VITE_API_BASE_URL_DEV=http://localhost:3000/api
+1
View File
@@ -0,0 +1 @@
VITE_API_BASE_URL_PROD=https://api.yourdomain.com
+1
View File
@@ -13,6 +13,7 @@
"format": "prettier --write src/"
},
"dependencies": {
"axios": "^1.7.7",
"lru-cache": "^11.0.1",
"lucide-vue-next": "^0.445.0",
"pinia": "^2.2.2",
+63
View File
@@ -12,6 +12,7 @@ specifiers:
'@vue/eslint-config-typescript': ^13.0.0
'@vue/tsconfig': ^0.5.1
autoprefixer: ^10.4.20
axios: ^1.7.7
eslint: ^8.57.1
eslint-plugin-vue: ^9.28.0
glob: ^11.0.0
@@ -32,6 +33,7 @@ specifiers:
vue-tsc: ^2.1.6
dependencies:
axios: 1.7.7
lru-cache: 11.0.1
lucide-vue-next: 0.445.0_vue@3.5.8
pinia: 2.2.2_typescript@5.4.5+vue@3.5.8
@@ -1391,6 +1393,10 @@ packages:
engines: {node: '>=8'}
dev: true
/asynckit/0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
dev: false
/autoprefixer/10.4.20_postcss@8.4.47:
resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==}
engines: {node: ^10 || ^12 || >=14}
@@ -1407,6 +1413,16 @@ packages:
postcss-value-parser: 4.2.0
dev: true
/axios/1.7.7:
resolution: {integrity: sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==}
dependencies:
follow-redirects: 1.15.9
form-data: 4.0.0
proxy-from-env: 1.1.0
transitivePeerDependencies:
- debug
dev: false
/balanced-match/1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
dev: true
@@ -1529,6 +1545,13 @@ packages:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
dev: true
/combined-stream/1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
dependencies:
delayed-stream: 1.0.0
dev: false
/commander/4.1.1:
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
engines: {node: '>= 6'}
@@ -1609,6 +1632,11 @@ packages:
engines: {node: '>=12'}
dev: true
/delayed-stream/1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
dev: false
/didyoumean/1.2.2:
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
dev: true
@@ -1929,6 +1957,16 @@ packages:
resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==}
dev: true
/follow-redirects/1.15.9:
resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'
peerDependenciesMeta:
debug:
optional: true
dev: false
/foreground-child/3.3.0:
resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==}
engines: {node: '>=14'}
@@ -1937,6 +1975,15 @@ packages:
signal-exit: 4.1.0
dev: true
/form-data/4.0.0:
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
engines: {node: '>= 6'}
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
mime-types: 2.1.35
dev: false
/fraction.js/4.3.7:
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
dev: true
@@ -2365,6 +2412,18 @@ packages:
picomatch: 2.3.1
dev: true
/mime-db/1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
dev: false
/mime-types/2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
dependencies:
mime-db: 1.52.0
dev: false
/mimic-fn/4.0.0:
resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==}
engines: {node: '>=12'}
@@ -2731,6 +2790,10 @@ packages:
hasBin: true
dev: true
/proxy-from-env/1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
dev: false
/punycode/2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
Binary file not shown.
+12 -3
View File
@@ -1,3 +1,12 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
@font-face {
font-family: 'DingTalk';
src: url('../assets/font/DingTalk.ttf') format('truetype');
}
* {
font-family: 'DingTalk', sans-serif !important;
}
+124
View File
@@ -0,0 +1,124 @@
<template>
<transition name="alert-fade">
<div v-if="show" class="fixed top-4 right-4 max-w-sm w-full z-50">
<div :class="[
'rounded-lg shadow-lg overflow-hidden transform transition-all duration-300',
alertTypeClasses[type]
]">
<div class="p-4">
<div class="flex items-start">
<div class="flex-shrink-0">
<CheckCircle v-if="type === 'success'" class="h-6 w-6 text-white" />
<AlertTriangle v-else-if="type === 'error'" class="h-6 w-6 text-white" />
<AlertCircle v-else-if="type === 'warning'" class="h-6 w-6 text-white" />
<Info v-else class="h-6 w-6 text-white" />
</div>
<div class="ml-3 w-0 flex-1 pt-0.5">
<p class="text-sm font-medium text-white">{{ message }}</p>
</div>
<div class="ml-4 flex-shrink-0 flex">
<button @click="$emit('close')" class="inline-flex text-white hover:text-gray-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-white">
<span class="sr-only">关闭</span>
<X class="h-5 w-5" />
</button>
</div>
</div>
</div>
<div class="h-1 bg-white bg-opacity-25">
<div
:class="['h-full transition-all duration-300 ease-out bg-white', { 'w-full': !autoClose }]"
:style="{ width: `${progressWidth}%` }"
></div>
</div>
</div>
</div>
</transition>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch } from 'vue';
import { CheckCircle, AlertTriangle, AlertCircle, Info, X } from 'lucide-vue-next';
const props = defineProps({
show: Boolean,
message: String,
type: {
type: String,
default: 'info',
validator: (value: string) => ['success', 'error', 'warning', 'info'].includes(value)
},
duration: {
type: Number,
default: 3000
},
autoClose: {
type: Boolean,
default: true
}
});
const emit = defineEmits(['close']);
const alertTypeClasses: {[key: string]: string} = {
success: 'bg-green-500',
error: 'bg-red-500',
warning: 'bg-yellow-500',
info: 'bg-blue-500'
};
const progressWidth = ref(100);
let timer: number | null = null;
const startTimer = () => {
if (timer) {
clearInterval(timer);
}
progressWidth.value = 100;
const startTime = Date.now();
const endTime = startTime + props.duration;
timer = setInterval(() => {
const now = Date.now();
const remaining = endTime - now;
progressWidth.value = (remaining / props.duration) * 100;
if (remaining <= 0) {
clearInterval(timer!);
emit('close');
}
}, 10);
};
watch(() => props.show, (newValue) => {
if (newValue && props.autoClose) {
startTimer();
} else if (!newValue && timer) {
clearInterval(timer);
}
});
onMounted(() => {
if (props.show && props.autoClose) {
startTimer();
}
});
onUnmounted(() => {
if (timer) {
clearInterval(timer);
}
});
</script>
<style scoped>
.alert-fade-enter-active,
.alert-fade-leave-active {
transition: opacity 0.3s, transform 0.3s;
}
.alert-fade-enter-from,
.alert-fade-leave-to {
opacity: 0;
transform: translateX(20px);
}
</style>
+66
View File
@@ -0,0 +1,66 @@
import axios from 'axios'
// 从环境变量中获取 API 基础 URL
const baseURL =
import.meta.env.MODE === 'production'
? import.meta.env.VITE_API_BASE_URL_PROD
: import.meta.env.VITE_API_BASE_URL_DEV
console.log(baseURL)
console.log(import.meta.env.MODE)
// 创建 axios 实例
const api = axios.create({
baseURL,
timeout: 10000, // 请求超时时间
headers: {
'Content-Type': 'application/json'
}
})
// 请求拦截器
api.interceptors.request.use(
(config) => {
// 从 localStorage 获取 token
const token = localStorage.getItem('token')
if (token) {
config.headers['Authorization'] = `Bearer ${token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)
// 响应拦截器
api.interceptors.response.use(
(response) => {
return response
},
(error) => {
// 处理错误响应
if (error.response) {
switch (error.response.status) {
case 401:
// 未授权,可能需要重新登录
console.error('未授权,请重新登录')
break
case 403:
// 禁止访问
console.error('禁止访问')
break
case 404:
// 未找到
console.error('请求的资源不存在')
break
default:
console.error('发生错误:', error.response.data)
}
} else {
console.error('发生错误:', error.message)
}
return Promise.reject(error)
}
)
export default api
+286 -87
View File
@@ -1,10 +1,12 @@
<template>
<div class="min-h-screen flex items-center justify-center p-4 overflow-hidden transition-colors duration-300">
<div
class="min-h-screen flex items-center justify-center p-4 overflow-hidden transition-colors duration-300"
>
<div
class="rounded-3xl shadow-2xl overflow-hidden border w-full max-w-md transition-colors duration-300"
:class="[
isDarkMode
? 'bg-white bg-opacity-10 backdrop-filter backdrop-blur-xl border-gray-700'
isDarkMode
? 'bg-white bg-opacity-10 backdrop-filter backdrop-blur-xl border-gray-700'
: 'bg-white border-gray-200'
]"
>
@@ -16,7 +18,7 @@
? 'text-transparent bg-clip-text bg-gradient-to-r from-indigo-300 via-purple-300 to-pink-300'
: 'text-indigo-600'
]"
@click="toRetrieve"
@click="toRetrieve"
>
FileCodeBox
</h2>
@@ -93,26 +95,28 @@
</div>
</div>
<div v-else key="text" class="grid grid-cols-1 gap-8">
<!-- 文本输入区域 -->
<div v-if="sendType === 'text'" class="flex flex-col">
<textarea
id="text-content"
v-model="textContent"
rows="7"
:class="[
'flex-grow px-4 py-3 rounded-xl placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 transition duration-300 resize-none',
isDarkMode
? 'bg-gray-800 bg-opacity-50 text-white'
: 'bg-white text-gray-900 border border-gray-300'
]"
placeholder="在此输入要发送的文本..."
></textarea>
</div>
<!-- 文本输入区域 -->
<div v-if="sendType === 'text'" class="flex flex-col">
<textarea
id="text-content"
v-model="textContent"
rows="7"
:class="[
'flex-grow px-4 py-3 rounded-xl placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 transition duration-300 resize-none',
isDarkMode
? 'bg-gray-800 bg-opacity-50 text-white'
: 'bg-white text-gray-900 border border-gray-300'
]"
placeholder="在此输入要发送的文本..."
></textarea>
</div>
</div>
</transition>
<!-- 过期方式选择 -->
<div class="flex flex-col space-y-4">
<label :class="['text-sm font-medium', isDarkMode ? 'text-gray-300' : 'text-gray-700']">过期方式</label>
<label :class="['text-sm font-medium', isDarkMode ? 'text-gray-300' : 'text-gray-700']"
>过期方式</label
>
<select
v-model="expirationMethod"
:class="[
@@ -170,13 +174,23 @@
</router-link>
</div>
</div>
<div class="px-8 py-4 bg-opacity-50 flex justify-between items-center" :class="[isDarkMode ? 'bg-gray-800' : 'bg-gray-100']">
<span class="text-sm flex items-center" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-800']">
<div
class="px-8 py-4 bg-opacity-50 flex justify-between items-center"
:class="[isDarkMode ? 'bg-gray-800' : 'bg-gray-100']"
>
<span
class="text-sm flex items-center"
:class="[isDarkMode ? 'text-gray-300' : 'text-gray-800']"
>
<ShieldCheckIcon class="w-4 h-4 mr-1 text-green-400" />
安全加密
</span>
<button @click="toggleDrawer" class="text-sm hover:text-indigo-300 transition duration-300 flex items-center" :class="[isDarkMode ? 'text-indigo-400' : 'text-indigo-600']">
<button
@click="toggleDrawer"
class="text-sm hover:text-indigo-300 transition duration-300 flex items-center"
:class="[isDarkMode ? 'text-indigo-400' : 'text-indigo-600']"
>
发件记录
<ClipboardListIcon class="w-4 h-4 ml-1" />
</button>
@@ -185,30 +199,72 @@
<!-- 抽屉式发件记录 -->
<transition name="drawer">
<div v-if="showDrawer" class="fixed inset-y-0 right-0 w-full sm:w-96 bg-opacity-70 backdrop-filter backdrop-blur-xl shadow-2xl z-50 overflow-hidden flex flex-col" :class="[isDarkMode ? 'bg-gray-900' : 'bg-white']">
<div class="flex justify-between items-center p-6 border-b" :class="[isDarkMode ? 'border-gray-700' : 'border-gray-200']">
<h3 class="text-2xl font-bold" :class="[isDarkMode ? 'text-white' : 'text-gray-800']">发件记录</h3>
<button @click="toggleDrawer" class="hover:text-white transition duration-300" :class="[isDarkMode ? 'text-gray-400' : 'text-gray-800']">
<div
v-if="showDrawer"
class="fixed inset-y-0 right-0 w-full sm:w-96 bg-opacity-70 backdrop-filter backdrop-blur-xl shadow-2xl z-50 overflow-hidden flex flex-col"
:class="[isDarkMode ? 'bg-gray-900' : 'bg-white']"
>
<div
class="flex justify-between items-center p-6 border-b"
:class="[isDarkMode ? 'border-gray-700' : 'border-gray-200']"
>
<h3 class="text-2xl font-bold" :class="[isDarkMode ? 'text-white' : 'text-gray-800']">
发件记录
</h3>
<button
@click="toggleDrawer"
class="hover:text-white transition duration-300"
:class="[isDarkMode ? 'text-gray-400' : 'text-gray-800']"
>
<XIcon class="w-6 h-6" />
</button>
</div>
<div class="flex-grow overflow-y-auto p-6">
<transition-group name="list" tag="div" class="space-y-4">
<div v-for="record in sendRecords" :key="record.id" class="bg-opacity-50 rounded-lg p-4 flex justify-between items-center shadow-md hover:shadow-lg transition duration-300 transform hover:scale-102" :class="[isDarkMode ? 'bg-gray-800 hover:bg-gray-700' : 'bg-gray-100 hover:bg-white']">
<div
v-for="record in sendRecords"
:key="record.id"
class="bg-opacity-50 rounded-lg p-4 flex justify-between items-center shadow-md hover:shadow-lg transition duration-300 transform hover:scale-102"
:class="[isDarkMode ? 'bg-gray-800 hover:bg-gray-700' : 'bg-gray-100 hover:bg-white']"
>
<div class="flex items-center space-x-4">
<div class="flex-shrink-0">
<FileIcon class="w-10 h-10" :class="[isDarkMode ? 'text-indigo-400' : 'text-indigo-600']" />
<FileIcon
class="w-10 h-10"
:class="[isDarkMode ? 'text-indigo-400' : 'text-indigo-600']"
/>
</div>
<div>
<p class="font-medium text-lg" :class="[isDarkMode ? 'text-white' : 'text-gray-800']">{{ record.filename }}</p>
<p class="text-sm" :class="[isDarkMode ? 'text-gray-400' : 'text-gray-600']">{{ record.date }} · {{ record.size }}</p>
<p
class="font-medium text-lg"
:class="[isDarkMode ? 'text-white' : 'text-gray-800']"
>
{{ record.filename }}
</p>
<p class="text-sm" :class="[isDarkMode ? 'text-gray-400' : 'text-gray-600']">
{{ record.date }} · {{ record.size }}
</p>
</div>
</div>
<div class="flex space-x-2">
<button @click="viewDetails(record)" class="p-2 rounded-full hover:bg-opacity-20 transition duration-300" :class="[isDarkMode ? 'hover:bg-indigo-400 text-indigo-400' : 'hover:bg-indigo-100 text-indigo-600']">
<button
@click="viewDetails(record)"
class="p-2 rounded-full hover:bg-opacity-20 transition duration-300"
:class="[
isDarkMode
? 'hover:bg-indigo-400 text-indigo-400'
: 'hover:bg-indigo-100 text-indigo-600'
]"
>
<EyeIcon class="w-5 h-5" />
</button>
<button @click="deleteRecord(record.id)" class="p-2 rounded-full hover:bg-opacity-20 transition duration-300" :class="[isDarkMode ? 'hover:bg-red-400 text-red-400' : 'hover:bg-red-100 text-red-600']">
<button
@click="deleteRecord(record.id)"
class="p-2 rounded-full hover:bg-opacity-20 transition duration-300"
:class="[
isDarkMode ? 'hover:bg-red-400 text-red-400' : 'hover:bg-red-100 text-red-600'
]"
>
<TrashIcon class="w-5 h-5" />
</button>
</div>
@@ -220,65 +276,133 @@
<!-- 记录详情弹窗 -->
<transition name="fade">
<div v-if="selectedRecord" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="p-8 rounded-2xl max-w-md w-full mx-4 shadow-2xl transform transition-all duration-300 ease-out backdrop-filter backdrop-blur-lg bg-opacity-70" :class="[isDarkMode ? 'bg-gray-800' : 'bg-white']">
<h3 class="text-2xl font-bold mb-6" :class="[isDarkMode ? 'text-white' : 'text-gray-800']">文件详情</h3>
<div
v-if="selectedRecord"
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
>
<div
class="p-8 rounded-2xl max-w-md w-full mx-4 shadow-2xl transform transition-all duration-300 ease-out backdrop-filter backdrop-blur-lg bg-opacity-70"
:class="[isDarkMode ? 'bg-gray-800' : 'bg-white']"
>
<h3
class="text-2xl font-bold mb-6"
:class="[isDarkMode ? 'text-white' : 'text-gray-800']"
>
文件详情
</h3>
<div class="space-y-4">
<div class="flex items-center">
<FileIcon class="w-6 h-6 mr-3" :class="[isDarkMode ? 'text-indigo-400' : 'text-indigo-600']" />
<p :class="[isDarkMode ? 'text-gray-300' : 'text-gray-800']"><span class="font-medium">文件名</span>{{ selectedRecord.filename }}</p>
<FileIcon
class="w-6 h-6 mr-3"
:class="[isDarkMode ? 'text-indigo-400' : 'text-indigo-600']"
/>
<p :class="[isDarkMode ? 'text-gray-300' : 'text-gray-800']">
<span class="font-medium">文件名</span>{{ selectedRecord.filename }}
</p>
</div>
<div class="flex items-center">
<CalendarIcon class="w-6 h-6 mr-3" :class="[isDarkMode ? 'text-indigo-400' : 'text-indigo-600']" />
<p :class="[isDarkMode ? 'text-gray-300' : 'text-gray-800']"><span class="font-medium">发送日期</span>{{ selectedRecord.date }}</p>
<CalendarIcon
class="w-6 h-6 mr-3"
:class="[isDarkMode ? 'text-indigo-400' : 'text-indigo-600']"
/>
<p :class="[isDarkMode ? 'text-gray-300' : 'text-gray-800']">
<span class="font-medium">发送日期</span>{{ selectedRecord.date }}
</p>
</div>
<div class="flex items-center">
<HardDriveIcon class="w-6 h-6 mr-3" :class="[isDarkMode ? 'text-indigo-400' : 'text-indigo-600']" />
<p :class="[isDarkMode ? 'text-gray-300' : 'text-gray-800']"><span class="font-medium">文件大小</span>{{ selectedRecord.size }}</p>
<HardDriveIcon
class="w-6 h-6 mr-3"
:class="[isDarkMode ? 'text-indigo-400' : 'text-indigo-600']"
/>
<p :class="[isDarkMode ? 'text-gray-300' : 'text-gray-800']">
<span class="font-medium">文件大小</span>{{ selectedRecord.size }}
</p>
</div>
<div class="flex items-center">
<ClockIcon class="w-6 h-6 mr-3" :class="[isDarkMode ? 'text-indigo-400' : 'text-indigo-600']" />
<p :class="[isDarkMode ? 'text-gray-300' : 'text-gray-800']"><span class="font-medium">过期时间</span>{{ selectedRecord.expiration }}</p>
<ClockIcon
class="w-6 h-6 mr-3"
:class="[isDarkMode ? 'text-indigo-400' : 'text-indigo-600']"
/>
<p :class="[isDarkMode ? 'text-gray-300' : 'text-gray-800']">
<span class="font-medium">过期时间</span>{{ selectedRecord.expiration }}
</p>
</div>
</div>
<!-- 取件码和二维码部分 -->
<div class="mt-6 flex justify-between items-center">
<div class="flex flex-col items-center w-1/2 pr-2">
<h4 class="text-lg font-semibold mb-3" :class="[isDarkMode ? 'text-white' : 'text-gray-800']">取件码</h4>
<div
<h4
class="text-lg font-semibold mb-3"
:class="[isDarkMode ? 'text-white' : 'text-gray-800']"
>
取件码
</h4>
<div
class="bg-gray-100 p-3 rounded-lg shadow-md cursor-pointer hover:bg-gray-200 transition-colors duration-300 w-full text-center"
@click="copyRetrieveCode(selectedRecord.retrieveCode)"
>
<p class="text-2xl font-bold text-indigo-600">{{ selectedRecord.retrieveCode }}</p>
</div>
<p class="mt-2 text-sm" :class="[isDarkMode ? 'text-gray-400' : 'text-gray-600']">点击复制取件码</p>
<p class="mt-2 text-sm" :class="[isDarkMode ? 'text-gray-400' : 'text-gray-600']">
点击复制取件码
</p>
</div>
<div class="flex flex-col items-center w-1/2 pl-2">
<h4 class="text-lg font-semibold mb-3" :class="[isDarkMode ? 'text-white' : 'text-gray-800']">二维码</h4>
<h4
class="text-lg font-semibold mb-3"
:class="[isDarkMode ? 'text-white' : 'text-gray-800']"
>
二维码
</h4>
<div class="bg-white p-2 rounded-lg shadow-md">
<QRCode :value="getQRCodeValue(selectedRecord)" :size="128" level="M" />
</div>
<p class="mt-2 text-sm" :class="[isDarkMode ? 'text-gray-400' : 'text-gray-600']">扫描二维码快速取件</p>
<p class="mt-2 text-sm" :class="[isDarkMode ? 'text-gray-400' : 'text-gray-600']">
扫描二维码快速取件
</p>
</div>
</div>
<button @click="selectedRecord = null" class="mt-8 w-full bg-gradient-to-r from-indigo-500 to-purple-600 text-white px-6 py-3 rounded-lg font-medium hover:from-indigo-600 hover:to-purple-700 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-opacity-50 transition duration-300 transform hover:scale-105">
<button
@click="selectedRecord = null"
class="mt-8 w-full bg-gradient-to-r from-indigo-500 to-purple-600 text-white px-6 py-3 rounded-lg font-medium hover:from-indigo-600 hover:to-purple-700 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-opacity-50 transition duration-300 transform hover:scale-105"
>
关闭
</button>
</div>
</div>
</transition>
<!-- 使用新的 AlertComponent -->
<AlertComponent
:show="showAlert"
:message="alertMessage"
:type="alertType"
@close="closeAlert"
/>
</div>
</template>
<script setup lang="ts">
import { ref, inject, onMounted } from 'vue'
import { UploadCloudIcon, SendIcon, ClipboardListIcon, XIcon, TrashIcon, FileIcon, CalendarIcon, HardDriveIcon, ClockIcon, EyeIcon, ShieldCheckIcon } from 'lucide-vue-next'
import {
UploadCloudIcon,
SendIcon,
ClipboardListIcon,
XIcon,
TrashIcon,
FileIcon,
CalendarIcon,
HardDriveIcon,
ClockIcon,
EyeIcon,
ShieldCheckIcon
} from 'lucide-vue-next'
import { useRouter } from 'vue-router'
import BorderProgressBar from '../components/BorderProgressBar.vue'
import QRCode from 'qrcode.vue'
import AlertComponent from '../components/AlertComponent.vue'
import api from '../utils/api'
const router = useRouter()
const isDarkMode = inject('isDarkMode')
@@ -292,15 +416,83 @@ const expirationViews = ref('')
const uploadProgress = ref(0)
const showDrawer = ref(false)
const selectedRecord = ref<any>(null)
api
.post('/', {
headers: {
'Content-Type': 'application/json'
},
data: {
sendType: sendType.value,
selectedFile: selectedFile.value,
textContent: textContent.value,
expirationMethod: expirationMethod.value,
expirationTime: expirationTime.value,
expirationViews: expirationViews.value
}
})
.then((res) => {
console.log(res)
})
const sendRecords = ref([
{ id: 1, filename: '项目报告.pdf', date: '2023-05-15', size: '3.2 MB', expiration: '2023-05-22', retrieveCode: 'ABC123' },
{ id: 2, filename: '会议纪要.docx', date: '2023-05-10', size: '1.5 MB', expiration: '2023-05-17', retrieveCode: 'DEF456' },
{ id: 3, filename: '财务数据.xlsx', date: '2023-05-05', size: '2.8 MB', expiration: '2023-05-12', retrieveCode: 'GHI789' },
{ id: 4, filename: '产品设计.psd', date: '2023-05-01', size: '15.7 MB', expiration: '2023-05-08', retrieveCode: 'JKL012' },
{ id: 5, filename: '客户反馈.txt', date: '2023-04-28', size: '0.5 MB', expiration: '2023-05-05', retrieveCode: 'MNO345' },
{
id: 1,
filename: '项目报告.pdf',
date: '2023-05-15',
size: '3.2 MB',
expiration: '2023-05-22',
retrieveCode: 'ABC123'
},
{
id: 2,
filename: '会议纪要.docx',
date: '2023-05-10',
size: '1.5 MB',
expiration: '2023-05-17',
retrieveCode: 'DEF456'
},
{
id: 3,
filename: '财务数据.xlsx',
date: '2023-05-05',
size: '2.8 MB',
expiration: '2023-05-12',
retrieveCode: 'GHI789'
},
{
id: 4,
filename: '产品设计.psd',
date: '2023-05-01',
size: '15.7 MB',
expiration: '2023-05-08',
retrieveCode: 'JKL012'
},
{
id: 5,
filename: '客户反馈.txt',
date: '2023-04-28',
size: '0.5 MB',
expiration: '2023-05-05',
retrieveCode: 'MNO345'
}
])
const showAlert = ref(false)
const alertMessage = ref('')
const alertType = ref('success')
const showAlertMessage = (
message: string,
type: 'success' | 'error' | 'warning' | 'info' = 'success'
) => {
alertMessage.value = message
alertType.value = type
showAlert.value = true
}
const closeAlert = () => {
showAlert.value = false
}
const triggerFileUpload = () => {
fileInput.value?.click()
}
@@ -322,36 +514,36 @@ const handleFileDrop = (event: DragEvent) => {
const simulateFileUpload = () => {
uploadProgress.value = 0
const duration = 2000; // 总动画时长(毫秒)
const steps = 100; // 动画步数
const stepDuration = duration / steps;
const stepIncrement = 100 / steps;
const duration = 2000 // 总动画时长(毫秒)
const steps = 100 // 动画步数
const stepDuration = duration / steps
const stepIncrement = 100 / steps
const animate = (step: number) => {
if (step <= steps) {
uploadProgress.value = step * stepIncrement;
setTimeout(() => animate(step + 1), stepDuration);
uploadProgress.value = step * stepIncrement
setTimeout(() => animate(step + 1), stepDuration)
}
};
}
animate(0);
animate(0)
}
const handleSubmit = () => {
if (sendType.value === 'file' && !selectedFile.value) {
alert('请选择要上传的文件')
showAlertMessage('请选择要上传的文件', 'error')
return
}
if (sendType.value === 'text' && !textContent.value.trim()) {
alert('请输入要发送的文本')
showAlertMessage('请输入要发送的文本', 'error')
return
}
if (expirationMethod.value === 'time' && !expirationTime.value) {
alert('请输入过期时间')
showAlertMessage('请输入过期时间', 'error')
return
}
if (expirationMethod.value === 'views' && !expirationViews.value) {
alert('请输入查看次数')
showAlertMessage('请输入查看次数', 'error')
return
}
@@ -368,16 +560,21 @@ const handleSubmit = () => {
id: sendRecords.value.length + 1,
filename: selectedFile.value ? selectedFile.value.name : '文本内容.txt',
date: new Date().toISOString().split('T')[0],
size: selectedFile.value ? `${(selectedFile.value.size / (1024 * 1024)).toFixed(1)} MB` : '0.1 MB',
expiration: expirationMethod.value === 'time'
? new Date(Date.now() + parseInt(expirationTime.value) * 60 * 60 * 1000).toISOString().split('T')[0]
: `${expirationViews.value}次查看后过期`,
size: selectedFile.value
? `${(selectedFile.value.size / (1024 * 1024)).toFixed(1)} MB`
: '0.1 MB',
expiration:
expirationMethod.value === 'time'
? new Date(Date.now() + parseInt(expirationTime.value) * 60 * 60 * 1000)
.toISOString()
.split('T')[0]
: `${expirationViews.value}次查看后过期`,
retrieveCode: Math.random().toString(36).substring(2, 8).toUpperCase()
}
sendRecords.value.unshift(newRecord)
// 显示发送成功消息
alert('文件发送成功!取件码:' + newRecord.retrieveCode)
showAlertMessage(`文件发送成功!取件码:${newRecord.retrieveCode}`, 'success')
// 重置表单
selectedFile.value = null
@@ -395,27 +592,27 @@ const toggleDrawer = () => {
showDrawer.value = !showDrawer.value
}
const viewDetails = (record:any) => {
const viewDetails = (record: any) => {
selectedRecord.value = record
}
const deleteRecord = (id:any) => {
sendRecords.value = sendRecords.value.filter(record => record.id !== id)
const deleteRecord = (id: any) => {
sendRecords.value = sendRecords.value.filter((record) => record.id !== id)
}
const getQRCodeValue = (record:any) => {
const getQRCodeValue = (record: any) => {
// 这里返回你想要在二维码中编码的信息
// 例如,可以是一个包含文件ID和取件码的URL
return `https://your-domain.com/retrieve/${record.id}?code=${record.retrieveCode}`
}
const copyRetrieveCode = async (code:any) => {
const copyRetrieveCode = async (code: string) => {
try {
await navigator.clipboard.writeText(code)
alert('取件码已复制到剪贴板')
showAlertMessage('取件码已复制到剪贴板', 'success')
} catch (err) {
console.error('无法复制取件码: ', err)
alert('复制失败,请手动复制取件码')
showAlertMessage('复制失败,请手动复制取件码', 'error')
}
}
@@ -429,7 +626,9 @@ onMounted(() => {
<style scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease, transform 0.3s ease;
transition:
opacity 0.3s ease,
transform 0.3s ease;
}
.fade-enter-from,
@@ -462,4 +661,4 @@ onMounted(() => {
opacity: 0;
transform: translateX(30px);
}
</style>
</style>
+1 -2
View File
@@ -5,5 +5,4 @@ export default {
extend: {},
},
plugins: [],
}
}