first commit

This commit is contained in:
Jeffrey Hsu 2024-10-01 23:17:57 +08:00
commit cc44132554
35 changed files with 3621 additions and 0 deletions

24
.gitignore vendored Normal file
View File

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

3
.vscode/extensions.json vendored Normal file
View File

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

5
README.md Normal file
View File

@ -0,0 +1,5 @@
# Vue 3 + TypeScript + Vite
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CANTYONION.SITE</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

3040
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

33
package.json Normal file
View File

@ -0,0 +1,33 @@
{
"name": "webindex",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.6.0",
"@fortawesome/free-brands-svg-icons": "^6.6.0",
"@fortawesome/free-regular-svg-icons": "^6.6.0",
"@fortawesome/free-solid-svg-icons": "^6.6.0",
"@fortawesome/vue-fontawesome": "^3.0.8",
"axios": "^1.7.7",
"lxgw-wenkai-lite-webfont": "^1.7.0",
"mitt": "^3.0.1",
"vue": "^3.4.37",
"vue-router": "^4.4.5"
},
"devDependencies": {
"@types/node": "^22.5.5",
"@vitejs/plugin-vue": "^5.1.2",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.47",
"tailwindcss": "^3.4.13",
"typescript": "^5.5.3",
"vite": "^5.4.1",
"vue-tsc": "^2.0.29"
}
}

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

1
public/vite.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

11
src/App.vue Normal file
View File

@ -0,0 +1,11 @@
<script lang="ts" setup>
</script>
<template>
<router-view/>
</template>
<style scoped>
</style>

8
src/api/blog.ts Normal file
View File

@ -0,0 +1,8 @@
import {request} from "@/utils/network";
export async function getBlogRecentPost(): Promise<IBlogResponse<IPostsData>> {
return request({
url: "/blog/index.php/api/posts",
method: "get"
})
}

BIN
src/assets/1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

BIN
src/assets/2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

BIN
src/assets/3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
src/assets/4.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

BIN
src/assets/5.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

BIN
src/assets/6.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

BIN
src/assets/7.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

View File

