소스 검색

```
docs(readme): 更新项目说明文档

将原有的简单项目标题和描述替换为详细的 Vue 3 + Vite 模板说明,包括
开发指南链接和 IDE 支持信息,帮助开发者更好地理解和使用该项目模板。
```

zhangningning 1 일 전
부모
커밋
ca6408546f

+ 24 - 0
.gitignore

@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?

+ 3 - 0
.vscode/extensions.json

@@ -0,0 +1,3 @@
+{
+  "recommendations": ["Vue.volar"]
+}

+ 4 - 2
README.md

@@ -1,3 +1,5 @@
-# ali_ai_learn_web
+# Vue 3 + Vite
 
-学习论坛
+This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
+
+Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).

+ 13 - 0
index.html

@@ -0,0 +1,13 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <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>video-learning-demo</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.js"></script>
+  </body>
+</html>

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 5540 - 0
package-lock.json


+ 30 - 0
package.json

@@ -0,0 +1,30 @@
+{
+  "name": "video-learning-demo",
+  "private": true,
+  "version": "0.0.0",
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "vite build",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "@blocknote/core": "^0.44.0",
+    "@blocknote/mantine": "^0.44.0",
+    "@blocknote/react": "^0.44.0",
+    "axios": "^1.13.2",
+    "element-plus": "^2.11.9",
+    "pinia": "^3.0.4",
+    "react": "^18.3.1",
+    "react-dom": "^18.3.1",
+    "sass": "^1.94.2",
+    "veaury": "^2.6.3",
+    "vue": "^3.5.24",
+    "vue-react-wrapper": "^0.3.1",
+    "vue-router": "^4.6.3"
+  },
+  "devDependencies": {
+    "@vitejs/plugin-vue": "^6.0.1",
+    "vite": "^7.2.4"
+  }
+}

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 0
public/vite.svg


+ 92 - 0
src/App.vue

@@ -0,0 +1,92 @@
+<template>
+  <div id="app">
+    <el-container style="min-height: 100vh;">
+      <el-header>
+        <div class="header-content">
+          <div class="logo" @click="$router.push('/')">视频学习平台</div>
+          <el-menu :default-active="activeIndex" mode="horizontal" :ellipsis="false">
+            <el-menu-item index="1" @click="$router.push('/')">AI工作流</el-menu-item>
+            <el-menu-item index="2" @click="$router.push('/my-learning')">工作流交易</el-menu-item>
+            <el-menu-item index="3" @click="$router.push('/my-learning')">学习教程系统</el-menu-item>
+            <el-menu-item index="4" @click="$router.push('/my-learning')">学习笔记</el-menu-item>
+            <el-menu-item index="5" @click="$router.push('/my-learning')">积分商城</el-menu-item>
+          </el-menu>
+          <div class="header-right">
+            <el-button type="text">登录</el-button>
+            <el-button type="primary">注册</el-button>
+          </div>
+        </div>
+      </el-header>
+      
+      <el-main>
+        <router-view />
+      </el-main>
+      
+      <el-footer>
+        <div class="footer-content">
+          <p>© 2025 视频学习平台 - 版权所有</p>
+        </div>
+      </el-footer>
+    </el-container>
+  </div>
+</template>
+
+<script setup>
+import { computed,ref } from 'vue'
+import { useRoute } from 'vue-router'
+
+const route = useRoute()
+
+const activeIndex = ref('1')
+</script>
+
+<style lang="scss">
+#app {
+  font-family: Avenir, Helvetica, Arial, sans-serif;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+  color: #2c3e50;
+}
+
+.el-header {
+  background-color: #fff;
+  color: #333;
+  line-height: 60px;
+  border-bottom: 1px solid #eee;
+  
+  .header-content {
+    max-width: 1200px;
+    margin: 0 auto;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    
+    .logo {
+      font-size: 1.5rem;
+      font-weight: bold;
+      cursor: pointer;
+    }
+    
+    .header-right {
+      display: flex;
+      gap: 10px;
+    }
+  }
+}
+
+.el-footer {
+  background-color: #f5f5f5;
+  color: #666;
+  text-align: center;
+  padding: 20px 0;
+  
+  .footer-content {
+    max-width: 1200px;
+    margin: 0 auto;
+  }
+}
+
+.el-main {
+  padding: 20px 0;
+}
+</style>

+ 25 - 0
src/api/course.js

@@ -0,0 +1,25 @@
+import request from './request.js'
+
+// 获取课程列表
+export function getCourseList() {
+  return request({
+    url: '/courses',
+    method: 'get'
+  })
+}
+
+// 获取课程详情
+export function getCourseDetail(id) {
+  return request({
+    url: `/courses/${id}`,
+    method: 'get'
+  })
+}
+
+// 获取课程章节
+export function getCourseChapters(id) {
+  return request({
+    url: `/courses/${id}/chapters`,
+    method: 'get'
+  })
+}

