前言

今天本文将会详细讲解智能电影推荐系统项目中的前端部分,包括其页面设计,算法设计,不同功能的实现,以及各页面之间是如何实现跳转的,并会附上相关核心代码。

技术栈

前端使用的技术栈有:

Vue 3(页面渲染)、Vue Router 4(路由管理)、Axios(接口请求)、CSS3(样式设计)。

项目配置依赖如下:

{
  "name": "movie-front",
  "version": "0.0.0",
  "scripts": {
    "dev": "vite",           // 启动开发服务器
    "build": "vite build",   // 构建生产版本
    "preview": "vite preview" // 预览生产构建
  },
  "dependencies": {
    "axios": "^1.6.0",       // HTTP客户端库
    "vue": "^3.3.4",         // Vue 3框架
    "vue-router": "^4.2.4"   // Vue路由
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^4.2.3", // Vite的Vue插件
    "vite": "^4.4.5"                // 构建工具
  }
}

前端详细设计

核心功能的设计思想

前端采用 Vue 3 的<script setup>语法,核心页面开发:

(1)首页(Home.vue):集成推荐列表、排行榜、搜索、登录弹窗,支持无刷新路由跳转;

(2)详情页(Detail.vue):沉浸式背景虚化设计,展示电影核心信息与相关推荐;

(3)AI 对话页(AiChat.vue):消息列表 + 输入框布局,支持自动滚动与异常兜底;

(4)管理员后台(Admin.vue):侧边栏 + 主面板布局,实现电影管理、用户审计、数据统计。

用户(未登录)→首页浏览(随机推荐)→搜索电影→查看详情→AI咨询→登录/注册 →查看最近观看。

用户(登录)→首页浏览(相似电影推荐)→搜索电影→查看详情→AI咨询→登录/注册 →查看最近观看。

管理员 → 管理员登录页 → 后台(数据统计/电影管理/用户审计)→ 退出。

实现方法

(1)全局路由与管理员权限首位

导入Vue与所有页面组件并定义路由规则。创建路由实例,配置 Hash 模式,挂载路由规则。

(2)统一 API 封装与工具函数内聚,登录/注册弹窗交互

通过axios调用HTTP请求库。创建axios实例并配置核心参数,导出 axios 实例封装业务接口。

(3)首页综合交互(Home.vue)

调用login/register接口,根据角色跳转(管理员→后台,普通用户→刷新首页),表单状态自动清空。输入关键词+选择类型,调用searchMovies接口、getRankings接口进行页面操作。Hero 页面hover放大、AI 悬浮入口hover显示提示、电影卡片hover 缩放。

(4)电影详情沉浸式展示(Detail.vue)

通过props接收路由传递的电影 ID。定义响应式数据movie存储电影详情。页面挂载时调用getDetail接口,加载对应电影的完整数据。

实现源代码

(1)全局路由与管理员权限首位

// 1. 定义路由映射
const routes = [
  { path: '/', component: Home }, // 首页路由
  { path: '/movie/:id', component: Detail }, // 电影详情页,:id是动态参数
  { path: '/admin', component: Admin }, // 管理页面
  { path: '/ai-chat', component: AiChat } // AI聊天页面
]

// 2. 创建路由实例
const router = createRouter({
  history: createWebHashHistory(), // 使用hash模式路由(URL中有#)
  routes // 路由配置
})

// 3. 创建并启动 Vue 应用
const app = createApp(App) // 创建Vue应用实例
app.use(router) // 注入路由
app.mount('#app') // 挂载到 index.html 中的 #app 节点

    路由配置的核心部分,实现了能够在页面之间跳转的功能,点击Button按钮后应用路由实列进行页面交互。

(2)首页核心交互

#————————搜索交互————————

<!-- 模板:搜索栏交互触发 -->

<select v-model="searchState.type" class="search-type">...</select>

<input v-model="searchState.keyword" @keyup.enter="handleSearch" />

<button @click="handleSearch">...</button>

<!-- 脚本:搜索逻辑实现 -->

