SyuanYu 1 年之前
父節點
當前提交
b12d195862
共有 39 個文件被更改,包括 1144 次插入373 次删除
  1. 1 1
      index.html
  2. 0 5
      src/App.vue
  3. 23 3
      src/assets/css/style.css
  4. 0 1
      src/assets/css/style.css.map
  5. 24 3
      src/assets/css/style.scss
  6. 二進制
      src/assets/img/course/background.png
  7. 二進制
      src/assets/img/course/banner.png
  8. 二進制
      src/assets/img/course/detail-background.png
  9. 二進制
      src/assets/img/course/探索課程素材-09.png
  10. 二進制
      src/assets/img/course/探索課程素材-11.png
  11. 二進制
      src/assets/img/course/探索課程素材-12.png
  12. 二進制
      src/assets/img/course/探索課程素材-13.png
  13. 二進制
      src/assets/img/course/探索課程素材-14.png
  14. 二進制
      src/assets/img/course/探索課程素材-15.png
  15. 二進制
      src/assets/img/course/探索課程素材-16.png
  16. 二進制
      src/assets/img/course/探索課程素材-17.png
  17. 二進制
      src/assets/img/course/探索課程素材-18.png
  18. 二進制
      src/assets/img/course/探索課程素材-19.png
  19. 二進制
      src/assets/img/course/探索課程素材-20.png
  20. 二進制
      src/assets/img/course/探索課程素材-21.png
  21. 二進制
      src/assets/img/course/探索課程素材-22.png
  22. 二進制
      src/assets/img/course/探索課程素材-23.png
  23. 二進制
      src/assets/img/course/探索課程素材-24.png
  24. 二進制
      src/assets/img/home/banner.png
  25. 18 12
      src/components/Navbar.vue
  26. 42 12
      src/router/index.js
  27. 18 11
      src/stores/store.js
  28. 1 1
      src/views/CourseDetail.vue
  29. 278 0
      src/views/CourseList copy.vue
  30. 312 197
      src/views/CourseList.vue
  31. 10 9
      src/views/Home.vue
  32. 0 14
      src/views/Login.vue
  33. 0 1
      src/views/News.vue
  34. 0 1
      src/views/NewsDetail.vue
  35. 176 0
      src/views/User/Dashboard.vue
  36. 206 0
      src/views/User/FavoriteClass.vue
  37. 18 0
      src/views/User/Passport.vue
  38. 17 0
      src/views/User/Profile.vue
  39. 0 102
      src/views/UserProfile.vue

+ 1 - 1
index.html

@@ -10,7 +10,7 @@
   <link rel="preconnect" href="https://fonts.googleapis.com">
   <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
   <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@100;300;400;500&display=swap" rel="stylesheet">
-
+  
   <title>國立臺灣工藝研究發展中心</title>
 </head>
 

+ 0 - 5
src/App.vue

@@ -7,9 +7,4 @@ import { RouterView } from "vue-router";
 </template>
 
 <style lang="scss">
-.v-container {
-  @media (min-width: 1920px) {
-    max-width: 1300px !important;
-  }
-}
 </style>

+ 23 - 3
src/assets/css/style.css

@@ -61,6 +61,12 @@ input:focus-visible {
   outline: 2px solid var(--sub-color);
 }
 