+ 32 - 0
src/api/request.js

@@ -0,0 +1,32 @@
+import axios from 'axios'
+
+// 创建 axios 实例
+const request = axios.create({
+  baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
+  timeout: 5000
+})
+
+// 请求拦截器
+request.interceptors.request.use(
+  config => {
+    // 可以在这里添加 token
+    return config
+  },
+  error => {
+    console.error('请求错误:', error)
+    return Promise.reject(error)
+  }
+)
+
+// 响应拦截器
+request.interceptors.response.use(
+  response => {
+    return response.data
+  },
+  error => {
+    console.error('响应错误:', error)
+    return Promise.reject(error)
+  }
+)
+
+export default request

+ 1 - 0
src/assets/vue.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

+ 62 - 0
src/components/BlockNoteEditor.vue

@@ -0,0 +1,62 @@
+<template>
+   <ReactWrapper 
+    component="BlockNoteReact" 
+    :props="editorProps" 
+    key="blocknote-editor" 
+    class="blocknote-container"
+
+  />
+</template>
+
+<script setup>
+import { ref, watch } from 'vue';
+import { createReactWrapper } from 'vue-react-wrapper';
+import BlockNoteReact from './react-components/BlockNoteReact';
+
+const props = defineProps({
+  modelValue: {
+    type: [Array, Object, null],
+    default: null
+  },
+  editable: {
+    type: Boolean,
+    default: true
+  }
+});
+
+const emit = defineEmits(['update:modelValue','getHtml']);
+
+// 创建响应式props对象
+const editorProps = ref({
+  modelValue: props.modelValue,
+  onUpdateModelValue: (value,html) => {
+    console.log('onUpdateModelValue', value,html);
+    emit('update:modelValue', value);
+    emit('getHtml', html);
+  },
+  editable: props.editable
+});
+const ReactWrapper = createReactWrapper(BlockNoteReact,editorProps)
+// 监听modelValue变化
+watch(() => props.modelValue, (newVal) => {
+  editorProps.value.modelValue = newVal;
+}, { deep: true });
+</script>
+
+<style scoped>
+.blocknote-container {
+  border: 1px solid #e5e7eb;
+  border-radius: 6px;
+  padding: 12px;
+  min-height: 250px;
+  margin: 16px 0;
+}
+:deep(.blocknote-editor) {
+  font-size: 16px;
+  line-height: 1.6;
+}
+:deep(.blocknote-toolbar) {
+  border-bottom: 1px solid #f3f4f6;
+  padding-bottom: 8px;
+}
+</style>

+ 53 - 0
src/components/Breadcrumb.vue

@@ -0,0 +1,53 @@
+<!-- src/components/Breadcrumb.vue -->
+<template>
+  <el-breadcrumb separator="/" class="app-breadcrumb">
+    <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
+    <el-breadcrumb-item
+      v-for="item in breadcrumbItems"
+      :key="item.path"
+      :to="item.path === route.path ? undefined : { path: item.path }"
+    >
+      {{ item.name }}
+    </el-breadcrumb-item>
+  </el-breadcrumb>
+</template>
+
+<script setup>
+import { computed } from 'vue'
+import { useRoute } from 'vue-router'
+
+const route = useRoute()
+
+const breadcrumbItems = computed(() => {
+  const items = []
+  
+  // 根据当前路由动态生成面包屑项
+  if (route.path.startsWith('/course/')) {
+    
+    // 如果有课程ID,可以从 store 或 API 获取课程名称
+    console.log('Generating breadcrumb for course detail',route)
+    if (route.params.id) {
+      items.push({
+        name: `课程详情-${route.query.title}`,
+        path: route.path
+      })
+    }
+  } else {
+    // 从路由元信息获取标题
+    const title = route.meta?.title || route.name || '未知页面'
+    items.push({
+      name: title,
+      path: route.path
+    })
+  }
+  
+  return items
+})
+</script>
+
+<style scoped>
+.app-breadcrumb {
+  margin-bottom: 20px;
+  padding: 10px 0;
+}
+</style>

+ 43 - 0
src/components/HelloWorld.vue

@@ -0,0 +1,43 @@
+<script setup>
+import { ref } from 'vue'
+
+defineProps({
+  msg: String,
+})
+
+const count = ref(0)
+</script>
+
+<template>
+  <h1>{{ msg }}</h1>
+
+  <div class="card">
+    <button type="button" @click="count++">count is {{ count }}</button>
+    <p>
+      Edit
+      <code>components/HelloWorld.vue</code> to test HMR
+    </p>
+  </div>
+
+  <p>
+    Check out
+    <a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
+      >create-vue</a
+    >, the official Vue + Vite starter
+  </p>
+  <p>
+    Learn more about IDE Support for Vue in the
+    <a
+      href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
+      target="_blank"
+      >Vue Docs Scaling up Guide</a
+    >.
+  </p>
+  <p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
+</template>
+
+<style scoped>
+.read-the-docs {
+  color: #888;
+}
+</style>

