1构思&架构设计
1.1构思
主页用来显示所有照片,照片应该缩小后显示,带边框,看起来整洁
显示照片的地方有筛选选项,可根据时间地点筛选
底部有上传照片的按钮,上传时应给出时间和地点
1.2架构
由于项目较小,不需要前后端分离
前端vue+ts
后端node
2新建项目
npm create vue@latest
安装必要包在后面
3项目目录
3.1PhotoGallery.vue用于展示照片
<template>
<div class="photo-gallery">
<h2>照片库</h2>
<div class="filters">
<input v-model="filterDate" type="date" placeholder="选择日期" />
<input v-model="filterLocation" type="text" placeholder="输入地点" />
<button @click="loadPhotos">筛选</button>
</div>
<div class="photos">
<div v-for="photo in filteredPhotos" :key="photo.id" class="photo-item" @click="showPhoto(photo)">
<img :src="photo.url" :alt="`Photo taken on ${photo.date} at ${photo.location}`" />
<p>{{ photo.date }} - {{ photo.location }}</p>
</div>
</div>
<div v-if="selectedPhoto" class="modal" @click="closeModal">
<div class="modal-content" @click.stop>
<span class="close" @click="closeModal">×</span>
<img :src="selectedPhoto.url" :alt="`Photo taken on ${selectedPhoto.date} at ${selectedPhoto.location}`" />
<p>{{ selectedPhoto.date }} - {{ selectedPhoto.location }}</p>
</div>
</div>
</div>
</template>
<script>
import axios from "axios";
export default {
name: "PhotoGallery",
data() {
return {
photos: [],
filterDate: "",
filterLocation: "",
selectedPhoto: null,
};
},
computed: {
filteredPhotos() {
return this.photos.filter((photo) => {
const matchesDate = this.filterDate ? photo.date === this.filterDate : true;
const matchesLocation = this.filterLocation
? photo.location.includes(this.filterLocation)
: true;
return matchesDate && matchesLocation;
});
},
},
methods: {
async loadPhotos() {
try {
const response = await axios.get("http://localhost:3000/photos");
this.photos = response.data;
} catch (error) {
console.error("Error loading photos:", error);
}
},
showPhoto(photo) {
this.selectedPhoto = photo;
},
closeModal() {
this.selectedPhoto = null;
},
},
created() {
this.loadPhotos();
},
};
</script>
<style>
.photo-gallery {
text-align: left;
}
.filters {
margin-bottom: 20px;
}
.photos {
display: flex;
flex-direction: column;
gap: 16px;
}
.photo-item {
border: 1px solid #ccc;
padding: 10px;
cursor: pointer;
transition: transform 0.2s;
}
.photo-item:hover {
transform: scale(1.05);
}
.photo-item img {
max-width: 100%;
height: auto;
display: block;
margin: 0 auto;
}
.modal {
display: flex;
justify-content: center;
align-items: center;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
}
.modal-content {
background-color: white;
padding: 20px;
border-radius: 5px;
text-align: center;
}
.modal-content img {
max-width: 100%;
height: auto;
}
.close {
position: absolute;
top: 10px;
right: 10px;
font-size: 24px;
cursor: pointer;
}
</style>
3.2UploadPhoto.vue 上传照片和前后端相连的逻辑
<template>
<div class="upload-photo">
<h2>上传照片</h2>
<form @submit.prevent="uploadPhoto">
<input type="file" @change="onFileChange" required />
<input type="date" v-model="date" required />
<input type="text" v-model="location" placeholder="输入地点" required />
<button type="submit">上传</button>
</form>
<div v-if="message">{{ message }}</div>
</div>
</template>
<script>
import axios from "axios";
export default {
name: "UploadPhoto",
data() {
return {
file: null,
date: "",
location: "",
message: "",
};
},
methods: {
onFileChange(event) {
this.file = event.target.files[0];
},
async uploadPhoto() {
const formData = new FormData();
formData.append("file", this.file);
formData.append("date", this.date);
formData.append("location", this.location);
try {
const response = await axios.post("http://localhost:3000/upload", formData, {
headers: {
"Content-Type": "multipart/form-data",
},
});
this.message = response.data.message; // 显示成功消息
this.$emit("photoUploaded"); // 通知父组件照片上传成功
} catch (error) {
this.message = "上传失败: " + (error.response?.data || error.message); // 显示错误消息
console.error("Error uploading photo:", error);
}
},
},
};
</script>
<style>
.upload-photo {
margin: 20px 0;
}
</style>
3.3App.vue
<template>
<div id="app">
<h1>照片管理系统</h1>
<!-- 照片库组件,显示所有照片 -->
<PhotoGallery ref="photoGallery" />
<!-- 上传照片组件,位于页面底部 -->
<UploadPhoto @photoUploaded="refreshPhotos" />
</div>
</template>
<script>
import PhotoGallery from "./components/PhotoGallery.vue";
import UploadPhoto from "./components/UploadPhoto.vue";
export default {
name: "App",
components: {
PhotoGallery,
UploadPhoto,
},
methods: {
// 调用 PhotoGallery 组件中的 loadPhotos 方法来加载照片
refreshPhotos() {
this.$refs.photoGallery.loadPhotos();
},
},
mounted() {
this.refreshPhotos(); // 页面加载时自动调用,显示所有照片
},
};
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
text-align: center;
color: #2c3e50;
margin-top: 20px;
}
h1 {
font-size: 2rem;
color: #42b983;
}
</style>
3.4 uploads文件夹和photos.json元数据
根目录创建该文件夹,用于存放上传的照片,如果项目较大,可以前后端分离,放在后端,让前端调用
photos.json用于存放照片的元数据,用于筛选功能,时间地点
其编写逻辑在前面PhotoGallery.vue
3.5server.js后端服务器逻辑
提供上传、显示照片的API 还有固定刷新
import express from "express";
import multer from "multer";
import cors from "cors";
import path from "path";
import { fileURLToPath } from "url";
import fs from "fs";
const app = express();
app.use(cors());
app.use(express.json());
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const uploadPath = path.join(__dirname, "uploads/");
if (!fs.existsSync(uploadPath)) {
fs.mkdirSync(uploadPath);
}
cb(null, uploadPath);
},
filename: (req, file, cb) => {
cb(null, `${Date.now()}-${file.originalname}`);
},
});
const upload = multer({ storage });
// 上传照片的接口
app.post("/upload", upload.single("file"), (req, res) => {
if (!req.file) {
return res.status(400).send("No file uploaded.");
}
const newPhoto = {
id: Date.now(),
url: `/uploads/${req.file.filename}`,
date: req.body.date,
location: req.body.location,
};
const photos = JSON.parse(fs.readFileSync("photos.json", "utf-8") || "[]");
photos.push(newPhoto);
fs.writeFileSync("photos.json", JSON.stringify(photos));
res.status(200).json({
message: "File uploaded successfully",
filePath: newPhoto.url,
});
});
// 获取照片列表的接口
app.get("/photos", (req, res) => {
const photos = JSON.parse(fs.readFileSync("photos.json", "utf-8") || "[]");
res.json(photos);
});
app.use("/uploads", express.static(path.join(__dirname, "uploads")));
app.listen(3000, () => {
console.log("Server running on http://localhost:3000");
});