@ -0,0 +1,52 @@
<script lang="ts" setup>
import {computed} from "vue";
import {getAssetURL} from "@/utils/function.ts";
import emitter from "@/utils/mitt.ts";
const props = defineProps<{
post: IPost
}>()
const thumbUrl = computed(() => {
//
props.post.fields.thumb.value = props.post.fields.thumb.value ? props.post.fields.thumb.value : getAssetURL(
`${Math.floor(Math.random() * (7 - 1 + 1)) + 1}.jpg`
)
return props.post.fields.thumb.value
})
const handleOnCardClick = () => {
emitter.emit('openPost', props.post)
}
</script>
<template>
<div :title="post.title"
class="container relative overflow-hidden rounded-2xl hover:cursor-pointer text-white hover:text-amber-200 h-56"
@click="handleOnCardClick">
<!-- 图片 -->
<div class="container h-full">
<img :src="thumbUrl" alt="" class="object-cover h-full w-full object-center transition-all hover:scale-110">
</div>
<!-- 文字部分 -->
<div class="container relative">
<div class="absolute h-12 bottom-0 w-full backdrop-blur-3xl bg-black bg-opacity-60 leading-8 p-2 truncate">
<span class="text-xl font-bold transition-all px-1">{{ post.title }}</span>
</div>
</div>
<!-- 分类 -->
<div class="absolute top-0 left-0 backdrop-blur-3xl bg-black bg-opacity-60 rounded-lg m-2 px-2 text-white">
<span>{{ post.category }}</span>
</div>
<!-- 日期 -->
<div class="absolute bottom-12 right-0 backdrop-blur-3xl bg-black bg-opacity-60 rounded-lg m-2 px-2 text-white">
<font-awesome-icon :icon="['far', 'clock']" class="text-sm mr-1"/>
<span>{{ post.date.year }}-{{ post.date.month }}-{{ post.date.day }}</span>
</div>
</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,70 @@
<script lang="ts" setup>
import {computed, onMounted, onUnmounted, ref} from "vue";
import emitter from "@/utils/mitt.ts";
import {getAssetURL} from "@/utils/function.ts";
const isShow = ref(false)
const currentPost = ref<IPost | null>(null)
const onOpenPost = (e: IPost) => {
isShow.value = true
currentPost.value = e;
}
const onClosePost = () => {
isShow.value = false
currentPost.value = null
}
const thumbUrl = computed(() => {
if (currentPost.value && currentPost.value.fields.thumb.value) {
return currentPost.value.fields.thumb.value
}
return getAssetURL(`${Math.floor(Math.random() * (7 - 1 + 1)) + 1}.jpg`)
})
onMounted(() => {
emitter.on('openPost', onOpenPost)
})
onUnmounted(() => {
emitter.off('openPost', onOpenPost)
})
</script>
<template>
<div v-show="isShow" class="absolute top-0 left-0 right-0 bottom-0 bg-black bg-opacity-60">
<div class="container bg-white mx-auto my-10 rounded-3xl overflow-hidden max-h-full">
<!-- 标题栏 -->
<div class="w-full relative border-b-2 border-b-gray-200">
<!-- 文章头图 -->
<div class="min-h-52">
<img :src="thumbUrl" alt="" class="object-cover object-center w-full max-h-64"/>
</div>
<!-- 预览标题 -->
<div
class="h-12 pl-8 py-1.5 pr-16 truncate absolute top-0 left-0 right-0 bg-black bg-opacity-60 backdrop-blur-3xl text-white">
<span v-if="currentPost" :title="currentPost.title" class="text-3xl font-bold">
文章预览 /
</span>
</div>
<!-- 关闭按钮 -->
<div
class="absolute top-0 right-2 text-white hover:bg-gray-200 hover:text-black transition-all w-9 h-9 my-1.5 rounded-full flex justify-center items-center cursor-pointer"
@click="onClosePost"
>
<font-awesome-icon :icon="['fas', 'xmark']" class="p-2"/>
</div>
</div>
<!-- 正文 -->
<div class="container p-8">
<div v-if="currentPost" class="container" v-html="currentPost.digest"/>
</div>
</div>
</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,73 @@
<template>
<div class="loader">
<svg
class="icon"
height="200"
p-id="1166"
t="1676267704832"
version="1.1"
viewBox="0 0 1024 1024"
width="200"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M787.009641 293.979897c21.792821-0.183795 51.922051-0.459487 53.39241-0.722051a12.826256 12.826256 0 0 0 6.642872-3.610256l58.092308-57.737846a12.589949 12.589949 0 0 0 3.662769-6.603488c0.393846-2.376205-0.131282-4.424205-1.496615-5.802666l-67.872821-67.413334c-1.352205-1.378462-3.452718-1.890462-5.77641-1.522871a12.616205 12.616205 0 0 0-6.695385 3.610256l-58.092307 57.737846a12.826256 12.826256 0 0 0-3.675898 6.669128c-0.236308 1.575385 4.030359 35.997538 3.977846 57.803488-54.350769-40.539897-114.346667-68.096-184.595692-78.336V116.36841h25.915077c2.691282 0 10.660103-3.990974 12.524308-5.356307 2.021744-1.378462 8.388923-6.445949 8.388923-8.375795V7.286154c0-1.929846-6.419692-3.807179-8.310154-5.172513-1.929846-1.352205-9.885538-2.113641-12.603077-2.113641H419.052308c-2.743795 0-11.106462 0.774564-13.049436 2.139897-1.890462 1.352205-8.756513 3.21641-8.756513 5.146257v95.310769c0 1.969231 6.892308 7.036718 8.795897 8.402051 1.929846 1.339077 10.266256 5.369436 13.036308 5.369436h24.996103v81.657436C256.761436 227.511795 91.897436 400.108308 91.897436 608.518564 91.897436 837.618872 281.888821 1024 512.380718 1024 742.859487 1024 931.577436 837.632 931.577436 608.518564c-0.026256-125.702564-55.492923-238.316308-144.567795-314.538667z m-67.413333 146.116924L530.825846 627.698872a25.665641 25.665641 0 0 1-35.551179-0.590769 25.337436 25.337436 0 0 1-0.577641-35.341129l188.783589-187.602051a25.521231 25.521231 0 0 1 18.064411-7.443692 25.665641 25.665641 0 0 1 18.051282 7.443692 25.403077 25.403077 0 0 1 0 35.905641z"
fill="#ffffff"
p-id="1167"
></path>
</svg>
</div>
</template>
<script lang="ts" setup>
import {watch} from "vue";
const props = defineProps({
loading: {
type: Boolean,
default: false,
},
});
watch(
() => props.loading,
(v) => {
const el: HTMLElement = document.querySelector("body") as HTMLElement;
if (v) {
el.classList.add("loading");
} else {
el.classList.remove("loading");
}
}
);
</script>
<style scoped>
.loader::before {
top: 50%;
left: 50%;
z-index: 2;
content: "";
width: 100vmax;
height: 100vmax;
position: fixed;
border-radius: 66%;
background: var(--lime-soap);
transition: transform 0.5s cubic-bezier(0, 0, 0.5, 1.25);
transform: translate(-50%, -50%) scale(0);
}
.loader svg {
top: 50%;
left: 50%;
opacity: 0;
z-index: 3;
height: 8em;
color: white;
position: fixed;
visibility: hidden;
transform: translate(-50%, -50%) scale(0);
transition: opacity 0.3s, transform 0.5s cubic-bezier(0.5, 0, 0.5, 1.5),
visibility 0.3s;
}
</style>

12
src/main.ts Normal file
View File

@ -0,0 +1,12 @@
import {createApp} from 'vue'
import './style.css'
import App from './App.vue'
import router from "@/router";
import {library} from '@fortawesome/fontawesome-svg-core'
import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome'
import {faClock} from '@fortawesome/free-regular-svg-icons'
import {faXmark, faBlog} from '@fortawesome/free-solid-svg-icons'
library.add(faClock, faXmark, faBlog)
createApp(App).use(router).component('font-awesome-icon', FontAwesomeIcon).mount('#app')

13
src/router/index.ts Normal file
View File