+ 63 - 0
src/components/react-components/BlockNoteReact.jsx

@@ -0,0 +1,63 @@
+import React from 'react';
+import { useCreateBlockNote } from '@blocknote/react';
+import { BlockNoteView } from '@blocknote/mantine';
+import { zh } from "@blocknote/core/locales";
+import '@blocknote/mantine/style.css';
+import '@blocknote/core/fonts/inter.css';
+
+const BlockNoteReact = ({ modelValue, onUpdateModelValue, editable = true }) => {
+  let isEditable = editable;
+  // 在React组件内部使用Hook
+  const editor = useCreateBlockNote({
+    id: 'blocknote-react-editor',
+    dictionary: zh,
+    initialContent: modelValue || undefined,
+    iseditable : false,
+    autofocus: false, // 默认 false,true 开启自动聚焦
+    // 3. 允许使用的块类型(限制编辑器功能)
+    // allowedBlockTypes: [
+    //   "paragraph", // 普通段落
+    //   "heading", // 标题
+    //   "list", // 列表(有序/无序)
+    //   "image", // 图片
+    //   "quote", // 引用
+    // ],
+    // uploadFile: async (file) => {
+    //   // 上传文件到服务器
+    //   const formData = new FormData();
+    //   formData.append('file', file);
+    //   const response = await fetch('/upload', {
+    //     method: 'POST',
+    //     body: formData,
+    //   });
+    //   const data = await response.json();
+    //   return data.url; // 返回图片 URL
+    // },
+    // placeholder: "请输入内容(支持块级编辑、格式设置)"
+  });
+  editor.onMount(() => {
+    console.log('BlockNote 编辑器已挂载(React 版)', editor);
+  });
+
+
+  // 监听编辑器内容变化
+  React.useEffect(() => {
+    const unsubscribe = editor.onChange(async() => {
+      const html = await editor.blocksToFullHTML(editor.document);
+      // onUpdateModelValue(editor.document);
+      onUpdateModelValue(editor.document,html);
+    });
+    return unsubscribe;
+  }, [editor, onUpdateModelValue]);
+
+  // 监听外部值变化
+  React.useEffect(() => {
+    if (modelValue && JSON.stringify(modelValue) !== JSON.stringify(editor.document)) {
+      editor.replaceBlocks(editor.document, modelValue);
+    }
+  }, [modelValue, editor]);
+
+  return <BlockNoteView editor={editor} key={editor.id} editable={isEditable}/>;
+};
+
+export default BlockNoteReact;

+ 25 - 0
src/main.js

@@ -0,0 +1,25 @@
+import { createApp } from 'vue'
+import { createPinia } from 'pinia'
+import ElementPlus from 'element-plus'
+import 'element-plus/dist/index.css'
+import router from './router'
+// import './styles/index.scss'
+import App from './App.vue'
+
+// 如果您正在使用CDN引入,请删除下面一行。
+import * as ElementPlusIconsVue from '@element-plus/icons-vue'
+import Breadcrumb from '@/components/Breadcrumb.vue'
+
+const app = createApp(App)
+
+for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
+  app.component(key, component)
+}
+
+app.use(createPinia())
+app.use(router)
+app.use(ElementPlus)
+// 全局注册 Breadcrumb 组件
+app.component('Breadcrumb', Breadcrumb)
+
+app.mount('#app')

+ 165 - 0
src/pages/CourseDetail.vue