const handleSearch = async () => {

if (!searchState.keyword) return;

searchState.isSearching = true;

constres=awaitapi.get(`/movies/search?q=${searchState.keyword}&type=${searchState.type}`);

searchResults.value = res.data;

};

// 重置搜索(返回首页)

const resetHome = () => {

searchState.isSearching = false;

searchState.keyword = "";

};

#————————登录/注册交互————————

<!-- 模板:弹窗显隐+表单交互 -->

<button v-else class="btn-login" @click="showAuth = true">登录 / 注册</button>

<div v-if="showAuth" class="auth-overlay" @click.self="closeAuthAndClear">

<input v-model="authForm.username" placeholder="用户名" />

<input v-model="authForm.password" type="password" @keyup.enter="handleAuth" />

<button class="btn-submit" @click="handleAuth">确认</button>

</div>



<!-- 脚本:登录/注册核心逻辑 -->

const handleAuth = async() => {

if (!authForm.username || !authForm.password) return alert("请填写完整");

const path = isLoginMode.value ? "/auth/login" : "/auth/register";

try {

const res = await api.post(path, { username: authForm.username, password: authForm.password, role: authForm.role });

if (res.data.role === "admin") router.push("/admin"); // 管理员跳转后台

else location.reload(); // 普通用户刷新加载个性化数据

} catch (e) { alert("账号或密码错误"); }

};

// 表单清空/弹窗关闭交互

const clearAuthForm = () => { authForm.username = ""; authForm.password = ""; };

const closeAuthAndClear = () => { showAuth.value = false; clearAuthForm(); };

#————————电影跳转交互————————

<!-- 模板:全页面电影卡片/巨幕跳转 -->

<div class="hero" @click="$router.push('/movie/' + recs[0].id)">...</div>

<div class="card" v-for="m in recs" :key="m.id" @click="$router.push('/movie/' + m.id)">...</div>

<div class="rank-item" v-for="(m, idx) in rankList" :key="m.id" @click="$router.push('/movie/' + m.id)">...</div>

#————————个性化推荐加载交互————————

<!-- 模板:全页面电影卡片/巨幕跳转 -->

<div class="hero" @click="$router.push('/movie/' + recs[0].id)">...</div>

<div class="card" v-for="m in recs" :key="m.id" @click="$router.push('/movie/' + m.id)">...</div>

<div class="rank-item" v-for="(m, idx) in rankList" :key="m.id" @click="$router.push('/movie/' + m.id)">...</div>

#————————AI 咨询入口交互————————

<!-- 模板:推荐列表渲染 -->

<div class="container" v-if="recs.length > 0">

<div class="movie-scroll">

<div class="card" v-for="m in recs" :key="m.id">...</div>

</div>

</div>

<!-- 脚本:页面挂载加载推荐数据 -->

onMounted(async () => { initPage(); });

const initPage = async () => {

const res = await api.get("/movies/home"); // 请求后端推荐算法结果

recs.value = res.data.recs; // 渲染“为你推荐”列表

fetchRankings(); // 加载电影榜单

// 登录用户加载观影足迹(个性化数据)

const userRes = await api.get("/auth/user_info");

if (userRes.data.isLogin) {

Object.assign(user, userRes.data);

const fRes = await api.get("/user/footprints");

footprints.value = fRes.data;

}

};

首页,包含导航栏、搜索、登录/注册、电影推荐列表、排行榜等。

模板:结构复杂,包含多个部分,如导航栏、登录注册弹窗、AI悬浮入口、英雄区、推荐列表、排行榜等。

脚本:处理用户认证、搜索、加载推荐电影和排行榜等。

样式:首页的样式,使用flex和grid布局,响应式设计。

(3)电影详情页(Detail.vue)

<template>

<div class="detail-root" v-if="movie">

<!-- 注释:背景虚化+海报+核心信息卡片,非核心渲染结构 -->

<div class="blur-bg" :style="{ backgroundImage: `url(${getImg(movie.id)})` }"></div>

<div class="content">

<!-- 核心:电影详情卡片 -->

