SyuanYu 2 rokov pred
rodič
commit
755e7a51f6
33 zmenil súbory, kde vykonal 2606 pridanie a 193 odobranie
  1. 1153 1
      package-lock.json
  2. 1 0
      package.json
  3. 5 12
      src/App.vue
  4. 38 0
      src/assets/css/style.css
  5. 1 1
      src/assets/css/style.css.map
  6. 46 2
      src/assets/css/style.scss
  7. BIN
      src/assets/img/college-group/background.png
  8. BIN
      src/assets/img/college-group/generation/世代工藝 橫幅.png
  9. BIN
      src/assets/img/college-group/generation/世代工藝 素材-07.jpg
  10. BIN
      src/assets/img/college-group/generation/世代工藝 素材-08.jpg
  11. BIN
      src/assets/img/college-group/generation/世代工藝 素材-09.jpg
  12. BIN
      src/assets/img/college-group/generation/世代工藝 素材-10.jpg
  13. BIN
      src/assets/img/college-group/generation/世代工藝 素材-11.jpg
  14. BIN
      src/assets/img/college-group/generation/世代工藝 素材-12.jpg
  15. BIN
      src/assets/img/college-group/img.jpg
  16. BIN
      src/assets/img/college-group/技藝橫幅.png
  17. BIN
      src/assets/img/news/news-02(邊框).png
  18. BIN
      src/assets/img/news/news-02.png
  19. BIN
      src/assets/img/news/news-detail-banner.png
  20. 3 7
      src/components/Map.vue
  21. 134 30
      src/components/Navbar.vue
  22. 5 1
      src/main.js
  23. 1 1
      src/plugins/vuetify.js
  24. 28 15
      src/router/index.js
  25. 26 0
      src/stores/store.js
  26. 304 0
      src/views/CollegeGroup/Craft.vue
  27. 304 0
      src/views/CollegeGroup/Generation.vue
  28. 2 2
      src/views/CourseDetail.vue
  29. 2 2
      src/views/CourseList.vue
  30. 4 4
      src/views/Home.vue
  31. 185 14
      src/views/Login.vue
  32. 184 101
      src/views/News.vue
  33. 180 0
      src/views/NewsDetail.vue

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 1153 - 1
package-lock.json


+ 1 - 0
package.json

@@ -15,6 +15,7 @@
     "leaflet": "^1.9.4",
     "leaflet.markercluster": "^1.5.3",
     "moment": "^2.29.4",
+    "pinia": "^2.1.4",
     "vue": "^3.2.47",
     "vue-router": "^4.2.2",
     "vuetify": "^3.3.2"

+ 5 - 12
src/App.vue

@@ -6,17 +6,10 @@ import { RouterView } from "vue-router";
   <RouterView />
 </template>
 
