初步完成页面搭建

This commit is contained in:
LAPTOP-MI\Lau-mi 2024-01-26 17:21:24 +08:00
commit aefd2850e6
42 changed files with 7550 additions and 0 deletions

4
.env.development Normal file
View File

@ -0,0 +1,4 @@
NODE_ENV = development
VITE_APP_TITLE = DEV标题
VITE_BASE_URL = /
VITE_API_BASE_URL = /api

4
.env.production Normal file
View File

@ -0,0 +1,4 @@
NODE_ENV = production
VITE_APP_TITLE = PROD标题
VITE_BASE_URL = ./
VITE_API_BASE_URL = /api

15
.eslintrc.cjs Normal file
View File

@ -0,0 +1,15 @@
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
root: true,
extends: [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-prettier/skip-formatting'
],
parserOptions: {
ecmaVersion: 'latest'
},
rules: {}
}

30
.gitignore vendored Normal file
View File

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

8
.prettierrc.json Normal file
View File

@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"tabWidth": 2,
"singleQuote": true,
"printWidth": 100,
"trailingComma": "none"
}

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

@ -0,0 +1,8 @@
{
"recommendations": [
"Vue.volar",
"Vue.vscode-typescript-vue-plugin",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
]
}

44
README.md Normal file
View File

@ -0,0 +1,44 @@
# vue3-template
基于vite的vue3 项目基础模板
vue33.3.11
vue-router4.2.5
pinia2.1.7
element-plus2.4.4
store持久化存储pinia-plugin-persistedstate 3.2.1
https://prazdevs.github.io/pinia-plugin-persistedstate/guide/config.html
mitt3.0.1
## Customize configuration
vite配置 [Vite Configuration Reference](https://vitejs.dev/config/).
## 项目初始化
```sh
npm install
```
### 运行dev服务
```sh
npm run dev
```
### 构建项目
```sh
npm run build
```
### eslint检测 [ESLint](https://eslint.org/)
```sh
npm run lint
```

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="zh-hans">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

8
jsconfig.json Normal file
View File

@ -0,0 +1,8 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

6059
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

36
package.json Normal file
View File

@ -0,0 +1,36 @@
{
"name": "vue3-blog-frontend",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite --mode development",
"serve": "vite --mode development",
"build": "vite build --mode production",
"preview": "vite preview --mode development",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore",
"format": "prettier --write src/"
},
"dependencies": {
"axios": "~1.6.2",
"element-plus": "~2.4.4",
"mitt": "~3.0.1",
"pinia": "~2.1.7",
"pinia-plugin-persistedstate": "~3.2.1",
"qs": "~6.11.2",
"vue": "~3.3.11",
"vue-router": "~4.2.5"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.3.3",
"@vitejs/plugin-vue": "^4.5.2",
"@vue/eslint-config-prettier": "^8.0.0",
"eslint": "^8.49.0",
"eslint-plugin-vue": "^9.17.0",
"prettier": "^3.0.3",
"sass": "^1.69.5",
"unplugin-auto-import": "^0.17.2",
"unplugin-vue-components": "^0.26.0",
"vite": "^5.0.10"
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

45
public/json/pageList.json Normal file
View File

@ -0,0 +1,45 @@
{
"code": 200,
"msg": "成功",
"data": {
"total": 4,
"pageList": [
{
"id": 1,
"title": "测试123标题1231",
"author": "lau",
"createTime": "2024-01-21",
"category": "分类",
"abstract": "e是一种现代化的前端开发工具其工作原理主要分为以下几个步骤基于ESM构建Vite作为一款基于ESM的前端构建工具通过ES模块提供的动态导入功能来实现快速的开发和构建。零配置开发Vite允许开发者在不需要任何配置的情况下启动一个服务器进行开发通过对文件的即时编译和缓存来提高开源代码加载模块而不是打包后的文件从而可以避免打包的过程带来的性能损失按需编译和缓存Vite会按需编译和缓存",
"tags": ["标签1", "标签2123"]
},
{
"id": 22,
"title": "测试标题12321321",
"author": "lau",
"createTime": "2024-01-22",
"category": "分类",
"abstract": "e是一种现代化的前端开发工具其工作原理主要分为以下几个步骤基于ESM构建Vite作为一款基于ESM的前端构建工具通过ES模块提供的动态导入功能来实现快速的开发和构建。零配置开发Vite允许开发者在不需要任何配置的情况下启动一个服务器进行开发通过对文件的即时编译和缓存来提高开发效率。基于浏览器原生的ESM加载Vite将所有文件视为ES模块并且在开发时会直接从源代码加载模块而不是打包后的文件从而可以避免打包的过程带来的性能损失按需编译和缓存Vite会按需编译和缓存",
"tags": ["标签1", "标签2123"]
},
{
"id": 23,
"title": "测试标题123123323",
"author": "lau",
"createTime": "2024-01-24",
"category": "分类",
"abstract": "e是一种现代化的前端开发工具其工作原理主要分为以下几个步骤基于ESM构建Vite作为一款基于ESM的前端构建工具通过ES模块提供的动态导入功能来实现快速的开发和构建。零配置开发Vite允许开发者在不需要任何配置的情况下启动一个服务器进行开发通过对文件的即时编译和缓存来提高开发效率。基于浏览器原生的ESM加载Vite将所有文件视为ES模块并且在开发时会直接从源代码加载模块而不是打包后的文件从而可以避免打包的过程带来的性能损失按需编译和缓存Vite会按需编译和缓存",
"tags": ["标签1", "标签2123"]
},
{
"id": 44,
"title": "测试标题1234211234",
"author": "lau",
"createTime": "2024-01-24",
"category": "分类",
"abstract": "e是一种现代化的前端开发工具其工作原理主要分为以下几个步骤基于ESM构建Vite作为一款基于ESM的前端构建工具通过ES模块提供的动态导入功能来实现快速的开发和构建。零配置开发Vite允许开发者在不需要任何配置的情况下启动一个服务器进行开发通过对文件的即时编译和缓存来提高开发效率。基于浏览器原生的ESM加载Vite将所有文件视为ES模块并且在开发时会直接从源代码加载模块而不是打包后的文件从而可以避免打包的过程带来的性能损失按需编译和缓存Vite会按需编译和缓存",
"tags": ["标签1", "标签2123"]
}
]
}
}

7
src/App.vue Normal file
View File

@ -0,0 +1,7 @@
<script setup></script>
<template>
<RouterView />
</template>
<style scoped></style>

17
src/api/home.js Normal file
View File

@ -0,0 +1,17 @@
import http from '@/utils/http'
/**
* @function getPageList 获取文章列表
* @param {Number} currentPage 当前分页
* @param {Number} pageSize 每次获取数量
* */
export const getPageList = (currentPage = 1, pageSize = 10) => {
return http({
url: '/page-list',
method: 'post',
data: {
currentPage,
pageSize
}
})
}

15
src/api/login.js Normal file
View File

@ -0,0 +1,15 @@
import http from '@/utils/http'
/**
* @function login 登录接口
* @param {Object} data 登录数据
* @param {String} data.account 账号
* @param {String} data.password 密码
* */
export const login = (data) => {
return http({
url: '/login',
method: 'post',
data
})
}

5
src/assets/base.css Normal file
View File

@ -0,0 +1,5 @@
body {
font-family: Inter, 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
}

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

1
src/assets/logo.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 276 B

21
src/assets/reset.css Normal file
View File

@ -0,0 +1,21 @@
html,
body,
ul,
ol,
li,
img,
h1,
h2,
h3,
h4,
h5,
h6,
p,
div {
margin: 0;
padding: 0;
}
ul{
list-style: none;
}

View File

@ -0,0 +1,86 @@
<script setup>
import { computed } from 'vue'
const props = defineProps({
width: {
type: String,
default: 'auto'
},
height: {
type: String,
default: 'auto'
},
backgroundColor: {
type: String,
default: '#fff'
},
border: {
type: String,
default: '1px solid #ddd'
},
color: {
type: String,
default: '#777'
},
fontSize: {
type: String,
default: '14px'
},
fontWeight: {
type: String,
default: '400'
},
size: {
type: String,
default: 'normal'
},
cursor: {
type: String,
default: 'pointer'
}
})
const style = computed(() => {
const obj = {}
obj['width'] = props.width
obj['height'] = props.height
obj['color'] = props.color
obj['background-color'] = props.backgroundColor
obj['font-size'] = props.fontSize
obj['font-weight'] = props.fontWeight
obj['border'] = props.border
obj['cursor'] = props.cursor
switch (props.size) {
case 'normal':
obj['padding'] = '12px 20px'
break
case 'small':
obj['padding'] = '8px 14px'
break
case 'large':
obj['padding'] = '16px 24px'
break
}
return obj
})
</script>
<template>
<!--
宽高背景颜色字体颜色字体大小字体粗细边框大小和颜色大小
-->
<button class="tag-item" :style="style">
<span>
<slot name="default"></slot>
</span>
</button>
</template>
<style lang="scss" scoped>
.tag-item {
border-radius: 8px;
&:hover {
filter: brightness(0.9);
}
}
</style>

View File

@ -0,0 +1,45 @@
<script setup></script>
<template>
<div class="footer">
<div class="copyright">
<p>
Copyright © 2016-{{ new Date().getFullYear() }}
<a href="https://git.6yim.com" target="_blank">Lauym</a>. All Rights Reserved.
</p>
</div>
<div class="beian">
<a href="http://beian.miit.gov.cn/" target="_blank">蜀ICP备19012284号</a>
</div>
</div>
</template>
<style lang="scss" scoped>
.footer {
width: 100%;
padding: 24px 0;
background-color: #444;
color: #aaa;
display: flex;
flex-direction: column;
// justify-content: center;
align-items: center;
font-size: 14px;
> div {
a {
font-size: 12px;
color: #777;
text-decoration: none;
&:hover{
text-decoration: solid;
}
&:active{
}
}
}
> .copyright {
// text-align: center;
}
}
</style>

View File

@ -0,0 +1,146 @@
<script setup>
import { ref } from 'vue'
import menuIcon from '@/components/Icon/btn/menuIcon.vue'
const props = defineProps({
menuList: {
type: Array,
default: () => [
{ label: '所有文章', path: '/pages', menuId: 1 },
{ label: '分类', path: '/categories', menuId: 2 },
{ label: '标签', path: '/tags', menuId: 3 },
{ label: '关于我', path: '/about-me', menuId: 4 },
{ label: '杂记', path: '/', menuId: 5 }
]
}
})
const isMobileMenuShow = ref(false)
const handleShowMobileMenu = (value) => {
isMobileMenuShow.value = value
}
</script>
<template>
<div class="header">
<el-row>
<el-col :lg="{ span: 3, offset: 4 }">
<div class="logo-and-title">
<img src="@/assets/logo.png" alt="" />
<h1 @click="$router.push('/')">YM Blog</h1>
<div class="mobile-menu-btn hidden-lg-only">
<menuIcon @click="handleShowMobileMenu" :open="isMobileMenuShow"></menuIcon>
</div>
<ul class="mobile-menu hidden-lg-only" v-show="isMobileMenuShow">
<li
v-for="item in props.menuList"
:key="item.menuId"
@click="handleShowMobileMenu(false)"
>
{{ item.label }}
</li>
</ul>
</div>
</el-col>
<el-col :lg="{ span: 8, offset: 2 }" class="hidden-md-and-down">
<ul class="menu">
<li v-for="item in props.menuList" :key="item.path" @click="$router.push(item.path)">
{{ item.label }}
</li>
</ul>
</el-col>
</el-row>
</div>
</template>
<style lang="scss" scoped>
@import 'element-plus/theme-chalk/display.css';
$heaher-height: 60px;
.header {
height: $heaher-height;
background-color: #fafafa;
position: relative;
z-index: 100;
transition: all 0.2s ease-in-out;
&:hover {
box-shadow: 0 4px 10px 0 rgba($color: #777, $alpha: 0.5);
}
.logo-and-title {
height: $heaher-height;
padding: 0 0 0 10px;
display: flex;
justify-content: flex-start;
align-items: center;
img {
width: 40px;
// height: 30px;
}
h1 {
font-size: 24px;
font-weight: 600;
margin-left: 1rem;
display: flex;
align-items: center;
cursor: pointer;
&:hover {
color: #2376b7;
}
}
> .mobile-menu-btn {
position: absolute;
top: calc($heaher-height - 50px);
right: 10px;
z-index: 101;
}
> .mobile-menu {
width: 200px;
background-color: #fff;
// border: 1px solid red;
position: absolute;
top: $heaher-height;
right: 0;
z-index: 101;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
> li {
width: inherit;
height: 34px;
border-bottom: 1px solid #eee;
border-left: 1px solid #eee;
border-right: 1px solid #eee;
font-size: 16px;
display: flex;
justify-content: center;
align-items: center;
}
}
}
.menu {
height: $heaher-height;
display: flex;
justify-content: flex-start;
align-items: center;
> li {
min-width: 80px;
height: inherit;
padding: 0 12px 0 12px;
display: flex;
justify-content: center;
align-items: center;
&:hover {
color: #fff;
background-color: #2376b7;
cursor: pointer;
}
&:nth-child(n + 2) {
margin-left: 10px;
}
}
}
}
</style>

View File

@ -0,0 +1,66 @@
<script setup>
import { ref, watch } from 'vue'
const props = defineProps({
open: { type: Boolean, default: false }
})
const emits = defineEmits(['click'])
const active = ref(false)
watch(
() => props.open,
() => {
active.value = props.open
}
)
const onClick = () => {
active.value = !active.value
emits('click', active.value)
}
</script>
<template>
<div class="-menu-icon" :class="{ active: active }" @click="onClick">
<div></div>
<div></div>
<div></div>
</div>
</template>
<style lang="scss" scoped>
.-menu-icon {
width: 28px;
height: 28px;
position: relative;
> div {
width: inherit;
height: 4px;
background-color: #444;
position: absolute;
left: 0;
transition: all 0.2s linear;
&:nth-child(1) {
top: 8px;
}
&:nth-child(2) {
top: 16px;
}
&:nth-child(3) {
top: 24px;
}
}
}
.-menu-icon.active {
> div {
&:nth-child(1) {
transform: rotate(-45deg) translate(-4px, 7px);
}
&:nth-child(2) {
opacity: 0;
}
&:nth-child(3) {
transform: rotate(45deg) translate(-4px, -7px);
}
}
}
</style>

View File

@ -0,0 +1,105 @@
<script setup>
// import { ref } from 'vue'
const props = defineProps({
list: {
type: Array,
default: () => []
}
})
</script>
<template>
<ul class="page-list">
<li v-for="item in props.list" :key="item.id">
<h2>
<router-link :to="`/detail/${item.title}`">{{ item.title }}</router-link>
</h2>
<div class="info">
<span>作者: {{ item.author }}</span>
<span>发布于: {{ item.createTime }}</span>
<span class="category"
>分类:
<router-link :to="`/categories/${item.category}`"> {{ item.category }}</router-link></span
>
</div>
<div class="abstract">{{ item.abstract }}...</div>
<div class="read">
<router-link :to="`/detail/${item.title}`">阅读全文</router-link>
</div>
<div class="tags">
<span v-for="tag in item.tags" :key="tag">
<router-link :to="`/tags/${tag}`">{{ tag }}</router-link
>
</span>
</div>
</li>
</ul>
</template>
<style lang="scss" scoped>
.page-list {
> li {
margin-bottom: 40px;
padding: 10px 20px;
background-color: #fff;
box-shadow: 0 1px 6px 0 rgba(177, 177, 177, 0.3);
transition: all 0.2s ease-in-out;
&:hover {
box-shadow: 0 6px 12px 0 rgba(177, 177, 177, 0.7);
}
a {
color: #444;
text-decoration: none;
&:hover {
color: #2376b7;
}
}
> h2 {
padding: 10px 0;
font-size: 20px;
font-weight: 600;
}
> .info {
> span {
font-size: 14px;
color: #999;
margin-right: 10px;
}
> .category {
a {
color: #2376b7;
&:hover {
text-decoration: underline;
}
}
}
}
.abstract {
padding: 10px 0;
font-size: 14px;
color: #aaa;
line-height: 22px;
text-indent: 2em;
}
> .read {
text-indent: 2em;
> a {
color: #2376b7;
}
}
> .tags {
font-size: 14px;
display: flex;
justify-content: flex-end;
> span {
margin-left: 10px;
position: relative;
top: -20px;
}
}
}
}
</style>

View File

@ -0,0 +1,59 @@
<script setup>
const props = defineProps({
currentPage: {
type: Number,
default: 1
},
pageSize: {
type: Number,
default: 10
},
total: {
type: Number,
default: 0
},
layout: {
type: String,
default: 'prev, pager, next'
},
pagerCount: {
type: Number,
default: 5
}
})
const emits = defineEmits(['currentChange'])
const handleCurrentChange = (val) => {
console.log(val)
emits('currentChange', val)
}
</script>
<template>
<div class="custom-pagination">
<el-pagination
:current-page="props.currentPage"
:page-size="props.pageSize"
:total="props.total"
:layout="props.layout"
:pager-count="props.pagerCount"
@current-change="handleCurrentChange"
/>
</div>
</template>
<style lang="scss" scoped>
.custom-pagination {
:deep(.el-pagination) {
> .el-pager {
> .is-active {
color: #2376b7;
}
> li:hover {
color: #2376b7;
}
}
}
}
</style>

64
src/layout/LayoutView.vue Normal file
View File

@ -0,0 +1,64 @@
<script setup>
import { onMounted, ref } from 'vue'
import HeaderCom from '@/components/Header/HeaderCom.vue'
import FooterCom from '@/components/Footer/FooterCom.vue'
const minHeight = ref(200)
onMounted(() => {
minHeight.value = document.documentElement.clientHeight - 144 + 'px'
})
</script>
<template>
<slot name="header">
<HeaderCom />
</slot>
<div class="body">
<el-row>
<el-col :lg="{ span: 4 }" class="hidden-md-and-down">
<div class="left-side">
<slot name="left"></slot>
</div>
</el-col>
<el-col :lg="{ span: 12, offset: 1 }">
<div class="center">
<slot name="content"></slot>
</div>
</el-col>
<el-col :lg="{ span: 5, offset: 1 }">
<div class="right-side">
<slot name="right"></slot>
</div>
</el-col>
</el-row>
</div>
<slot name="footer">
<FooterCom />
</slot>
</template>
<style lang="scss" scoped>
@import 'element-plus/theme-chalk/display.css';
.body {
// margin-top: 20px;
.left-side {
width: 100%;
height: 500px;
// background-color: red;
}
.center {
width: 100%;
min-height: v-bind(minHeight);
padding: 20px 5px;
box-sizing: border-box;
// background-color:;
}
.right-side {
width: 100%;
height: 500px;
// background-color: blue;
}
}
</style>

26
src/main.js Normal file
View File

@ -0,0 +1,26 @@
import './assets/reset.css'
import { createApp } from 'vue'
import pinia from './stores/pinia'
import App from './App.vue'
import router from './router'
import 'element-plus/dist/index.css'
const app = createApp(App)
// 全局自定义指令
app.directive('permission', {
mounted(el, binding) {
const { value } = binding
if (value.includes(1)) {
el['hidden'] = true
}
}
})
app.use(pinia)
app.use(router)
app.mount('#app')

17
src/router/index.js Normal file
View File

@ -0,0 +1,17 @@
import { createRouter, createWebHistory } from 'vue-router'
import routes from './routes'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
scrollBehavior: () => ({ left: 0, top: 0, behavior: 'smooth' })
})
// 路由守卫
router.beforeEach(() => {})
router.afterEach((to) => {
document.title = import.meta.env.VITE_APP_TITLE + ` | ${to.meta.title || ''}`
})
export default router

84
src/router/routes.js Normal file
View File

@ -0,0 +1,84 @@
import HomePage from '@/views/HomePage/HomeView.vue'
const routes = [
{
path: '/',
name: 'index',
meta: {
title: '首页'
},
component: HomePage
},
{
path: '/detail/:title',
name: 'detail',
meta: {
title: '详情',
keepAlive: true
},
component: () => import('@/views/DetailPage/DetailView.vue')
},
{
path: '/pages',
name: 'pages',
meta: {
title: '所有内容',
keepAlive: true
},
component: () => import('@/views/ListPage/ListView.vue')
},
{
path: '/categories',
name: 'categories',
meta: {
title: '分类列表',
keepAlive: true
},
component: () => import('@/views/CategoriesPage/CategoriesView.vue')
},
{
path: '/categories/:categoryName',
name: 'category',
meta: {
title: '分类',
keepAlive: true
},
component: () => import('@/views/ListPage/ListView.vue')
},
{
path: '/tags',
name: 'tags',
meta: {
title: '标签列表',
keepAlive: true
},
component: () => import('@/views/TagsPage/TagsView.vue')
},
{
path: '/tags/:tagName',
name: 'tag',
meta: {
title: '标签',
keepAlive: true
},
component: () => import('@/views/ListPage/ListView.vue')
},
{
path: '/about-me',
name: 'about',
meta: {
title: '关于我',
keepAlive: true
},
component: () => import('@/views/AboutPage/AboutMe.vue')
},
{
path: '/:pathMatch(.*)*',
name: '404',
meta: {
title: '404 Not Found'
},
component: () => import('@/views/404.vue')
}
]
export default routes

7
src/stores/pinia.js Normal file
View File

@ -0,0 +1,7 @@
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const store = createPinia()
store.use(piniaPluginPersistedstate)
export default store

29
src/stores/user.js Normal file
View File

@ -0,0 +1,29 @@
import { defineStore } from 'pinia'
import { ref, reactive } from 'vue'
const useUser = defineStore(
'user',
() => {
const name = ref('张三')
const age = ref(18)
const loginInfo = reactive({
token: '123',
lastLoginTime: ''
})
const modifyLoginInfo = (data) => {
loginInfo.token = data.token
loginInfo.lastLoginTime = data.lastLoginTime
}
return { name, age, loginInfo, modifyLoginInfo }
},
{
// 持久化存储
persist: {
storage: window.localStorage
}
}
)
export default useUser

61
src/utils/http.js Normal file
View File

@ -0,0 +1,61 @@
import axios from 'axios'
import router from '@/router'
import { ElMessage, ElNotification } from 'element-plus'
const API_URL = import.meta.env.VITE_API_BASE_URL
// console.log(API_URL)
const http = axios.create({
baseURL: API_URL,
timeout: 30 * 1000,
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
// 请求不携带cookie
withCredentials: false
})
// 请求拦截
http.interceptors.request.use(
(config) => {
config.headers.Authorization = `Bearer ${localStorage.getItem('token') || 'NoToken'}`
return config
},
(error) => {
return Promise.reject(error)
}
)
// 响应拦截
http.interceptors.response.use(
(res) => {
if (res.status === 200) {
const code = res.data.code || 0
// 根据返回的自定义code来做不同的处理
if (code === 401) {
ElMessage({
type: 'warning',
message: '登录已过期 或 未登录,请重新登录',
duration: 2000
})
router.push({ path: '/login' })
}
} else {
ElNotification({
title: '错误',
message: res.data.message,
type: 'error',
duration: 2000
})
}
return res.data
},
(error) => {
return Promise.reject(error)
}
)
export default http

23
src/utils/ls.js Normal file
View File

@ -0,0 +1,23 @@
const getItem = (itemName) => {
return window.localStorage.getItem(itemName)
}
const setItem = (itemName, itemValue) => {
return window.localStorage.setItem(itemName, itemValue)
}
const removeItem = (itemName) => {
return window.localStorage.removeItem(itemName)
}
const clear = () => {
return window.localStorage.clear()
}
export const getToken = () => getItem('token')
export const setToken = (token) => setItem('token', token)
export const clearToken = () => removeItem('token')
export const clearAll = () => clear()

33
src/views/404.vue Normal file
View File

@ -0,0 +1,33 @@
<script setup>
import LayoutView from '@/layout/LayoutView.vue'
</script>
<template>
<LayoutView>
<template #content>
<div class="not-found">
<el-card class="box-card">
<div class="text-center">
<h1>404</h1>
<p>抱歉访问的页面不存在</p>
<p>
<el-button type="" @click="$router.push('/')">返回首页</el-button>
</p>
</div>
</el-card>
</div>
</template>
</LayoutView>
</template>
<style lang="scss" scoped>
.not-found {
text-align: center;
h1 {
margin: 40px;
}
p {
margin: 20px 0;
}
}
</style>

View File

@ -0,0 +1,13 @@
<script setup>
import LayoutView from '@/layout/LayoutView.vue'
</script>
<template>
<LayoutView>
<template #content>
about me
</template>
</LayoutView>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,95 @@
<script setup>
import LayoutView from '@/layout/LayoutView.vue'
</script>
<template>
<LayoutView>
<template #content>
<div class="categories">
<div class="title">所有分类<span>4</span></div>
<div class="category-items">
<el-row>
<el-col :lg="{ span: 8 }" :md="{ span: 12 }">
<div class="category-title">Vue <span>12</span></div>
<ul>
<li>
<router-link :to="`/detail/test似懂非懂`"
>te吃啥地啥的神盾局t似懂非懂</router-link
>
</li>
<li>
<router-link :to="`/detail/test似懂非懂`">test似懂非懂</router-link>
</li>
<li>
<router-link :to="`/detail/test似懂非懂`">test似懂非懂</router-link>
</li>
</ul>
</el-col>
<el-col :lg="{ span: 8 }" :md="{ span: 12 }">
<div class="category-title">Vue <span>12</span></div>
<ul>
<li>
<router-link :to="`/detail/test似懂非懂`"
>te吃啥地啥的神盾局t似懂非懂</router-link
>
</li>
<li>
<router-link :to="`/detail/test似懂非懂`">test似懂非懂</router-link>
</li>
<li>
<router-link :to="`/detail/test似懂非懂`">test似懂非懂</router-link>
</li>
</ul>
</el-col>
</el-row>
</div>
</div>
</template>
</LayoutView>
</template>
<style lang="scss" scoped>
.categories {
> .title {
font-size: 24px;
font-weight: 600;
display: flex;
justify-content: flex-end;
> span {
text-indent: 6px;
font-size: 16px;
position: relative;
top: -2px;
}
}
> .category-items {
padding: 20px 0;
.category-title {
font-size: 20px;
font-weight: 600;
> span {
font-size: 14px;
position: relative;
top: -6px;
left: 4px;
}
}
ul {
padding: 10px 14px;
> li {
margin: 0 0 8px 0;
font-size: 16px;
>a{
color: #444;
text-decoration: none;
&:hover{
color: #2376b7;
}
}
}
}
}
}
</style>

View File

@ -0,0 +1,30 @@
<script setup>
import LayoutView from '@/layout/LayoutView.vue'
</script>
<template>
<LayoutView>
<template #content>
<div class="detail-page">
<h2 class="title">title</h2>
<div class="info">
<div class="author">
<span>lau</span>发布在
<span>
<router-link to="/categories/vue">detail</router-link>
</span>
</div>
<div class="time">发布时间<span>2023-12-21</span></div>
</div>
<div class="detail-content">内容</div>
</div>
</template>
</LayoutView>
</template>
<style lang="scss" scoped>
.detail-page {
padding: 20px;
}
</style>

View File

@ -0,0 +1,54 @@
<script setup>
import { ref, onMounted } from 'vue'
import LayoutView from '@/layout/LayoutView.vue'
import PageList from '@/components/List/PageList.vue'
import PaginationCom from '@/components/List/PaginationCom.vue'
const pageList = ref([])
const currentPage = ref(1)
onMounted(() => {
fetch('/json/pageList.json')
.then((res) => {
return res.json()
})
.then((res) => {
pageList.value.splice(0, pageList.value.length)
res.data.pageList.forEach((item) => {
pageList.value.push(item)
})
})
})
const handleCurrentPageChange = (val) => {
currentPage.value = val
}
</script>
<template>
<LayoutView>
<template #content>
<div class="page-list">
<PageList :list="pageList"></PageList>
</div>
<div class="pagination">
<PaginationCom
:currentPage="currentPage"
:total="14"
@currentChange="handleCurrentPageChange"
/>
</div>
</template>
</LayoutView>
</template>
<style lang="scss" scoped>
.page-list {
}
.pagination {
padding: 10px 20px 50px 0;
display: flex;
justify-content: flex-end;
}
</style>

View File

@ -0,0 +1,101 @@
<script setup>
import LayoutView from '@/layout/LayoutView.vue'
const props = defineProps({
list: {
type: Array,
default: () => {
return [
{
title: 'Vue 3',
description: 'Vue 3 is a progressive framework for building user interfaces.',
url: 'https://v3.vuejs.org/',
createTime: '2023-12-21'
},
{
title: 'Vue1大山',
description: 'Vue 3 is a progressive framework for building user interfaces.',
url: 'https://v3.vuejs.org/',
createTime: '2023-12-21'
},
{
title: 'Vue saDv',
description: 'Vue 3 is a progressive framework for building user interfaces.',
url: 'https://v3.vuejs.org/',
createTime: '2023-12-21'
},
{
title: '123123十多个十多个',
description: 'Vue 3 is a progressive framework for building user interfaces.',
url: 'https://v3.vuejs.org/',
createTime: '2023-12-21'
}
]
}
},
total: {
type: Number,
default: 0
},
type: {
type: String,
default: '列表'
}
})
</script>
<template>
<LayoutView>
<template #content>
<div class="page-list">
<div class="title">{{ props.type }}</div>
<div class="total">{{ props.total }} </div>
<ul>
<li v-for="item in props.list" :key="item.title">
<router-link :to="`/detail/${item.title}`">{{ item.title }}</router-link>
<span>{{ item.createTime }}</span>
</li>
</ul>
</div>
</template>
</LayoutView>
</template>
<style lang="scss" scoped>
.page-list {
> .title {
font-size: 20px;
font-weight: 600;
color: #444;
display: flex;
justify-content: flex-end;
}
>.total{
padding: 10px 0;
font-size: 18px;
color: #666;
}
> ul {
padding: 20px 40px;
> li {
height: 26px;
display: flex;
justify-content: space-between;
align-items: center;
&:hover {
background-color: #f5f5f5;
}
> a {
color: #444;
text-decoration: none;
&:hover {
color: #2376b7;
}
}
>span{
color: #999;
}
}
}
}
</style>

View File

@ -0,0 +1,30 @@
<script setup>
import LayoutView from '@/layout/LayoutView.vue'
import TagCom from '@/components/Button/TagCom.vue'
</script>
<template>
<LayoutView>
<template #content>
<div class="tags">
<ul>
<li><TagCom @click="$router.push('/detail/title')">测试阿斯达</TagCom></li>
</ul>
</div>
</template>
</LayoutView>
</template>
<style lang="scss" scoped>
.tags {
> ul {
padding: 20px 40px;
display: flex;
justify-content: flex-start;
flex-wrap: wrap;
> li {
margin: 20px 0 0 20px;
}
}
}
</style>

36
vite.config.js Normal file
View File

@ -0,0 +1,36 @@
import { defineConfig, loadEnv } from 'vite'
import { fileURLToPath, URL } from 'node:url'
import process from 'node:process'
import vue from '@vitejs/plugin-vue'
// 按需导入 Element-plus 模块
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
console.log(mode)
return {
server: {
port: 3002,
proxy: {}
},
base: loadEnv(mode, process.cwd()).VITE_BASE_URL,
plugins: [
vue(),
// Element-plus 按需导入模块
AutoImport({
resolvers: [ElementPlusResolver()]
}),
Components({
resolvers: [ElementPlusResolver()]
})
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
}
})