+@media (min-width: 1920px) {
+  .v-container {
+    max-width: 1300px !important;
+  }
+}
+
 .search {
   display: flex;
   flex-direction: column;
@@ -143,6 +149,7 @@ input:focus-visible {
 .college-content {
   padding: 0;
   width: 1300px !important;
+  position: relative;
 }
 @media (max-width: 600px) {
   .college-content {
@@ -152,11 +159,16 @@ input:focus-visible {
 .college-content .main-block {
   padding: 150px 80px;
   margin-top: -30%;
-  background-color: #fff;
+  background-image: url("@/assets/img/course/background.png");
+  background-size: cover;
+  position: relative;
+  left: 0;
+  right: 0;
 }
 @media (max-width: 960px) {
   .college-content .main-block {
     padding: 100px 50px;
+    background-position: 0 1vw;
   }
 }
 @media (max-width: 600px) {
@@ -182,11 +194,11 @@ input:focus-visible {
   }
 }
 .college-content .main-block .title {
-  margin: 80px 0;
+  padding: 80px 0;
 }
 @media (max-width: 960px) {
   .college-content .main-block .title {
-    margin: 50px 0;
+    padding: 50px 0;
   }
 }
 .college-content .main-block .v-breadcrumbs {
@@ -253,4 +265,12 @@ input:focus-visible {
   position: absolute;
   bottom: 10px;
   right: 10px;
+}
+
+.progress-item {
+  position: fixed;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  z-index: 1000;
 }/*# sourceMappingURL=style.css.map */

文件差異過大導致無法顯示
+ 0 - 1
src/assets/css/style.css.map


+ 24 - 3
src/assets/css/style.scss

@@ -64,6 +64,12 @@ input:focus-visible {
   outline: 2px solid (var(--sub-color));
 }
 
+.v-container {
+  @media (min-width: 1920px) {
+    max-width: 1300px !important;
+  }
+}
+
 .search {
   display: flex;
   flex-direction: column;
@@ -151,6 +157,7 @@ input:focus-visible {
 .college-content {
   padding: 0;
   width: 1300px !important;
+  position: relative;
 
   @media (max-width: 600px) {
     width: 85%;
@@ -159,10 +166,16 @@ input:focus-visible {
   .main-block {
     padding: 150px 80px;
     margin-top: -30%;
-    background-color: #fff;
+    background-image: url('@/assets/img/course/background.png');
+    background-size: cover;
+    position: relative;
+    left: 0;
+    right: 0;
+    // background-color: #fff;
 
     @media (max-width: 960px) {
       padding: 100px 50px;
+      background-position: 0 1vw;
     }
 
     @media (max-width: 600px) {
@@ -186,10 +199,10 @@ input:focus-visible {
     }
 
     .title {
-      margin: 80px 0;
+      padding: 80px 0;
 
       @media (max-width: 960px) {
-        margin: 50px 0;
+        padding: 50px 0;
       }
     }
 
@@ -266,4 +279,12 @@ input:focus-visible {
   position: absolute;
   bottom: 10px;
   right: 10px;
+}
+
+.progress-item {
+  position: fixed;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  z-index: 1000;
 }

二進制
src/assets/img/course/background.png


二進制
src/assets/img/course/banner.png


二進制
src/assets/img/course/detail-background.png


二進制
src/assets/img/course/探索課程素材-09.png


二進制
src/assets/img/course/探索課程素材-11.png


二進制
src/assets/img/course/探索課程素材-12.png


二進制
src/assets/img/course/探索課程素材-13.png


二進制
src/assets/img/course/探索課程素材-14.png


二進制
src/assets/img/course/探索課程素材-15.png


二進制
src/assets/img/course/探索課程素材-16.png


二進制
src/assets/img/course/探索課程素材-17.png


二進制
src/assets/img/course/探索課程素材-18.png


二進制
src/assets/img/course/探索課程素材-19.png


二進制
src/assets/img/course/探索課程素材-20.png


二進制
src/assets/img/course/探索課程素材-21.png


二進制
src/assets/img/course/探索課程素材-22.png


二進制
src/assets/img/course/探索課程素材-23.png


二進制
src/assets/img/course/探索課程素材-24.png


二進制
src/assets/img/home/banner.png


+ 18 - 12
src/components/Navbar.vue

@@ -6,15 +6,14 @@ import Login from "@/views/Login.vue";
 const store = useMainStore();
 let menuShow = ref(false);
 let collegeMenuShow = ref(false);
-let dialog = ref(false);
 
 function toggleMenu() {
   collegeMenuShow.value = false;
   menuShow.value = !menuShow.value;
 }
 
-function handleClose(value) {
-  dialog.value = value;
+function handleClose() {
+  store.loginDialog = false;
 }
 
 function handleMouseEvents(event) {
@@ -113,13 +112,17 @@ const collegeList = reactive([
         <!-- <router-link :to="'/login'">
           學員登入
         </router-link> -->
-        <v-dialog v-model="dialog" max-width="450" v-if="!store.loginState">
+        <v-dialog
+          v-model="store.loginDialog"
+          max-width="450"
+          v-if="!store.loginState"
+        >
           <template v-slot:activator="{ props }">
             <a href="javascript:;" v-bind="props">學員登入</a>
           </template>
           <Login @close="handleClose" />
         </v-dialog>
-        <router-link :to="'/user-profile'" v-else>學員專區</router-link>
+        <router-link :to="'/user/profile'" v-else>學員專區</router-link>
       </li>
       <!-- <li>EN</li> -->
       <li class="d-none d-lg-block">
@@ -168,18 +171,21 @@ const collegeList = reactive([
     display: flex;
     align-items: center;
     list-style: none;
+    // box-shadow: 1px 1px 4px #ccc;
 
     @media (max-width: 1280px) {
       position: absolute;
-      top: 82px;
-      left: 0;
-      right: 0;
+      top: 80px;
+    left: -1px;
+    right: -1px;
       z-index: 100;
       overflow: hidden;
       flex-direction: column;
-      box-shadow: 1px 1px 4px #ccc;
+      // box-shadow: 1px 1px 4px #ccc;
       max-height: 0;
       transition: max-height 0.3s ease-in-out;
+      border: 1px solid;
+    border-top: none;
     }
 
     &.slider {
@@ -219,6 +225,7 @@ const collegeList = reactive([
     }
 
     .college-slider {
+      display: none;
       position: absolute;
       top: 80px;
       left: -30px;
@@ -226,7 +233,6 @@ const collegeList = reactive([
       background: #fff;
       text-align: center;
       box-shadow: 1px 1px 4px #ccc;
-      opacity: 0;
       transition: all 0.3s;
 
       @media (max-width: 1280px) {
@@ -249,7 +255,7 @@ const collegeList = reactive([
       // }
 
       &.slider {
-        opacity: 1;
+        display: block;
         @media (max-width: 1280px) {
           max-height: 100%;
         }
@@ -261,7 +267,7 @@ const collegeList = reactive([
         }
         li {
           transition: all 0.3s;
-          
+
           &:last-child {
             border-bottom: none;
           }

+ 42 - 12
src/router/index.js

@@ -4,7 +4,6 @@ import { useMainStore } from "@/stores/store";
 
 const Home = defineAsyncComponent(() => import('@/views/Home.vue'));
 const Login = defineAsyncComponent(() => import('@/views/Login.vue'));
-const UserProfile = defineAsyncComponent(() => import('@/views/UserProfile.vue'));
 const News = defineAsyncComponent(() => import('@/views/News.vue'));
 const NewsDetail = defineAsyncComponent(() => import('@/views/NewsDetail.vue'));
 const CourseList = defineAsyncComponent(() => import('@/views/CourseList.vue'));
@@ -15,6 +14,11 @@ const Future = defineAsyncComponent(() => import('@/views/CollegeGroup/Future.vu
 const Repair = defineAsyncComponent(() => import('@/views/CollegeGroup/Repair.vue'));
 const Teenager = defineAsyncComponent(() => import('@/views/CollegeGroup/Teenager.vue'));
 const Cross = defineAsyncComponent(() => import('@/views/CollegeGroup/Cross.vue'));
+// 學員頁面
+const Dashboard = defineAsyncComponent(() => import('@/views/User/Dashboard.vue'));
+const Profile = defineAsyncComponent(() => import('@/views/User/Profile.vue'));
+const Passport = defineAsyncComponent(() => import('@/views/User/Passport.vue'));
+const FavoriteClass = defineAsyncComponent(() => import('@/views/User/FavoriteClass.vue'));
 
 const routes = [
   {
@@ -27,10 +31,32 @@ const routes = [
     name: 'Login',
     component: Login,
   },
+  // {
+  //   path: '/user/profile',
+  //   name: 'UserProfile',
+  //   component: UserProfile,
+  // },
   {
-    path: '/user-profile',
-    name: 'UserProfile',
-    component: UserProfile,
+    path: '/user',
+    component: Dashboard,
+    meta: { requiresAuth: true }, // 需驗證登入權限
+    children: [
+      {
+        path: 'profile',
+        name: 'Profile',
+        component: Profile,
+      },
+      {
+        path: 'passport',
+        name: 'Passport',
+        component: Passport,
+      },
+      {
+        path: 'favorite-class',
+        name: 'FavoriteClass',
+        component: FavoriteClass,
+      },
+    ],
   },
   {
     path: '/news',
@@ -92,18 +118,22 @@ const router = createRouter({
 
 // 檢查登入狀態
 router.beforeEach((to, from, next) => {
-  const token = localStorage.getItem('token');
   const store = useMainStore();
+  store.checkToken();
 
-  if (token) {
-    store.setLoginState(true);
-    console.log('檢查登入狀態', store.loginState);
+  // 檢查目標路由是否需要驗證權限
+  if (to.meta.requiresAuth) {
+    const haveToken = store.loginState;
+    if (haveToken) {
+      next(); // 允許使用者訪問目標頁面
+    } else {
+      store.loginDialog = true; // 開啟登入視窗
+      next('/'); // 若無 token 直接輸入網址前往時,將重定向至首頁
+    }
   } else {
-    store.setLoginState(false);
-    console.log('檢查登入狀態', store.loginState);
+    // 如果目標路由不需要驗證權限,直接允許使用者訪問
+    next();
   }
-
-  next();
 });
 
 export default router;

+ 18 - 11
src/stores/store.js

@@ -3,19 +3,26 @@ import { defineStore } from 'pinia'
 export const useMainStore = defineStore('mainStore', {
   state: () => ({
     count: 0,
-    loginState: false // 登入狀態
+    token: "",
+    loginState: false, // 登入狀態
+    loginDialog: false, // 登入視窗
   }),
-  getters: {
-    doubleCount() {
-      return this.count * 2;
-    },
-    tripleCount() {
-      return this.count * 3;
-    }
-  },
+  // getters: {
+  // },
   actions: {
-    setLoginState(newState) {
-      this.loginState = newState;
+    // 檢查登入狀態
+    checkToken() {
+      const token = localStorage.getItem('token');
+      console.log('checkToken', token);
+      if (token) {
+        this.token = token;
+        this.loginState = true;
+      } else {
+        this.token = null;
+        this.loginState = false;
+      }
+
+      return this.loginState;
     },
     getImageUrl(name) {
       console.log('name', name);

+ 1 - 1
src/views/CourseDetail.vue

@@ -127,7 +127,7 @@ const dynamicCols = computed(() => {
     ></v-breadcrumbs>
     <v-row class="justify-center">
       <v-col cols="3" class="title pa-0">
-        <img src="@/assets/img/course/background.png" alt="" class="bg-img" />
+        <img src="@/assets/img/course/detail-background.png" alt="" class="bg-img" />
         <h2>{{ course.data.name }}</h2>
       </v-col>
       <v-col :cols="dynamicCols" class="pa-0 d-flex justify-center">

+ 278 - 0
src/views/CourseList copy.vue

@@ -0,0 +1,278 @@
+<script setup>
+import { ref, reactive } from "vue";
+import axios from "axios";
+import moment from "moment";
+import Navbar from "@/components/NavbarSub.vue";
+
+let searchInput = ref("");
+let searchError = ref(false);
+const courseAll = reactive({
+  classes: [],
+});
+const courseData = reactive({
+  classes: [],
+});
+
+(async function getData() {
+  try {
+    const response = await axios.get("https://cmm.ai:8088/api/get_class_name");
+    console.log("response.data.classes", response.data.classes);
+    courseAll.classes = response.data.classes;
+    courseData.classes = response.data.classes;
+  } catch (error) {
+    console.error(error);
+  }
+})();
+
+// 搜尋
+async function search() {
+  searchError.value = false;
+  let keyword = searchInput.value;
+  if (keyword !== "") {
+    try {
+      const response = await axios.get(
+        `https://cmm.ai:8088/api/search_class_like?keyword=${keyword}`
+      );
+      if (response.data.classes.length !== 0) {
+        courseData.classes = response.data.classes;
+      } else {
+        searchError.value = true;
+      }
+    } catch (error) {
+      console.error(error);
+    }
+  } else {
+    courseData.classes = courseAll.classes;
+  }
+}
+
+// async function setFavoriteClass(classId, userId = 1) {
+//   // /api/add_favorite_class
+//   console.log("classId", classId);
+//   console.log("userId", userId);
+
+//   const formData = new FormData();
+//   formData.append("class_event_id", classId);
+//   formData.append("user_id", userId);
+
+//   try {
+//     const response = await axios.post("https://cmm.ai:8088/api/add_favorite_class", formData);
+//     console.log("add_favorite_class response", response);
+//   } catch (error) {
+//     console.error(error);
+//   }
+// }
+
+// 收藏課程清單
+let favorites = reactive({
+  list: [],
+});
+
+let token = localStorage.getItem("token");
+console.log("token", token);
+
+async function setFavoriteClass(classId) {
+  const url = `https://cmm.ai:8088/api/add_favorite_class?class_event_id=${classId}&access_token=${token}`;
+  try {
+    const response = await axios.post(url, "", {
+      headers: {
+        Accept: "application/json",
+      },
+    });
+    console.log("add_favorite_class response", response);
+    getFavoriteClass();
+  } catch (error) {
+    console.error(error);
+  }
+}
+
+async function getFavoriteClass() {
+  console.log("getFavoriteClass");
+  try {
+    const response = await axios.get(
+      `https://cmm.ai:8088/api/get_favorite_class?access_token=${token}`
+    );
+    console.log("getFavoriteClass response", response);
+  } catch (error) {
+    console.error(error);
+  }
+}
+
+function isClassFavorite(classId) {
+  console.log("isClassFavorite", classId);
+  console.log(
+    "favorites.list.includes(classId)",
+    favorites.list.includes(classId)
+  );
+  return favorites.list.includes(classId);
+}
+</script>
+
+<template>
+  <Navbar />
+  <div class="banner">
+    <img src="@/assets/img/img-01.jpg" alt="" />
+    <h3>課程清單</h3>
+  </div>
+
+  <v-container class="my-16 pa-0">
+    <div
+      class="d-flex align-center ms-auto mb-16 search-btn"
+      style="max-width: 300px"
+    >
+      <v-text-field
+        label="課程搜尋"
+        v-model="searchInput"
+        variant="outlined"
+        density="compact"
+        hide-details
+        @keyup.enter="search()"
+      ></v-text-field>
+      <div v-if="searchError" class="d-flex justify-center error">
+        <v-icon color="primary" icon="mdi-alert" class="me-2"></v-icon>
+        沒有符合搜尋條件的項目
+      </div>
+      <button @click="search()" class="btn">
+        <img src="@/assets/img/icon/search.png" alt="" width="25" />
+      </button>
+    </div>
+    <v-row no-gutters>
+      <v-col
+        v-for="item in courseData.classes"
+        :key="item"
+        cols="12"
+        xl="3"
+        lg="4"
+        sm="6"
+        class="mb-10"
+      >
+        <v-card class="mx-5 h-100">
+          <!-- 傳遞課程 id -->
+          <router-link
+            :to="`/course-detail/${item.class_name_id}`"
+            class="cover-img"
+          >
+            <v-img :src="item.cover_img" height="220px" cover></v-img>
+          </router-link>
+          <v-card-title class="font-weight-medium">
+            {{ item.name }}
+          </v-card-title>
+
+          <v-card-text>
+            <p class="text-gray font-weight-light">{{ item.introduction }}</p>
+            <div class="d-flex align-center mt-2">
+              <img src="@/assets/img/icon/location_icon.png" alt="" />
+              <p class="mb-0 ms-3">
+                {{ item.school }}
+              </p>
+            </div>
+
+            <!-- <ul>
+              <li class="d-flex align-center">
+                <img src="@/assets/img/icon/date_icon.png" alt="" />
+                <p class="mb-0 ms-3">
+                  {{ moment(`${item.start_time}`).format("YYYY/MM/DD") }} -
+                  {{ moment(`${item.end_time}`).format("YYYY/MM/DD") }}
+                </p>
+              </li>
+              <li class="d-flex align-center mt-2">
+                <img src="@/assets/img/icon/location_icon.png" alt="" />
+                <p class="mb-0 ms-3">
+                  {{ item.school }}
+                </p>
+              </li>
+            </ul> -->
+          </v-card-text>
+          <v-card-action class="d-block mt-5">
+            <button
+              class="favorites-btn"
+              @click="setFavoriteClass(item.class_name_id)"
+            >
+              <v-icon
+                v-if="isClassFavorite(item.class_name_id)"
+                color="primary"
+                icon="mdi-bookmark"
+                size="large"
+              ></v-icon>
+              <v-icon
+                v-else
+                color="primary"
+                icon="mdi-bookmark-outline"
+                size="large"
+              ></v-icon>
+            </button>
+          </v-card-action>
+        </v-card>
+      </v-col>
+    </v-row>
+  </v-container>
+</template>
+
+<style lang="scss" scoped>
+p {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  display: -webkit-box;
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical;
+  line-break: after-white-space;
+  line-height: 22px;
+}
+
+.banner {
+  position: relative;
+  img {
+    width: 100%;
+    height: 400px;
+    object-fit: cover;
+    object-position: 0 80%;
+    @media (max-width: 767px) {
+      object-position: center;
+    }
+  }
+  h3 {
+    display: inline-block;
+    padding: 30px 80px;
+    font-size: 26px;
+    font-weight: 500;
+    position: absolute;
+    z-index: 10;
+    top: 50%;
+    left: 50%;
+    letter-spacing: 1px;
+    transform: translate(-50%, -50%);
+    color: #fff;
+    background-color: var(--main-color);
+    @media (max-width: 575px) {
+      padding: 25px 50px;
+      font-size: 20px;
+    }
+  }
+}
+
+.search-btn {
+  position: relative;
+  .btn {
+    margin: 2px 10px 0;
+    transition: all 0.3s;
+    &:hover {
+      opacity: 0.8;
+    }
+  }
+  .error {
+    position: absolute;
+    bottom: -30px;
+  }
+}
+
+.cover-img {
+  display: block;
+  overflow: hidden;
+  .v-img {
+    transition: all 0.5s;
+    &:hover {
+      transform: scale(1.2);
+    }
+  }
+}
+</style>

+ 312 - 197
src/views/CourseList.vue

@@ -1,8 +1,14 @@
 <script setup>
-import { ref, reactive } from "vue";
+import { ref, reactive, watch } from "vue";
 import axios from "axios";
 import moment from "moment";
-import Navbar from "@/components/NavbarSub.vue";
+import Navbar from "@/components/Navbar.vue";
+import { useMainStore } from "@/stores/store";
+
+const store = useMainStore();
+let pageNum = ref(1); // 頁數(預設第一頁)
+let pageAmount = ref(9); // 每頁顯示筆數
+let totalPages = ref(1); // 總頁數
 
 let searchInput = ref("");
 let searchError = ref(false);
@@ -13,243 +19,352 @@ const courseData = reactive({
   classes: [],
 });
 
-// 取得資料
-(async function getData() {
+const listLocation = ref(null);
+
+// 切換分頁時回到列表上方
+watch(pageNum, () => {
+  getClass();
+  listLocation.value.scrollIntoView({ behavior: "smooth", block: "start" });
+});
+
+async function getClass() {
+  let url = `https://cmm.ai:8088/api/get_class_name?page_num=${pageNum.value}&page_amount=${pageAmount.value}`;
+
   try {
-    const response = await axios.get("https://cmm.ai:8088/api/get_class_name");
-    console.log("response.data.classes", response.data.classes);
+    const response = await axios.get(url);
+    totalPages.value = store.getTotalPages(response.data.total_num, 9);
     courseAll.classes = response.data.classes;
     courseData.classes = response.data.classes;
   } catch (error) {
     console.error(error);
   }
-})();
-
-// 搜尋
-async function search() {
-  searchError.value = false;
-  let keyword = searchInput.value;
-  if (keyword !== "") {
-    try {
-      const response = await axios.get(
-        `https://cmm.ai:8088/api/search_class_like?keyword=${keyword}`
-      );
-      if (response.data.classes.length !== 0) {
-        courseData.classes = response.data.classes;
-      } else {
-        searchError.value = true;
-      }
-    } catch (error) {
-      console.error(error);
-    }
-  } else {
-    courseData.classes = courseAll.classes;
-  }
 }
 
-// async function setFavoriteClass(classId, userId = 1) {
-//   // /api/add_favorite_class
-//   console.log("classId", classId);
-//   console.log("userId", userId);
+getClass();
+
+// 開啟登入視窗
+function openLoginDialog() {
+  store.loginDialog = true;
+}
+
+// 收藏課程清單
+let favorites = reactive({
+  list: [],
+});
+
+let progress = ref(false);
+let token = localStorage.getItem("token");
 
-//   const formData = new FormData();
-//   formData.append("class_event_id", classId);
-//   formData.append("user_id", userId);
+// 加入收藏課程
+async function setFavoriteClass(classId) {
+  let isLogin = store.checkToken();
+  if (!isLogin) {
+    openLoginDialog();
+    return;
+  }
 
-//   try {
-//     const response = await axios.post("https://cmm.ai:8088/api/add_favorite_class", formData);
-//     console.log("add_favorite_class response", response);
-//   } catch (error) {
-//     console.error(error);
-//   }
-// }
+  progress.value = true;
 
-async function setFavoriteClass(classId, userId = 2) {
-  const url = `https://cmm.ai:8088/api/add_favorite_class?class_event_id=${classId}&user_id=${userId}`;
+  const url = `https://cmm.ai:8088/api/add_favorite_class?class_name_id=${classId}&access_token=${token}`;
+  try {
+    const response = await axios.post(url);
+    getFavoriteClass();
+    progress.value = false;
+  } catch (error) {
+    console.error(error);
+  }
+}
 
+// 取得收藏課程
+async function getFavoriteClass() {
   try {
-    const response = await axios.post(url, '', {
-      headers: {
-        'Accept': 'application/json'
-      }
-    });
-    console.log("add_favorite_class response", response);
-    getFavoriteClass(userId);
+    const response = await axios.get(
+      `https://cmm.ai:8088/api/get_favorite_class?access_token=${token}`
+    );
+    favorites.list = response.data.favorite_courses;
   } catch (error) {
     console.error(error);
   }
 }
 
-async function getFavoriteClass(userId) {
-  console.log("getFavoriteClass userId", userId);
+getFavoriteClass();
+
+// 刪除收藏課程
+async function deleteFavoriteClass(classId) {
+  progress.value = true;
   try {
-    const response = await axios.get(`https://cmm.ai:8088/api/get_favorite_class?user_id=${userId}`);
-    console.log("getFavoriteClass response", response);
+    const response = await axios.post(
+      `https://cmm.ai:8088/api/delete_favorite_class?class_name_id=${classId}&access_token=${token}`
+    );
+    progress.value = false;
+    getFavoriteClass();
   } catch (error) {
     console.error(error);
   }
 }
+
+// 檢查收藏狀態
+function isClassFavorite(classId) {
+  let list = favorites.list.map((e) => e.class_name_id);
+  return list.includes(classId);
+}
+
+const breadcrumbs = reactive([
+  {
+    title: "首頁",
+    disabled: false,
+    href: "/",
+  },
+  {
+    title: "探索課程",
+    disabled: true,
+  },
+]);
+
+const testData = [
+  {
+    title: "種子教師研習",
+    start_time: "2023/06/15",
+    end_time: "2023/06/20",
+    address: "地方工藝館 工藝教室",
+    img: store.getImageUrl("college-group/img.jpg"),
+  },
+  {
+    title: "種子教師研習",
+    start_time: "2023/06/15",
+    end_time: "2023/06/20",
+    address: "地方工藝館 工藝教室",
+    img: store.getImageUrl("college-group/img.jpg"),
+  },
+  {
+    title: "種子教師研習",
+    start_time: "2023/06/15",
+    end_time: "2023/06/20",
+    address: "地方工藝館 工藝教室",
+    img: store.getImageUrl("college-group/img.jpg"),
+  },
+];
 </script>
 
 <template>
-  <Navbar />
-  <div class="banner">
-    <img src="@/assets/img/img-01.jpg" alt="" />
-    <h3>課程清單</h3>
-  </div>
-
-  <v-container class="my-16 pa-0">
-    <div
-      class="d-flex align-center ms-auto mb-16 search-btn"
-      style="max-width: 300px"
-    >
-      <v-text-field
-        label="課程搜尋"
-        v-model="searchInput"
-        variant="outlined"
-        density="compact"
-        hide-details
-        @keyup.enter="search()"
-      ></v-text-field>
-      <div v-if="searchError" class="d-flex justify-center error">
-        <v-icon color="primary" icon="mdi-alert" class="me-2"></v-icon>
-        沒有符合搜尋條件的項目
+  <div class="college-bg-img">
+    <Navbar />
+    <v-container fluid class="college-content pb-16 px-sm-0">
+      <div class="banner">
+        <img src="@/assets/img/course/banner.png" alt="" />
       </div>
-      <button @click="search()" class="btn">
-        <img src="@/assets/img/icon/search.png" alt="" width="25" />
-      </button>
-    </div>
-    <v-row no-gutters>
-      <v-col
-        v-for="item in courseData.classes"
-        :key="item"
-        cols="12"
-        xl="3"
-        lg="4"
-        sm="6"
-        class="mb-10"
-      >
-        <v-card class="mx-5 h-100">
-          <!-- 傳遞課程 id -->
-          <router-link
-            :to="`/course-detail/${item.class_name_id}`"
-            class="cover-img"
+      <div class="main-block">
+        <v-breadcrumbs
+          :items="breadcrumbs"
+          divider="/"
+          class="mt-10 pa-0"
+        ></v-breadcrumbs>
+
+        <div
+          class="d-flex flex-column flex-sm-row align-center justify-space-between title"
+        >
+          <h2>最新開課</h2>
+          <div class="search">
+            <span>
+              <input
+                v-model="searchInput"
+                type="text"
+                @keyup.enter="search()"
+              />
+              <button @click="search()">
+                <img src="@/assets/img/news/news-search-icon.png" alt="" />
+              </button>
+            </span>
+            <div
+              v-if="searchError"
+              class="d-flex justify-center align-center error me-4"
+            >
+              <v-icon color="primary" icon="mdi-alert" class="me-2"></v-icon>
+              沒有符合搜尋條件的項目
+            </div>
+          </div>
+        </div>
+
+        <v-row>
+          <v-col
+            sm="6"
+            md="4"
+            cols="12"
+            v-for="(item, index) in testData"
+            :key="index"
+            class="pa-5"
           >
-            <v-img :src="item.cover_img" height="220px" cover></v-img>
-          </router-link>
-          <v-card-title class="font-weight-medium">
-            {{ item.name }}
-          </v-card-title>
-
-          <v-card-text>
-            <p class="text-gray font-weight-light">{{ item.introduction }}</p>
-            <div class="d-flex align-center mt-2">
-              <img src="@/assets/img/icon/location_icon.png" alt="" />
-              <p class="mb-0 ms-3">
-                {{ item.school }}
-              </p>
+            <div class="main-card">
+              <section class="card-title">
+                <h3>{{ item.title }}</h3>
+              </section>
+              <div class="card-info">
+                <img :src="item.img" alt="" class="cover-img" />
+                <ul>
+                  <li class="d-flex align-center">
+                    <img src="@/assets/img/icon/date_icon.png" alt="" />
+                    <p class="mb-0 ms-3">
+                      {{ moment(`${item.start_time}`).format("YYYY/MM/DD") }} -
+                      {{ moment(`${item.end_time}`).format("YYYY/MM/DD") }}
+                    </p>
+                  </li>
+                  <li class="d-flex align-center mt-3">
+                    <img src="@/assets/img/icon/location_icon.png" alt="" />
+                    <p class="mb-0 ms-3">
+                      {{ item.address }}
+                    </p>
+                  </li>
+                </ul>
+              </div>
             </div>
+          </v-col>
+        </v-row>
 
-            <!-- <ul>
-              <li class="d-flex align-center">
-                <img src="@/assets/img/icon/date_icon.png" alt="" />
-                <p class="mb-0 ms-3">
-                  {{ moment(`${item.start_time}`).format("YYYY/MM/DD") }} -
-                  {{ moment(`${item.end_time}`).format("YYYY/MM/DD") }}
-                </p>
-              </li>
-              <li class="d-flex align-center mt-2">
-                <img src="@/assets/img/icon/location_icon.png" alt="" />
-                <p class="mb-0 ms-3">
-                  {{ item.school }}
-                </p>
-              </li>
-            </ul> -->
-          </v-card-text>
-          <v-card-action class="d-block mt-5">
-            <button
-              class="favorites-btn"
-              @click="setFavoriteClass(item.class_name_id)"
+        <div
+          class="d-flex flex-column flex-sm-row align-center justify-space-between title"
+          ref="listLocation"
+        >
+          <h2>課程清單</h2>
+          <div class="search">
+            <span>
+              <input
+                v-model="searchInput"
+                type="text"
+                @keyup.enter="search()"
+              />
+              <button @click="search()">
+                <img src="@/assets/img/news/news-search-icon.png" alt="" />
+              </button>
+            </span>
+            <div
+              v-if="searchError"
+              class="d-flex justify-center align-center error me-4"
             >
-              <v-icon
-                color="primary"
-                icon="mdi-bookmark-outline"
-                size="large"
-              ></v-icon>
-              <v-icon color="primary" icon="mdi-bookmark" size="large"></v-icon>
-            </button>
-          </v-card-action>
-        </v-card>
-      </v-col>
-    </v-row>
-  </v-container>
+              <v-icon color="primary" icon="mdi-alert" class="me-2"></v-icon>
+              沒有符合搜尋條件的項目
+            </div>
+          </div>
+        </div>
+
+        <v-row>
+          <v-col
+            sm="6"
+            md="4"
+            cols="12"
+            v-for="(item, index) in courseData.classes"
+            :key="index"
+            class="pa-5"
+          >
+            <div class="main-card">
+              <section class="card-title">
+                <h3>{{ item.name }}</h3>
+              </section>
+              <div class="card-info">
+                <router-link
+                  :to="`/course-detail/${item.class_name_id}`"
+                  class="cover-img"
+                >
+                  <v-img
+                    class="mx-auto cover-img"
+                    :lazy-src="item.cover_img"
+                    height="220px"
+                    cover
+                    :src="item.cover_img"
+                  >
+                    <template v-slot:placeholder>
+                      <div
+                        class="d-flex align-center justify-center fill-height"
+                      >
+                        <v-progress-circular
+                          color="grey-lighten-4"
+                          indeterminate
+                        ></v-progress-circular>
+                      </div>
+                    </template>
+                  </v-img>
+                  <!-- <v-img :src="item.cover_img" height="220px" cover></v-img> -->
+                </router-link>
+                <ul>
+                  <li class="d-flex align-center">
+                    <p class="text-gray font-weight-light">
+                      {{ item.introduction }}
+                    </p>
+                  </li>
+                  <li class="d-flex align-center mt-3">
+                    <img src="@/assets/img/icon/location_icon.png" alt="" />
+                    <p class="mb-0 ms-3">
+                      {{ item.school }}
+                    </p>
+                  </li>
+                  <li>
+                    <div class="d-block mt-5">
+                      <button class="favorites-btn">
+                        <v-icon
+                          v-if="isClassFavorite(item.class_name_id)"
+                          @click="deleteFavoriteClass(item.class_name_id)"
+                          color="primary"
+                          icon="mdi-bookmark"
+                          size="large"
+                        ></v-icon>
+                        <v-icon
+                          v-else
+                          @click="setFavoriteClass(item.class_name_id)"
+                          color="primary"
+                          icon="mdi-bookmark-outline"
+                          size="large"
+                        ></v-icon>
+                      </button>
+                    </div>
+                  </li>
+                </ul>
+              </div>
+            </div>
+          </v-col>
+        </v-row>
+        <div class="progress-item" v-if="progress">
+          <v-progress-circular
+            :size="50"
+            indeterminate
+            color="primary"
+          ></v-progress-circular>
+        </div>
+        <v-pagination
+          v-model="pageNum"
+          :length="totalPages"
+          rounded="circle"
+          class="mt-16"
+        ></v-pagination>
+      </div>
+    </v-container>
+  </div>
 </template>
 
 <style lang="scss" scoped>
-p {
-  overflow: hidden;
-  text-overflow: ellipsis;
-  display: -webkit-box;
-  -webkit-line-clamp: 2;
-  -webkit-box-orient: vertical;
-  line-break: after-white-space;
-  line-height: 22px;
-}
-
 .banner {
-  position: relative;
+  display: flex;
+  justify-content: end;
   img {
-    width: 100%;
-    height: 400px;
-    object-fit: cover;
-    object-position: 0 80%;
-    @media (max-width: 767px) {
-      object-position: center;
-    }
-  }
-  h3 {
-    display: inline-block;
-    padding: 30px 80px;
-    font-size: 26px;
-    font-weight: 500;
-    position: absolute;
-    z-index: 10;
-    top: 50%;
-    left: 50%;
-    letter-spacing: 1px;
-    transform: translate(-50%, -50%);
-    color: #fff;
-    background-color: var(--main-color);
-    @media (max-width: 575px) {
-      padding: 25px 50px;
-      font-size: 20px;
-    }
+    width: 75%;
   }
 }
 
-.search-btn {
-  position: relative;
-  .btn {
-    margin: 2px 10px 0;
-    transition: all 0.3s;
-    &:hover {
-      opacity: 0.8;
+.main-block {
+  margin-top: -20%;
+
+  .main-card {
+    height: 100%;
+    position: relative;
+    .card-info {
+      line-height: 24px;
+      ul {
+        padding: 20px 10px;
+      }
     }
   }
-  .error {
-    position: absolute;
-    bottom: -30px;
-  }
 }
 
-.cover-img {
-  display: block;
-  overflow: hidden;
-  .v-img {
-    transition: all 0.5s;
-    &:hover {
-      transform: scale(1.2);
-    }
-  }
+.v-pagination {
+  margin: auto;
+  max-width: 500px;
 }
 </style>

+ 10 - 9
src/views/Home.vue

@@ -70,14 +70,14 @@ const getClassList = async (locationId) => {
         以佈局具國際視野之工藝學習共享平台為目標,藉由「工藝學校」的主體概念,推動臺灣工藝學校全球學習平台,以共享、友善、全人、全民的終身工藝手作平台進行人才、課程、知識、教材之工藝資源嫁接媒合與內容設計,以在地、就近、線上、線下等多元方式提供不同型態之學習體驗內容及選擇。
       </p>
       <p>
-        With the goal of laying out a craft learning sharing platform with an
-        international perspective, through the main concept of "craft school",
-        promote the global learning platform of Taiwan craft schools, and use a
-        lifelong craft handicraft platform of sharing, friendliness, whole
-        people and the whole people to graft and match and design the craft
-        resources of talents, courses, knowledge and teaching materials, and
-        provide different types of learning experience content and choices in
-        local, nearby, online and offline ways.
+        With the goal of laying out a craft learning and sharing platform with
+        an international perspective, through the main concept of "craft
+        school", we promote the global learning platform of International Craft
+        Learning Platform co-ops, and use a lifelong craft platform run on the
+        values of sharing, friendliness, and holisticness to design and
+        integrate craft resources such as talents, courses, knowledge and
+        teaching materials, and provide different types of learning experiences
+        in local ways, both online and offline.
       </p>
     </section>
 
@@ -179,8 +179,9 @@ const getClassList = async (locationId) => {
           </v-list>
           <v-pagination
             v-model="pageNum"
-            class="my-4"
             :length="totalPages"
+            class="my-4"
+            rounded="circle"
           ></v-pagination>
         </v-col>
       </div>

+ 0 - 14
src/views/Login.vue

@@ -2,7 +2,6 @@
 import { ref, reactive, onMounted, watch } from "vue";
 import { useMainStore } from "@/stores/store";
 import axios from "axios";
-import Navbar from "@/components/Navbar.vue";
 
 const store = useMainStore();
 const emit = defineEmits(["close"]);
@@ -334,19 +333,6 @@ console.log("JWT 過期時間:", formattedExpiration);
       <a href="">忘記密碼</a><span class="mx-1">/</span><a href="">忘記帳號</a>
     </v-card-actions>
   </v-card>
-
-  <!-- <div style="width: 200px; margin: 20px 0">
-    <div
-      id="g_id_onload"
-      data-client_id="626437744072-q6djn202411is5vdk2v0tu8fo7n07qr0.apps.googleusercontent.com"
-      data-callback="onSignIn1"
-    ></div>
-    <div class="g_id_signin" data-type="standard"></div>
-
-    <p class="mt-3">目前狀態:</p>
-
-    <span id="GOOGLE_STATUS_1"></span>
-  </div> -->
 </template>
 
 <style lang="scss">

+ 0 - 1
src/views/News.vue

@@ -18,7 +18,6 @@ const newsData = reactive({
   list: [],
 });
 
-// 取得資料
 (async function getData() {
   try {
     const response = await axios.get("https://cmm.ai:8088/api/get_news");

+ 0 - 1
src/views/NewsDetail.vue

@@ -11,7 +11,6 @@ const news = reactive({
   data: [],
 });
 
-// 取得資料
 (async function getData() {
   try {
     const response = await axios.get(

+ 176 - 0
src/views/User/Dashboard.vue

@@ -0,0 +1,176 @@
+<script setup>
+import { ref, reactive, computed } from "vue";
+import axios from "axios";
+import Navbar from "@/components/Navbar.vue";
+
+let token = localStorage.getItem("token");
+let username = ref("");
+
+(async () => {
+  try {
+    const response = await axios.get(
+      `https://cmm.ai:8088/api/information?token=${token}`
+    );
+    console.log("response", response.data.msg);
+    username.value = response.data.msg.username;
+  } catch (error) {
+    console.error(error);
+  }
+})();
+
+const firstName = computed(() => {
+  const firstChar = username.value.charAt(0);
+  return firstChar.toUpperCase();
+});
+
+let items = [
+  { type: "divider" },
+  {
+    title: "個人檔案",
+    icon: "mdi-account",
+    url: "/user/profile",
+    value: 1,
+  },
+  {
+    title: "學習護照",
+    icon: "mdi-book",
+    url: "/user/passport",
+    value: 2,
+  },
+  {
+    title: "我的收藏",
+    icon: "mdi-bookmark",
+    url: "/user/favorite-class",
+    value: 3,
+  },
+  {
+    title: "帳號設定",
+    icon: "mdi-cog",
+    url: "/",
+    value: 4,
+  },
+  { type: "divider" },
+];
+</script>
+
+<template>
+  <Navbar />
+  <v-container class="py-16 px-lg-0 dashboard-container">
+    <v-row>
+      <v-col cols="12" md="3">
+        <v-card class="pa-5">
+          <div class="user-info">
+            <div class="img">
+              <span> {{ firstName }}</span>
+            </div>
+            <p class="name">{{ username }}</p>
+          </div>
+
+          <v-list class="w-100 mt-1">
+            <v-list-item v-for="(item, index) in items" :key="index">
+              <v-divider v-if="item.type === 'divider'"></v-divider>
+              <v-list-item-content v-else class="d-flex justify-center">
+                <router-link :to="item.url" class="d-flex">
+                  <v-icon color="gray" class="me-2">{{ item.icon }}</v-icon>
+                  <v-list-item-title>{{ item.title }}</v-list-item-title>
+                </router-link>
+              </v-list-item-content>
+            </v-list-item>
+            <v-list-item>
+              <v-list-item-content class="d-flex justify-center">
+                <router-link to="/" class="d-flex">
+                  <v-icon color="gray" class="me-2">mdi-logout</v-icon>
+                  <v-list-item-title>登出</v-list-item-title>
+                </router-link>
+              </v-list-item-content>
+            </v-list-item>
+          </v-list>
+        </v-card>
+      </v-col>
+
+      <v-col cols="12" md="9">
+        <router-view></router-view>
+        <!-- <v-card class="h-100">
+          <v-tabs v-model="tab" color="primary" align-tabs="start">
+            <v-tab :value="1">我的收藏</v-tab>
+            <v-tab :value="2">我的學習</v-tab>
+          </v-tabs>
+          <v-window v-model="tab">
+            <v-window-item :value="1">
+              <v-container>
+                <v-row>
+                  <v-col> 我的收藏 </v-col>
+                </v-row>
+              </v-container>
+            </v-window-item>
+            <v-window-item :value="2">
+              <v-container>
+                <v-row>
+                  <v-col> 我的學習 </v-col>
+                </v-row>
+              </v-container>
+            </v-window-item>
+          </v-window>
+        </v-card> -->
+      </v-col>
+    </v-row>
+  </v-container>
+</template>
+
+<style lang="scss">
+.dashboard-container {
+  .title {
+    display: flex;
+    justify-content: center;
+    h4 {
+      margin: 20px 0;
+      padding-bottom: 20px;
+      display: inline-block;
+      font-size: 22px;
+      font-weight: 500;
+      text-align: center;
+      border-bottom: 3px solid var(--main-color);
+    }
+  }
+
+  .user-info {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    .img {
+      width: 70px;
+      height: 70px;
+      background-color: var(--main-color);
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      border-radius: 100px;
+      span {
+        color: #fff;
+        font-size: 34px;
+        font-weight: 500;
+      }
+    }
+    .name {
+      font-size: 28px;
+      margin-top: 15px;
+    }
+  }
+
+  .v-card {
+    padding: 25px;
+    .v-btn {
+      font-size: 18px;
+    }
+  }
+
+  .v-list {
+    a {
+      transition: all 0.3s;
+      &:hover {
+        opacity: 0.8;
+      }
+    }
+  }
+}
+</style>

+ 206 - 0
src/views/User/FavoriteClass.vue

@@ -0,0 +1,206 @@
+<script setup>
+import { ref, reactive, watch } from "vue";
+import { useMainStore } from "@/stores/store";
+import axios from "axios";
+
+const store = useMainStore();
+let favorites = reactive({
+  list: [],
+});
+let favoritesAll = reactive({
+  list: [],
+});
+
+let token = store.token;
+
+// 取得收藏課程
+async function getFavoriteClass() {
+  try {
+    const response = await axios.get(
+      `https://cmm.ai:8088/api/get_favorite_class?access_token=${token}`
+    );
+    console.log(
+      "response.data.favorite_courses",
+      response.data.favorite_courses
+    );
+    favorites.list = response.data.favorite_courses;
+    favoritesAll.list = response.data.favorite_courses;
+  } catch (error) {
+    console.error(error);
+  }
+}
+
+getFavoriteClass();
+
+let progress = ref(false);
+
+// 刪除收藏課程
+async function deleteFavoriteClass(classId) {
+  progress.value = true;
+  console.log("刪除");
+  try {
+    const response = await axios.post(
+      `https://cmm.ai:8088/api/delete_favorite_class?class_name_id=${classId}&access_token=${token}`
+    );
+    progress.value = false;
+    getFavoriteClass();
+    console.log("deleteFavoriteClass", response);
+  } catch (error) {
+    console.error(error);
+  }
+}
+
+let timeoutId = null;
+let searchError = ref(false);
+const searchText = ref("");
+const filtered = reactive({
+  list: [],
+});
+
+watch(searchText, (val) => {
+  if (val === "") {
+    favorites.list = favoritesAll.list;
+  }
+});
+
+// 延遲觸發
+const handleInput = () => {
+  clearTimeout(timeoutId);
+  timeoutId = setTimeout(handleSearch, 500);
+};
+
+// 課程搜尋
+const handleSearch = () => {
+  filtered.list = favorites.list.filter((item) =>
+    item.name.includes(searchText.value)
+  );
+  if (filtered.list.length) {
+    favorites.list = filtered.list;
+    searchError.value = false;
+  } else {
+    console.log("無");
+    favorites.list = favoritesAll.list;
+    searchError.value = true;
+    setTimeout(() => {
+      searchError.value = false;
+    }, 2000);
+  }
+  console.log("filtered", filtered);
+};
+</script>
+
+<template>
+  <v-card class="h-100">
+    <div class="title">
+      <h4>我的收藏</h4>
+    </div>
+    <v-text-field
+      v-model.lazy="searchText"
+      class="ms-5 mt-5"
+      hide-details
+      @input="handleInput"
+    >
+      <template v-slot:label>
+        <span>
+          搜尋課程<v-icon
+            icon="mdi-text-box-search-outline"
+            class="pb-1 ps-1"
+          ></v-icon>
+        </span>
+      </template>
+    </v-text-field>
+    <div class="error">
+      <div v-if="searchError" class="d-flex align-center ms-6 pt-2">
+        <v-icon color="primary" icon="mdi-alert" class="me-2"></v-icon>
+        沒有符合搜尋條件的項目
+      </div>
+    </div>
+    <v-container>
+      <v-row>
+        <v-col
+          sm="6"
+          cols="12"
+          v-for="(item, index) in favorites.list"
+          :key="index"
+          class="pa-5"
+        >
+          <div class="h-100 main-card">
+            <section class="card-title">
+              <h3>{{ item.name }}</h3>
+            </section>
+            <div class="card-info">
+              <router-link
+                :to="`/course-detail/${item.class_name_id}`"
+                class="cover-img"
+              >
+                <v-img
+                  class="mx-auto cover-img"
+                  :lazy-src="item.cover_img"
+                  height="220px"
+                  cover
+                  :src="item.cover_img"
+                >
+                  <template v-slot:placeholder>
+                    <div class="d-flex align-center justify-center fill-height">
+                      <v-progress-circular
+                        color="grey-lighten-4"
+                        indeterminate
+                      ></v-progress-circular>
+                    </div>
+                  </template>
+                </v-img>
+              </router-link>
+              <ul>
+                <li class="d-flex align-center">
+                  <p class="text-gray font-weight-light">
+                    {{ item.introduction }}
+                  </p>
+                </li>
+                <li class="d-flex align-center mt-3">
+                  <img src="@/assets/img/icon/location_icon.png" alt="" />
+                  <p class="mb-0 ms-3">
+                    {{ item.school }}
+                  </p>
+                </li>
+                <li>
+                  <div class="d-block mt-5">
+                    <button class="favorites-btn">
+                      <v-icon
+                        @click="deleteFavoriteClass(item.class_name_id)"
+                        color="primary"
+                        icon="mdi-bookmark"
+                        size="large"
+                      ></v-icon>
+                    </button>
+                  </div>
+                </li>
+              </ul>
+            </div>
+          </div>
+        </v-col>
+      </v-row>
+      <div class="progress-item" v-if="progress">
+        <v-progress-circular
+          :size="50"
+          indeterminate
+          color="primary"
+        ></v-progress-circular>
+      </div>
+    </v-container>
+  </v-card>
+</template>
+
+<style lang="scss" scoped>
+.main-card {
+  position: relative;
+  .card-info {
+    line-height: 24px;
+    ul {
+      padding: 20px 10px;
+    }
+  }
+}
+.error {
+  height: 25px;
+}
+</style>

+ 18 - 0
src/views/User/Passport.vue

@@ -0,0 +1,18 @@
+<script setup>
+import { ref, reactive } from "vue";
+import { useMainStore } from "@/stores/store";
+import PDFViewer from "@/components/PDFViewer.vue";
+
+const store = useMainStore();
+</script>
+
+<template>
+  <v-card class="h-100">
+    <div class="title">
+      <h4>學習護照</h4>
+    </div>
+    <PDFViewer />
+  </v-card>
+</template>
+
+<style lang="scss" scoped></style>

+ 17 - 0
src/views/User/Profile.vue

@@ -0,0 +1,17 @@
+<script setup>
+import { ref, reactive } from "vue";
+import { useMainStore } from "@/stores/store";
+import axios from "axios";
+
+const store = useMainStore();
+</script>
+
+<template>
+  <v-card class="h-100">
+    <div class="title">
+      <h4>個人檔案</h4>
+    </div>
+  </v-card>
+</template>
+
+<style lang="scss" scoped></style>

+ 0 - 102
src/views/UserProfile.vue

@@ -1,102 +0,0 @@
-<script setup>
-import { ref, reactive, watch, computed } from "vue";
-import { useMainStore } from "@/stores/store";
-import axios from "axios";
-import moment from "moment";
-import Navbar from "@/components/Navbar.vue";
-
-const store = useMainStore();
-const user = reactive({
-  data: [],
-});
-let token = localStorage.getItem("token");
-console.log("getItem token", token);
-let username = ref("");
-
-// 取得資料
-(async () => {
-  try {
-    const response = await axios.get(
-      `https://cmm.ai:8088/api/information?token=${token}`
-    );
-    console.log("response", response.data.msg);
-    username.value = response.data.msg.username;
-  } catch (error) {
-    console.error(error);
-  }
-})();
-
-const firstName = computed(() => {
-  const firstChar = username.value.charAt(0);
-  return firstChar.toUpperCase();
-});
-
-let tab = ref(null);
-</script>
-
-<template>
-  <Navbar />
-  <v-container>
-    <div class="user-info ma-5">
-      <div class="img">
-        <span> {{ firstName }}</span>
-      </div>
-      <p class="name">{{ username }}</p>
-    </div>
-
-    <v-card>
-      <v-tabs v-model="tab" color="primary" align-tabs="start">
-        <v-tab :value="1">我的收藏</v-tab>
-        <v-tab :value="2">我的學習</v-tab>
-      </v-tabs>
-      <v-window v-model="tab">
-        <v-window-item :value="1">
-          <v-container>
-            <v-row>
-              <v-col> 我的收藏 </v-col>
-            </v-row>
-          </v-container>
-        </v-window-item>
-        <v-window-item :value="2">
-          <v-container>
-            <v-row>
-              <v-col> 我的學習 </v-col>
-            </v-row>
-          </v-container>
-        </v-window-item>
-      </v-window>
-    </v-card>
-  </v-container>
-</template>
-
-<style lang="scss" scoped>
-.user-info {
-  display: flex;
-  align-items: center;
-  .img {
-    width: 70px;
-    height: 70px;
-    background-color: var(--main-color);
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    border-radius: 100px;
-    span {
-      color: #fff;
-      font-size: 34px;
-      font-weight: 500;
-    }
-  }
-  .name {
-    font-size: 28px;
-    margin-left: 15px;
-  }
-}
-
-.v-card {
-  padding: 25px;
-  .v-btn {
-    font-size: 18px;
-  }
-}
-</style>

部分文件因文件數量過多而無法顯示