<div class="card">

<img :src="getImg(movie.id)" class="poster" />

<div class="info">

<h1>{{ movie.title }}</h1>

<p class="meta">评分: <span> {{ movie.score }}</span></p>

<!-- 注释:导演/主演/标签/简介,非核心渲染 -->

</div>

</div>

<!-- 核心:相似电影推荐(跳转逻辑) -->

<div class="movie-scroll">

<div class="card-small" v-for="r in movie.related" :key="r.id" @click="reload(r.id)">

<img :src="getImg(r.id)" />

</div>

</div>

</div>

</div>

</template>

<script setup>

import { ref, onMounted } from "vue";

import { useRoute, useRouter } from "vue-router";

import { getDetail } from "../api/index.js";

// 1. 关键性代码:路由与响应式数据定义

const route = useRoute();

const router = useRouter();

const movie = ref(null); // 电影详情核心数据

// 注释:图片映射算法,与首页一致

const getImg = (id) => `/images/${((id * 17) % 300) + 1}.jpg`;

// 2. 关键性代码:加载电影详情核心逻辑

const load = async (id) => {

try {

// 核心:调用详情接口,获取电影数据

const detailRes = await getDetail(id);

movie.value = detailRes;

window.scrollTo({ top: 0, behavior: "smooth" }); // 核心:滚动至顶部,优化体验

} catch (e) {

alert("加载详情失败!");

router.push("/");

}

};

// 3. 关键性代码:页面挂载与相似电影跳转

onMounted(() => load(route.params.id)); // 初始化加载

const reload = (id) => { // 相似电影跳转

router.push("/movie/" + id);

load(id);

};

</script>

<style scoped>