-<style scoped>
-.logo {
-  height: 6em;
-  padding: 1.5em;
-  will-change: filter;
-  transition: filter 300ms;
-}
-.logo:hover {
-  filter: drop-shadow(0 0 2em #646cffaa);
-}
-.logo.vue:hover {
-  filter: drop-shadow(0 0 2em #42b883aa);
+<style lang="scss">
+.v-container {
+  @media (min-width: 1920px) {
+    max-width: 1300px !important;
+  }
 }
 </style>

+ 38 - 0
src/assets/css/style.css

@@ -50,8 +50,46 @@ a {
 /* CSS Reset End */
 :root {
   --main-color: #C39F68;
+  --sub-color: #E9F1F4;
 }
 
 * {
   font-family: "Noto Sans TC", sans-serif;
+}
+
+input:focus-visible {
+  outline: 2px solid var(--sub-color);
+}
+
+.search {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-end;
+  justify-content: end;
+  position: relative;
+}
+.search span {
+  position: relative;
+}
+.search input {
+  padding: 5px 10px;
+  border-radius: 100px;
+  border: 1px solid #ccc;
+  background-color: #fff;
+}
+.search button {
+  position: absolute;
+  right: 10px;
+  left: 0;
+  top: 3px;
+}
+.search button img {
+  width: 25px;
+  position: absolute;
+  top: 2px;
+  right: 0;
+}
+.search .error {
+  position: absolute;
+  bottom: -30px;
 }/*# sourceMappingURL=style.css.map */

+ 1 - 1
src/assets/css/style.css.map

@@ -1 +1 @@
-{"version":3,"sources":["style.scss","style.css"],"names":[],"mappings":"AAAA,cAAA;AAEA;;;;;;;;;;;;;;;;;;;;EAoBE,SAAA;EACA,UAAA;EACA,SAAA;EACA,eAAA;EACA,wBAAA;EACA,sBAAA;ACAF;;ADGA,sBAAA;AAEA;EACE,cAAA;ACDF;;ADIA;;EAEE,gBAAA;ACDF;;ADIA;EACE,eAAA;EACA,YAAA;ACDF;;ADIA;EACE,qBAAA;EACA,cAAA;ACDF;;ADIA,kBAAA;AAEA;EACI,qBAAA;ACFJ;;ADKA;EACI,uCAAA;ACFJ","file":"style.css"}
+{"version":3,"sources":["style.scss","style.css"],"names":[],"mappings":"AAAA,cAAA;AAEA;;;;;;;;;;;;;;;;;;;;EAoBE,SAAA;EACA,UAAA;EACA,SAAA;EACA,eAAA;EACA,wBAAA;EACA,sBAAA;ACAF;;ADGA,sBAAA;AAEA;EACE,cAAA;ACDF;;ADIA;;EAEE,gBAAA;ACDF;;ADIA;EACE,eAAA;EACA,YAAA;ACDF;;ADIA;EACE,qBAAA;EACA,cAAA;ACDF;;ADIA,kBAAA;AAEA;EACE,qBAAA;EACA,oBAAA;ACFF;;ADKA;EACE,uCAAA;ACFF;;ADKA;EACE,mCAAA;ACFF;;ADKA;EACE,aAAA;EACA,sBAAA;EACA,qBAAA;EACA,oBAAA;EACA,kBAAA;ACFF;ADIE;EACE,kBAAA;ACFJ;ADKE;EACE,iBAAA;EACA,oBAAA;EACA,sBAAA;EACA,sBAAA;ACHJ;ADOE;EACE,kBAAA;EACA,WAAA;EACA,OAAA;EACA,QAAA;ACLJ;ADOI;EACE,WAAA;EACA,kBAAA;EACA,QAAA;EACA,QAAA;ACLN;ADSE;EACE,kBAAA;EACA,aAAA;ACPJ","file":"style.css"}

+ 46 - 2
src/assets/css/style.scss

@@ -52,9 +52,53 @@ a {
 /* CSS Reset End */
 
 :root {
-    --main-color: #C39F68;
+  --main-color: #C39F68;
+  --sub-color: #E9F1F4;
 }
 
 * {
-    font-family: 'Noto Sans TC', sans-serif;
+  font-family: 'Noto Sans TC', sans-serif;
+}
+
+input:focus-visible {
+  outline: 2px solid (var(--sub-color));
+}
+
+.search {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-end;
+  justify-content: end;
+  position: relative;
+
+  span {
+    position: relative;
+  }
+
+  input {
+    padding: 5px 10px;
+    border-radius: 100px;
+    border: 1px solid #ccc;
+    background-color: #fff;
+    // box-shadow: 1px 1px 3px #ccc;
+  }
+
+  button {
+    position: absolute;
+    right: 10px;
+    left: 0;
+    top: 3px;
+
+    img {
+      width: 25px;
+      position: absolute;
+      top: 2px;
+      right: 0;
+    }
+  }
+
+  .error {
+    position: absolute;
+    bottom: -30px;
+  }
 }

BIN
src/assets/img/college-group/background.png


BIN
src/assets/img/college-group/generation/世代工藝 橫幅.png


BIN
src/assets/img/college-group/generation/世代工藝 素材-07.jpg


BIN
src/assets/img/college-group/generation/世代工藝 素材-08.jpg


BIN
src/assets/img/college-group/generation/世代工藝 素材-09.jpg


BIN
src/assets/img/college-group/generation/世代工藝 素材-10.jpg


BIN
src/assets/img/college-group/generation/世代工藝 素材-11.jpg


BIN
src/assets/img/college-group/generation/世代工藝 素材-12.jpg


BIN
src/assets/img/college-group/img.jpg


BIN
src/assets/img/college-group/技藝橫幅.png


BIN
src/assets/img/news/news-02(邊框).png


BIN
src/assets/img/news/news-02.png


BIN
src/assets/img/news/news-detail-banner.png


+ 3 - 7
src/components/Map.vue

@@ -6,8 +6,9 @@ import "leaflet.markercluster/dist/MarkerCluster.css";
 import "leaflet.markercluster/dist/MarkerCluster.Default.css";
 import { ref, reactive, onMounted, getCurrentInstance } from "vue";
 import axios from "axios";
-// import mapData from "@/assets/mapData.json";
+import { useMainStore } from "@/stores/store";
 
+const store = useMainStore();
 const mapContainer = ref(null);
 const location_id = ref("");
 const { proxy } = getCurrentInstance();
@@ -26,7 +27,7 @@ onMounted(() => {
 
       mapData.data.forEach((item) => {
         let customIcon = L.icon({
-          iconUrl: getImageUrl('icon_house07.png'),
+          iconUrl: store.getImageUrl("map-icon/icon_house06.png"),
           iconSize: [30, 50],
         });
 
@@ -78,11 +79,6 @@ onMounted(() => {
     L.latLng(25.56, 122.48)
   );
   map.setMaxBounds(taiwanBounds);
-
-  const getImageUrl = (name) => {
-    return new URL(`/src/assets/img/map-icon/${name}`, import.meta.url).href;
-  };
-
   map.setMaxZoom(16); // 最大縮放級別
   map.setMinZoom(8); // 最小縮放級別
 

+ 134 - 30
src/components/Navbar.vue

@@ -1,11 +1,19 @@
 <script setup>
 import { ref } from "vue";
+import Login from "@/views/Login.vue";
 
 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;
+}
 </script>
 
 <template>
@@ -13,26 +21,69 @@ function toggleMenu() {
     <router-link :to="'/'">
       <img src="@/assets/img/logo.png" alt="" />
     </router-link>
-    <ul class="menu d-md-flex align-center" :class="{ slider: menuShow }">
+    <ul class="menu align-center" :class="{ slider: menuShow }">
       <li>
         <router-link :to="'/news'">最新消息</router-link>
       </li>
-      <li>
-       工藝學群
+      <li class="position-relative">
+        <a href="javascript:;" @click="collegeMenuShow = !collegeMenuShow"
+          >工藝學群</a
+        >
+        <!-- <router-link :to="'/college-group/craft'">工藝學群</router-link> -->
+        <div class="college-slider" :class="{ slider: collegeMenuShow }">
+          <ul>
+            <li>
+              <router-link :to="'/college-group/generation'">世代工藝</router-link>
+            </li>
+            <li>
+              <router-link :to="'/college-group/craft'">未來工藝</router-link>
+            </li>
+            <li>
+              <router-link :to="'/college-group/craft'">生活工藝</router-link>
+            </li>
+            <li>
+              <router-link :to="'/college-group/craft'">技藝工藝</router-link>
+            </li>
+            <li>
+              <router-link :to="'/college-group/craft'">青年工藝</router-link>
+            </li>
+            <li>
+              <router-link :to="'/college-group/craft'">修護工藝</router-link>
+            </li>
+            <li>
+              <router-link :to="'/college-group/craft'">跨域工藝</router-link>
+            </li>
+            <li>
+              <router-link :to="'/college-group/craft'">線上工藝</router-link>
+            </li>
+          </ul>
+        </div>
       </li>
       <li>
         <router-link :to="'/course-list'">探索課程</router-link>
       </li>
-      <li>瀕危工藝</li>
-      <li>知識文章</li>
       <li>
-        <router-link :to="'/login'">學員登入</router-link>
+        <router-link :to="'/'">瀕危工藝</router-link>
       </li>
-      <!-- <li>EN</li> -->
       <li>
-        <v-icon icon="mdi-magnify"></v-icon>
+        <router-link :to="'/'">知識文章</router-link>
       </li>
       <li>
+        <!-- <router-link :to="'/login'">
+          學員登入
+        </router-link> -->
+        <v-dialog v-model="dialog" max-width="450">
+          <template v-slot:activator="{ props }">
+            <a href="javascript:;" v-bind="props">學員登入</a>
+          </template>
+          <Login @close="handleClose" />
+        </v-dialog>
+      </li>
+      <!-- <li>EN</li> -->
+      <li class="d-none d-lg-block">
+        <v-icon icon="mdi-magnify"></v-icon>
+      </li>
+      <li class="d-none d-lg-block">
         <v-icon icon="mdi-menu"></v-icon>
       </li>
     </ul>
@@ -45,23 +96,26 @@ function toggleMenu() {
 
 <style lang="scss" scoped>
 .navbar {
-  width: 95vw;
-  max-width: 1200px;
-  margin: 40px auto;
-  padding: 20px 50px 20px 30px;
+  width: 90%;
+  max-width: 1300px;
+  margin: 0 auto 40px;
+  padding: 20px 35px 20px 15px;
   border: 1px solid;
   position: relative;
   z-index: 1000;
   background: transparent;
-  top: 0;
+  top: 40px;
   left: 0;
   right: 0;
 
-  @media (max-width: 1200px) {
-    margin: 40px;
+  @media (max-width: 1280px) {
     padding: 10px 20px 10px 10px;
   }
 
+  @media (max-width: 767px) {
+    top: 20px;
+  }
+
   img {
     width: 100%;
     max-width: 370px;
@@ -69,40 +123,44 @@ function toggleMenu() {
   }
 
   .menu {
+    display: flex;
+    align-items: center;
     list-style: none;
-    overflow: hidden;
-    transition: height 0.3s ease-in-out;
 
-    @media (max-width: 1200px) {
-      height: 0;
+    @media (max-width: 1280px) {
       position: absolute;
-      background: #fff;
-      top: 80px;
+      top: 80.5px;
       left: 0;
       right: 0;
       z-index: 100;
-      display: flex;
+      overflow: hidden;
       flex-direction: column;
-      align-items: center;
+      box-shadow: 1px 1px 4px #ccc;
+      max-height: 0;
+      transition: max-height 0.3s ease-in-out;
     }
 
     &.slider {
-      height: 285px;
+      max-height: 700px;
+      overflow: initial;
     }
 
-    li {
-      margin-left: 30px;
+    & > li {
+      margin-left: 28px;
       font-weight: 400;
 
-      @media (max-width: 1200px) {
+      @media (max-width: 1280px) {
+        width: 100%;
         margin-left: 0;
+        background: #fff;
         border-bottom: 1px solid #f0f0f0;
         font-weight: 500;
-        width: 100%;
         text-align: center;
-        padding: 20px 0;
       }
       a {
+        width: 100%;
+        display: block;
+        padding: 20px 0;
         color: #000;
         text-decoration: none;
         transition: all 0.3s;
@@ -111,13 +169,48 @@ function toggleMenu() {
         }
       }
     }
+
+    .college-slider {
+      position: absolute;
+      top: 60px;
+      left: -30px;
+      width: 130px;
+      background: #fff;
+      text-align: center;
+      box-shadow: 1px 1px 4px #ccc;
+      opacity: 0;
+      transition: all 0.3s;
+
+      @media (max-width: 1280px) {
+        top: 30px;
+        left: 50vw;
+        width: 130px;
+      }
+
+      &.slider {
+        opacity: 1;
+      }
+      li {
+        &:last-child {
+          border-bottom: none;
+        }
+        &:hover {
+          background-color: #eeeeee;
+        }
+        a {
+          display: block;
+          width: 100%;
+          padding: 15px 10px;
+        }
+      }
+    }
   }
 
   .icon {
     display: none;
     transition: all 0.3s;
 
-    @media (max-width: 1200px) {
+    @media (max-width: 1280px) {
       display: block;
     }
 
@@ -129,5 +222,16 @@ function toggleMenu() {
       font-size: 2rem;
     }
   }
+
+  &::before {
+    content: "";
+    position: absolute;
+    top: 6px;
+    left: 6px;
+    width: 99%;
+    height: 88%;
+    background-color: rgba(255, 255, 255, 0.5);
+    z-index: -1;
+  }
 }
 </style>

+ 5 - 1
src/main.js

@@ -3,7 +3,11 @@ import '@/assets/css/style.css'
 import App from './App.vue'
 import router from "./router";
 import vuetify from './plugins/vuetify'
+import { createPinia } from 'pinia'
 
+const pinia = createPinia();
+
+// Google Login
 const script = document.createElement('script');
 script.src = 'https://accounts.google.com/gsi/client';
 script.async = true;
@@ -12,6 +16,6 @@ document.head.appendChild(script);
 
 // 加載完成後再掛載 Vue
 script.onload = () => {
-    createApp(App).use(router).use(vuetify).mount('#app')
+    createApp(App).use(router).use(vuetify).use(pinia).mount('#app')
 };
 

+ 1 - 1
src/plugins/vuetify.js

@@ -22,7 +22,7 @@ export default createVuetify({
         colors: {
           primary: '#C39F68',
           gray: '#888888',
-          brown: '#3E3A39'
+          brown: '#3E3A39',
         },
       },
     },

+ 28 - 15
src/router/index.js

@@ -1,42 +1,55 @@
 import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router';
 import { defineAsyncComponent } from 'vue';
 
-const home = defineAsyncComponent(() => import('@/views/Home.vue'));
-const login = defineAsyncComponent(() => import('@/views/Login.vue'));
-const news = defineAsyncComponent(() => import('@/views/News.vue'));
-const courseList = defineAsyncComponent(() => import('@/views/CourseList.vue'));
-const courseDetail = defineAsyncComponent(() => import('@/views/CourseDetail.vue'));
+const Home = defineAsyncComponent(() => import('@/views/Home.vue'));
+const Login = defineAsyncComponent(() => import('@/views/Login.vue'));
+const News = defineAsyncComponent(() => import('@/views/News.vue'));
+const NewsDetail = defineAsyncComponent(() => import('@/views/NewsDetail.vue'));
+const CourseList = defineAsyncComponent(() => import('@/views/CourseList.vue'));
+const CourseDetail = defineAsyncComponent(() => import('@/views/CourseDetail.vue'));
+const Craft = defineAsyncComponent(() => import('@/views/CollegeGroup/Craft.vue'));
+const Generation = defineAsyncComponent(() => import('@/views/CollegeGroup/Generation.vue'));
 
 const routes = [
   {
     path: '/',
     name: 'Home',
-    component: home,
+    component: Home,
   },
   {
     path: '/login',
     name: 'Login',
-    component: login,
-  },
-  {
-    path: '/login',
-    name: 'Login',
-    component: login,
+    component: Login,
   },
   {
     path: '/news',
     name: 'News',
-    component: news,
+    component: News,
+  },
+  {
+    path: '/news/:id',
+    name: 'NewsDetail',
+    component: NewsDetail
   },
   {
     path: '/course-list',
     name: 'CourseList',
-    component: courseList
+    component: CourseList
   },
   {
     path: '/course-detail/:id',
     name: 'CourseDetail',
-    component: courseDetail
+    component: CourseDetail
+  },
+  {
+    path: '/college-group/craft',
+    name: 'Craft',
+    component: Craft
+  },
+  {
+    path: '/college-group/generation',
+    name: 'Generation',
+    component: Generation
   },
 ];
 

+ 26 - 0
src/stores/store.js

@@ -0,0 +1,26 @@
+import { defineStore } from 'pinia'
+
+export const useMainStore = defineStore('mainStore', {
+    state: () => ({
+        count: 0
+    }),
+    getters: {
+        doubleCount() {
+            return this.count * 2;
+        },
+        tripleCount() {
+            return this.count * 3;
+        }
+    },
+    actions: {
+        // async incrementAsync() {
+        //     // 模擬非同步操作
+        //     await new Promise(resolve => setTimeout(resolve, 1000));
+        //     this.count++;
+        // }
+        getImageUrl(name) {
+            console.log('name',name);
+            return new URL(`/src/assets/img/${name}`, import.meta.url).href;
+        },
+    },
+})

+ 304 - 0
src/views/CollegeGroup/Craft.vue

@@ -0,0 +1,304 @@
+<script setup>
+import { ref, reactive } from "vue";
+import axios from "axios";
+import moment from "moment";
+import Navbar from "@/components/Navbar.vue";
+import { useMainStore } from "@/stores/store";
+
+const store = useMainStore();
+
+const breadcrumbs = reactive([
+  {
+    title: "首頁",
+    disabled: false,
+    href: "/",
+  },
+  {
+    title: "工藝學群",
+    disabled: false,
+    href: "/ntcri/college-group/craft",
+  },
+  {
+    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>
+  <div class="bg-img">
+    <Navbar />
+    <!-- <div class="banner">
+      <img src="@/assets/img/college-group/技藝橫幅.png" alt="" />
+      <h1>技藝工藝</h1>
+    </div> -->
+    <v-container fluid class="content pb-16">
+      <div class="banner">
+        <img src="@/assets/img/college-group/技藝橫幅.png" alt="" />
+        <h1>技藝工藝</h1>
+      </div>
+      <div class="main-block">
+        <v-breadcrumbs
+          :items="breadcrumbs"
+          divider="/"
+          class="mt-10 p-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"
+          >
+            <div class="card">
+              <h3>{{ item.title }}</h3>
+              <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>
+          </v-col>
+        </v-row>
+
+        <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"
+          >
+            <div class="card">
+              <h3>{{ item.title }}</h3>
+              <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>
+          </v-col>
+        </v-row>
+      </div>
+    </v-container>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.bg-img {
+  width: 100vw;
+  background-image: url("@/assets/img/college-group/background.png");
+  background-position: center;
+  background-size: cover;
+  background-repeat: no-repeat;
+}
+
+.banner {
+  display: flex;
+  justify-content: center;
+  position: relative;
+  top: -10vw;
+  right: 0;
+  left: 0;
+  z-index: 1;
+
+  img {
+    width: 100%;
+  }
+
+  h1 {
+    position: absolute;
+    top: 52%;
+    left: 11%;
+    font-size: 40px;
+    font-weight: 500;
+    word-wrap: break-word;
+
+    @media (max-width: 1200px) {
+      font-size: 36px;
+    }
+
+    @media (max-width: 960px) {
+      font-size: 24px;
+    }
+
+    @media (max-width: 600px) {
+      font-size: 20px;
+      top: 50%;
+      left: 5%;
+    }
+  }
+}
+
+.content {
+  padding: 0;
+  width: 90%;
+  
+  @media (max-width: 600px) {
+    width: 85%;
+  }
+  .main-block {
+    padding: 100px 80px;
+    margin-top: -30%;
+    background-color: #fff;
+
+    @media (max-width: 960px) {
+      padding: 100px 50px;
+    }
+
+    @media (max-width: 600px) {
+      padding: 100px 20px;
+    }
+
+    h2 {
+      font-size: 30px;
+      font-weight: 500;
+      line-height: 32px;
+      margin-left: 10px;
+
+      @media (max-width: 960px) {
+        font-size: 24px;
+      }
+
+      @media (max-width: 600px) {
+        margin-left: 0;
+        margin-bottom: 50px;
+      }
+    }
+    .title {
+      margin: 80px 0;
+      @media (max-width: 600px) {
+        margin: 50px 0;
+      }
+    }
+    .v-breadcrumbs {
+      position: relative;
+      z-index: 100;
+      justify-content: flex-start;
+      @media (max-width: 600px) {
+        justify-content: center;
+      }
+    }
+    .card {
+      letter-spacing: 1px;
+      border-radius: 10px;
+      box-shadow: 2px 2px 10px #aaaaaa;
+      background-color: var(--sub-color);
+      h3 {
+        padding: 15px;
+        font-size: 22px;
+        font-weight: 400;
+        text-align: center;
+        margin-bottom: 15px;
+        border-bottom: 2px solid #fff;
+      }
+      ul {
+        padding: 20px;
+      }
+      .cover-img {
+        padding: 0 15px;
+      }
+    }
+  }
+}
+</style>

+ 304 - 0
src/views/CollegeGroup/Generation.vue

@@ -0,0 +1,304 @@
+<script setup>
+import { ref, reactive } from "vue";
+import axios from "axios";
+import moment from "moment";
+import Navbar from "@/components/Navbar.vue";
+import { useMainStore } from "@/stores/store";
+
+const store = useMainStore();
+
+const breadcrumbs = reactive([
+  {
+    title: "首頁",
+    disabled: false,
+    href: "/",
+  },
+  {
+    title: "工藝學群",
+    disabled: false,
+    href: "/ntcri/college-group/craft",
+  },
+  {
+    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>
+  <div class="bg-img">
+    <Navbar />
+    <!-- <div class="banner">
+      <img src="@/assets/img/college-group/技藝橫幅.png" alt="" />
+      <h1>技藝工藝</h1>
+    </div> -->
+    <v-container fluid class="content pb-16">
+      <div class="banner">
+        <img src="@/assets/img/college-group/generation/世代工藝 橫幅.png" alt="" />
+        <h1>世代工藝</h1>
+      </div>
+      <div class="main-block">
+        <v-breadcrumbs
+          :items="breadcrumbs"
+          divider="/"
+          class="mt-10 p-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"
+          >
+            <div class="card">
+              <h3>{{ item.title }}</h3>
+              <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>
+          </v-col>
+        </v-row>
+
+        <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"
+          >
+            <div class="card">
+              <h3>{{ item.title }}</h3>
+              <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>
+          </v-col>
+        </v-row>
+      </div>
+    </v-container>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.bg-img {
+  width: 100vw;
+  background-image: url("@/assets/img/college-group/background.png");
+  background-position: center;
+  background-size: cover;
+  background-repeat: no-repeat;
+}
+
+.banner {
+  display: flex;
+  justify-content: center;
+  position: relative;
+  top: -10vw;
+  right: 0;
+  left: 0;
+  z-index: 1;
+
+  img {
+    width: 100%;
+  }
+
+  h1 {
+    position: absolute;
+    top: 52%;
+    left: 11%;
+    font-size: 40px;
+    font-weight: 500;
+    word-wrap: break-word;
+
+    @media (max-width: 1200px) {
+      font-size: 36px;
+    }
+
+    @media (max-width: 960px) {
+      font-size: 24px;
+    }
+
+    @media (max-width: 600px) {
+      font-size: 20px;
+      top: 50%;
+      left: 5%;
+    }
+  }
+}
+
+.content {
+  padding: 0;
+  width: 90%;
+  
+  @media (max-width: 600px) {
+    width: 85%;
+  }
+  .main-block {
+    padding: 100px 80px;
+    margin-top: -30%;
+    background-color: #fff;
+
+    @media (max-width: 960px) {
+      padding: 100px 50px;
+    }
+
+    @media (max-width: 600px) {
+      padding: 100px 20px;
+    }
+
+    h2 {
+      font-size: 30px;
+      font-weight: 500;
+      line-height: 32px;
+      margin-left: 10px;
+
+      @media (max-width: 960px) {
+        font-size: 24px;
+      }
+
+      @media (max-width: 600px) {
+        margin-left: 0;
+        margin-bottom: 50px;
+      }
+    }
+    .title {
+      margin: 80px 0;
+      @media (max-width: 600px) {
+        margin: 50px 0;
+      }
+    }
+    .v-breadcrumbs {
+      position: relative;
+      z-index: 100;
+      justify-content: flex-start;
+      @media (max-width: 600px) {
+        justify-content: center;
+      }
+    }
+    .card {
+      letter-spacing: 1px;
+      border-radius: 10px;
+      box-shadow: 2px 2px 10px #aaaaaa;
+      background-color: var(--sub-color);
+      h3 {
+        padding: 15px;
+        font-size: 22px;
+        font-weight: 400;
+        text-align: center;
+        margin-bottom: 15px;
+        border-bottom: 2px solid #fff;
+      }
+      ul {
+        padding: 20px;
+      }
+      .cover-img {
+        padding: 0 15px;
+      }
+    }
+  }
+}
+</style>

+ 2 - 2
src/views/CourseDetail.vue

@@ -10,7 +10,7 @@ const course = reactive({
   data: [],
 });
 
-// 取得課程資訊
+// 取得資料
 (async function getData() {
   try {
     const response = await axios.get(
@@ -56,7 +56,7 @@ const course = reactive({
 .course-detail {
   width: 1000px;
 
-  @media (max-width: 991px) {
+  @media (max-width: 960px) {
     width: 100%;
   }
 

+ 2 - 2
src/views/CourseList.vue

@@ -13,7 +13,7 @@ const courseData = reactive({
   classes: [],
 });
 
-// 取得課程清單
+// 取得資料
 (async function getData() {
   try {
     const response = await axios.get("https://cmm.ai:8088/api/get_class_name");
@@ -25,7 +25,7 @@ const courseData = reactive({
   }
 })();
 
-// 課程搜尋
+// 搜尋
 async function search() {
   searchError.value = false;
   let keyword = searchInput.value;

+ 4 - 4
src/views/Home.vue

@@ -143,7 +143,7 @@ const previous = () => {
   height: 150vw; // 捲軸高度
   overflow: hidden;
 
-  @media (max-width: 991px) {
+  @media (max-width: 960px) {
     height: 300vw;
   }
 
@@ -208,7 +208,7 @@ const previous = () => {
         height: 84vh;
       }
 
-      @media (max-width: 991px) {
+      @media (max-width: 960px) {
         width: 150vw;
       }
     }
@@ -217,7 +217,7 @@ const previous = () => {
       height: 82vh;
 
       .navbar {
-        right: 50%;
+        right: 50.5%;
         position: absolute;
       }
     }
@@ -232,7 +232,7 @@ const previous = () => {
 
         .map,
         .list {
-          @media (max-width: 991px) {
+          @media (max-width: 960px) {
             width: 100%;
           }
         }

+ 185 - 14
src/views/Login.vue

@@ -1,8 +1,16 @@
 <script setup>
-import { ref, onMounted } from "vue";
+import { ref, onMounted, watch } from "vue";
 import axios from "axios";
 import Navbar from "@/components/Navbar.vue";
 
+let tab = ref("");
+const emit = defineEmits(["close"]);
+
+function close() {
+  emit("close", false);
+}
+
+// Google Login
 const handleSignIn = async (response) => {
   const credential = response.credential;
   const profile = JSON.parse(
@@ -17,15 +25,6 @@ const handleSignIn = async (response) => {
 
   console.log("profile", profile);
 
-  const target = document.getElementById("GOOGLE_STATUS_1");
-  const html = `
-    ID: ${profile.sub}<br/>
-    會員暱稱: ${profile.name}<br/>
-    會員頭像: ${profile.picture}<br/>
-    會員 email: ${profile.email}<br/>
-  `;
-  target.innerHTML = html;
-
   let data = {
     username: profile.name,
     password: "googleLogin",
@@ -52,12 +51,135 @@ onMounted(() => {
 
   window.onSignIn1 = handleSignIn;
 });
+
+let token =
+  "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIiLCJleHAiOjE2ODg2MjU5MzN9.rMJGZxHeKaxBQx0VuTBk8AtYhhLyIPxNUHBrsKWXJjE";
+
+// 解碼 JWT
+const base64Url = token.split(".")[1]; // 取得 JWT 的 payload
+const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/"); // 處理 URL 安全的 Base64 字串
+const decodedPayload = atob(base64); // Base64 解碼
+
+// 將解碼後的 payload 轉換為 JSON 物件
+const payload = JSON.parse(decodedPayload);
+
+// 獲取過期時間(exp)
+const expirationTime = payload.exp; // 過期時間的 UNIX 時間戳
+
+// 創建 Date 物件並設定時間戳
+const expirationDate = new Date(expirationTime * 1000); // JS 時間戳以毫秒為單位故乘以 1000
+
+// 取得日期和時間的字串表示
+const formattedExpiration = expirationDate.toLocaleString();
+
+console.log("JWT 過期時間:", formattedExpiration);
 </script>
 
 <template>
-  <Navbar />
-  <div>Login</div>
-  <div style="width: 200px; margin: 20px 0">
+  <!-- <Navbar />
+  <div>Login</div> -->
+  <!-- <v-container class="pa-0">
+    <v-row>
+      <v-col cols="6">
+        <img src="@/assets/img/img-08.jpg" alt="">
+      </v-col>
+      <v-col cols="6"></v-col>
+    </v-row>
+  </v-container> -->
+
+  <v-card class="login-card">
+    <div class="d-flex justify-end pa-3">
+      <v-icon
+        size="large"
+        icon="mdi-close"
+        color="gray"
+        @click.prevent="close()"
+      ></v-icon>
+    </div>
+    <v-tabs v-model="tab" color="primary" align-tabs="center">
+      <v-tab :value="1" class="me-5">登入</v-tab>
+      <v-tab :value="2">註冊</v-tab>
+    </v-tabs>
+    <v-window v-model="tab">
+      <v-window-item :value="1">
+        <div class="px-15 pt-10">
+          <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"
+            data-theme="outline"
+            data-size="large"
+            data-logo_alignment="left"
+          ></div>
+
+          <p class="text-center my-7">使用平台帳號登入</p>
+
+          <v-form @submit.prevent>
+            <v-text-field
+              label="帳號"
+              :rules="[(v) => !!v || '請輸入您的帳號']"
+              prepend-inner-icon="mdi-account"
+              variant="solo"
+              density="compact"
+            ></v-text-field>
+            <v-text-field
+              label="密碼"
+              :rules="[(v) => !!v || '請輸入您的密碼']"
+              prepend-inner-icon="mdi-lock"
+              variant="solo"
+              density="compact"
+            ></v-text-field>
+            <v-btn
+              type="submit"
+              size="large"
+              color="primary"
+              block
+              @click="login()"
+              >登入</v-btn
+            >
+          </v-form>
+        </div>
+      </v-window-item>
+      <v-window-item :value="2">
+        <div class="px-15 pt-10">
+          <v-form @submit.prevent>
+            <v-text-field
+              label="帳號"
+              :rules="[(v) => !!v || '請輸入您的帳號']"
+              prepend-inner-icon="mdi-account"
+              variant="solo"
+              density="compact"
+            ></v-text-field>
+            <v-text-field
+              label="密碼"
+              :rules="[(v) => !!v || '請輸入您的密碼']"
+              prepend-inner-icon="mdi-lock"
+              variant="solo"
+              density="compact"
+            ></v-text-field>
+            <v-btn
+              type="submit"
+              size="large"
+              color="primary"
+              @click="register()"
+              block
+              >註冊</v-btn
+            >
+          </v-form>
+        </div>
+      </v-window-item>
+    </v-window>
+
+    <v-card-actions class="d-flex justify-center py-10 forget">
+      <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"
@@ -68,5 +190,54 @@ onMounted(() => {
     <p class="mt-3">目前狀態:</p>
 
     <span id="GOOGLE_STATUS_1"></span>
-  </div>
+  </div> -->
 </template>
+
+<style lang="scss">
+.login-card {
+  .v-tab {
+    padding-bottom: 20px;
+    font-size: 28px;
+  }
+  .tab {
+    button {
+      margin: 0 20px;
+      padding-bottom: 20px;
+      font-size: 28px;
+      font-weight: 500;
+      text-align: center;
+      color: #d9d9d9;
+      border-bottom: 3px solid transparent;
+      transition: all 0.3s;
+      &.active {
+        color: var(--main-color);
+        border-bottom: 3px solid var(--main-color);
+      }
+    }
+  }
+  h3,
+  p {
+    color: #595959;
+  }
+  .google-btn {
+    width: 130px;
+    margin: auto;
+  }
+  .v-text-field .v-input__details {
+    position: relative;
+    top: -4px;
+  }
+  .forget {
+    a,
+    span {
+      color: #969696;
+    }
+    a {
+      transition: all 0.3s;
+      &:hover {
+        opacity: 0.8;
+      }
+    }
+  }
+}
+</style>

+ 184 - 101
src/views/News.vue

@@ -1,141 +1,201 @@
 <script setup>
 import { ref, reactive } from "vue";
+import axios from "axios";
+import moment from "moment";
 import Navbar from "@/components/Navbar.vue";
 
+const newsAll = reactive({
+  list: [],
+});
+const newsData = reactive({
+  list: [],
+});
+let searchInput = ref("");
+let searchError = ref(false);
+
+// 取得資料
+(async function getData() {
+  try {
+    const response = await axios.get("https://cmm.ai:8088/api/get_news");
+    console.log("response", response.data.news);
+    newsAll.list = response.data.news;
+    newsData.list = response.data.news;
+  } catch (error) {
+    console.error(error);
+  }
+})();
+
+// 搜尋
+async function search() {
+  searchError.value = false;
+  let keyword = searchInput.value;
+  console.log("keyword", keyword);
+  if (keyword !== "") {
+    try {
+      const response = await axios.get(
+        `https://cmm.ai:8088/api/search_news_like?keyword=${keyword}`
+      );
+      if (response.data.news.length !== 0) {
+        newsData.list = response.data.news;
+      } else {
+        searchError.value = true;
+      }
+    } catch (error) {
+      console.error(error);
+    }
+  } else {
+    newsData.list = newsAll.list;
+  }
+}
+
 const breadcrumbs = reactive([
   {
-    title: '首頁',
+    title: "首頁",
     disabled: false,
-    href: '/',
-
+    href: "/",
   },
   {
-    title: '最新消息',
+    title: "最新消息",
     disabled: true,
-    href: '/news',
-
-  }
+    href: "/news",
+  },
 ]);
 
 const categoryList = reactive([
   {
-    title: '總覽'
+    title: "總覽",
   },
   {
-    title: '一日學徒'
+    title: "一日學徒",
   },
   {
-    title: '工藝行旅'
+    title: "工藝行旅",
   },
   {
-    title: '希望工程'
+    title: "希望工程",
   },
   {
-    title: '展覽資訊'
+    title: "展覽資訊",
   },
   {
-    title: '補助計畫'
+    title: "補助計畫",
   },
   {
-    title: '策展補助'
-  }
-])
-
-const getImageUrl = (name) => {
-  return new URL(`/src/assets/img/${name}`, import.meta.url).href;
-};
-
-console.log("getImageUrl('img-11.jpg')", getImageUrl('img-11.jpg'));
-const newsData = reactive([
-  {
-    date: '2023.06.19',
-    category: '工藝行旅',
-    img: getImageUrl('img-11.jpg'),
-    title: '臺灣綠工藝Taiwan Green Craft 健全臺灣工藝生態系 工藝價值的社會實踐',
-    describe: '臺灣綠工藝兼具「頂真精神、工作倫理、愉悅勞動、生活美學」的精神,以及「自然、循環、平衡、寬容、生命力」的本質,從生態系的宏觀視野,關注跨越文化面向、經濟面向、社會面向等廣大範疇,從而以工藝產業鏈的架構...'
-  },
-  {
-    date: '2023.06.19',
-    category: '工藝行旅',
-    img: getImageUrl('img-11.jpg'),
-    title: '臺灣綠工藝Taiwan Green Craft 健全臺灣工藝生態系 工藝價值的社會實踐',
-    describe: '臺灣綠工藝兼具「頂真精神、工作倫理、愉悅勞動、生活美學」的精神,以及「自然、循環、平衡、寬容、生命力」的本質,從生態系的宏觀視野,關注跨越文化面向、經濟面向、社會面向等廣大範疇,從而以工藝產業鏈的架構...'
-  },
-  {
-    date: '2023.06.19',
-    category: '工藝行旅',
-    img: getImageUrl('img-11.jpg'),
-    title: '臺灣綠工藝Taiwan Green Craft 健全臺灣工藝生態系 工藝價值的社會實踐',
-    describe: '臺灣綠工藝兼具「頂真精神、工作倫理、愉悅勞動、生活美學」的精神,以及「自然、循環、平衡、寬容、生命力」的本質,從生態系的宏觀視野,關注跨越文化面向、經濟面向、社會面向等廣大範疇,從而以工藝產業鏈的架構...'
+    title: "策展補助",
   },
-])
+]);
 </script>
 
 <template>
   <Navbar />
   <div class="position-relative">
-    <img src="@/assets/img/news/news-01.png" alt="" class="material-img">
+    <img src="@/assets/img/news/news-01.png" alt="" class="material-img" />
     <v-container>
-      <img src="@/assets/img/news/news-banner.png" alt="">
+      <img src="@/assets/img/news/news-banner.png" alt="" />
 
-      <div class="content pt-10 mt-16">
+      <div class="content py-10 mt-16">
         <div class="bg-img">
-          <img src="@/assets/img/news/news-02.png" alt="">
+          <img src="@/assets/img/news/news-02.png" alt="" />
         </div>
 
-        <v-breadcrumbs :items="breadcrumbs" divider="/" class="pt-16 mb-5"></v-breadcrumbs>
+        <v-breadcrumbs
+          :items="breadcrumbs"
+          divider="/"
+          class="my-5"
+        ></v-breadcrumbs>
 
         <div class="category-btn">
-          <v-btn v-for="(item, index) in categoryList" :key="index" rounded="xl" color="brown"
-            :class="{ 'me-5': index !== categoryList.length - 1 }" class="mb-5">
+          <v-btn
+            v-for="(item, index) in categoryList"
+            :key="index"
+            rounded="xl"
+            color="brown"
+            :class="{ 'me-5': index !== categoryList.length - 1 }"
+            class="mb-5"
+          >
             {{ item.title }}
           </v-btn>
         </div>
 
         <div class="search mt-5 mb-10 me-16">
           <span>
-            <input type="text">
-            <button>
-              <img src="@/assets/img/news/news-search-icon.png" alt="">
+            <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>
         <ul>
-          <li v-for="(item, index) in newsData" :key="index" class="mb-10">
+          <li v-for="(item, index) in newsData.list" :key="index" class="mb-10">
             <section class="d-flex">
               <p class="category mb-5">
                 <span></span>
                 {{ item.category }}
               </p>
-              <p class="ms-5">{{ item.date }}</p>
+              <p class="ms-5">
+                {{ moment(`${item.create_time}`).format("YYYY-MM-DD") }}
+              </p>
             </section>
-            <v-card variant="outlined" class="d-flex align-center pa-5">
+            <v-card
+              variant="outlined"
+              class="d-flex flex-md-row flex-column align-center pa-5"
+            >
+              <v-row class="align-center">
+                <v-col cols="4">
+                  <router-link :to="`/news/${item.news_id}`" class="cover-img">
+                    <img src="@/assets/img/img-04.jpg" alt="" />
+                  </router-link>
+                </v-col>
+                <v-col cols="8">
+                  <section
+                    class="d-flex flex-column px-5 px-sm-10 py-5 py-md-0"
+                  >
+                    <h2>{{ item.title }}</h2>
+                    <p v-html="item.content"></p>
+                  </section>
+                </v-col>
+              </v-row>
+
+              <!-- <router-link :to="`/news/${item.news_id}`" class="cover-img">
+              <img src="@/assets/img/img-04.jpg" alt="" />
+            </router-link>
               <img :src="item.img" alt="">
-              <section class="d-flex flex-column px-10">
+              <section class="d-flex flex-column px-5 px-sm-10 py-5 py-md-0">
                 <h2>{{ item.title }}</h2>
-                <p>{{ item.describe }}</p>
-              </section>
+                <p v-html="item.content"></p>
+              </section> -->
             </v-card>
           </li>
         </ul>
       </div>
     </v-container>
-    <img src="@/assets/img/news/news-01.png" alt="" class="material-img">
+    <img src="@/assets/img/news/news-01.png" alt="" class="material-img" />
   </div>
 </template>
 
-<style lang="scss">
+<style lang="scss" scoped>
 .material-img {
   position: absolute;
 
   &:first-child {
-    top: -185px;
+    top: -15vw;
   }
 
   &:last-child {
     bottom: 0;
     z-index: -5;
     transform: scaleX(-1);
+
+    @media (max-width: 600px) {
+      bottom: 10px;
+    }
   }
 }
 
@@ -149,41 +209,14 @@ const newsData = reactive([
   }
 }
 
-.search {
-  display: flex;
-  align-items: center;
-  justify-content: end;
-
-  span {
-    position: relative;
-  }
-
-  input {
-    padding: 5px 10px;
-    border-radius: 100px;
-    background-color: #fff;
-    box-shadow: 1px 1px 3px #ccc;
-  }
-
-  button {
-    position: absolute;
-    right: 10px;
-    left: 0;
-    top: 3px;
-
-    img {
-      width: 25px;
-      position: absolute;
-      right: 0;
-    }
-  }
-
-}
-
 .content {
   position: relative;
   margin-bottom: 200px;
 
+  @media (max-width: 960px) {
+    margin-bottom: 100px;
+  }
+
   .bg-img {
     display: flex;
     justify-content: center;
@@ -194,6 +227,10 @@ const newsData = reactive([
     left: 0;
     bottom: -110px;
 
+    @media (max-width: 960px) {
+      bottom: -30px;
+    }
+
     img {
       min-width: 105%;
     }
@@ -202,6 +239,10 @@ const newsData = reactive([
   ul {
     padding: 0 80px;
 
+    @media (max-width: 600px) {
+      padding: 0 20px;
+    }
+
     li {
       letter-spacing: 1px;
 
@@ -217,16 +258,59 @@ const newsData = reactive([
           }
 
           p {
+            // 超過三行則省略
+            overflow: hidden;
+            text-overflow: ellipsis;
+            display: -webkit-box;
+            -webkit-line-clamp: 3;
+            -webkit-box-orient: vertical;
+            line-break: after-white-space;
             line-height: 22px;
           }
         }
 
-        img {
-          width: 100%;
-          max-width: 330px;
-          height: 200px;
-          object-fit: cover;
+        .cover-img {
+          display: block;
+          overflow: hidden;
+          img {
+            width: 100%;
+            height: 200px;
+            object-fit: cover;
+            transition: all 0.5s;
+            &:hover {
+              transform: scale(1.1);
+            }
+
+            @media (max-width: 960px) {
+              width: 100%;
+              height: 300px;
+            }
+
+            @media (max-width: 600px) {
+              min-width: auto;
+              height: 200px;
+            }
+          }
         }
+        // img {
+        //   width: 100%;
+        //   height: 200px;
+        //   object-fit: cover;
+        //   transition: all 0.5s;
+        //   &:hover {
+        //     transform: scale(1.1);
+        //   }
+
+        //   @media (max-width: 960px) {
+        //     width: 100%;
+        //     height: 300px;
+        //   }
+
+        //   @media (max-width: 600px) {
+        //     min-width: auto;
+        //     height: 200px;
+        //   }
+        // }
       }
 
       .category {
@@ -241,9 +325,8 @@ const newsData = reactive([
           border-radius: 10px;
           background-color: #000;
         }
-
       }
     }
   }
 }
-</style>
+</style>

+ 180 - 0
src/views/NewsDetail.vue

@@ -0,0 +1,180 @@
+<script setup>
+import { reactive } from "vue";
+import { useRoute } from "vue-router";
+import axios from "axios";
+import moment from "moment";
+import Navbar from "@/components/Navbar.vue";
+
+const route = useRoute();
+const newsId = route.params.id; // 網址參數
+const news = reactive({
+  data: [],
+});
+
+// 取得資料
+(async function getData() {
+  try {
+    const response = await axios.get(
+      `https://cmm.ai:8088/api/get_news?news_id=${newsId}`
+    );
+    news.data = response.data.news[0];
+    console.log("news.data", news.data);
+  } catch (error) {
+    console.error(error);
+  }
+})();
+</script>
+
+<template>
+  <Navbar />
+  <div class="position-relative">
+    <img src="@/assets/img/news/news-01.png" alt="" class="material-img" />
+    <v-container class="pa-0 position-relative">
+      <img
+        src="@/assets/img/news/news-detail-banner.png"
+        alt=""
+        class="bg-img"
+      />
+
+      <div class="content">
+        <v-row>
+          <v-col cols="12">
+            <div class="d-flex align-center mb-5">
+              <v-chip size="large" variant="elevated" class="px-8 me-3">
+                {{ news.data.category }}
+              </v-chip>
+              <p>
+                {{ moment(`${news.data.create_time}`).format("YYYY.MM.DD") }}
+              </p>
+            </div>
+            <h2>【{{ news.data.title }}】</h2>
+            <img src="@/assets/img/img-04.jpg" alt="" class="cover-img" />
+            <!-- <img :src="`https://cmm.ai/ntcri/${news.data.cover_img}`" alt="" /> -->
+          </v-col>
+          <v-col cols="12">
+            <section class="h-100 d-flex flex-column pa-0">
+              <p v-html="news.data.content"></p>
+            </section>
+          </v-col>
+        </v-row>
+      </div>
+
+      <div class="btn-block">
+        <v-btn rounded="xl" color="grey-darken-2" class="px-7">
+          檔案下載
+        </v-btn>
+        <v-btn rounded="xl" color="grey-darken-2" class="px-7 ms-3">
+          前往連結
+        </v-btn>
+      </div>
+    </v-container>
+    <router-link to="/news" class="mt-10 back-link"
+      >< 返回最新消息</router-link
+    >
+    <img src="@/assets/img/news/news-01.png" alt="" class="material-img" />
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.btn-block {
+  position: absolute;
+  bottom: 80px;
+  right: 180px;
+  @media (max-width: 600px) {
+    bottom: 65px;
+    right: unset;
+    width: 100%;
+    display: flex;
+    justify-content: center;
+  }
+}
+.content {
+  position: absolute;
+  right: 0;
+  left: 0;
+  top: 75px;
+  height: 780px;
+  overflow-y: scroll;
+  overflow-x: hidden;
+  display: flex;
+  margin: auto;
+  padding-top: 50px;
+  justify-content: center;
+  width: 75%;
+
+  @media (max-width: 960px) {
+    width: 80%;
+  }
+
+  @media (max-width: 600px) {
+    top: 55px;
+    height: 470px;
+    padding-top: 10px;
+  }
+
+  h2 {
+    margin: 40px auto;
+    font-size: 28px;
+    font-weight: 500;
+    line-height: 38px;
+    letter-spacing: 2px;
+  }
+  section {
+    height: 100%;
+    padding: 30px;
+    p {
+      padding: 20px;
+      margin-bottom: 20px;
+      font-size: 18px;
+      font-weight: 400;
+      line-height: 32px;
+      letter-spacing: 1px;
+    }
+  }
+
+  .cover-img {
+    width: 100%;
+    height: 500px;
+    object-fit: cover;
+    @media (max-width: 600px) {
+      height: auto;
+    }
+  }
+}
+.bg-img {
+  height: 1000px;
+  position: relative;
+  z-index: -1;
+  @media (max-width: 600px) {
+    height: 650px;
+  }
+}
+
+.back-link {
+  display: block;
+  text-align: center;
+  transition: all 0.3s;
+  letter-spacing: 1px;
+
+  &:hover {
+    opacity: 0.8;
+  }
+}
+.material-img {
+  position: absolute;
+
+  &:first-child {
+    top: -15vw;
+  }
+
+  &:last-child {
+    bottom: -150px;
+    z-index: -5;
+    transform: scaleX(-1);
+
+    @media (max-width: 600px) {
+      bottom: -50px;
+    }
+  }
+}
+</style>

Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov