初始化

This commit is contained in:
2025-09-09 13:24:51 +08:00
parent 16f533aa57
commit 61c13b88fc
11 changed files with 1109 additions and 89 deletions

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue</title>
<title>相册</title>
</head>
<body>
<div id="app"></div>

View File

@@ -1,5 +1,5 @@
{
"name": "spa-view",
"name": "album-view",
"private": true,
"version": "0.0.0",
"type": "module",

61
pnpm-lock.yaml generated
View File

@@ -14,6 +14,9 @@ importers:
'@ant-design/icons-vue':
specifier: ^7.0.1
version: 7.0.1(vue@3.5.13)
'@fortawesome/fontawesome-free':
specifier: ^6.7.2
version: 6.7.2
'@tailwindcss/vite':
specifier: ^4.1.4
version: 4.1.4(vite@6.3.3(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.88.0))
@@ -23,9 +26,6 @@ importers:
ant-design-vue:
specifier: ^4.1.1
version: 4.2.6(vue@3.5.13)
aos:
specifier: ^2.3.4
version: 2.3.4
axios:
specifier: ^1.6.2
version: 1.9.0
@@ -263,6 +263,10 @@ packages:
cpu: [x64]
os: [win32]
'@fortawesome/fontawesome-free@6.7.2':
resolution: {integrity: sha512-JUOtgFW6k9u4Y+xeIaEiLr3+cjoUPiAuLXoyKOJSia6Duzb7pq+A76P9ZdPDoAoxHdHzq6gE9/jKBGXlZT8FbA==}
engines: {node: '>=6'}
'@jridgewell/sourcemap-codec@1.5.0':
resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
@@ -295,42 +299,36 @@ packages:
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-arm-musl@2.5.1':
resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==}
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
libc: [musl]
'@parcel/watcher-linux-arm64-glibc@2.5.1':
resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-arm64-musl@2.5.1':
resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@parcel/watcher-linux-x64-glibc@2.5.1':
resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-x64-musl@2.5.1':
resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
'@parcel/watcher-win32-arm64@2.5.1':
resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==}
@@ -388,67 +386,56 @@ packages:
resolution: {integrity: sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.40.0':
resolution: {integrity: sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.40.0':
resolution: {integrity: sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.40.0':
resolution: {integrity: sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-loongarch64-gnu@4.40.0':
resolution: {integrity: sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==}
cpu: [loong64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-powerpc64le-gnu@4.40.0':
resolution: {integrity: sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-gnu@4.40.0':
resolution: {integrity: sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.40.0':
resolution: {integrity: sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.40.0':
resolution: {integrity: sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.40.0':
resolution: {integrity: sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.40.0':
resolution: {integrity: sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/rollup-win32-arm64-msvc@4.40.0':
resolution: {integrity: sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==}
@@ -506,28 +493,24 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-arm64-musl@4.1.4':
resolution: {integrity: sha512-X3As2xhtgPTY/m5edUtddmZ8rCruvBvtxYLMw9OsZdH01L2gS2icsHRwxdU0dMItNfVmrBezueXZCHxVeeb7Aw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@tailwindcss/oxide-linux-x64-gnu@4.1.4':
resolution: {integrity: sha512-2VG4DqhGaDSmYIu6C4ua2vSLXnJsb/C9liej7TuSO04NK+JJJgJucDUgmX6sn7Gw3Cs5ZJ9ZLrnI0QRDOjLfNQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-x64-musl@4.1.4':
resolution: {integrity: sha512-v+mxVgH2kmur/X5Mdrz9m7TsoVjbdYQT0b4Z+dr+I4RvreCNXyCFELZL/DO0M1RsidZTrm6O1eMnV6zlgEzTMQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@tailwindcss/oxide-wasm32-wasi@4.1.4':
resolution: {integrity: sha512-2TLe9ir+9esCf6Wm+lLWTMbgklIjiF0pbmDnwmhR9MksVOq+e8aP3TSsXySnBDDvTTVd/vKu1aNttEGj3P6l8Q==}
@@ -632,9 +615,6 @@ packages:
peerDependencies:
vue: '>=3.2.0'
aos@2.3.4:
resolution: {integrity: sha512-zh/ahtR2yME4I51z8IttIt4lC1Nw0ktsFtmeDzID1m9naJnWXhCoARaCgNOGXb5CLy3zm+wqmRAEgMYB5E2HUw==}
array-tree-filter@2.1.0:
resolution: {integrity: sha512-4ROwICNlNw/Hqa9v+rk5h22KjmzB1JGTMVKP2AKJBOCgb0yL0ASf0+YvCcLNNwquOHNX48jkeZIJ3a+oOQqKcw==}
@@ -659,9 +639,6 @@ packages:
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
engines: {node: '>= 14.16.0'}
classlist-polyfill@1.2.0:
resolution: {integrity: sha512-GzIjNdcEtH4ieA2S8NmrSxv7DfEV5fmixQeyTmqmRmRJPGpRBaSnA2a0VrCjyT8iW8JjEdMbKzDotAJf+ajgaQ==}
combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
@@ -851,28 +828,24 @@ packages:
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
lightningcss-linux-arm64-musl@1.29.2:
resolution: {integrity: sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
lightningcss-linux-x64-gnu@1.29.2:
resolution: {integrity: sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
lightningcss-linux-x64-musl@1.29.2:
resolution: {integrity: sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
lightningcss-win32-arm64-msvc@1.29.2:
resolution: {integrity: sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==}
@@ -893,12 +866,6 @@ packages:
lodash-es@4.17.21:
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
lodash.debounce@4.0.8:
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
lodash.throttle@4.1.1:
resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==}
lodash@4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
@@ -1229,6 +1196,8 @@ snapshots:
'@esbuild/win32-x64@0.25.3':
optional: true
'@fortawesome/fontawesome-free@6.7.2': {}
'@jridgewell/sourcemap-codec@1.5.0': {}
'@parcel/watcher-android-arm64@2.5.1':
@@ -1547,12 +1516,6 @@ snapshots:
vue-types: 3.0.2(vue@3.5.13)
warning: 4.0.3
aos@2.3.4:
dependencies:
classlist-polyfill: 1.2.0
lodash.debounce: 4.0.8
lodash.throttle: 4.1.1
array-tree-filter@2.1.0: {}
async-validator@4.2.5: {}
@@ -1581,8 +1544,6 @@ snapshots:
dependencies:
readdirp: 4.1.2
classlist-polyfill@1.2.0: {}
combined-stream@1.0.8:
dependencies:
delayed-stream: 1.0.0
@@ -1787,10 +1748,6 @@ snapshots:
lodash-es@4.17.21: {}
lodash.debounce@4.0.8: {}
lodash.throttle@4.1.1: {}
lodash@4.17.21: {}
loose-envify@1.4.0:

View File

@@ -1,10 +1,153 @@
<script setup>
</script>
<template>
<router-view />
<div id="app">
<!-- Snowflake effect as global background -->
<div class="snowflakes" aria-hidden="true">
<div class="snowflake" v-for="i in 50" :key="i"></div>
</div>
<!-- Router view for page content -->
<router-view />
</div>
</template>
<style lang='scss' scoped>
<script setup>
import { onMounted } from 'vue'
onMounted(() => {
// Create snowflake animation
const snowflakes = document.querySelectorAll('.snowflake')
snowflakes.forEach((snowflake, index) => {
const delay = Math.random() * 5
const duration = 3 + Math.random() * 2
const size = 0.5 + Math.random() * 0.8
const opacity = 0.3 + Math.random() * 0.7
const leftPosition = Math.random() * 100
snowflake.style.left = leftPosition + '%'
snowflake.style.animationDelay = delay + 's'
snowflake.style.animationDuration = duration + 's'
snowflake.style.fontSize = size + 'rem'
snowflake.style.opacity = opacity
})
})
</script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;600;700&display=swap');
:root {
--primary-color: #e63946;
--secondary-color: #a8dadc;
--dark-color: #1d3557;
--light-color: #f1faee;
--accent-color: #457b9d;
--transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
--card-shadow: 0 15px 30px rgba(0, 0, 0, 0.2);
--card-hover-shadow: 0 20px 40px rgba(231, 57, 70, 0.3);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Montserrat', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
min-height: 100vh;
overflow-x: hidden;
line-height: 1.6;
color: var(--light-color);
position: relative;
background: #0b0000; /* Deep black background */
}
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background:
radial-gradient(circle at 30% 20%, rgba(230, 57, 70, 0.25), transparent 40%),
radial-gradient(circle at 70% 80%, rgba(230, 57, 70, 0.1), transparent 40%),
radial-gradient(circle at 50% 50%, rgba(230, 57, 70, 0.15), transparent 50%),
linear-gradient(135deg, rgba(20, 0, 0, 0.9), rgba(11, 0, 0, 0.95));
backdrop-filter: blur(25px);
-webkit-backdrop-filter: blur(25px);
z-index: -2;
}
body::after {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background:
radial-gradient(circle at 10% 20%, rgba(255, 100, 100, 0.1), transparent 20%),
radial-gradient(circle at 90% 60%, rgba(255, 80, 80, 0.08), transparent 25%),
radial-gradient(circle at 50% 50%, rgba(255, 120, 120, 0.05), transparent 30%);
animation: glowEffect 15s infinite alternate;
z-index: -1;
mix-blend-mode: overlay;
}
@keyframes glowEffect {
0% {
opacity: 0.6;
background-position: 0% 0%;
}
33% {
opacity: 0.7;
background-position: 30% 30%;
}
66% {
opacity: 0.8;
background-position: 70% 70%;
}
100% {
opacity: 0.9;
background-position: 100% 100%;
}
}
#app {
position: relative;
min-height: 100vh;
}
/* Snowflake animation styles */
.snowflakes {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1;
}
.snowflake {
position: absolute;
top: -10px;
color: rgba(255, 200, 200, 0.8);
user-select: none;
pointer-events: none;
opacity: 0.5;
text-shadow: 0 0 10px rgba(230, 57, 70, 0.5);
animation: fall linear infinite;
}
@keyframes fall {
0% {
transform: translateY(-100vh) rotate(0deg);
}
100% {
transform: translateY(100vh) rotate(360deg);
}
}
</style>

View File

@@ -53,7 +53,7 @@ service.interceptors.response.use(
// 封装 get 请求
const get = (url, params = {}) => {
return service.get(url, { params });
return service.get(url, { params: params });
};
// 封装 post 请求

17
src/api/request/api.js Normal file
View File

@@ -0,0 +1,17 @@
import {get, post} from '../request.js'
const prefix = ''
/**
* 获取相册列表
*/
export function albumListApi() {
return get(`${prefix}/list`)
}
/**
* 获取相册详情
*/
export function albumDetailApi(data) {
// return get(`${prefix}/detail?id=${data.id}`, data)
return get(`${prefix}/detail`, data)
}

View File

@@ -2,29 +2,29 @@
</script>
<template>
<header class="bg-gradient-to-r from-blue-500 to-indigo-600 text-white py-4 px-6 flex items-center justify-between">
<div class="flex items-center space-x-4">
<!-- Logo -->
<img src="https://via.placeholder.com/40" alt="Logo" class="h-8 w-8 rounded-full">
<span class="font-bold">My App</span>
</div>
<!-- <header class="bg-gradient-to-r from-blue-500 to-indigo-600 text-white py-4 px-6 flex items-center justify-between">-->
<!-- <div class="flex items-center space-x-4">-->
<!-- &lt;!&ndash; Logo &ndash;&gt;-->
<!-- <img src="https://via.placeholder.com/40" alt="Logo" class="h-8 w-8 rounded-full">-->
<!-- <span class="font-bold">My App</span>-->
<!-- </div>-->
<!-- Menu -->
<nav class="hidden md:flex space-x-4">
<a href="#" class="hover:text-gray-300">首页</a>
<a href="#" class="hover:text-gray-300">About</a>
<a href="#" class="hover:text-gray-300">Services</a>
<a href="#" class="hover:text-gray-300">Contact</a>
</nav>
<!-- &lt;!&ndash; Menu &ndash;&gt;-->
<!-- <nav class="hidden md:flex space-x-4">-->
<!-- <a href="#" class="hover:text-gray-300">首页</a>-->
<!-- <a href="#" class="hover:text-gray-300">About</a>-->
<!-- <a href="#" class="hover:text-gray-300">Services</a>-->
<!-- <a href="#" class="hover:text-gray-300">Contact</a>-->
<!-- </nav>-->
<!-- User Profile -->
<div class="flex items-center space-x-4">
<button class="text-sm bg-gray-800 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded">
Personal Center
</button>
<img src="https://via.placeholder.com/40" alt="User Avatar" class="h-8 w-8 rounded-full cursor-pointer">
</div>
</header>
<!-- &lt;!&ndash; User Profile &ndash;&gt;-->
<!-- <div class="flex items-center space-x-4">-->
<!-- <button class="text-sm bg-gray-800 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded">-->
<!-- Personal Center-->
<!-- </button>-->
<!-- <img src="https://via.placeholder.com/40" alt="User Avatar" class="h-8 w-8 rounded-full cursor-pointer">-->
<!-- </div>-->
<!-- </header>-->
</template>
<style lang="scss" scoped>

View File

@@ -6,9 +6,10 @@ import MainLayout from '@/layouts/Header.vue'
const routes = [
{
path: '/',
component: MainLayout,
// component: MainLayout,
children: [
{ path: '', name: 'Home', component: () => import('@/views/index/index.vue') },
{ path: '/detail/:id', name: 'Detail', component: () => import('@/views/index/detail.vue') },
]
},
]

607
src/views/index/detail.vue Normal file
View File

@@ -0,0 +1,607 @@
<template>
<div class="container">
<!-- Updated main title styling to match original -->
<h1 class="main-title">我们的相册</h1>
<!-- Loading state -->
<div v-if="loading" class="loading-container">
<div class="loading-spinner"></div>
<p>加载照片中...</p>
</div>
<!-- 相册详情模块 -->
<div v-else class="album-detail" id="albumDetail">
<button class="back-button" @click="goBack">
<i class="fas fa-arrow-left"></i> 返回相册列表
</button>
<div class="detail-header">
<h2 class="detail-title">{{ albumData.title }}</h2>
</div>
<div class="photo-grid" id="photoGrid">
<div
v-for="(photo, index) in albumData.photos"
:key="index"
class="photo-item"
@click="showPhotoPreview(photo.compressed, index)"
>
<!-- Added loading spinner for individual photos -->
<div v-if="!photo.loaded" class="photo-loading-spinner"></div>
<img
:src="`http://i.nailaoyun.cn${photo.compressed}`"
:alt="`照片 ${index + 1}`"
class="photo-img"
:class="{ loaded: photo.loaded }"
@load="onPhotoLoad(photo)"
@error="onPhotoError(photo)"
loading="lazy"
/>
</div>
</div>
</div>
</div>
<!-- 图片预览模态框 -->
<div
v-if="previewModal.active"
class="preview-modal active"
:style="{ opacity: previewModal.opacity }"
>
<button class="close-modal" @click="closePhotoPreview">
<i class="fas fa-times"></i>
</button>
<button
class="nav-btn prev-btn"
@click="showPrevPhoto"
:disabled="currentPhotoIndex <= 0"
>
<i class="fas fa-chevron-left"></i>
</button>
<div class="modal-content">
<img
class="modal-img"
:class="{ active: previewModal.imgActive }"
:src="`http://i.nailaoyun.cn${previewModal.currentImg}`"
alt="预览图片"
:style="{ transform: `rotate(${currentRotation}deg)` }"
/>
<div class="modal-tools">
<button class="tool-btn" @click="rotatePhoto('left')" title="向左旋转">
<i class="fas fa-undo"></i>
</button>
<button class="tool-btn" @click="rotatePhoto('right')" title="向右旋转">
<i class="fas fa-redo"></i>
</button>
<button class="tool-btn" @click="downloadPhoto" title="下载图片">
<i class="fas fa-download"></i>
</button>
</div>
</div>
<button
class="nav-btn next-btn"
@click="showNextPhoto"
:disabled="currentPhotoIndex >= albumData.photos.length - 1"
>
<i class="fas fa-chevron-right"></i>
</button>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, onUnmounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { albumDetailApi } from "@/api/request/api.js"
const router = useRouter()
const route = useRoute()
const loading = ref(true)
const albumData = reactive({
title: '',
photos: []
})
const previewModal = reactive({
active: false,
opacity: '0',
imgActive: false,
currentImg: ''
})
const currentPhotoIndex = ref(0)
const currentRotation = ref(0)
const fetchAlbumDetail = async (albumId) => {
try {
loading.value = true
const res = await albumDetailApi({ id: albumId })
console.log(res)
albumData.photos = res.photos.map(photo => ({
...photo,
loaded: false
}))
albumData.title = res.title
} catch (error) {
console.error('获取相册详情失败:', error)
// Fallback to empty state on error
albumData.photos = []
albumData.title = '相册加载失败'
} finally {
loading.value = false
}
}
const onPhotoLoad = (photo) => {
photo.loaded = true
}
const onPhotoError = (photo) => {
photo.loaded = true
// Could add error state handling here
console.warn('Photo failed to load:', photo.compressed)
}
const goBack = () => {
router.push('/')
}
const showPhotoPreview = (photo, index) => {
previewModal.currentImg = photo
currentPhotoIndex.value = index
currentRotation.value = 0
previewModal.active = true
setTimeout(() => {
previewModal.opacity = '1'
setTimeout(() => {
previewModal.imgActive = true
}, 50)
}, 10)
}
const closePhotoPreview = () => {
previewModal.imgActive = false
previewModal.opacity = '0'
setTimeout(() => {
previewModal.active = false
}, 300)
}
const showPrevPhoto = () => {
if (currentPhotoIndex.value > 0) {
currentPhotoIndex.value--
previewModal.imgActive = false
setTimeout(() => {
previewModal.currentImg = albumData.photos[currentPhotoIndex.value].compressed
currentRotation.value = 0
setTimeout(() => {
previewModal.imgActive = true
}, 50)
}, 300)
}
}
const showNextPhoto = () => {
if (currentPhotoIndex.value < albumData.photos.length - 1) {
currentPhotoIndex.value++
previewModal.imgActive = false
setTimeout(() => {
previewModal.currentImg = albumData.photos[currentPhotoIndex.value].compressed
currentRotation.value = 0
setTimeout(() => {
previewModal.imgActive = true
}, 50)
}, 300)
}
}
const rotatePhoto = (direction) => {
if (direction === 'left') {
currentRotation.value -= 90
} else {
currentRotation.value += 90
}
}
const downloadPhoto = () => {
if (!albumData.photos.length) return
const link = document.createElement('a')
link.href = `http://i.nailaoyun.cn${albumData.photos[currentPhotoIndex.value].original || albumData.photos[currentPhotoIndex.value].compressed}`
link.download = `photo_${albumData.title}_${Date.now()}.jpg`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
// 键盘导航
const handleKeydown = (e) => {
if (previewModal.active) {
if (e.key === 'ArrowLeft') {
showPrevPhoto()
} else if (e.key === 'ArrowRight') {
showNextPhoto()
} else if (e.key === 'Escape') {
closePhotoPreview()
}
}
}
onMounted(() => {
const albumId = route.params.id
fetchAlbumDetail(albumId)
document.addEventListener('keydown', handleKeydown)
window.scrollTo({ top: 0, behavior: 'smooth' })
})
onUnmounted(() => {
document.removeEventListener('keydown', handleKeydown)
})
</script>
<style scoped>
/* Updated all styling to match original dark red theme with frosted glass effects */
.container {
max-width: 1400px;
margin: 0 auto;
padding: 3rem;
position: relative;
font-family: 'Montserrat', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.main-title {
color: var(--light-color);
text-align: center;
margin-bottom: 3rem;
font-weight: 300;
letter-spacing: 3px;
font-size: 2.5rem;
text-transform: uppercase;
position: relative;
padding-bottom: 1rem;
text-shadow: 0 0 10px rgba(230, 57, 70, 0.3);
}
.main-title::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 100px;
height: 3px;
background: var(--primary-color);
box-shadow: 0 0 10px rgba(230, 57, 70, 0.5);
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 300px;
color: var(--light-color);
}
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid rgba(168, 218, 220, 0.3);
border-radius: 50%;
border-top-color: var(--secondary-color);
animation: spin 1s ease-in-out infinite;
margin-bottom: 1rem;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.album-detail {
animation: fadeIn 0.5s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.back-button {
background: rgba(230, 57, 70, 0.3);
border: none;
font-size: 1rem;
color: var(--light-color);
cursor: pointer;
margin-bottom: 2rem;
display: flex;
align-items: center;
gap: 0.5rem;
transition: var(--transition);
padding: 0.8rem 1.5rem;
border-radius: 30px;
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
}
.back-button:hover {
background: rgba(230, 57, 70, 0.5);
transform: translateX(-5px);
box-shadow: 0 0 10px rgba(230, 57, 70, 0.3);
}
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 3rem;
}
.detail-title {
font-size: 2.5rem;
font-weight: 300;
color: var(--light-color);
margin: 0;
letter-spacing: 2px;
text-transform: uppercase;
text-shadow: 0 0 10px rgba(230, 57, 70, 0.2);
}
.photo-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 2rem;
}
.photo-item {
position: relative;
border-radius: 12px;
overflow: hidden;
box-shadow: var(--card-shadow);
transition: var(--transition);
aspect-ratio: 4/3;
background: rgba(29, 0, 0, 0.5);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
transform: scale(1);
border: 1px solid rgba(230, 57, 70, 0.1);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.photo-item:hover {
transform: scale(1.03);
box-shadow: var(--card-hover-shadow);
z-index: 2;
border-color: var(--primary-color);
}
.photo-img {
width: 100%;
height: 100%;
object-fit: cover;
transition: var(--transition);
opacity: 0;
transform: scale(0.95);
}
.photo-img.loaded {
opacity: 1;
transform: scale(1);
}
/* Added optimized loading spinner for individual photos */
.photo-loading-spinner {
width: 30px;
height: 30px;
border: 3px solid rgba(168, 218, 220, 0.3);
border-radius: 50%;
border-top-color: var(--secondary-color);
animation: spin 1s ease-in-out infinite;
position: absolute;
z-index: 2;
}
.preview-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.95);
z-index: 1000;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s ease;
backdrop-filter: blur(15px);
-webkit-backdrop-filter: blur(15px);
display: flex;
}
.preview-modal.active {
opacity: 1;
}
.modal-content {
position: relative;
max-width: 90%;
max-height: 90%;
display: flex;
flex-direction: column;
align-items: center;
}
.modal-img {
max-width: 100%;
max-height: 80vh;
object-fit: contain;
border-radius: 8px;
transform: scale(0.5);
transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.3s ease;
opacity: 0;
box-shadow: 0 20px 40px rgba(230, 57, 70, 0.2);
}
.modal-img.active {
transform: scale(1);
opacity: 1;
}
.modal-tools {
display: flex;
gap: 1.5rem;
margin-top: 2rem;
}
.tool-btn {
background: rgba(230, 57, 70, 0.3);
border: none;
width: 50px;
height: 50px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: var(--transition);
color: white;
font-size: 1.2rem;
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
}
.tool-btn:hover {
background: rgba(230, 57, 70, 0.5);
transform: scale(1.1);
box-shadow: 0 0 10px rgba(230, 57, 70, 0.3);
}
.nav-btn {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 60px;
height: 60px;
background: rgba(230, 57, 70, 0.3);
border: none;
border-radius: 50%;
color: white;
font-size: 1.5rem;
cursor: pointer;
transition: var(--transition);
z-index: 10;
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
}
.nav-btn:hover:not(:disabled) {
background: rgba(230, 57, 70, 0.5);
transform: translateY(-50%) scale(1.1);
}
.nav-btn:disabled {
opacity: 0.3;
cursor: not-allowed;
}
.prev-btn {
left: 30px;
}
.next-btn {
right: 30px;
}
.close-modal {
position: absolute;
top: 40px;
right: 40px;
background: rgba(230, 57, 70, 0.3);
border: none;
width: 50px;
height: 50px;
border-radius: 50%;
color: white;
font-size: 1.5rem;
cursor: pointer;
transition: var(--transition);
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
}
.close-modal:hover {
background: rgba(230, 57, 70, 0.5);
transform: rotate(90deg);
}
@media (max-width: 1024px) {
.container {
padding: 2rem;
}
.photo-grid {
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
}
}
@media (max-width: 768px) {
.container {
padding: 1.5rem;
}
.detail-title {
font-size: 2rem;
}
.photo-grid {
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1.5rem;
}
.tool-btn, .close-modal {
width: 45px;
height: 45px;
font-size: 1.1rem;
}
.nav-btn {
width: 50px;
height: 50px;
font-size: 1.3rem;
}
}
@media (max-width: 480px) {
.container {
padding: 1rem;
}
.detail-title {
font-size: 1.8rem;
}
.photo-grid {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -1,11 +1,305 @@
<script setup lang="ts">
</script>
<template>
你好
<div class="album-index">
<div class="container">
<h1 class="main-title">我们的相册</h1>
<div v-if="loading" class="loading-container">
<div class="loading-spinner"></div>
<p>加载中...</p>
</div>
<div v-else class="album-selection">
<div
v-for="album in albums"
:key="album.id"
class="album-card"
@click="goToAlbum(album.id)"
>
<div class="album-cover">
<!-- Added optimized image loading with lazy loading and error handling -->
<img
:src="`http://i.nailaoyun.cn${album.url}`"
:alt="album.title"
loading="lazy"
@load="onImageLoad"
@error="onImageError"
class="cover-image"
/>
<div class="album-overlay">
<div class="album-info">
<h3 class="album-title">{{ album.title }}</h3>
<p class="album-date">{{ formatDate(album.created_at) }}</p>
<!-- Added photo count display -->
<p class="album-count">{{ album.photo_count }} 张照片</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { albumListApi } from "@/api/request/api.js"
const router = useRouter()
const albums = ref([])
const loading = ref(true)
const getList = async () => {
loading.value = true
try {
const res = await albumListApi()
albums.value = res
} catch (error) {
console.error('获取相册列表失败:', error)
// Fallback to empty array on error
albums.value = []
} finally {
loading.value = false
}
}
const onImageLoad = (event) => {
event.target.classList.add('loaded')
}
const onImageError = (event) => {
// Fallback to placeholder on error
event.target.src = '/placeholder.svg?height=300&width=400&text=加载失败'
event.target.classList.add('error')
}
const formatDate = (dateString) => {
const date = new Date(dateString)
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric'
})
}
const goToAlbum = (albumId) => {
router.push(`/detail/${albumId}`)
}
onMounted(() => {
getList()
})
</script>
<style scoped>
.album-index {
font-family: 'Montserrat', sans-serif;
min-height: 100vh;
position: relative;
z-index: 2;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 3rem;
position: relative;
}
.main-title {
color: var(--light-color);
text-align: center;
margin-bottom: 3rem;
font-weight: 300;
letter-spacing: 3px;
font-size: 2.5rem;
text-transform: uppercase;
position: relative;
padding-bottom: 1rem;
text-shadow: 0 0 10px rgba(230, 57, 70, 0.3);
}
.main-title::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 100px;
height: 3px;
background: var(--primary-color);
box-shadow: 0 0 10px rgba(230, 57, 70, 0.5);
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 300px;
color: var(--light-color);
}
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid rgba(168, 218, 220, 0.3);
border-radius: 50%;
border-top-color: var(--secondary-color);
animation: spin 1s ease-in-out infinite;
margin-bottom: 1rem;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.album-selection {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 3rem;
margin-top: 3rem;
}
.album-card {
background: rgba(29, 0, 0, 0.7);
border-radius: 16px;
overflow: hidden;
box-shadow: var(--card-shadow);
transition: var(--transition);
cursor: pointer;
position: relative;
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(230, 57, 70, 0.2);
transform: translateY(0);
}
.album-card:hover {
transform: translateY(-10px);
box-shadow: var(--card-hover-shadow);
border-color: var(--primary-color);
}
.album-cover {
height: 240px;
position: relative;
transition: var(--transition);
background-color: rgba(20, 0, 0, 0.6);
overflow: hidden;
}
/* Added optimized image styles */
.cover-image {
width: 100%;
height: 100%;
object-fit: cover;
transition: all 0.3s ease;
opacity: 0;
transform: scale(1.1);
}
.cover-image.loaded {
opacity: 1;
transform: scale(1);
}
.cover-image.error {
opacity: 0.5;
filter: grayscale(100%);
}
.album-card:hover .cover-image {
transform: scale(1.05);
}
.album-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(to top, rgba(0, 0, 0, 0.8), transparent);
display: flex;
align-items: flex-end;
justify-content: center;
padding: 2rem;
opacity: 1;
transition: var(--transition);
}
.album-info {
padding: 2rem;
text-align: center;
}
.album-title {
font-size: 1.4rem;
font-weight: 600;
margin-bottom: 0.5rem;
color: var(--light-color);
letter-spacing: 1px;
text-shadow: 0 0 5px rgba(0, 0, 0, 0.5);
}
.album-date {
font-size: 0.9rem;
color: var(--secondary-color);
letter-spacing: 1px;
}
/* Added photo count styling */
.album-count {
font-size: 0.8rem;
color: var(--secondary-color);
opacity: 0.8;
margin-top: 0.3rem;
}
@media (max-width: 1024px) {
.container {
padding: 2rem;
}
.album-selection {
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 2rem;
}
}
@media (max-width: 768px) {
.container {
padding: 1.5rem;
}
.main-title {
font-size: 2rem;
margin-bottom: 2rem;
}
.album-selection {
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 1.5rem;
}
.album-cover {
height: 200px;
}
}
@media (max-width: 480px) {
.container {
padding: 1rem;
}
.main-title {
font-size: 1.8rem;
}
.album-selection {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -19,7 +19,8 @@ export default defineConfig({
proxy: {
'/api': {
// target: 'http://127.0.0.1:18007/api/',
target: 'http://127.0.0.1:18001/api/',
// target: 'http://127.0.0.1:18001/api/',
target: 'http://i.nailaoyun.cn/api/',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}