.detail-root { background: #000; min-height: 100vh; color: #fff; }

.blur-bg { position: fixed; top: 0; width: 100%; height: 100%; filter: blur(60px) brightness(0.2); }

.card { background: rgba(255,255,255,0.05); backdrop-filter: blur(20px); border-radius: 24px; padding: 40px; }

</style>

 电影详情页面,展示电影的详细信息及相关推荐。

 模板:电影海报、标题、导演、演员、简介等,以及相关推荐电影列表。

 脚本:根据路由参数加载电影详情,并定义图片获取方法(与首页一致)。

 样式:电影详情页的样式,包括背景虚化、卡片布局等。

(4)AI 对话页(AiChat.vue

<template>

# 聊天页面整体容器

<div class="ai-page">

# 导航栏(返回、标题)

<nav class="ai-nav">...</nav>

# 聊天窗口(消息列表)

<div class="chat-container">

<div class="chat-window" id="chatBox">

# 消息项循环渲染

<div v-for="(msg, idx) in messages" :key="idx" :class="['msg-wrapper', msg.role]">

<div class="bubble">{{ msg.content }}</div>

</div>

# 加载中动画

<div v-if="loading" class="loading">...</div>

</div>

# 输入框

<div class="input-area">

<input

v-model="userInput"

@keyup.enter="sendMessage"

placeholder="输入你想聊的电影话题..."

:disabled="loading"

/>

<button @click="sendMessage" :disabled="loading">发送</button>

</div>

</div>

</div>

</template>

<script setup>

# 导入核心依赖

# 响应式数据定义

const userInput = ref("");

const loading = ref(false);

const messages = reactive([

{ role: "assistant", content: "你是一个电影专家,请简洁地回答用户关于电影的问题。" }

]);

# 核心:发送消息方法

const sendMessage = async () => {

# 输入校验(

if (!userInput.value.trim() || loading.value) return;

# 添加用户消息,清空输入框

messages.push({ role: "user", content: userInput.value });

userInput.value = "";

loading.value = true;

# 滚动到底部

scrollToBottom();

try {

const res = await aiChat(messages[messages.length - 1].content);

messages.push({ role: "assistant", content: res.reply });

} catch (e) {

# 异常兜底

messages.push({ role: "assistant", content: "抱歉,暂时无法回复你的问题。" });

} finally {

# 关闭加载状态,滚动到底部

loading.value = false;

scrollToBottom();

}

};

# 自动滚动到底部方法

const scrollToBottom = () => {

nextTick(() => {

const box = document.getElementById("chatBox");

box.scrollTop = box.scrollHeight;

});

};

</script>

<style scoped>

# 页面样式

</style>

AI聊天页面,用户可以与AI助手对话。

模板:聊天消息列表和输入框。

脚本:维护消息列表,发送用户消息到后端AI接口,并接收回复。

样式:聊天界面的样式,包括消息气泡、加载动画。

(5)后台登入页面(AdminLogin.vue)
<template>
  <div class="admin-auth-container">
    <div class="login-box">
      <h1 class="logo">CINEMA<span>ADMIN</span></h1>
      <p class="subtitle">管理后台安全认证</p>
      <div class="form">
        <!-- 用户名输入框 -->
        <input
          v-model="form.username"
          placeholder="管理员账号"
          class="admin-input"
        />
        <!-- 密码输入框,type="password"隐藏输入 -->
        <input
          v-model="form.password"
          type="password"
          placeholder="认证密码"
          class="admin-input"
          @keyup.enter="handleLogin" <!-- 按回车键触发登录 -->
        />
        <!-- 登录按钮 -->
        <button class="admin-btn" @click="handleLogin">认证并进入</button>
      </div>
      <!-- 返回首页链接 -->
      <p class="back-link" @click="$router.push('/')">← 返回普通用户首页</p>
    </div>
  </div>
</template>

<script setup>
import { ref } from "vue";
import api from "../api";
import { useRouter } from "vue-router";

const router = useRouter();
const form = ref({ username: "", password: "", role: "admin" }); // 表单数据

const handleLogin = async () => {
  try {
    const res = await api.post("/auth/login", form.value);
    alert(res.data.msg);
    router.push("/admin"); // 登录成功跳转到管理页面
  } catch (e) {
    alert("认证失败:非管理员账号或密码错误");
  }
};
</script>

后台登录页面,用于管理员登录。

模板:一个简单的登录表单,包含用户名和密码输入框。

脚本:处理登录逻辑,提交表单到后端,登录成功后跳转到后台管理页面。

样式:设置了登录框的样式。

(6)后台管理页面模板部分(AdminLogin.vue)
<template>
  <div class="admin-wrapper">  <!-- 管理员页面容器 -->
    <!-- 侧边栏 -->
    <aside class="sidebar">
      <div class="logo">ADMIN<span>SYSTEM</span></div> <!-- 系统Logo -->
      <nav>
        <!-- 导航菜单,点击切换tab -->
        <div :class="{ active: tab === 'stats' }" @click="tab = 'stats'">
          📊 数据统计
        </div>
        <!-- 其他菜单项... -->
      </nav>
      <button class="exit-btn" @click="handleLogout">退出回到首页</button>
    </aside>

    <!-- 主面板 -->
    <main class="main-panel">
      <!-- 1. 统计面板 -->
      <div v-if="tab === 'stats'">
        <h2>系统运行状态</h2>
        <div class="stats-grid">
          <!-- 统计卡片,显示电影总量 -->
          <div class="box">
            <h3>{{ stats.movie_count || 0 }}</h3>
            <p>电影总量</p>
          </div>
          <!-- 其他统计卡片... -->
        </div>
      </div>

      <!-- 2. 电影管理面板 -->
      <div v-if="tab === 'movies'">
        <div class="table-header">
          <h2>影片库管理</h2>
          <!-- 新增影片按钮 -->
          <button class="add-btn" @click="openAdd">+ 新增影片</button>
        </div>
        <!-- 电影列表表格 -->
        <table class="table">
          <thead>
            <tr>
              <th>ID</th>
              <th>标题</th>
              <th>导演</th>
              <th>操作</th>
            </tr>
          </thead>
          <tbody>
            <!-- 循环渲染电影列表 -->
            <tr v-for="m in movies" :key="m.id">
              <td>{{ m.id }}</td>
              <td>{{ m.title }}</td>
              <td>{{ m.director }}</td>
              <td>
                <!-- 编辑和删除按钮 -->
                <button class="btn-edit" @click="openEdit(m)">修改</button>
                <button class="btn-del" @click="doDelete(m.id)">删除</button>
              </td>
            </tr>
          </tbody>
        </table>
      </div>

      <!-- 3. 用户管理面板 -->
      <div v-if="tab === 'users'">
        <h2>系统会员审计</h2>
        <table class="table">
          <thead>
            <tr>
              <th>ID</th>
              <th>用户名</th>
              <th>设置密码(明文)</th> <!-- 显示明文密码(安全风险) -->
              <th>注册日期</th>
            </tr>
          </thead>
          <tbody>
            <!-- 循环渲染用户列表 -->
            <tr v-for="c in clients" :key="c.id">
              <td>{{ c.id }}</td>
              <td>{{ c.username }}</td>
              <td style="color: #e50914; font-family: monospace; font-weight: bold;">
                {{ c.password }} <!-- 明文密码显示为红色 -->
              </td>
              <td>{{ c.date }}</td>
            </tr>
          </tbody>
        </table>
      </div>
    </main>

    <!-- 弹窗组件(添加/编辑电影) -->
    <div v-if="showModal" class="modal-mask">
      <div class="modal-box">
        <h3>{{ form.id ? "编辑影片信息" : "录入新影片" }}</h3>
        <!-- 表单输入项 -->
        <div class="form-item">
          <label>影片名称</label><input v-model="form.title" />
        </div>
        <!-- 其他表单字段... -->
        <div class="modal-btns">
          <button class="save" @click="saveMovie">确认提交</button>
          <button class="cancel" @click="showModal = false">取消</button>
        </div>
      </div>
    </div>
  </div>
</template>

后台管理页面,包含数据统计、电影管理和用户管理三个标签页。

接口交互模块

设计思想

通过开启withCredentials: true,保障跨域请求中Session信息不丢失,支撑项目中的用户/管理员登录认证功能,是整个项目身份校验的核心基础。封装项目中实际调用到的接口,沿用 Axios 原生调用格式,降低组件调用的学习和适配成本。

实现方法

导入项目 Axios模块,作为接口通信的基础工具。配置baseURL(统一接口前缀)和withCredentials: true两个核心参数,形成项目统一的接口请求入口。采用 Axios 原生get()/post()方法进行简单封装。默认暴露 Axios 实例,按需暴露封装好的接口函数,供页面组件直接导入调用。

实现源代码

import axios from 'axios';  // 导入axios库

// 创建axios实例,配置基础URL和跨域设置
const api = axios.create({
    baseURL: 'http://127.0.0.1:5000/api', // 后端API地址
    withCredentials: true // 允许跨域携带Session/Cookie
});

// 导出默认的api实例
export default api;

// 导出具体的API请求函数

// 获取首页数据
export const getHome = () => api.get('/movies/home');

// 获取电影详情
export const getDetail = (id) => api.get(`/movies/detail/${id}`);

// 用户登录
export const login = (data) => api.post('/auth/login', data);

// 获取用户足迹
export const getFootprints = () => api.get('/user/footprints');

封装所有与后端API通信的请求,API接口地址起到请求通信的作用,前端通过API向后端请求数据,后端收到请求后将各种数据(比如电影详情数据等)返回给前端,就像在前端和后端之间修建了一条隧道,让数据可以在两者之间直接往返,并且在axios配置中设置了withCredentials: true来允许携带凭证,同时后端需要配置CORS,解决了跨域问题。

总结

本电影推荐系统项目前端使用组件式开发,采用了现代化的Vue 3开发模式,代码结构清晰,功能完善,具有良好的用户体验。每个页面是一个独立的Vue组件,API请求集中管理在src/api/index.js,路由配置在src/main.js,样式采用Scoped CSS,避免全局污染。

这种响应式设计,适配不同设备,图片动态映射,保证一致性,具有完善的表单验证和清空逻辑,平滑的页面过渡和交互效果。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