@@ -0,0 +1,165 @@
+<template>
+  <div class="course-detail-page" v-if="currentCourse">
+    <!-- <el-page-header @back="goBack">
+      <template #content>{{ currentCourse.title }}</template>
+    </el-page-header> -->
+    <Breadcrumb />
+    
+    <div class="course-detail">
+      <div class="course-main">
+        <div class="course-video">
+          <!-- 视频播放器占位 -->
+          <div class="video-player">
+            <img :src="currentCourse.cover" :alt="currentCourse.title">
+            <div class="play-button">▶</div>
+          </div>
+          
+          <div class="video-info">
+            <h2>{{ currentCourse.title }}</h2>
+            <div class="course-meta">
+              <span>讲师:{{ currentCourse.teacher }}</span>
+              <span>价格:¥{{ currentCourse.price }}</span>
+            </div>
+            <p class="course-desc">{{ currentCourse.description }}</p>
+            <el-button type="primary" size="large">立即学习</el-button>
+          </div>
+        </div>
+        
+        <div class="course-chapters">
+          <h3>课程章节</h3>
+          <el-collapse>
+            <el-collapse-item 
+              v-for="chapter in currentCourseChapters" 
+              :key="chapter.id" 
+              :title="chapter.title"
+            >
+              <div 
+                v-for="video in chapter.videos" 
+                :key="video.id" 
+                class="chapter-video"
+              >
+                {{ video.title }} ({{ video.duration }})
+              </div>
+            </el-collapse-item>
+          </el-collapse>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { onMounted,ref } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import { useCourseStore } from '@/pinia/courseStore'
+
+const route = useRoute()
+const router = useRouter()
+const courseStore = useCourseStore()
+
+const courseId = route.params.id
+const currentCourse = ref(null)
+const currentCourseChapters = ref(null)
+
+
+onMounted(() => {
+  courseStore.fetchCourseDetail(courseId)
+  courseStore.fetchCourseChapters(courseId)
+  
+  currentCourse.value = courseStore.currentCourse
+  currentCourseChapters.value = courseStore.currentCourseChapters
+})
+
+const goBack = () => {
+  router.back()
+}
+</script>
+
+<style scoped lang="scss">
+.course-detail-page {
+  max-width: 1200px;
+  margin: 0 auto;
+  padding: 20px;
+}
+
+.course-detail {
+  .course-video {
+    display: flex;
+    gap: 20px;
+    margin-bottom: 40px;
+    
+    @media (max-width: 768px) {
+      flex-direction: column;
+    }
+    
+    .video-player {
+      flex: 2;
+      position: relative;
+      
+      img {
+        width: 100%;
+        height: auto;
+        border-radius: 8px;
+      }
+      
+      .play-button {
+        position: absolute;
+        top: 50%;
+        left: 50%;
+        transform: translate(-50%, -50%);
+        width: 80px;
+        height: 80px;
+        background: rgba(0,0,0,0.5);
+        color: white;
+        border-radius: 50%;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        font-size: 30px;
+        cursor: pointer;
+      }
+    }
+    
+    .video-info {
+      flex: 1;
+      
+      h2 {
+        font-size: 1.8rem;
+        margin-bottom: 15px;
+      }
+      
+      .course-meta {
+        margin-bottom: 15px;
+        color: #666;
+        
+        span {
+          display: block;
+          margin-bottom: 5px;
+        }
+      }
+      
+      .course-desc {
+        margin-bottom: 20px;
+        line-height: 1.6;
+      }
+    }
+  }
+  
+  .course-chapters {
+    h3 {
+      font-size: 1.5rem;
+      margin-bottom: 15px;
+    }
+    
+    .chapter-video {
+      padding: 10px;
+      border-bottom: 1px solid #eee;
+      cursor: pointer;
+      
+      &:hover {
+        background-color: #f5f5f5;
+      }
+    }
+  }
+}
+</style>

+ 171 - 0
src/pages/Home.vue

