Part I: https://hackmd.io/@aaronlife/2023-java-1-1
Part II: https://hackmd.io/@aaronlife/2023-java-1
/opt/tomcat/apache-tomcat-10.1.11/logs
目錄
如果有權限問題,可以透過
chmod
指令來開放權限
$ sudo chmod 777 logs
請在/opt/tomcat/apache-tomcat-10.1.11
目錄下該指令
catalina.out
檔案:$ cat catalina.out
package com.aaronlife.blog.repository;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Repository;
import com.aaronlife.blog.model.MyConfig;
@Repository
public class BaseRepository {
@Autowired
private MyConfig myConfig;
private Connection conn = null;
public void init() {
// 如果已經連線過,就不需要再連線
if(this.conn != null) return;
try {
// 註冊驅動程式
Class.forName(myConfig.getDriverClassName());
// 建立連線
conn = DriverManager.getConnection(myConfig.getUrl() + "?allowPublicKeyRetrieval=true&useSSL=false&user=" +
myConfig.getUsername() + "&password=" + myConfig.getPassword());
} catch(SQLException e) {
conn = null;
System.out.println("BaseRespsition: " + e.getMessage());
System.out.println(e.getErrorCode());
System.out.println(e.getSQLState());
} catch(ClassNotFoundException e) {
conn = null;
System.out.println(e.getMessage());
}
}
public Connection getConnection() {
return this.conn;
}
// 關閉練線並清除連線物件
public void closeConnection() {
try {
conn.close();
} catch (SQLException e) {
System.out.println(e.getMessage());
}
conn = null;
}
}
package com.aaronlife.blog.repository;
import java.sql.Array;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Repository;
import com.aaronlife.blog.model.ArticleDetailModel;
import com.aaronlife.blog.model.ArticleListModel;
import com.aaronlife.blog.model.MyConfig;
@Repository
public class ArticleRepository extends BaseRepository {
@Autowired
private MyConfig myConfig;
public ArrayList<ArticleListModel> findAll() {
super.init();
if(getConnection() != null) // 確定有連線成功
{
try {
Statement stmt = getConnection().createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM articles");
ArrayList<ArticleListModel> data = new ArrayList<>();
// 取得每一筆文章資訊
while(rs.next()) {
ArticleListModel a = new ArticleListModel();
a.setId(rs.getInt("id")); // id欄位名稱必須和資料庫蘭為名稱一致
a.setTitle(rs.getString("title"));
a.setImgUrl(rs.getString("img_url"));
a.setContent(rs.getString("content"));
data.add(a);
}
// 模擬資料庫斷線
getConnection().close();
return data;
} catch (SQLException e) {
System.out.println("ArticleRepository: " + e.getMessage());
System.out.println(e.getErrorCode());
System.out.println(e.getSQLState());
if(e.getErrorCode() == 0 && e.getSQLState().equals("08003")) {
closeConnection(); // 斷線並釋放資源
init(); // 重新連線
return findAll();
}
return null;
}
}
return null;
}
public ArticleDetailModel findArticleById(int id) {
super.init();
if(getConnection() != null) // 確定有連線成功
{
try {
PreparedStatement stmt = getConnection()
.prepareStatement("SELECT * FROM articles WHERE id=?");
stmt.setInt(1, id);
ResultSet rs = stmt.executeQuery();
boolean isExists = rs.next(); // 判斷有沒有資料
if(isExists) { // true = 有資料
ArticleDetailModel a = new ArticleDetailModel();
a.setId(rs.getInt("id"));
a.setTitle(rs.getString("title"));
a.setSubtitle(rs.getString("subtitle"));
a.setContent(rs.getString("content"));
a.setDatetime(rs.getDate("update_time"));
a.setAuthor(rs.getString("author"));
return a;
} else {
return null;
}
} catch (SQLException e) {
System.out.println("ArticleRepository: " + e.getMessage());
System.out.println(e.getErrorCode());
System.out.println(e.getSQLState());
if(e.getErrorCode() == 0 && e.getSQLState().equals("08003")) {
closeConnection(); // 斷線並釋放資源
init(); // 重新連線
return findArticleById(id);
}
return null;
}
}
return null;
}
}
注意:
修改完後須重新佈署
package com.aaronlife.blog;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class BlogApplication {
public static void main(String[] args) {
System.out.println("版本: 1.00.01");
SpringApplication.run(BlogApplication.class, args);
}
}
main/resources/application.properites
內心增設定:spring.banner.charset=UTF-8
spring.banner.location=classpath:banner.txt
banner.txt
檔案,跟application.properties
放在同一個目錄下
,--,--. ,--,--.,--.--. ,---. ,--,--,
' ,-. |' ,-. || .--'| .-. || \
\ '-' |\ '-' || | ' '-' '| || |
`--`--' `--`--'`--' `---' `--''--'
Version: 1.00.01
Banner產生器: https://devops.datenkollektiv.de/banner.txt/index.html
211.23.203.0/24
220.130.40.0/24
60.0.0.0/8
GCE免費方案: https://cloud.google.com/free/docs/free-cloud-features?hl=zh-tw#compute
$ ssh-keygen -t rsa -f ~/.ssh/user -C user
Compute Engine -> 中繼資料 -> 安全殼層資料 -> 編輯 -> 新增項目
$ chmod 600 ~/.ssh/{account}
$ ssh -i ~/.ssh/{account} {account}@{ip}
更新套件管理工具
$ sudo apt update
安裝JDK 17
$ sudo apt install openjdk-17-jdk -y
$ java --version
確認有無安裝成功
安裝nginx
$ sudo apt install nginx
https://winscp.net/download/WinSCP-6.1.1-Setup.exe
scp -i ~/.ssh/my-ssh-name -r ~/Downloads/dist my-ssh-name@35.238.194.116:~
$ cd ~
$ ls
$ ls -l
$ cd dist
$ ls -l
$ systemctl status nginx
$ sudo systemctl start nginx
/var/www/html
目錄下$ sudo cp -r ~/dist/* /var/www/html
需先使用WinSCP複製到家目錄
$ cd /var/www/html
可以切到nginx的網站home目錄
切換到`/etc/nginx/sites-enabled目錄下
$ cd /etc/nginx/sites-enabled
編輯default檔案
sudo vim default
重要: 在vim內按i
進入編輯模式
畫面下方會出現
--INSERT--
文字
找到location {} 區塊,然後加上#
字號註解掉後加上vue-router官方的設定
location / {
try_files $uri $uri/ /index.html;
}
參考:
https://router.vuejs.org/guide/essentials/history-mode.html
編輯完後,按ESC
離開編輯模式(--INSERT--
文字消失)
接著輸入:wq
儲存並離開vim
$ sudo systemctl restart nginx
網址: https://tomcat.apache.org/download-10.cgi
下載: https://dlcdn.apache.org/tomcat/tomcat-10/v10.1.16/bin/apache-tomcat-10.1.16.tar.gz
$ cd ~
$ wget https://dlcdn.apache.org/tomcat/tomcat-10/v10.1.16/bin/apache-tomcat-10.1.16.tar.gz
$ cd /opt
$ sudo mkdir tomcat
$ cd tomcat
$ sudo tar -xvf ~/apache-tomcat-10.1.16.tar.gz -C .
$ cd apache-tomcat-10.1.16
$ sudo su
$ cd bin
$ ./startup.sh
名稱: tomcat
目的標記: tomcat
來源IPv4範圍: 0.0.0.0/0
指定的通訊部號: TCP 打勾, 通訊埠: 8080
回到VM清單後點VM名稱進入,再點編輯,在網路標記的地方加上tomcat
標記,否則防火牆設定不會套用。
切換到webapps目錄下
$ cd /opt/tomcat/apache-tomcat-10.1.16/webapps
將原本的ROOT目錄改名為ROOT_bkp
$ mv ROOT ROOT_bkp
將上船的war檔複製到webapps目錄下並改名為ROOT.war
$ cp /home/{account}/myspring-0.0.1-SNAPSHOT.war ROOT.war
補充:
- 以上指令必須先使用
sudo su
切換到root帳號- war檔複製到webapps內後,tomcat會自動解壓縮還原
http://{ip}:8080/v1/product
會出現
{"code":999,"message":"無法取得文章清單","data":null}
有看到JSON response代表佈署成功,因為資料庫未安裝,所以還沒有資料。
# cd ~
$ wget https://dev.mysql.com/get/mysql-apt-config_0.8.24-1_all.deb
$ sudo apt install ./mysql-apt-config_0.8.24-1_all.deb
$ sudo apt update
須先
exit
離開su模式
$ sudo apt install mysql-server
root密碼請設複雜一點,因為如果開放到網路會很容易被駭客入侵
$ sudo service mysql status
$ sudo netstat -tulpn | grep LISTEN
$ mysql -u root -p
mysql> CREATE USER 'root'@'%' IDENTIFIED BY '0000';
mysql> GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION;
7.調整後端Spring Boot的BaseRepository.java建立連線時需要加上allowPublicKeyRetrieval=true&useSSL=false&
// 建立連線
conn = DriverManager.getConnection(myConfig.getUrl() + "?allowPublicKeyRetrieval=true&useSSL=false&user=" +
myConfig.getUsername() + "&password=" + myConfig.getPassword());
Table articles {
id integer [primary key, increment]
title varchar(128)
subtitle varchar(128)
img_url varchar
content varchar
author varchar(32)
update_time datetime
}
資料庫名稱: blog
也順便加入一些測試用資料到Table中
測試資料SQL dump:
-- MySQL dump 10.13 Distrib 8.0.19, for Win64 (x86_64)
--
-- Host: localhost Database: blog
-- ------------------------------------------------------
-- Server version 8.0.17
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!50503 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
--
-- Table structure for table `articles`
--
DROP TABLE IF EXISTS `articles`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `articles` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(128) DEFAULT NULL,
`subtitle` varchar(128) DEFAULT NULL,
`img_url` varchar(255) DEFAULT NULL,
`content` varchar(255) DEFAULT NULL,
`author` varchar(32) DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `articles`
--
LOCK TABLES `articles` WRITE;
/*!40000 ALTER TABLE `articles` DISABLE KEYS */;
INSERT INTO `articles` VALUES (1,'張育成穩了?紅襪交易內野工具人赫南德茲回道奇!換回2右投','三立新聞網','https://s.yimg.com/ny/api/res/1.2/L7zdh9J3E6ZHFY1B3KFkvA--/YXBwaWQ9aGlnaGxhbmRlcjt3PTk2MDtoPTU0MDtjZj13ZWJw/https://media.zenfs.com/ko/setn.com.tw/4ecc2b856d399e42bc580f45518da290','張育成穩了?紅襪把他受傷期間頂替守游擊的工具人赫南德茲(Enrique Hernandez)交易給前東家道奇,換回2位投手。','記者劉彥池','2023-07-26 00:00:00'),(2,'SpeXial前成員疑遭毆濺血 肇事女子被攔「當場跪地求饒」','中時新聞網','https://s.yimg.com/ny/api/res/1.2/0pKlgZcKz4LPVvK5qt5zJw--/YXBwaWQ9aGlnaGxhbmRlcjt3PTk2MDtoPTY0MDtjZj13ZWJw/https://media.zenfs.com/ko/mirrormedia.mg/4fdd62e508a9db728a06efa875c2b762','Teddy(陳向熙)和《加油喜事》演員臧芮軒等人開心聚餐飲酒。想不到卻變成喋血事件,因被隔壁桌客人的衝突波及,竟被打到鼻孔爆血,還報警、送急診室。藝人常常成團歡聚,卻也有不少嗨事演變成衝突的例子。','娛樂組','2023-07-25 00:00:00'),(3,'杜蘇芮「蛇行打轉中」速度變了!暴雨炸10縣市 大台北風狂吹','三立新聞網','https://s.yimg.com/ny/api/res/1.2/9zTJXy8vgZs_MuWmSlbCvg--/YXBwaWQ9aGlnaGxhbmRlcjt3PTk2MDtoPTk2MDtjZj13ZWJw/https://media.zenfs.com/ko/setn.com.tw/7014e688bae91ea7beb70175773d16a7','根據今(26)早8點資料,中颱「杜蘇芮」在菲律賓近海有打轉現象,北上速度稍微變慢,預計今日下午會逐漸觸碰到恆春半島陸地。中央氣象局目前針對台南、高雄、屏東等地發布陸警;屏東縣山區、宜蘭縣山區、花蓮縣、台東縣等10縣市大、豪雨特報,民眾務必多加留意。','記者楊晏琳','2023-07-26 00:00:00'),(4,'麥特戴蒙拒演《阿凡達》擦身78億片酬!自嘲「推掉最多錢的演員」','現年52歲的美國男星','https://s.yimg.com/ny/api/res/1.2/OlTxRh5FjU4i7p6Sulolcw--/YXBwaWQ9aGlnaGxhbmRlcjt3PTk2MDtoPTY3MjtjZj13ZWJw/https://media.zenfs.com/en/innews_com_tw_7/ca5e5e9b5f888826dfa04d9386cebe07','據外媒報導,近日麥特戴蒙接受《CNN》專訪時,談及2009年間,《阿凡達》導演詹姆斯卡麥隆(James Cameron)曾有意邀他擔綱男主角,還附上一定比例的票房分紅作為條件,待遇非常優渥。但當時他覺得仍有責任在身,需先完成手上的動作電影《神鬼認證》系列,不想因演出《阿凡達》而拋下劇組,因此婉拒了片約。','張疏影','2023-07-25 00:00:00');
/*!40000 ALTER TABLE `articles` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Dumping routines for database 'blog'
--
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2023-07-26 9:48:28
Ctrl
+Shift
+P
打開VSCode功能選單地點: /src/main/java/{gorup id}/{artiface id}/
spring.datasource.url=jdbc:mysql://localhost/blog
spring.datasource.username=root
spring.datasource.password=0000
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# 設定Tomcat使用的port
server.port=8080
package com.aaronlife.blog.model;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import lombok.Data;
@Component
@Data
public class MyConfig {
@Value("${spring.datasource.url}")
private String url;
@Value("${spring.datasource.username}")
private String username;
@Value("${spring.datasource.password}")
private String password;
@Value("${spring.datasource.driver-class-name}")
private String driverClassName;
}
功能:
package com.aaronlife.blog.repository;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Repository;
import com.aaronlife.blog.model.MyConfig;
@Repository
public class BaseRepository {
@Autowired
private MyConfig myConfig;
private Connection conn = null;
public void init() {
// 如果已經連線過,就不需要再連線
if(this.conn != null) return;
try {
// 註冊驅動程式
Class.forName(myConfig.getDriverClassName());
// 建立連線
conn = DriverManager.getConnection(myConfig.getUrl() + "?user=" +
myConfig.getUsername() + "&password=" + myConfig.getPassword());
} catch(SQLException e) {
conn = null;
System.out.println(e.getMessage());
} catch(ClassNotFoundException e) {
conn = null;
System.out.println(e.getMessage());
}
}
public Connection getConnection() {
return this.conn;
}
}
package com.aaronlife.blog.model;
import lombok.Data;
@Data
public class ArticleListModel {
private int id;
private String imgUrl;
private String title;
private String content;
}
package com.aaronlife.blog.model;
import java.sql.Date;
import lombok.Data;
@Data
public class ArticleDetailModel {
private int id;
private String title;
private String subtitle;
private String content;
private String author;
private Date datetime;
}
處理關於article table的CRUD
package com.aaronlife.blog.repository;
import java.sql.Array;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Repository;
import com.aaronlife.blog.model.ArticleDetailModel;
import com.aaronlife.blog.model.ArticleListModel;
import com.aaronlife.blog.model.MyConfig;
@Repository
public class ArticleRepository extends BaseRepository {
@Autowired
private MyConfig myConfig;
public ArrayList<ArticleListModel> findAll() {
super.init();
if(getConnection() != null) // 確定有連線成功
{
try {
Statement stmt = getConnection().createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM articles");
ArrayList<ArticleListModel> data = new ArrayList<>();
// 取得每一筆文章資訊
while(rs.next()) {
ArticleListModel a = new ArticleListModel();
a.setId(rs.getInt("id")); // id欄位名稱必須和資料庫蘭為名稱一致
a.setTitle(rs.getString("title"));
a.setImgUrl(rs.getString("img_url"));
a.setContent(rs.getString("content"));
data.add(a);
}
return data;
} catch (SQLException e) {
System.out.println(e.getMessage());
return null;
}
}
return null;
}
public ArticleDetailModel findArticleById(int id) {
super.init();
if(getConnection() != null) // 確定有連線成功
{
try {
PreparedStatement stmt = getConnection()
.prepareStatement("SELECT * FROM articles WHERE id=?");
stmt.setInt(1, id);
ResultSet rs = stmt.executeQuery();
boolean isExists = rs.next(); // 判斷有沒有資料
if(isExists) { // true = 有資料
ArticleDetailModel a = new ArticleDetailModel();
a.setId(rs.getInt("id"));
a.setTitle(rs.getString("title"));
a.setSubtitle(rs.getString("subtitle"));
a.setContent(rs.getString("content"));
a.setDatetime(rs.getDate("update_time"));
a.setAuthor(rs.getString("author"));
return a;
} else {
return null;
}
} catch (SQLException e) {
System.out.println(e.getMessage());
return null;
}
}
return null;
}
}
package com.aaronlife.blog.service;
import java.util.ArrayList;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.aaronlife.blog.model.ArticleDetailModel;
import com.aaronlife.blog.model.ArticleListModel;
import com.aaronlife.blog.repository.ArticleRepository;
@Service
public class ArticleService {
@Autowired
private ArticleRepository articleRepository;
// 取得文章清單
public ArrayList<ArticleListModel> getAllArticle() {
return articleRepository.findAll();
}
// 以id取得該篇文章
public ArticleDetailModel getArticle(int id) {
return articleRepository.findArticleById(id);
}
}
因為code和message欄位是所有response都會需要的,所以抽離出來成為父類別
package com.aaronlife.blog.model;
import lombok.Data;
@Data
public class BaseResponseModel {
private int code;
private String message;
public BaseResponseModel(int code, String message) {
this.code = code;
this.message = message;
}
}
package com.aaronlife.blog.model;
import java.util.ArrayList;
import lombok.Data;
@Data
public class ArticleListResponseModel extends BaseResponseModel {
private ArrayList<ArticleListModel> data;
public ArticleListResponseModel(int code, String message
, ArrayList<ArticleListModel> data) {
super(code, message);
this.data = data;
}
}
package com.aaronlife.blog.model;
import lombok.Data;
@Data
public class ArticleDetailResponseModel extends BaseResponseModel {
private ArticleDetailModel data;
public ArticleDetailResponseModel(int code, String message
, ArticleDetailModel data) {
super(code, message);
this.data = data;
}
}
package com.aaronlife.blog.controller;
import java.util.ArrayList;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import com.aaronlife.blog.model.ArticleDetailModel;
import com.aaronlife.blog.model.ArticleDetailResponseModel;
import com.aaronlife.blog.model.ArticleListModel;
import com.aaronlife.blog.model.ArticleListResponseModel;
import com.aaronlife.blog.service.ArticleService;
@RestController
@RequestMapping("/v1")
@CrossOrigin(value = "*") // 允許任何網站打這裡的API
public class ArticleController {
@Autowired
private ArticleService articleService;
@RequestMapping(value = "/articles", method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
public ArticleListResponseModel getArticleList() {
// 取得全部文章
ArrayList<ArticleListModel> a = articleService.getAllArticle();
if(a == null)
return new ArticleListResponseModel(999, "無法取得文章清單", null);
else
return new ArticleListResponseModel(0, "成功", a);
}
@RequestMapping(value = "/article", method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
public ArticleDetailResponseModel getArticle(int id) {
// 取得全部文章
ArticleDetailModel a = articleService.getArticle(id);
if(a == null)
return new ArticleDetailResponseModel(999, "無法取得文章", null);
else
return new ArticleDetailResponseModel(0, "成功", a);
}
}
官網: https://github.com/axios/axios#installing
$ yarn add axios
<template>
<Banner />
<h1 class="title text-center mt-5 mb-3 fw-bold">熱門文章</h1>
<section class="articles">
<Card v-for="item in articles" :content="item" />
</section>
</template>
<script>
import Banner from '/src/components/Banner.vue'
import Card from '/src/components/Card.vue'
import axios from 'axios';
export default {
name: 'HomePage',
components: {
Banner,
Card,
},
data() {
return {
articles: [
{
id: 0,
imgUrl: 'https://picsum.photos/id/1011/800/450',
title: 'This is some title',
content: 'Curabitur convallis ac quam vitae laoreet. Nulla mauris ante, euismod sed lacus sit amet, congue bibendum eros. Etiam mattis lobortis porta. Vestibulum ultrices iaculis enim imperdiet egestas.',
},
{
id: 1,
imgUrl: 'https://picsum.photos/id/1005/800/450',
title: 'This is some title',
content: 'Curabitur convallis ac quam vitae laoreet. Nulla mauris ante, euismod sed lacus sit amet, congue bibendum eros. Etiam mattis lobortis porta. Vestibulum ultrices iaculis enim imperdiet egestas.',
},
{
id: 2,
imgUrl: 'https://picsum.photos/id/103/800/450',
title: 'This is some title',
content: 'Curabitur convallis ac quam vitae laoreet. Nulla mauris ante, euismod sed lacus sit amet, congue bibendum eros. Etiam mattis lobortis porta. Vestibulum ultrices iaculis enim imperdiet egestas.',
},
{
id: 3,
imgUrl: 'https://picsum.photos/id/103/800/450',
title: 'This is some title',
content: 'Curabitur convallis ac quam vitae laoreet. Nulla mauris ante, euismod sed lacus sit amet, congue bibendum eros. Etiam mattis lobortis porta. Vestibulum ultrices iaculis enim imperdiet egestas.',
},
]
}
},
mounted() {
axios.get("http://localhost:8080/v1/articles")
.then((response) => {
console.log(response.data);
if(response.data.code == 0) {
this.articles = response.data.data; // 將資料換成API給的
} else {
console.log('取得文章清單失敗');
}
}).catch((error) => {
// 發生錯誤
console.log(error);
})
}
}
</script>
<style scoped>
.articles {
display: grid;
max-width: 1200px;
margin-inline: auto;
padding-inline: 24px;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 24px;
}
.title {
}
</style>
示意圖:
<template>
<div class="w-100 mt-5">
<div class="bg-body-tertiary pt-3 px-3 pt-md-5 px-md-5 text-center overflow-hidden">
<div class="my-3 p-3">
<h2 class="display-5">{{ content.title }}</h2>
<p class="lead">{{ content.subtitle }}</p>
</div>
<div class="bg-dark shadow-sm mx-auto p-5" style="width: 80%; height: fit-content; border-radius: 21px 21px 21px 21px;">
<h5 class="text-white-50 text-start">{{ content.datetime }}</h5>
<h5 class="text-white-50 text-start">{{ content.author }}</h5>
<p class="text-white text-start text-break">{{ content.content }}</p>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: 'Article',
data() {
return{
content: {
id: this.$route.query.id, // 把id從網址列上取下來
title: '文章標題',
subtitle: '文章副標題',
content: '文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容',
author: '作者',
datetime: '2023-08-01'
}
}
},
mounted() {
axios.get("http://localhost:8080/v1/article?id=" + this.$route.query.id)
.then((response) => {
console.log(response.data);
if(response.data.code == 0) {
this.content = response.data.data;
} else {
console.log('無法取得文章內容');
}
})
.catch((error) => {
console.log(error);
});
}
}
</script>
示意圖:
# 開頭一定是 VITE_
VITE_API_HOST=http://localhost:8080
注意:
該檔案比須放在專案的跟目錄vite才能讀取到
接著再呼叫API的地方將環境變數設定給axios,之後呼叫API時就不需要給在host了。
mounted() {
// 設定axios的基本URL
axios.defaults.baseURL = import.meta.env.VITE_API_HOST;
axios.get("/v1/articles")
.then((response) => {
console.log(response.data);
if(response.data.code == 0) {
this.articles = response.data.data; // 將資料換成API給的
} else {
console.log('取得文章清單失敗');
}
}).catch((error) => {
// 發生錯誤
console.log(error);
})
}
補充:
import.meta.env.VITE_API_HOST
用來讀取上面設定在.env內的環境變數
$ yarn create vite simple-blog --template vue
$ cd simple-blog
$ yarn
$ yarn dev
你會看到下面畫面:
VITE v4.4.7 ready in 480 ms
➜ Local: http://localhost:5173/
➜ Network: use --host to expose
➜ press h to show help
http://localhost:5173/
網頁$ yarn add vue-router@4
$ yarn add bootstrap@5.3.0
$ yarn add @popperjs/core
// 引入vue-router
import {createRouter, createWebHistory} from 'vue-router'
import HelloWorld from './components/HelloWorld.vue'
// 定義路由
const routes = [
{
path: '/helloworld',
component: HelloWorld
},
];
// 建立VueRouter
const router = createRouter({
mode: 'history',
history: createWebHistory(),
routes, // short for `routes: routes`
});
export default {
router
}
/src/App.vue
下的<template>
標籤內新增<router-view />
並刪除<template>
內的其他元素<script setup>
</script>
<template>
<router-view />
</template>
<style scoped>
</style>
補充:
也順便把import和<style>
內容清除
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from './router'
const app = createApp(App);
// 將vue-router傳遞給vue app
app.use(router.router);
app.mount('#app')
http://localhost:5173/helloworld
確認可以看到HelloWorld.vue元件內的內容因為style.css路徑改變,所以必須同步修改main.js裡面的style.css的引入路徑
import { createApp } from 'vue'
import './css/style.css'
import App from './App.vue'
import router from './router'
const app = createApp(App);
// 將vue-router傳遞給vue app
app.use(router.router);
app.mount('#app')
/src/style.css
內容並引入Bootstrap樣式@import 'bootstrap/dist/css/bootstrap.css';
網址: https://getbootstrap.com/docs/5.3/examples/
<template>
<header class="p-3 text-bg-dark">
<div class="container">
<div class="d-flex flex-wrap align-items-center justify-content-center justify-content-lg-start">
<a href="/" class="d-flex align-items-center mb-2 mb-lg-0 text-white text-decoration-none">
<svg class="bi me-2" width="40" height="32" role="img" aria-label="Bootstrap"><use xlink:href="#bootstrap"/></svg>
</a>
<ul class="nav col-12 col-lg-auto me-lg-auto mb-2 justify-content-center mb-md-0">
<li><a href="#" class="nav-link px-2 text-secondary">Home</a></li>
<li><a href="#" class="nav-link px-2 text-white">Features</a></li>
<li><a href="#" class="nav-link px-2 text-white">Pricing</a></li>
<li><a href="#" class="nav-link px-2 text-white">FAQs</a></li>
<li><a href="#" class="nav-link px-2 text-white">About</a></li>
</ul>
<form class="col-12 col-lg-auto mb-3 mb-lg-0 me-lg-3" role="search">
<input type="search" class="form-control form-control-dark text-bg-dark" placeholder="Search..." aria-label="Search">
</form>
<div class="text-end">
<button type="button" class="btn btn-outline-light me-2">Login</button>
<button type="button" class="btn btn-warning">Sign-up</button>
</div>
</div>
</div>
</header>
</template>
<template>
<Header />
</template>
<script>
import Header from '/src/components/Header.vue'
export default {
name: 'HomePage',
components: {
Header
}
}
</script>
<style scoped>
</style>
// 引入vue-router
import {createRouter, createWebHistory} from 'vue-router'
import HelloWorld from './components/HelloWorld.vue'
import HomePage from '/src/pages/HomePage.vue'
// 定義路由
const routes = [
{
path: '/helloworld',
component: HelloWorld
},
// 首頁
{
path: '/',
component: HomePage
},
];
// 建立VueRouter
const router = createRouter({
mode: 'history',
history: createWebHistory(),
routes, // short for `routes: routes`
});
export default {
router
}
.form-control-dark {
border-color: var(--bs-gray);
}
.form-control-dark:focus {
border-color: #fff;
box-shadow: 0 0 0 .25rem rgba(255, 255, 255, .25);
}
.text-small {
font-size: 85%;
}
.dropdown-toggle:not(:focus) {
outline: 0;
}
<template>
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="bootstrap" viewBox="0 0 118 94">
<title>Bootstrap</title>
<path fill-rule="evenodd" clip-rule="evenodd" fill="#ffffff"
d="M24.509 0c-6.733 0-11.715 5.893-11.492 12.284.214 6.14-.064 14.092-2.066 20.577C8.943 39.365 5.547 43.485 0 44.014v5.972c5.547.529 8.943 4.649 10.951 11.153 2.002 6.485 2.28 14.437 2.066 20.577C12.794 88.106 17.776 94 24.51 94H93.5c6.733 0 11.714-5.893 11.491-12.284-.214-6.14.064-14.092 2.066-20.577 2.009-6.504 5.396-10.624 10.943-11.153v-5.972c-5.547-.529-8.934-4.649-10.943-11.153-2.002-6.484-2.28-14.437-2.066-20.577C105.214 5.894 100.233 0 93.5 0H24.508zM80 57.863C80 66.663 73.436 72 62.543 72H44a2 2 0 01-2-2V24a2 2 0 012-2h18.437c9.083 0 15.044 4.92 15.044 12.474 0 5.302-4.01 10.049-9.119 10.88v.277C75.317 46.394 80 51.21 80 57.863zM60.521 28.34H49.948v14.934h8.905c6.884 0 10.68-2.772 10.68-7.727 0-4.643-3.264-7.207-9.012-7.207zM49.948 49.2v16.458H60.91c7.167 0 10.964-2.876 10.964-8.281 0-5.406-3.903-8.178-11.425-8.178H49.948z">
</path>
</symbol>
<symbol id="home" viewBox="0 0 16 16">
<path
d="M8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4.5a.5.5 0 0 0 .5-.5v-4h2v4a.5.5 0 0 0 .5.5H14a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146zM2.5 14V7.707l5.5-5.5 5.5 5.5V14H10v-4a.5.5 0 0 0-.5-.5h-3a.5.5 0 0 0-.5.5v4H2.5z" />
</symbol>
<symbol id="speedometer2" viewBox="0 0 16 16">
<path
d="M8 4a.5.5 0 0 1 .5.5V6a.5.5 0 0 1-1 0V4.5A.5.5 0 0 1 8 4zM3.732 5.732a.5.5 0 0 1 .707 0l.915.914a.5.5 0 1 1-.708.708l-.914-.915a.5.5 0 0 1 0-.707zM2 10a.5.5 0 0 1 .5-.5h1.586a.5.5 0 0 1 0 1H2.5A.5.5 0 0 1 2 10zm9.5 0a.5.5 0 0 1 .5-.5h1.5a.5.5 0 0 1 0 1H12a.5.5 0 0 1-.5-.5zm.754-4.246a.389.389 0 0 0-.527-.02L7.547 9.31a.91.91 0 1 0 1.302 1.258l3.434-4.297a.389.389 0 0 0-.029-.518z" />
<path fill-rule="evenodd"
d="M0 10a8 8 0 1 1 15.547 2.661c-.442 1.253-1.845 1.602-2.932 1.25C11.309 13.488 9.475 13 8 13c-1.474 0-3.31.488-4.615.911-1.087.352-2.49.003-2.932-1.25A7.988 7.988 0 0 1 0 10zm8-7a7 7 0 0 0-6.603 9.329c.203.575.923.876 1.68.63C4.397 12.533 6.358 12 8 12s3.604.532 4.923.96c.757.245 1.477-.056 1.68-.631A7 7 0 0 0 8 3z" />
</symbol>
<symbol id="table" viewBox="0 0 16 16">
<path
d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2zm15 2h-4v3h4V4zm0 4h-4v3h4V8zm0 4h-4v3h3a1 1 0 0 0 1-1v-2zm-5 3v-3H6v3h4zm-5 0v-3H1v2a1 1 0 0 0 1 1h3zm-4-4h4V8H1v3zm0-4h4V4H1v3zm5-3v3h4V4H6zm4 4H6v3h4V8z" />
</symbol>
<symbol id="people-circle" viewBox="0 0 16 16">
<path d="M11 6a3 3 0 1 1-6 0 3 3 0 0 1 6 0z" />
<path fill-rule="evenodd"
d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm8-7a7 7 0 0 0-5.468 11.37C3.242 11.226 4.805 10 8 10s4.757 1.225 5.468 2.37A7 7 0 0 0 8 1z" />
</symbol>
<symbol id="grid" viewBox="0 0 16 16">
<path
d="M1 2.5A1.5 1.5 0 0 1 2.5 1h3A1.5 1.5 0 0 1 7 2.5v3A1.5 1.5 0 0 1 5.5 7h-3A1.5 1.5 0 0 1 1 5.5v-3zM2.5 2a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 0-.5-.5h-3zm6.5.5A1.5 1.5 0 0 1 10.5 1h3A1.5 1.5 0 0 1 15 2.5v3A1.5 1.5 0 0 1 13.5 7h-3A1.5 1.5 0 0 1 9 5.5v-3zm1.5-.5a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 0-.5-.5h-3zM1 10.5A1.5 1.5 0 0 1 2.5 9h3A1.5 1.5 0 0 1 7 10.5v3A1.5 1.5 0 0 1 5.5 15h-3A1.5 1.5 0 0 1 1 13.5v-3zm1.5-.5a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 0-.5-.5h-3zm6.5.5A1.5 1.5 0 0 1 10.5 9h3a1.5 1.5 0 0 1 1.5 1.5v3a1.5 1.5 0 0 1-1.5 1.5h-3A1.5 1.5 0 0 1 9 13.5v-3zm1.5-.5a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 0-.5-.5h-3z" />
</symbol>
</svg>
<header class="p-3 text-bg-dark">
<div class="container">
<div class="d-flex flex-wrap align-items-center justify-content-center justify-content-lg-start">
<a href="/" class="d-flex align-items-center mb-2 mb-lg-0 text-white text-decoration-none">
<svg class="bi me-2" width="40" height="32" role="img" aria-label="Bootstrap">
<use xlink:href="#bootstrap" />
</svg>
</a>
<ul class="nav col-12 col-lg-auto me-lg-auto mb-2 justify-content-center mb-md-0">
<li><a href="#" class="nav-link px-2 text-secondary">Home</a></li>
<li><a href="#" class="nav-link px-2 text-white">Features</a></li>
<li><a href="#" class="nav-link px-2 text-white">Pricing</a></li>
<li><a href="#" class="nav-link px-2 text-white">FAQs</a></li>
<li><a href="#" class="nav-link px-2 text-white">About</a></li>
</ul>
<form class="col-12 col-lg-auto mb-3 mb-lg-0 me-lg-3" role="search">
<input type="search" class="form-control form-control-dark text-bg-dark" placeholder="Search..."
aria-label="Search">
</form>
<div class="text-end">
<button type="button" class="btn btn-outline-light me-2">Login</button>
<button type="button" class="btn btn-warning">Sign-up</button>
</div>
</div>
</div>
</header>
</template>
<style scoped>
@import '/src/css/headers.css'
</style>
來源: https://getbootstrap.com/docs/5.3/examples/product/
<template>
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="aperture" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" />
<path
d="M14.31 8l5.74 9.94M9.69 8h11.48M7.38 12l5.74-9.94M9.69 16L3.95 6.06M14.31 16H2.83m13.79-4l-5.74 9.94" />
</symbol>
<symbol id="cart" viewBox="0 0 16 16">
<path
d="M0 1.5A.5.5 0 0 1 .5 1H2a.5.5 0 0 1 .485.379L2.89 3H14.5a.5.5 0 0 1 .49.598l-1 5a.5.5 0 0 1-.465.401l-9.397.472L4.415 11H13a.5.5 0 0 1 0 1H4a.5.5 0 0 1-.491-.408L2.01 3.607 1.61 2H.5a.5.5 0 0 1-.5-.5zM3.102 4l.84 4.479 9.144-.459L13.89 4H3.102zM5 12a2 2 0 1 0 0 4 2 2 0 0 0 0-4zm7 0a2 2 0 1 0 0 4 2 2 0 0 0 0-4zm-7 1a1 1 0 1 1 0 2 1 1 0 0 1 0-2zm7 0a1 1 0 1 1 0 2 1 1 0 0 1 0-2z" />
</symbol>
<symbol id="chevron-right" viewBox="0 0 16 16">
<path fill-rule="evenodd"
d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z" />
</symbol>
</svg>
<div style="background-size: cover; background-image: url(https://png.pngtree.com/background/20210709/original/pngtree-simple-technology-business-line-picture-image_938374.jpg);"
class="position-relative overflow-hidden p-md-5 p-lg-0 text-center bg-body-tertiary">
<div class="col-md-6 p-lg-5 mx-auto my-5">
<h1 class="text-white display-3 fw-bold">Designed for engineers</h1>
<h3 class="text-white fw-normal mb-3">Build anything you want with Aperture</h3>
<div class="d-flex gap-3 justify-content-center lead fw-normal">
<a class="icon-link bg-body-secondary" href="#">
Learn more
<svg class="bi">
<use xlink:href="#chevron-right" />
</svg>
</a>
<a class="icon-link bg-body-secondary" href="#">
Buy
<svg class="bi">
<use xlink:href="#chevron-right" />
</svg>
</a>
</div>
</div>
<div class="product-device shadow-sm d-none"></div>
<div class="product-device product-device-2 shadow-sm d-none"></div>
</div>
</template>
<style scoped>@import '/src/css/product.css'</style>
這裡必須將product.css加入到/src/css中
.container {
max-width: 100%;
}
.icon-link > .bi {
width: .75em;
height: .75em;
}
/*
* Custom translucent site header
*/
.site-header {
background-color: rgba(0, 0, 0, .85);
-webkit-backdrop-filter: saturate(180%) blur(20px);
backdrop-filter: saturate(180%) blur(20px);
}
.site-header a {
color: #8e8e8e;
transition: color .15s ease-in-out;
}
.site-header a:hover {
color: #fff;
text-decoration: none;
}
/*
* Dummy devices (replace them with your own or something else entirely!)
*/
.product-device {
position: absolute;
right: 10%;
bottom: -30%;
width: 300px;
height: 540px;
background-color: #333;
border-radius: 21px;
transform: rotate(30deg);
}
.product-device::before {
position: absolute;
top: 10%;
right: 10px;
bottom: 10%;
left: 10px;
content: "";
background-color: rgba(255, 255, 255, .1);
border-radius: 5px;
}
.product-device-2 {
top: -25%;
right: auto;
bottom: 0;
left: 5%;
background-color: #e5e5e5;
}
/*
* Extra utilities
*/
.flex-equal > * {
flex: 1;
}
@media (min-width: 768px) {
.flex-md-equal > * {
flex: 1;
}
}
<template>
<Header />
<Banner />
</template>
<script>
import Header from '/src/components/Header.vue'
import Banner from '/src/components/Banner.vue'
export default {
name: 'HomePage',
components: {
Header,
Banner
}
}
</script>
<style scoped>
</style>
目前畫面:
來源: https://codepen.io/utilitybend/pen/bGvjLba
<template>
<article>
<div class="article-wrapper">
<figure>
<img src="https://picsum.photos/id/1011/800/450" alt="" />
</figure>
<div class="article-body">
<h2>This is some title</h2>
<p>
Curabitur convallis ac quam vitae laoreet. Nulla mauris ante, euismod sed lacus sit amet, congue
bibendum eros. Etiam mattis lobortis porta. Vestibulum ultrices iaculis enim imperdiet egestas.
</p>
<a href="#" class="read-more">
Read more <span class="sr-only">about this is some title</span>
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z"
clip-rule="evenodd" />
</svg>
</a>
</div>
</div>
</article>
<!-- <article>
<div class="article-wrapper">
<figure>
<img src="https://picsum.photos/id/1005/800/450" alt="" />
</figure>
<div class="article-body">
<template>
<article>
<div class="article-wrapper">
<figure>
<img src="https://picsum.photos/id/1011/800/450" alt="" />
</figure>
<div class="article-body">
<h2>This is some title</h2>
<p>
Curabitur convallis ac quam vitae laoreet. Nulla mauris ante, euismod sed lacus sit amet, congue
bibendum eros. Etiam mattis lobortis porta. Vestibulum ultrices iaculis enim imperdiet egestas.
</p>
<a href="#" class="read-more">
Read more <span class="sr-only">about this is some title</span>
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z"
clip-rule="evenodd" />
</svg>
</a>
</div>
</div>
</article>
</template>
<style scoped>
article {
--img-scale: 1.001;
--title-color: black;
--link-icon-translate: -20px;
--link-icon-opacity: 0;
position: relative;
border-radius: 16px;
box-shadow: none;
background: #fff;
transform-origin: center;
transition: all 0.4s ease-in-out;
overflow: hidden;
}
article a::after {
position: absolute;
inset-block: 0;
inset-inline: 0;
cursor: pointer;
content: "";
}
/* basic article elements styling */
article h2 {
margin: 0 0 18px 0;
font-family: "Bebas Neue", cursive;
font-size: 1.9rem;
letter-spacing: 0.06em;
color: var(--title-color);
transition: color 0.3s ease-out;
}
figure {
margin: 0;
padding: 0;
aspect-ratio: 16 / 9;
overflow: hidden;
}
article img {
max-width: 100%;
transform-origin: center;
transform: scale(var(--img-scale));
transition: transform 0.4s ease-in-out;
}
.article-body {
padding: 24px;
}
article a {
display: inline-flex;
align-items: center;
text-decoration: none;
color: #28666e;
}
article a:focus {
outline: 1px dotted #28666e;
}
article a .icon {
min-width: 24px;
width: 24px;
height: 24px;
margin-left: 5px;
transform: translateX(var(--link-icon-translate));
opacity: var(--link-icon-opacity);
transition: all 0.3s;
}
/* using the has() relational pseudo selector to update our custom properties */
article:has(:hover, :focus) {
--img-scale: 1.1;
--title-color: #28666e;
--link-icon-translate: 0;
--link-icon-opacity: 1;
box-shadow: rgba(0, 0, 0, 0.16) 0px 10px 36px 0px, rgba(0, 0, 0, 0.06) 0px 0px 0px 1px;
}
/************************
Generic layout (demo looks)
**************************/
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
padding: 48px 0;
font-family: "Figtree", sans-serif;
font-size: 1.2rem;
line-height: 1.6rem;
background-image: linear-gradient(45deg, #7c9885, #b5b682);
min-height: 100vh;
}
@media screen and (max-width: 960px) {
article {
container: card/inline-size;
}
.article-body p {
display: none;
}
}
@container card (min-width: 380px) {
.article-wrapper {
display: grid;
grid-template-columns: 100px 1fr;
gap: 16px;
}
.article-body {
padding-left: 0;
}
figure {
width: 100%;
height: 100%;
overflow: hidden;
}
figure img {
height: 100%;
aspect-ratio: 1;
object-fit: cover;
}
}
.sr-only:not(:focus):not(:active) {
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
}
</style>
<template>
<Header />
<Banner />
<h1 class="title text-center mt-5 mb-3 fw-bold">熱門文章</h1>
<section class="articles">
<Card />
</section>
</template>
<script>
import Header from '/src/components/Header.vue'
import Banner from '/src/components/Banner.vue'
import Card from '/src/components/Card.vue'
export default {
name: 'HomePage',
components: {
Header,
Banner,
Card
}
}
</script>
<style scoped>
.articles {
display: grid;
max-width: 1200px;
margin-inline: auto;
padding-inline: 24px;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 24px;
}
.title {
}
</style>
補充:
加上了文章標題與樣式
目前畫面:
<template>
<Header />
<Banner />
<h1 class="title text-center mt-5 mb-3 fw-bold">熱門文章</h1>
<section class="articles">
<Card v-for="item in articles" :content="item" />
</section>
</template>
<script>
import Header from '/src/components/Header.vue'
import Banner from '/src/components/Banner.vue'
import Card from '/src/components/Card.vue'
export default {
name: 'HomePage',
components: {
Header,
Banner,
Card
},
data() {
return {
articles: [
{
id: 0,
imgUrl: 'https://picsum.photos/id/1011/800/450',
title: 'This is some title',
content: 'Curabitur convallis ac quam vitae laoreet. Nulla mauris ante, euismod sed lacus sit amet, congue bibendum eros. Etiam mattis lobortis porta. Vestibulum ultrices iaculis enim imperdiet egestas.',
},
{
id: 1,
imgUrl: 'https://picsum.photos/id/1005/800/450',
title: 'This is some title',
content: 'Curabitur convallis ac quam vitae laoreet. Nulla mauris ante, euismod sed lacus sit amet, congue bibendum eros. Etiam mattis lobortis porta. Vestibulum ultrices iaculis enim imperdiet egestas.',
},
{
id: 2,
imgUrl: 'https://picsum.photos/id/103/800/450',
title: 'This is some title',
content: 'Curabitur convallis ac quam vitae laoreet. Nulla mauris ante, euismod sed lacus sit amet, congue bibendum eros. Etiam mattis lobortis porta. Vestibulum ultrices iaculis enim imperdiet egestas.',
},
]
}
}
}
</script>
<style scoped>
.articles {
display: grid;
max-width: 1200px;
margin-inline: auto;
padding-inline: 24px;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 24px;
}
.title {
}
</style>
<template>
<article>
<div class="article-wrapper">
<figure>
<img :src="content.imgUrl" alt="" />
</figure>
<div class="article-body">
<h2>{{ content.title }}</h2>
<p>{{ content.content }}</p>
<a href="#" class="read-more">
Read more <span class="sr-only">about this is some title</span>
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z"
clip-rule="evenodd" />
</svg>
</a>
</div>
</div>
</article>
</template>
<script>
export default {
name: 'Card',
props: ["content"],
}
</script>
<style scoped>
article {
--img-scale: 1.001;
--title-color: black;
--link-icon-translate: -20px;
--link-icon-opacity: 0;
position: relative;
border-radius: 16px;
box-shadow: none;
background: #fff;
transform-origin: center;
transition: all 0.4s ease-in-out;
overflow: hidden;
}
article a::after {
position: absolute;
inset-block: 0;
inset-inline: 0;
cursor: pointer;
content: "";
}
/* basic article elements styling */
article h2 {
margin: 0 0 18px 0;
font-family: "Bebas Neue", cursive;
font-size: 1.9rem;
letter-spacing: 0.06em;
color: var(--title-color);
transition: color 0.3s ease-out;
}
figure {
margin: 0;
padding: 0;
aspect-ratio: 16 / 9;
overflow: hidden;
}
article img {
max-width: 100%;
transform-origin: center;
transform: scale(var(--img-scale));
transition: transform 0.4s ease-in-out;
}
.article-body {
padding: 24px;
}
article a {
display: inline-flex;
align-items: center;
text-decoration: none;
color: #28666e;
}
article a:focus {
outline: 1px dotted #28666e;
}
article a .icon {
min-width: 24px;
width: 24px;
height: 24px;
margin-left: 5px;
transform: translateX(var(--link-icon-translate));
opacity: var(--link-icon-opacity);
transition: all 0.3s;
}
/* using the has() relational pseudo selector to update our custom properties */
article:has(:hover, :focus) {
--img-scale: 1.1;
--title-color: #28666e;
--link-icon-translate: 0;
--link-icon-opacity: 1;
box-shadow: rgba(0, 0, 0, 0.16) 0px 10px 36px 0px, rgba(0, 0, 0, 0.06) 0px 0px 0px 1px;
}
/************************
Generic layout (demo looks)
**************************/
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
padding: 48px 0;
font-family: "Figtree", sans-serif;
font-size: 1.2rem;
line-height: 1.6rem;
background-image: linear-gradient(45deg, #7c9885, #b5b682);
min-height: 100vh;
}
@media screen and (max-width: 960px) {
article {
container: card/inline-size;
}
.article-body p {
display: none;
}
}
@container card (min-width: 380px) {
.article-wrapper {
display: grid;
grid-template-columns: 100px 1fr;
gap: 16px;
}
.article-body {
padding-left: 0;
}
figure {
width: 100%;
height: 100%;
overflow: hidden;
}
figure img {
height: 100%;
aspect-ratio: 1;
object-fit: cover;
}
}
.sr-only:not(:focus):not(:active) {
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
}
</style>
<template>
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="bootstrap" viewBox="0 0 118 94">
<title>Bootstrap</title>
<path fill-rule="evenodd" clip-rule="evenodd"
d="M24.509 0c-6.733 0-11.715 5.893-11.492 12.284.214 6.14-.064 14.092-2.066 20.577C8.943 39.365 5.547 43.485 0 44.014v5.972c5.547.529 8.943 4.649 10.951 11.153 2.002 6.485 2.28 14.437 2.066 20.577C12.794 88.106 17.776 94 24.51 94H93.5c6.733 0 11.714-5.893 11.491-12.284-.214-6.14.064-14.092 2.066-20.577 2.009-6.504 5.396-10.624 10.943-11.153v-5.972c-5.547-.529-8.934-4.649-10.943-11.153-2.002-6.484-2.28-14.437-2.066-20.577C105.214 5.894 100.233 0 93.5 0H24.508zM80 57.863C80 66.663 73.436 72 62.543 72H44a2 2 0 01-2-2V24a2 2 0 012-2h18.437c9.083 0 15.044 4.92 15.044 12.474 0 5.302-4.01 10.049-9.119 10.88v.277C75.317 46.394 80 51.21 80 57.863zM60.521 28.34H49.948v14.934h8.905c6.884 0 10.68-2.772 10.68-7.727 0-4.643-3.264-7.207-9.012-7.207zM49.948 49.2v16.458H60.91c7.167 0 10.964-2.876 10.964-8.281 0-5.406-3.903-8.178-11.425-8.178H49.948z">
</path>
</symbol>
<symbol id="facebook" viewBox="0 0 16 16">
<path
d="M16 8.049c0-4.446-3.582-8.05-8-8.05C3.58 0-.002 3.603-.002 8.05c0 4.017 2.926 7.347 6.75 7.951v-5.625h-2.03V8.05H6.75V6.275c0-2.017 1.195-3.131 3.022-3.131.876 0 1.791.157 1.791.157v1.98h-1.009c-.993 0-1.303.621-1.303 1.258v1.51h2.218l-.354 2.326H9.25V16c3.824-.604 6.75-3.934 6.75-7.951z" />
</symbol>
<symbol id="instagram" viewBox="0 0 16 16">
<path
d="M8 0C5.829 0 5.556.01 4.703.048 3.85.088 3.269.222 2.76.42a3.917 3.917 0 0 0-1.417.923A3.927 3.927 0 0 0 .42 2.76C.222 3.268.087 3.85.048 4.7.01 5.555 0 5.827 0 8.001c0 2.172.01 2.444.048 3.297.04.852.174 1.433.372 1.942.205.526.478.972.923 1.417.444.445.89.719 1.416.923.51.198 1.09.333 1.942.372C5.555 15.99 5.827 16 8 16s2.444-.01 3.298-.048c.851-.04 1.434-.174 1.943-.372a3.916 3.916 0 0 0 1.416-.923c.445-.445.718-.891.923-1.417.197-.509.332-1.09.372-1.942C15.99 10.445 16 10.173 16 8s-.01-2.445-.048-3.299c-.04-.851-.175-1.433-.372-1.941a3.926 3.926 0 0 0-.923-1.417A3.911 3.911 0 0 0 13.24.42c-.51-.198-1.092-.333-1.943-.372C10.443.01 10.172 0 7.998 0h.003zm-.717 1.442h.718c2.136 0 2.389.007 3.232.046.78.035 1.204.166 1.486.275.373.145.64.319.92.599.28.28.453.546.598.92.11.281.24.705.275 1.485.039.843.047 1.096.047 3.231s-.008 2.389-.047 3.232c-.035.78-.166 1.203-.275 1.485a2.47 2.47 0 0 1-.599.919c-.28.28-.546.453-.92.598-.28.11-.704.24-1.485.276-.843.038-1.096.047-3.232.047s-2.39-.009-3.233-.047c-.78-.036-1.203-.166-1.485-.276a2.478 2.478 0 0 1-.92-.598 2.48 2.48 0 0 1-.6-.92c-.109-.281-.24-.705-.275-1.485-.038-.843-.046-1.096-.046-3.233 0-2.136.008-2.388.046-3.231.036-.78.166-1.204.276-1.486.145-.373.319-.64.599-.92.28-.28.546-.453.92-.598.282-.11.705-.24 1.485-.276.738-.034 1.024-.044 2.515-.045v.002zm4.988 1.328a.96.96 0 1 0 0 1.92.96.96 0 0 0 0-1.92zm-4.27 1.122a4.109 4.109 0 1 0 0 8.217 4.109 4.109 0 0 0 0-8.217zm0 1.441a2.667 2.667 0 1 1 0 5.334 2.667 2.667 0 0 1 0-5.334z" />
</symbol>
<symbol id="twitter" viewBox="0 0 16 16">
<path
d="M5.026 15c6.038 0 9.341-5.003 9.341-9.334 0-.14 0-.282-.006-.422A6.685 6.685 0 0 0 16 3.542a6.658 6.658 0 0 1-1.889.518 3.301 3.301 0 0 0 1.447-1.817 6.533 6.533 0 0 1-2.087.793A3.286 3.286 0 0 0 7.875 6.03a9.325 9.325 0 0 1-6.767-3.429 3.289 3.289 0 0 0 1.018 4.382A3.323 3.323 0 0 1 .64 6.575v.045a3.288 3.288 0 0 0 2.632 3.218 3.203 3.203 0 0 1-.865.115 3.23 3.23 0 0 1-.614-.057 3.283 3.283 0 0 0 3.067 2.277A6.588 6.588 0 0 1 .78 13.58a6.32 6.32 0 0 1-.78-.045A9.344 9.344 0 0 0 5.026 15z" />
</symbol>
</svg>
<div class="container">
<footer class="d-flex flex-wrap justify-content-between align-items-center py-3 my-4 border-top">
<div class="col-md-4 d-flex align-items-center">
<a href="/" class="mb-3 me-2 mb-md-0 text-body-secondary text-decoration-none lh-1">
<img src="/src/assets/vue.svg" />
</a>
<span class="mb-3 mb-md-0 text-body-secondary">© 2023 Company, Inc</span>
</div>
<ul class="nav col-md-4 justify-content-end list-unstyled d-flex">
<li class="ms-3"><a class="text-body-secondary" href="#"><svg class="bi" width="24" height="24">
<use xlink:href="#twitter" />
</svg></a></li>
<li class="ms-3"><a class="text-body-secondary" href="#"><svg class="bi" width="24" height="24">
<use xlink:href="#instagram" />
</svg></a></li>
<li class="ms-3"><a class="text-body-secondary" href="#"><svg class="bi" width="24" height="24">
<use xlink:href="#facebook" />
</svg></a></li>
</ul>
</footer>
</div>
</template>
<template>
<Header />
<Banner />
<h1 class="title text-center mt-5 mb-3 fw-bold">熱門文章</h1>
<section class="articles">
<Card v-for="item in articles" :content="item" />
</section>
<Footer />
</template>
<script>
import Header from '/src/components/Header.vue'
import Banner from '/src/components/Banner.vue'
import Card from '/src/components/Card.vue'
import Footer from '/src/components/Footer.vue'
export default {
name: 'HomePage',
components: {
Header,
Banner,
Card,
Footer
},
data() {
return {
articles: [
{
id: 0,
imgUrl: 'https://picsum.photos/id/1011/800/450',
title: 'This is some title',
content: 'Curabitur convallis ac quam vitae laoreet. Nulla mauris ante, euismod sed lacus sit amet, congue bibendum eros. Etiam mattis lobortis porta. Vestibulum ultrices iaculis enim imperdiet egestas.',
},
{
id: 1,
imgUrl: 'https://picsum.photos/id/1005/800/450',
title: 'This is some title',
content: 'Curabitur convallis ac quam vitae laoreet. Nulla mauris ante, euismod sed lacus sit amet, congue bibendum eros. Etiam mattis lobortis porta. Vestibulum ultrices iaculis enim imperdiet egestas.',
},
{
id: 2,
imgUrl: 'https://picsum.photos/id/103/800/450',
title: 'This is some title',
content: 'Curabitur convallis ac quam vitae laoreet. Nulla mauris ante, euismod sed lacus sit amet, congue bibendum eros. Etiam mattis lobortis porta. Vestibulum ultrices iaculis enim imperdiet egestas.',
},
{
id: 3,
imgUrl: 'https://picsum.photos/id/103/800/450',
title: 'This is some title',
content: 'Curabitur convallis ac quam vitae laoreet. Nulla mauris ante, euismod sed lacus sit amet, congue bibendum eros. Etiam mattis lobortis porta. Vestibulum ultrices iaculis enim imperdiet egestas.',
},
]
}
}
}
</script>
<style scoped>
.articles {
display: grid;
max-width: 1200px;
margin-inline: auto;
padding-inline: 24px;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 24px;
}
.title {
}
</style>
<template>
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="aperture" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" />
<path
d="M14.31 8l5.74 9.94M9.69 8h11.48M7.38 12l5.74-9.94M9.69 16L3.95 6.06M14.31 16H2.83m13.79-4l-5.74 9.94" />
</symbol>
<symbol id="cart" viewBox="0 0 16 16">
<path
d="M0 1.5A.5.5 0 0 1 .5 1H2a.5.5 0 0 1 .485.379L2.89 3H14.5a.5.5 0 0 1 .49.598l-1 5a.5.5 0 0 1-.465.401l-9.397.472L4.415 11H13a.5.5 0 0 1 0 1H4a.5.5 0 0 1-.491-.408L2.01 3.607 1.61 2H.5a.5.5 0 0 1-.5-.5zM3.102 4l.84 4.479 9.144-.459L13.89 4H3.102zM5 12a2 2 0 1 0 0 4 2 2 0 0 0 0-4zm7 0a2 2 0 1 0 0 4 2 2 0 0 0 0-4zm-7 1a1 1 0 1 1 0 2 1 1 0 0 1 0-2zm7 0a1 1 0 1 1 0 2 1 1 0 0 1 0-2z" />
</symbol>
<symbol id="chevron-right" viewBox="0 0 16 16">
<path fill-rule="evenodd"
d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z" />
</symbol>
</svg>
<div style="background-size: cover; background-image: url(https://png.pngtree.com/background/20210709/original/pngtree-simple-technology-business-line-picture-image_938374.jpg);"
class="position-relative overflow-hidden p-md-5 p-lg-0 text-center bg-body-tertiary">
<div class="col-md-6 p-lg-5 mx-auto my-5">
<h1 class="text-white display-3 fw-bold">Designed for engineers</h1>
<h3 class="text-white fw-normal mb-3">Build anything you want with Aperture</h3>
<div class="d-flex gap-3 justify-content-center lead fw-normal">
<button type="button" class="btn btn-info">Learn More</button>
<button type="button" class="btn btn-info">Buy</button>
</div>
</div>
<div class="product-device shadow-sm d-none"></div>
<div class="product-device product-device-2 shadow-sm d-none"></div>
</div>
</template>
<script></script>
<style scoped>
@import '/src/css/product.css';
button[type="button"] {
color: #fff;
box-shadow: 2px 2px 5px #777;
}
h1, h3 {
text-shadow: 5px 5px 5px #000;
}
</style>
來源: https://getbootstrap.com/docs/5.3/examples/product/
<template>
<div class="w-100 mt-5">
<div class="bg-body-tertiary pt-3 px-3 pt-md-5 px-md-5 text-center overflow-hidden">
<div class="my-3 p-3">
<h2 class="display-5">Another headline</h2>
<p class="lead">And an even wittier subheading.</p>
</div>
<div class="bg-dark shadow-sm mx-auto p-5" style="width: 80%; height: 300px; border-radius: 21px 21px 21px 21px;">
<h5 class="text-white-50 text-start">2023-08-01</h5>
<h5 class="text-white-50 text-start">Aaron</h5>
<p class="text-white text-start text-break">dsafjlkdsajflkjdlfkajsldfkjalkfjlkadjflkajdslfkajdfjlkfdasjdsafjlkdsajflkjdlfkajsldfkjalkfjlkadjflkajdslfkajdfjlkfdasjdsafjlkdsajflkjdlfkajsldfkjalkfjlkadjflkajdslfkajdfjlkfdasjdsafjlkdsajflkjdlfkajsldfkjalkfjlkadjflkajdslfkajdfjlkfdasjdsafjlkdsajflkjdlfkajsldfkjalkfjlkadjflkajdslfkajdfjlkfdasj</p>
</div>
</div>
</div>
</template>
<template>
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="bootstrap" viewBox="0 0 118 94">
<title>Bootstrap</title>
<path fill-rule="evenodd" clip-rule="evenodd" fill="#ffffff"
d="M24.509 0c-6.733 0-11.715 5.893-11.492 12.284.214 6.14-.064 14.092-2.066 20.577C8.943 39.365 5.547 43.485 0 44.014v5.972c5.547.529 8.943 4.649 10.951 11.153 2.002 6.485 2.28 14.437 2.066 20.577C12.794 88.106 17.776 94 24.51 94H93.5c6.733 0 11.714-5.893 11.491-12.284-.214-6.14.064-14.092 2.066-20.577 2.009-6.504 5.396-10.624 10.943-11.153v-5.972c-5.547-.529-8.934-4.649-10.943-11.153-2.002-6.484-2.28-14.437-2.066-20.577C105.214 5.894 100.233 0 93.5 0H24.508zM80 57.863C80 66.663 73.436 72 62.543 72H44a2 2 0 01-2-2V24a2 2 0 012-2h18.437c9.083 0 15.044 4.92 15.044 12.474 0 5.302-4.01 10.049-9.119 10.88v.277C75.317 46.394 80 51.21 80 57.863zM60.521 28.34H49.948v14.934h8.905c6.884 0 10.68-2.772 10.68-7.727 0-4.643-3.264-7.207-9.012-7.207zM49.948 49.2v16.458H60.91c7.167 0 10.964-2.876 10.964-8.281 0-5.406-3.903-8.178-11.425-8.178H49.948z">
</path>
</symbol>
<symbol id="home" viewBox="0 0 16 16">
<path
d="M8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4.5a.5.5 0 0 0 .5-.5v-4h2v4a.5.5 0 0 0 .5.5H14a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146zM2.5 14V7.707l5.5-5.5 5.5 5.5V14H10v-4a.5.5 0 0 0-.5-.5h-3a.5.5 0 0 0-.5.5v4H2.5z" />
</symbol>
<symbol id="speedometer2" viewBox="0 0 16 16">
<path
d="M8 4a.5.5 0 0 1 .5.5V6a.5.5 0 0 1-1 0V4.5A.5.5 0 0 1 8 4zM3.732 5.732a.5.5 0 0 1 .707 0l.915.914a.5.5 0 1 1-.708.708l-.914-.915a.5.5 0 0 1 0-.707zM2 10a.5.5 0 0 1 .5-.5h1.586a.5.5 0 0 1 0 1H2.5A.5.5 0 0 1 2 10zm9.5 0a.5.5 0 0 1 .5-.5h1.5a.5.5 0 0 1 0 1H12a.5.5 0 0 1-.5-.5zm.754-4.246a.389.389 0 0 0-.527-.02L7.547 9.31a.91.91 0 1 0 1.302 1.258l3.434-4.297a.389.389 0 0 0-.029-.518z" />
<path fill-rule="evenodd"
d="M0 10a8 8 0 1 1 15.547 2.661c-.442 1.253-1.845 1.602-2.932 1.25C11.309 13.488 9.475 13 8 13c-1.474 0-3.31.488-4.615.911-1.087.352-2.49.003-2.932-1.25A7.988 7.988 0 0 1 0 10zm8-7a7 7 0 0 0-6.603 9.329c.203.575.923.876 1.68.63C4.397 12.533 6.358 12 8 12s3.604.532 4.923.96c.757.245 1.477-.056 1.68-.631A7 7 0 0 0 8 3z" />
</symbol>
<symbol id="table" viewBox="0 0 16 16">
<path
d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2zm15 2h-4v3h4V4zm0 4h-4v3h4V8zm0 4h-4v3h3a1 1 0 0 0 1-1v-2zm-5 3v-3H6v3h4zm-5 0v-3H1v2a1 1 0 0 0 1 1h3zm-4-4h4V8H1v3zm0-4h4V4H1v3zm5-3v3h4V4H6zm4 4H6v3h4V8z" />
</symbol>
<symbol id="people-circle" viewBox="0 0 16 16">
<path d="M11 6a3 3 0 1 1-6 0 3 3 0 0 1 6 0z" />
<path fill-rule="evenodd"
d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm8-7a7 7 0 0 0-5.468 11.37C3.242 11.226 4.805 10 8 10s4.757 1.225 5.468 2.37A7 7 0 0 0 8 1z" />
</symbol>
<symbol id="grid" viewBox="0 0 16 16">
<path
d="M1 2.5A1.5 1.5 0 0 1 2.5 1h3A1.5 1.5 0 0 1 7 2.5v3A1.5 1.5 0 0 1 5.5 7h-3A1.5 1.5 0 0 1 1 5.5v-3zM2.5 2a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 0-.5-.5h-3zm6.5.5A1.5 1.5 0 0 1 10.5 1h3A1.5 1.5 0 0 1 15 2.5v3A1.5 1.5 0 0 1 13.5 7h-3A1.5 1.5 0 0 1 9 5.5v-3zm1.5-.5a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 0-.5-.5h-3zM1 10.5A1.5 1.5 0 0 1 2.5 9h3A1.5 1.5 0 0 1 7 10.5v3A1.5 1.5 0 0 1 5.5 15h-3A1.5 1.5 0 0 1 1 13.5v-3zm1.5-.5a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 0-.5-.5h-3zm6.5.5A1.5 1.5 0 0 1 10.5 9h3a1.5 1.5 0 0 1 1.5 1.5v3a1.5 1.5 0 0 1-1.5 1.5h-3A1.5 1.5 0 0 1 9 13.5v-3zm1.5-.5a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 0-.5-.5h-3z" />
</symbol>
</svg>
<header class="p-3 text-bg-dark my-header">
<div class="container">
<div class="d-flex flex-wrap align-items-center justify-content-center justify-content-lg-start">
<a href="/" class="d-flex align-items-center mb-2 mb-lg-0 text-white text-decoration-none">
<svg class="bi me-2" width="40" height="32" role="img" aria-label="Bootstrap">
<use xlink:href="#bootstrap" />
</svg>
</a>
<ul class="nav col-12 col-lg-auto me-lg-auto mb-2 justify-content-center mb-md-0">
<li><a href="#" class="nav-link px-2 text-secondary">Home</a></li>
<li><a href="#" class="nav-link px-2 text-white">Features</a></li>
<li><a href="#" class="nav-link px-2 text-white">Pricing</a></li>
<li><a href="#" class="nav-link px-2 text-white">FAQs</a></li>
<li><a href="#" class="nav-link px-2 text-white">About</a></li>
</ul>
<form class="col-12 col-lg-auto mb-3 mb-lg-0 me-lg-3" role="search">
<input type="search" class="form-control form-control-dark text-bg-dark" placeholder="Search..."
aria-label="Search">
</form>
<div class="text-end">
<button type="button" class="btn btn-outline-light me-2">Login</button>
<button type="button" class="btn btn-warning">Sign-up</button>
</div>
</div>
</div>
</header>
</template>
<style scoped>
@import '/src/css/headers.css';
.my-header {
position: fixed;
top: 0;
left: 0;
z-index: 1;
width: 100%;
}
</style>
<script setup>
import Header from '/src/components/Header.vue'
import Footer from '/src/components/Footer.vue'
</script>
<template>
<Header />
<router-view />
<Footer />
</template>
<style scoped>
</style>
因為已經移到App.vue, 所以HomePage.vue刪除Header和Footer
<template>
<Banner />
<h1 class="title text-center mt-5 mb-3 fw-bold">熱門文章</h1>
<section class="articles">
<Card v-for="item in articles" :content="item" />
</section>
</template>
<script>
import Banner from '/src/components/Banner.vue'
import Card from '/src/components/Card.vue'
export default {
name: 'HomePage',
components: {
Banner,
Card,
},
data() {
return {
articles: [
{
id: 0,
imgUrl: 'https://picsum.photos/id/1011/800/450',
title: 'This is some title',
content: 'Curabitur convallis ac quam vitae laoreet. Nulla mauris ante, euismod sed lacus sit amet, congue bibendum eros. Etiam mattis lobortis porta. Vestibulum ultrices iaculis enim imperdiet egestas.',
},
{
id: 1,
imgUrl: 'https://picsum.photos/id/1005/800/450',
title: 'This is some title',
content: 'Curabitur convallis ac quam vitae laoreet. Nulla mauris ante, euismod sed lacus sit amet, congue bibendum eros. Etiam mattis lobortis porta. Vestibulum ultrices iaculis enim imperdiet egestas.',
},
{
id: 2,
imgUrl: 'https://picsum.photos/id/103/800/450',
title: 'This is some title',
content: 'Curabitur convallis ac quam vitae laoreet. Nulla mauris ante, euismod sed lacus sit amet, congue bibendum eros. Etiam mattis lobortis porta. Vestibulum ultrices iaculis enim imperdiet egestas.',
},
{
id: 3,
imgUrl: 'https://picsum.photos/id/103/800/450',
title: 'This is some title',
content: 'Curabitur convallis ac quam vitae laoreet. Nulla mauris ante, euismod sed lacus sit amet, congue bibendum eros. Etiam mattis lobortis porta. Vestibulum ultrices iaculis enim imperdiet egestas.',
},
]
}
}
}
</script>
<style scoped>
.articles {
display: grid;
max-width: 1200px;
margin-inline: auto;
padding-inline: 24px;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 24px;
}
.title {
}
</style>
// 引入vue-router
import {createRouter, createWebHistory} from 'vue-router'
import HelloWorld from './components/HelloWorld.vue'
import HomePage from '/src/pages/HomePage.vue'
import Article from '/src/pages/Article.vue'
// 定義路由
const routes = [
{
path: '/helloworld',
component: HelloWorld
},
// 首頁
{
path: '/',
component: HomePage
},
// 文章頁
{
path: '/article',
component: Article
},
];
// 建立VueRouter
const router = createRouter({
mode: 'history',
history: createWebHistory(),
routes, // short for `routes: routes`
});
export default {
router
}
完成後的文章畫面:
<template>
<article>
<div class="article-wrapper">
<figure>
<img :src="content.imgUrl" alt="" />
</figure>
<div class="article-body">
<h2>{{ content.title }}</h2>
<p>{{ content.content }}</p>
<a :href="'/article?id=' + content.id" class="read-more">
Read more <span class="sr-only">about this is some title</span>
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z"
clip-rule="evenodd" />
</svg>
</a>
</div>
</div>
</article>
</template>
<script>
export default {
name: 'Card',
props: ["content"],
}
</script>
<style scoped>
article {
--img-scale: 1.001;
--title-color: black;
--link-icon-translate: -20px;
--link-icon-opacity: 0;
position: relative;
border-radius: 16px;
box-shadow: none;
background: #fff;
transform-origin: center;
transition: all 0.4s ease-in-out;
overflow: hidden;
}
article a::after {
position: absolute;
inset-block: 0;
inset-inline: 0;
cursor: pointer;
content: "";
}
/* basic article elements styling */
article h2 {
margin: 0 0 18px 0;
font-family: "Bebas Neue", cursive;
font-size: 1.9rem;
letter-spacing: 0.06em;
color: var(--title-color);
transition: color 0.3s ease-out;
}
figure {
margin: 0;
padding: 0;
aspect-ratio: 16 / 9;
overflow: hidden;
}
article img {
max-width: 100%;
transform-origin: center;
transform: scale(var(--img-scale));
transition: transform 0.4s ease-in-out;
}
.article-body {
padding: 24px;
}
article a {
display: inline-flex;
align-items: center;
text-decoration: none;
color: #28666e;
}
article a:focus {
outline: 1px dotted #28666e;
}
article a .icon {
min-width: 24px;
width: 24px;
height: 24px;
margin-left: 5px;
transform: translateX(var(--link-icon-translate));
opacity: var(--link-icon-opacity);
transition: all 0.3s;
}
/* using the has() relational pseudo selector to update our custom properties */
article:has(:hover, :focus) {
--img-scale: 1.1;
--title-color: #28666e;
--link-icon-translate: 0;
--link-icon-opacity: 1;
box-shadow: rgba(0, 0, 0, 0.16) 0px 10px 36px 0px, rgba(0, 0, 0, 0.06) 0px 0px 0px 1px;
}
/************************
Generic layout (demo looks)
**************************/
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
padding: 48px 0;
font-family: "Figtree", sans-serif;
font-size: 1.2rem;
line-height: 1.6rem;
background-image: linear-gradient(45deg, #7c9885, #b5b682);
min-height: 100vh;
}
@media screen and (max-width: 960px) {
article {
container: card/inline-size;
}
.article-body p {
display: none;
}
}
@container card (min-width: 380px) {
.article-wrapper {
display: grid;
grid-template-columns: 100px 1fr;
gap: 16px;
}
.article-body {
padding-left: 0;
}
figure {
width: 100%;
height: 100%;
overflow: hidden;
}
figure img {
height: 100%;
aspect-ratio: 1;
object-fit: cover;
}
}
.sr-only:not(:focus):not(:active) {
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
}
</style>
<template>
<div class="w-100 mt-5">
<div class="bg-body-tertiary pt-3 px-3 pt-md-5 px-md-5 text-center overflow-hidden">
<div class="my-3 p-3">
<h2 class="display-5">{{ content.title }}</h2>
<p class="lead">{{ content.subtitle }}</p>
</div>
<div class="bg-dark shadow-sm mx-auto p-5" style="width: 80%; height: fit-content; border-radius: 21px 21px 21px 21px;">
<h5 class="text-white-50 text-start">{{ content.datetime }}</h5>
<h5 class="text-white-50 text-start">{{ content.author }}</h5>
<p class="text-white text-start text-break">{{ content.content }}</p>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Article',
data() {
return{
content: {
id: this.$route.query.id, // 把id從網址列上取下來
title: '文章標題',
subtitle: '文章副標題',
content: '文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容文章內容',
author: '作者',
datetime: '2023-08-01'
}
}
}
}
</script>
完成後點擊手頁的文章卡片會切換到內容頁,且網址上會有該篇文章的id。
or
By clicking below, you agree to our terms of service.
New to HackMD? Sign up