1
This commit is contained in:
@@ -0,0 +1 @@
|
||||
VITE_API_BASE_URL_DEV=http://localhost:3000/api
|
||||
@@ -0,0 +1 @@
|
||||
VITE_API_BASE_URL_PROD=https://api.yourdomain.com
|
||||
@@ -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",
|
||||
|
||||
Generated
+63
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
+262
-63
@@ -1,5 +1,7 @@
|
||||
<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="[
|
||||
@@ -112,7 +114,9 @@
|
||||
</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="[
|
||||
@@ -171,12 +175,22 @@
|
||||
</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>
|
||||
<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]
|
||||
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,
|
||||
|
||||
@@ -6,4 +6,3 @@ export default {
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user