@@ -0,0 +1,171 @@
+<template>
+  <div class="home-page">
+    <!-- 使用 BlockNote 编辑器 -->
+    <BlockNoteEditor v-model="editorContent" @getHtml="getHtml" :editable="true"/>
+    <div v-html="editorContent_html"></div>
+    <div class="banner">
+      <h1>欢迎来到视频学习平台</h1>
+      <p>发现优质课程,提升你的技能</p>
+    </div>
+    
+    <div class="course-list">
+      <h2>热门课程</h2>
+      <div class="course-grid">
+        <div 
+          v-for="course in courseList" 
+          :key="course.id" 
+          class="course-card"
+          @click="goToCourseDetail(course.id, course.title)"
+        >
+          <img :src="course.cover" :alt="course.title" class="course-cover">
+          <div class="course-info">
+            <h3>{{ course.title }}</h3>
+            <p>讲师:{{ course.teacher }}</p>
+            <div class="course-price">¥{{ course.price }}</div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { onMounted,ref } from 'vue'
+import { useRouter } from 'vue-router'
+import { useCourseStore } from '../pinia/courseStore'
+
+// 导入封装好的 BlockNote 组件
+import BlockNoteEditor from '@/components/BlockNoteEditor.vue';
+
+// 绑定编辑器内容
+// [
+//   {
+//     type: "paragraph",
+//     content: [{ type: "text", text: "只读模式" }]
+//   }
+// ]
+
+const editorContent = ref(
+  // [
+  //   {
+  //     type: "paragraph",
+  //     content: [{ type: "text", text: "只读模式" }]
+  //   },
+  //   {
+  //   "id": "378ce968-02c2-4856-888b-c35a355aa84b",
+  //   "type": "codeBlock",
+  //   "props": {
+  //       "language": "text"
+  //   },
+  //   "content": [],
+  //   "children": []
+  //   }
+  // ]
+);
+const editorContent_html = ref();
+
+const router = useRouter()
+const courseStore = useCourseStore()
+const courseList = ref([])
+
+onMounted(() => {
+  console.log('Home mounted')
+  courseStore.fetchCourseList()
+  courseList.value = courseStore.courseList;
+})
+
+const getHtml = (html) => {
+  editorContent_html.value = html
+}
+
+const goToCourseDetail = (id,title) => {
+  //增加参数名称
+  router.push({
+    path: `/course/${id}`,
+    query: {
+      title: title
+    }
+  })
+}
+
+</script>
+
+<style scoped lang="scss">
+.home-page {
+  max-width: 1200px;
+  margin: 0 auto;
+  padding: 20px;
+}
+
+.banner {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: white;
+  padding: 60px 20px;
+  text-align: center;
+  border-radius: 8px;
+  margin-bottom: 40px;
+  
+  h1 {
+    font-size: 2.5rem;
+    margin-bottom: 10px;
+  }
+  
+  p {
+    font-size: 1.2rem;
+    opacity: 0.9;
+  }
+}
+
+.course-list {
+  h2 {
+    font-size: 1.8rem;
+    margin-bottom: 20px;
+    border-bottom: 2px solid #eee;
+    padding-bottom: 10px;
+  }
+  
+  .course-grid {
+    display: grid;
+    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+    gap: 20px;
+  }
+  
+  .course-card {
+    border: 1px solid #eee;
+    border-radius: 8px;
+    overflow: hidden;
+    cursor: pointer;
+    transition: transform 0.3s;
+    
+    &:hover {
+      transform: translateY(-5px);
+      box-shadow: 0 5px 15px rgba(0,0,0,0.1);
+    }
+    
+    .course-cover {
+      width: 100%;
+      height: 180px;
+      object-fit: cover;
+    }
+    
+    .course-info {
+      padding: 15px;
+      
+      h3 {
+        margin-bottom: 8px;
+        font-size: 1.1rem;
+      }
+      
+      p {
+        color: #666;
+        margin-bottom: 10px;
+      }
+      
+      .course-price {
+        color: #e63946;
+        font-weight: bold;
+      }
+    }
+  }
+}
+</style>

+ 331 - 0
src/pages/MyLearning.vue

