CQU Web开发实验3 照片管理器

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">&times;</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");
});

本技术内容仅供学习和交流使用,如有疑问请联系qq2014160588并注明来意。请确保在使用过程中遵守相关法律法规。任何因使用本技术内容而导致的直接或间接损失,作者概不负责。用户需自行承担因使用本技术内容而产生的所有风险和责任。请勿将本技术内容用于任何非法用途。
上一篇
下一篇