@ -0,0 +1,13 @@
import {createRouter, createWebHashHistory} from "vue-router";
const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL),
routes: [
{
path: "/",
component: () => import('@/views/home/HomeView.vue')
},
]
});
export default router;

13
src/style.css Normal file
View File

@ -0,0 +1,13 @@
@import 'lxgw-wenkai-lite-webfont/style.css';
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
font-family: "LXGW WenKai Lite", sans-serif;
}
pre, code {
font-family: "LXGW WenKai Mono Lite", sans-serif;
}

59
src/types/blog.d.ts vendored Normal file
View File

@ -0,0 +1,59 @@
declare interface IPostsData {
page: number
pageSize: number
pages: number
count: number
dataSet: []
}
declare interface IPost {
cid: string
title: string
created: string
modified: string
slug: string
commentsNum: string
type: string
digest: string
password: string
categories: ICategory[]
category: string
directory: string[]
date: IDate
year: string
month: string
day: string
hidden: boolean
pathinfo: string
permalink: string
url: string
isMarkdown: boolean
feedUrl: string
feedRssUrl: string
feedAtomUrl: string
fields: any
}
declare interface ICategory {
mid: string
name: string
slug: string
type: string
description: string
count: string
order: string
parent: string
cid: string
directory: string[]
url: string
feedUrl: string
feedRssUrl: string
feedAtomUrl: string
}
declare interface IDate {
timestamp: number
year: string
month: string
day: string
}

5
src/types/index.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
declare interface IBlogResponse<T = null> {
status: string
message: string
data: T
}

5
src/utils/function.ts Normal file
View File

@ -0,0 +1,5 @@
export const getAssetURL = (image: string) => {
// 参数一: 相对路径
// 参数二: 当前路径的URL
return new URL(`../assets/${image}`, import.meta.url).href
}

8
src/utils/mitt.ts Normal file
View File

@ -0,0 +1,8 @@
import mitt from 'mitt'
type Events = {
openPost: IPost
}
const emitter = mitt<Events>()
export default emitter

23
src/utils/network.ts Normal file
View File

@ -0,0 +1,23 @@
import axios, {AxiosError, AxiosRequestConfig} from 'axios'
export async function request<T = unknown>(config: AxiosRequestConfig<any>): Promise<T> {
const instance = axios.create({
baseURL: "/api",
timeout: 5000,
headers: {},
})
instance.interceptors.request.use(config => {
return config
}, error => error)
// bug fixed on csdn https://blog.csdn.net/qq_45325810/article/details/120704910
instance.interceptors.response.use(resource => {
if (resource.status === 200) return resource
return Promise.reject(new Error(resource.data))
}, (error: AxiosError) => {
return Promise.reject(error.response ? error.response.data : error.code)
})
return instance.request<T>(config).then(res => res.data)
}

View File

@ -0,0 +1,41 @@
<script lang="ts" setup>
import {computed, onMounted, ref} from "vue";
import ArticleCard from "@/components/card/ArticleCard.vue";
import {getBlogRecentPost} from "@/api/blog.ts";
import ArticleDigestViewer from "@/components/card/ArticleDigestViewer.vue";
const recentPosts = ref<IPost[] | null>(null);
const hasPosts = computed(() => (recentPosts.value ?? []).length > 0);
onMounted(async () => {
const postData = await getBlogRecentPost();
recentPosts.value = postData.data.dataSet
})
</script>
<template>
<div class="container mx-auto">
<!-- 标题部分 -->
<div class="text-2xl mb-4 text-white font-bold">
<div class="bg-pink-500 w-max rounded-2xl overflow-hidden">
<div class="inline-block py-2 px-4 bg-pink-700 rounded-2xl">
<font-awesome-icon :icon="['fas', 'blog']"/>
</div>
<span class="px-4">最新博文</span>
</div>
</div>
<!-- 内容 -->
<div :class="{'justify-center': !hasPosts}" class="container flex">
<span v-if="!hasPosts" class="text-2xl font-bold">最近没有动态</span>
<div v-else class="grid grid-cols-4 gap-4 w-full">
<article-card v-for="item in recentPosts" :key="item.cid" :post="item"/>
</div>
</div>
</div>
<article-digest-viewer/>
</template>
<style scoped>
</style>

1
src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

12
tailwind.config.js Normal file
View File

@ -0,0 +1,12 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{vue,js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}

37
tsconfig.app.json Normal file
View File

@ -0,0 +1,37 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": [
"ES2020",
"DOM",
"DOM.Iterable"
],
"skipLibCheck": true,
"baseUrl": "./",
"paths": {
"@/*": [
"src/*"
]
},
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "preserve",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"src/**/**/*.vue"
]
}

7
tsconfig.json Normal file
View File

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

22
tsconfig.node.json Normal file
View File

@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}

24
vite.config.ts Normal file
View File

@ -0,0 +1,24 @@
import {defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
define: {
'process.env': {...process.env}
},
resolve: {
alias: {'@': path.resolve(__dirname, 'src')},
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'],
},
server: {
proxy: {
'/api': {
target: 'http://cantyonion.site',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
},
})