@@ -0,0 +1,331 @@
+<template>
+  <div class="my-learning-page">
+    <!-- <el-page-header content="我的学习"></el-page-header> -->
+    <Breadcrumb />
+    
+    <!-- 学习统计卡片 -->
+    <div class="learning-stats">
+      <el-card shadow="hover" class="stat-card">
+        <div class="stat-item">
+          <!-- <i class="el-icon-video-play"></i> -->
+          <el-icon><VideoPlay /></el-icon>
+          <div class="stat-info">
+            <p class="stat-label">已购课程</p>
+            <p class="stat-value">{{ myCourses.length }}</p>
+          </div>
+        </div>
+      </el-card>
+      
+      <el-card shadow="hover" class="stat-card">
+        <div class="stat-item">
+          <el-icon><Clock /></el-icon>
+          <div class="stat-info">
+            <p class="stat-label">学习时长</p>
+            <p class="stat-value">{{ studyTime }} 小时</p>
+          </div>
+        </div>
+      </el-card>
+      
+      <el-card shadow="hover" class="stat-card">
+        <div class="stat-item">
+          <el-icon><Check /></el-icon>
+          <div class="stat-info">
+            <p class="stat-label">已完成课程</p>
+            <p class="stat-value">{{ completedCourses }}</p>
+          </div>
+        </div>
+      </el-card>
+    </div>
+    
+    <!-- 已购课程列表 -->
+    <div class="my-courses">
+      <h2>我的课程</h2>
+      
+      <div v-if="myCourses.length === 0" class="no-courses">
+        <el-empty description="你还没有购买任何课程"></el-empty>
+        <el-button type="primary" @click="$router.push('/')">去选课</el-button>
+      </div>
+      
+      <div v-else class="course-list">
+        <el-card 
+          v-for="course in myCourses" 
+          :key="course.id" 
+          class="my-course-card"
+        >
+          <div class="course-header">
+            <img :src="course.cover" :alt="course.title" class="course-cover">
+            <div class="course-info">
+              <h3>{{ course.title }}</h3>
+              <p>讲师:{{ course.teacher }}</p>
+              <div class="progress-container">
+                <el-progress 
+                  :percentage="course.progress" 
+                  :status="course.progress === 100 ? 'success' : 'processing'"
+                  size="small"
+                ></el-progress>
+                <span class="progress-text">{{ course.progress }}%</span>
+              </div>
+            </div>
+          </div>
+          
+          <div class="course-actions">
+            <el-button 
+              type="primary" 
+              icon="el-icon-play-circle"
+              @click="continueLearning(course.id)"
+            >
+              继续学习
+            </el-button>
+            <el-button 
+              type="text" 
+              icon="el-icon-star-off"
+              @click="toggleCollect(course.id)"
+            >
+              {{ course.isCollected ? '取消收藏' : '收藏' }}
+            </el-button>
+          </div>
+        </el-card>
+      </div>
+      
+      <!-- 最近学习记录 -->
+      <div class="recent-learning" v-if="recentRecords.length > 0">
+        <h2>最近学习</h2>
+        <el-table :data="recentRecords" border style="width: 100%">
+          <el-table-column label="课程" prop="courseTitle"></el-table-column>
+          <el-table-column label="最近学习视频" prop="videoTitle"></el-table-column>
+          <el-table-column label="学习时间" prop="studyTime"></el-table-column>
+          <el-table-column label="操作">
+            <template #default="scope">
+              <el-button 
+                type="text" 
+                @click="continueLearning(scope.row.courseId)"
+              >
+                继续学习
+              </el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+  // import { VideoPlay, Clock, Check } from '@element-plus/icons-vue'
+  import { ElProgress } from 'element-plus'
+import { ref, computed } from 'vue'
+import { useRouter } from 'vue-router'
+
+const router = useRouter()
+
+// 模拟已购课程数据(实际项目中从 Pinia 或接口获取)
+const myCourses = ref([
+  {
+    id: 1,
+    title: 'Vue 3 从入门到精通',
+    cover: 'https://picsum.photos/400/225?random=1',
+    teacher: '张三',
+    progress: 65, // 学习进度(百分比)
+    isCollected: true // 是否收藏
+  },
+  {
+    id: 2,
+    title: 'JavaScript 高级编程',
+    cover: 'https://picsum.photos/400/225?random=2',
+    teacher: '李四',
+    progress: 30,
+    isCollected: false
+  }
+])
+
+// 模拟最近学习记录
+const recentRecords = ref([
+  {
+    courseId: 1,
+    courseTitle: 'Vue 3 从入门到精通',
+    videoTitle: 'Composition API 基础',
+    studyTime: '2025-01-15 19:30'
+  },
+  {
+    courseId: 2,
+    courseTitle: 'JavaScript 高级编程',
+    videoTitle: '闭包与作用域',
+    studyTime: '2025-01-14 16:45'
+  }
+])
+
+// 计算属性:学习时长(模拟数据,实际可累加视频观看时长)
+const studyTime = computed(() => {
+  // 简单模拟:每个课程进度每10%对应1小时学习时长
+  return myCourses.value.reduce((total, course) => {
+    return total + Math.floor(course.progress / 10)
+  }, 0)
+})
+
+// 计算属性:已完成课程数(进度100%)
+const completedCourses = computed(() => {
+  return myCourses.value.filter(course => course.progress === 100).length
+})
+
+// 继续学习(跳转到课程详情页)
+const continueLearning = (courseId) => {
+  router.push(`/course/${courseId}`)
+}
+
+// 收藏/取消收藏课程
+const toggleCollect = (courseId) => {
+  const course = myCourses.value.find(item => item.id === courseId)
+  if (course) {
+    course.isCollected = !course.isCollected
+    // 实际项目中这里需要调用接口保存状态到后端
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.my-learning-page {
+  max-width: 1200px;
+  margin: 0 auto;
+  padding: 20px;
+}
+
+// 学习统计卡片
+.learning-stats {
+  display: flex;
+  gap: 20px;
+  margin-bottom: 30px;
+  flex-wrap: wrap;
+  
+  .stat-card {
+    flex: 1;
+    min-width: 200px;
+    
+    .stat-item {
+      display: flex;
+      align-items: center;
+      padding: 15px 0;
+      
+      i {
+        font-size: 2rem;
+        color: $primary-color;
+        margin-right: 15px;
+      }
+      
+      .stat-label {
+        color: $text-light-color;
+        font-size: 0.9rem;
+        margin-bottom: 5px;
+      }
+      
+      .stat-value {
+        font-size: 1.8rem;
+        font-weight: bold;
+      }
+    }
+  }
+}
+
+// 我的课程列表
+.my-courses {
+  h2 {
+    font-size: 1.5rem;
+    margin-bottom: 15px;
+    border-bottom: 2px solid $border-color;
+    padding-bottom: 10px;
+  }
+  
+  .no-courses {
+    text-align: center;
+    padding: 50px 0;
+    
+    .el-empty {
+      margin-bottom: 20px;
+    }
+  }
+  
+  .course-list {
+    display: grid;
+    grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
+    gap: 20px;
+  }
+  
+  .my-course-card {
+    display: flex;
+    flex-direction: column;
+    height: 100%;
+    
+    .course-header {
+      display: flex;
+      gap: 15px;
+      margin-bottom: 15px;
+      
+      .course-cover {
+        width: 120px;
+        height: 80px;
+        object-fit: cover;
+        border-radius: 4px;
+      }
+      
+      .course-info {
+        flex: 1;
+        
+        h3 {
+          font-size: 1.1rem;
+          margin-bottom: 8px;
+          display: -webkit-box;
+          -webkit-line-clamp: 1;
+          -webkit-box-orient: vertical;
+          overflow: hidden;
+        }
+        
+        p {
+          color: $text-light-color;
+          font-size: 0.9rem;
+          margin-bottom: 8px;
+        }
+        
+        .progress-container {
+          display: flex;
+          align-items: center;
+          gap: 10px;
+          
+          .progress-text {
+            font-size: 0.8rem;
+            color: $text-light-color;
+          }
+        }
+      }
+    }
+    
+    .course-actions {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      margin-top: auto;
+    }
+  }
+}
+
+// 最近学习记录
+.recent-learning {
+  margin-top: 40px;
+  
+  h2 {
+    font-size: 1.5rem;
+    margin-bottom: 15px;
+    border-bottom: 2px solid $border-color;
+    padding-bottom: 10px;
+  }
+}
+
+// 响应式调整
+@media (max-width: 768px) {
+  .learning-stats {
+    flex-direction: column;
+  }
+  
+  .my-courses .course-list {
+    grid-template-columns: 1fr;
+  }
+}
+</style>

+ 66 - 0
src/pinia/courseStore.js

@@ -0,0 +1,66 @@
+import { defineStore } from 'pinia'
+import { getCourseList, getCourseDetail, getCourseChapters } from '../api/course'
+
+export const useCourseStore = defineStore('course', {
+  state: () => ({
+    courseList: [],
+    currentCourse: null,
+    currentCourseChapters: []
+  }),
+  actions: {
+    async fetchCourseList() {
+      try {
+        // 模拟数据,实际项目中使用 API 请求
+        this.courseList = [
+          { id: 1, title: 'Vue 3 从入门到精通', cover: 'https://picsum.photos/400/225?random=1', teacher: '张三', price: 99 },
+          { id: 2, title: 'JavaScript 高级编程', cover: 'https://picsum.photos/400/225?random=2', teacher: '李四', price: 129 },
+          { id: 3, title: 'TypeScript 完全指南', cover: 'https://picsum.photos/400/225?random=3', teacher: '王五', price: 89 },
+          { id: 4, title: 'TypeScript 完全指南2', cover: 'https://picsum.photos/400/225?random=4', teacher: '王五', price: 99 },
+        ]
+        // 真实项目中使用:
+        // const data = await getCourseList()
+        // this.courseList = data
+      } catch (error) {
+        console.error('获取课程列表失败:', error)
+      }
+    },
+    async fetchCourseDetail(id) {
+      try {
+        // 模拟数据
+        this.currentCourse = {
+          id,
+          title: id === 1 ? 'Vue 3 从入门到精通' : id === 2 ? 'JavaScript 高级编程' : 'TypeScript 完全指南',
+          cover: `https://picsum.photos/800/450?random=${id}`,
+          teacher: id === 1 ? '张三' : id === 2 ? '李四' : '王五',
+          price: id === 1 ? 99 : id === 2 ? 129 : 89,
+          description: '这是一门非常棒的课程,包含了从基础到进阶的所有内容。'
+        }
+        // 真实项目中使用:
+        // const data = await getCourseDetail(id)
+        // this.currentCourse = data
+      } catch (error) {
+        console.error('获取课程详情失败:', error)
+      }
+    },
+    async fetchCourseChapters(id) {
+      try {
+        // 模拟数据
+        this.currentCourseChapters = [
+          { id: 1, title: '第一章:Vue 3 基础', videos: [
+            { id: 101, title: 'Vue 3 介绍与环境搭建', duration: '15:00' },
+            { id: 102, title: 'Composition API 基础', duration: '20:00' }
+          ]},
+          { id: 2, title: '第二章:Vue 3 进阶', videos: [
+            { id: 201, title: '响应式原理', duration: '25:00' },
+            { id: 202, title: '自定义 Hooks', duration: '18:00' }
+          ]}
+        ]
+        // 真实项目中使用:
+        // const data = await getCourseChapters(id)
+        // this.currentCourseChapters = data
+      } catch (error) {
+        console.error('获取课程章节失败:', error)
+      }
+    }
+  }
+})

+ 32 - 0
src/router/index.js

@@ -0,0 +1,32 @@
+import { createRouter, createWebHistory } from 'vue-router'
+import Home from '../pages/Home.vue'
+import CourseDetail from '../pages/CourseDetail.vue'
+import MyLearning from '../pages/MyLearning.vue'
+
+const routes = [
+  {
+    path: '/',
+    name: 'Home',
+    component: Home,
+    meta: { title: '首页' }
+  },
+  {
+    path: '/course/:id',
+    name: 'CourseDetail',
+    component: CourseDetail,
+    meta: { title: '课程详情' }
+  },
+  {
+    path: '/my-learning',
+    name: 'MyLearning',
+    component: MyLearning,
+    meta: { title: '我的学习' }
+  }
+]
+
+const router = createRouter({
+  history: createWebHistory(import.meta.env.BASE_URL),
+  routes
+})
+
+export default router

+ 79 - 0
src/style.css

@@ -0,0 +1,79 @@
+:root {
+  font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
+  line-height: 1.5;
+  font-weight: 400;
+
+  color-scheme: light dark;
+  color: rgba(255, 255, 255, 0.87);
+  background-color: #242424;
+
+  font-synthesis: none;
+  text-rendering: optimizeLegibility;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+a {
+  font-weight: 500;
+  color: #646cff;
+  text-decoration: inherit;
+}
+a:hover {
+  color: #535bf2;
+}
+
+body {
+  margin: 0;
+  display: flex;
+  place-items: center;
+  min-width: 320px;
+  min-height: 100vh;
+}
+
+h1 {
+  font-size: 3.2em;
+  line-height: 1.1;
+}
+
+button {
+  border-radius: 8px;
+  border: 1px solid transparent;
+  padding: 0.6em 1.2em;
+  font-size: 1em;
+  font-weight: 500;
+  font-family: inherit;
+  background-color: #1a1a1a;
+  cursor: pointer;
+  transition: border-color 0.25s;
+}
+button:hover {
+  border-color: #646cff;
+}
+button:focus,
+button:focus-visible {
+  outline: 4px auto -webkit-focus-ring-color;
+}
+
+.card {
+  padding: 2em;
+}
+
+#app {
+  max-width: 1280px;
+  margin: 0 auto;
+  padding: 2rem;
+  text-align: center;
+}
+
+@media (prefers-color-scheme: light) {
+  :root {
+    color: #213547;
+    background-color: #ffffff;
+  }
+  a:hover {
+    color: #747bff;
+  }
+  button {
+    background-color: #f9f9f9;
+  }
+}

+ 71 - 0
src/styles/index.scss

@@ -0,0 +1,71 @@
+// 全局样式文件:src/styles/index.scss
+// 1. 全局变量(可统一管理颜色、字体、间距等)
+$primary-color: #409eff; // 主色调(和 Element Plus 主色一致)
+$secondary-color: #667eea; // 辅助色
+$text-color: #333333; // 主要文字色
+$text-light-color: #666666; // 次要文字色
+$border-color: #eeeeee; // 边框色
+$bg-color: #f9f9f9; // 背景色
+$danger-color: #e63946; // 危险色(价格、警告等)
+
+
+// 2. 全局重置样式(统一浏览器默认样式)
+* {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+}
+
+body {
+  font-family: "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif;
+  color: $text-color;
+  background-color: #ffffff;
+  line-height: 1.6;
+}
+
+// 3. 公共样式类(全局可复用)
+// 容器宽度限制(统一页面最大宽度,居中显示)
+.container {
+  max-width: 1200px;
+  margin: 0 auto;
+  padding: 0 20px;
+}
+
+// 清除浮动
+.clearfix::after {
+  content: "";
+  display: block;
+  clear: both;
+}
+
+// 禁用选中
+.no-select {
+  user-select: none;
+  -webkit-user-select: none;
+}
+
+// 4. 全局组件样式覆盖(可选,如需调整 Element Plus 组件默认样式)
+// 例如:调整按钮圆角
+.el-button {
+  border-radius: 4px;
+}
+
+// 调整折叠面板样式
+.el-collapse-item__header {
+  background-color: $bg-color;
+}
+
+// 5. 响应式相关样式(全局通用)
+// 小屏幕(手机)
+@media (max-width: 768px) {
+  .container {
+    padding: 0 10px;
+  }
+}
+
+// 中屏幕(平板)
+@media (min-width: 769px) and (max-width: 1024px) {
+  .container {
+    padding: 0 15px;
+  }
+}

+ 24 - 0
vite.config.js

@@ -0,0 +1,24 @@
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+import path from 'path' //
+
+// https://vite.dev/config/
+export default defineConfig({
+  plugins: [vue()],
+  resolve: {
+    // 配置路径别名(可选,但推荐,避免相对路径层级混乱)
+    alias: {
+      '@': path.resolve(__dirname, './src') // 用 @ 代替 src 目录
+    },
+    extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json'] // 自动解析这些后缀
+  },
+  css: {
+    preprocessorOptions: {
+      scss: {
+        // 全局注入 SCSS 变量文件:所有组件可直接使用 index.scss 中的变量
+        // 注意:路径必须正确,基于项目根目录
+        additionalData: '@use "@/styles/index.scss" as *;' // 用别名 @ 更可靠
+      }
+    }
+  }
+})