一、使用 Vite 创建 Vue 3 + TypeScript 项目
PS E:\web\cursor-project\web> npm create vite@latest yf-blog -- --template vue-ts
> npx
> create-vite yf-blog --template vue-ts
Scaffolding project in E:\web\cursor-project\web\yf-blog...
Done. Now run:
cd yf-blog
npm install
npm run dev
PS E:\web\cursor-project\web> cd yf-blog
PS E:\web\cursor-project\web\yf-blog> npm install
added 47 packages in 7s
5 packages are looking for funding
run `npm fund` for details
PS E:\web\cursor-project\web\yf-blog> npm run dev
> yf-blog@0.0.0 dev
> vite
VITE v6.0.3 ready in 594 ms
➜ Local: http://localhost:5173/
➜ Network: use --host to expose
➜ press h + enter to show help
二、安装生产必要依赖
PS E:\web\cursor-project\web\yf-blog> npm install vue-router@4 pinia element-plus @element-plus/icons-vue axios marked highlight.js
added 16 packages in 4s
9 packages are looking for funding
run `npm fund` for details
三、安装开发依赖
PS E:\web\cursor-project\web\yf-blog> npm install -D sass sass-loader mockjs @types/mockjs vite-plugin-mock cross-env unplugin-auto-import unplugin-vue-components
added 87 packages in 8s
26 packages are looking for funding
run `npm fund` for details
四、配置别名
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
// https://vite.dev/config/
export default defineConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
}
},
plugins: [vue()],
})
在src下的vite-env.d.ts文件增加模块定义,否则别名引用会报错找不到模块
npm i @types/node --D
declare module "*.vue" {
import type { DefineComponent } from "vue";
const component: DefineComponent<typeof DefineComponent>;
export default component;
}
在tsconfig.app.json添加
{
"compilerOptions": {
"paths": {
"@": ["./src"]
}
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
}
五、依赖的功能和使用方法
1. vue-router@4
功能: Vue.js的官方路由管理器
安装: npm install vue-router@4
使用步骤:
1、新建src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue')
},
{
path: '/article/:id',
name: 'Article',
component: () => import('@/views/Article.vue')
}
]
})
export default router
2、main.ts引入router
import router from '@/router/index'
const app = createApp(App)
app.use(router)
app.mount('#app')
3、在组件中使用:
<script setup lang="ts">
const router = useRouter()
const route = useRoute()
// 编程式导航
const goToArticle = (id: number) => {
router.push(`/article/${id}`)
}
// 获取路由参数
const articleId = route.params.id
</script>
2. element-plus
功能: 基于 Vue 3的UI组件库
安装: npm install element-plus
使用步骤:
1、main.ts引入 element-plus
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from '@/router/index'
import ElementPlus from 'element-plus'
// 样式导入
import 'element-plus/dist/index.css'
// 创建应用实例
const app = createApp(App)
app.use(router)
app.use(ElementPlus, {
size: 'default',
zIndex: 3000
})
app.mount('#app')
2、路由layout组件化处理
3、新建layout模块
3.1、src/layout/index.vue内容如下
<template>
<el-container class="common-layout">
<el-aside class="aside">
<Aside/>
</el-aside>
<el-container>
<el-header class="header">
<Header/>
</el-header>
<el-main>
<router-view></router-view>
</el-main>
<el-footer class="footer">Footer</el-footer>
</el-container>
</el-container>
</template>
<script setup lang="ts">
import Aside from './components/aside.vue'
import Header from './components/header.vue'
</script>
<style lang="scss" scoped>
.common-layout{
width: 100%;
height: 100vh;
.aside{
height: 100vh;
width: 200px;
background-color: #ccc;
}
.header{
height: 50px;
background-color: #c9c1c1;
border-bottom: 1px solid #c9c6c6;
}
.footer{
height: 50px;
background-color: #c9c1c1;
}
}
</style>
3.2、aside.vue、header.vue、home.vue内容相似如下
<template>
<div class="home">
<span>侧边/头部/博客首页</span>
</div>
</template>
<script setup lang="ts">
</script>
<style lang="scss" scoped>
</style>
最终页面效果如下
3. element-plus/icons-vue
功能: Element Plus的图标库
安装: npm install @element-plus/icons-vue
使用步骤:
1、main.ts引入 element-plus/icons-vue
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from '@/router/index'
import ElementPlus from 'element-plus'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
// 样式导入
import 'element-plus/dist/index.css'
// 创建应用实例
const app = createApp(App)
// 注册 Element Plus 图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.use(router)
app.use(ElementPlus, {
size: 'default',
zIndex: 3000
})
app.mount('#app')
2、在Home.vue组件中应用
<template>
<div class="home">
<h1>博客首页</h1>
<el-icon :size="50"><House /></el-icon>
</div>
</template>
<script setup lang="ts">
</script>
<style lang="scss" scoped>
</style>
4. unplugin-auto-import
功能: 自动导入API插件
安装: npm install -D unplugin-auto-import
使用步骤:
1、在vite.config.ts文件中添加配置
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
import AutoImport from 'unplugin-auto-import/vite'
// https://vite.dev/config/
export default defineConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
}
},
plugins: [
vue(),
AutoImport({
imports:["vue","vue-router"],
dts:'src/auto-import.d.ts', // 路径下自动生成文件夹存放全局指令
eslintrc: {
enabled: true, // 1、改为true用于生成eslint配置。2、生成后改回false,避免重复生成消耗
}
})
],
})
这里ref正常使用,并没有报错,说明已经自动导入完成
5. unplugin-auto-import
功能: 在Vue文件中自动引入组件
安装: npm install -D unplugin-vue-components
使用步骤:
1、在vite.config.ts文件中添加配置
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
// https://vite.dev/config/
export default defineConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
}
},
plugins: [
vue(),
AutoImport({
imports:["vue","vue-router"],
dts:'src/auto-import.d.ts', // 路径下自动生成文件夹存放全局指令
eslintrc: {
enabled: false, // 1、改为true用于生成eslint配置。2、生成后改回false,避免重复生成消耗
},
resolvers: [ElementPlusResolver()]
}),
Components({
dirs: ['src/components'], // 配置需要默认导入的自定义组件文件夹,该文件夹下的所有组件都会自动 import
resolvers: [ElementPlusResolver()],
}),
],
})
1、在home.vue组件中使用
<template>
<div class="home">
<h1>{{ title }}</h1>
<!-- <el-icon :size="50"><House /></el-icon> -->
<el-button type="primary">点击</el-button>
</div>
</template>
<script setup lang="ts">
const title = ref('首页')
</script>
<style lang="scss" scoped>
</style>
这里直接使用,样式效果都正常显示,证明引入成功。
6. Pinia
功能: Vue 3的状态管理库
安装: npm install pinia pinia-plugin-persistedstate
使用步骤:
1、创建src/stores/index.ts
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
export default pinia
2、main.ts引入 Pinia
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from '@/router/index'
import ElementPlus from 'element-plus'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import store from './stores/index'
// 样式导入
import 'element-plus/dist/index.css'
// 创建应用实例
const app = createApp(App)
// 注册 Element Plus 图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.use(router)
app.use(ElementPlus, {
size: 'default',
zIndex: 3000
})
app.use(store)
app.mount('#app')
3、创建src/stores/article.ts
import { defineStore } from 'pinia'
interface Article {
id: number
title: string
content: string
}
export const useArticleStore = defineStore('article', {
state: () => ({
articles: [] as Article[],
currentArticle:null as Article | null
}),
actions: {
fetchArticles() {
this.articles = [{
id: 1,
title: '文章1',
content: '文章内容1'
}]
}
},
getters: {
getArticleById: (state) => (id: number) => {
return state.articles.find(article => article.id === id)
}
},
persist: true,//持久化存储
})
4、在组件中应用
<template>
<div class="home">
<h1>{{ title }}</h1>
<!-- <el-icon :size="50"><House /></el-icon> -->
<!-- <el-button type="primary">点击</el-button> -->
<div v-for="article in articles" :key="article.id">
<h2>{{ article.title }}</h2>
<p>{{ article.content }}</p>
</div>
</div>
</template>
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useArticleStore } from '@/stores/article'
const articleStore = useArticleStore()
const { articles } = storeToRefs(articleStore)
const title = ref('首页')
onMounted(() => {
articleStore.fetchArticles()
})
</script>
<style lang="scss" scoped>
</style>
7. axios
功能: 基于Promise的HTTP客户端
安装: npm install axios
使用步骤:
开发前提需要配置代理
server: {
host: '0.0.0.0',
port: port,
open: true,
proxy: {
[VITE_APP_BASE_API]: {
target: VITE_SERVE,
changeOrigin: true,
rewrite: path => path.replace(RegExp(`^${VITE_APP_BASE_API}`), '')
}
},
disableHostCheck: true
},
1、创建src/utils/request.ts
import axios, { AxiosRequestConfig } from 'axios'
import { ElNotification , ElMessageBox, ElMessage, ElLoading, LoadingParentElement } from 'element-plus'
import { getToken } from '@/utils/auth'
import { tansParams, blobValidate } from '@/utils/mis'
import cache from '@/plugins/cache'
import { saveAs } from 'file-saver'
import useUserStore from '@/stores/modules/user'
import { ComponentOptionsBase, ComponentProvideOptions } from 'vue'
var downloadLoadingInstance: { close: any; setText?: (text: string) => void; removeElLoadingChild?: () => void; handleAfterLeave?: () => void; vm?: globalThis.ComponentPublicInstance<{}, {}, {}, {}, {}, {}, {}, {}, false, ComponentOptionsBase<any, any, any, any, any, any, any, any, any, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions>, {}, {}, "", {}, any>; $el?: HTMLElement; originalPosition?: globalThis.Ref<string, string>; originalOverflow?: globalThis.Ref<string, string>; visible?: globalThis.Ref<boolean, boolean>; parent?: globalThis.Ref<LoadingParentElement, LoadingParentElement>; background?: globalThis.Ref<string, string>; svg?: globalThis.Ref<string, string>; svgViewBox?: globalThis.Ref<string, string>; spinner?: globalThis.Ref<string | boolean, string | boolean>; text?: globalThis.Ref<string, string>; fullscreen?: globalThis.Ref<boolean, boolean>; lock?: globalThis.Ref<boolean, boolean>; customClass?: globalThis.Ref<string, string>; target?: globalThis.Ref<HTMLElement, HTMLElement>; beforeClose?: globalThis.Ref<(() => boolean) | undefined, (() => boolean) | undefined> | undefined; closed?: globalThis.Ref<(() => void) | undefined, (() => void) | undefined> | undefined };
let errorCode = {
'401': '登录状态已过期,您可以继续留在该页面,或者重新登录',
'403': '当前操作没有权限',
'404': '请求的资源不存在',
'500': '服务器错误',
'601': '请求参数错误',
'default': '请求失败,请稍后再试'
}
// 是否显示重新登录
export let isRelogin = { show: false };
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
// 创建axios实例
const service = axios.create({
// axios中请求配置有baseURL选项,表示请求URL公共部分
baseURL: import.meta.env.VITE_APP_BASE_API,
// 超时
timeout: 60000
})
// request拦截器
service.interceptors.request.use(config => {
// 是否需要设置 token
const isToken = (config.headers || {}).isToken === false
// 是否需要防止数据重复提交
const isRepeatSubmit = (config.headers || {}).repeatSubmit === false
if (getToken() && !isToken) {
config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
}
if(config.headers){
config.headers['Content-Type'] = config.headers['Content-Type']
}
// get请求映射params参数
if (config.method === 'get' && config.params) {
let url = config.url + '?' + tansParams(config.params);
url = url.slice(0, -1);
config.params = {};
config.url = url;
}
if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put'|| config.method === 'delete')) {
const requestObj = {
url: config.url,
data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data,
time: new Date().getTime()
}
const requestSize = Object.keys(JSON.stringify(requestObj)).length; // 请求数据大小
const limitSize = 5 * 1024 * 1024; // 限制存放数据5M
if (requestSize >= limitSize) {
console.warn(`[${config.url}]: ` + '请求数据大小超出允许的5M限制,无法进行防重复提交验证。')
return config;
}
const sessionObj = cache.session.getJSON('sessionObj')
if (sessionObj === undefined || sessionObj === null || sessionObj === '') {
cache.session.setJSON('sessionObj', requestObj as any)
} else {
const s_url = sessionObj.url; // 请求地址
const s_data = sessionObj.data; // 请求数据
const s_time = sessionObj.time; // 请求时间
const interval = 100; // 间隔时间(ms),小于此时间视为重复提交
if (s_data === requestObj.data && s_url === requestObj.url&& requestObj.time - s_time < interval) {
const message = '数据正在处理,请勿重复提交';
console.warn(`[${s_url}]: ` + message)
return Promise.reject(new Error(message))
} else {
cache.session.setJSON('sessionObj', requestObj as any)
}
}
}
return config
}, error => {
console.log(error)
Promise.reject(error)
})
// 响应拦截器
service.interceptors.response.use(res => {
// 未设置状态码则默认成功状态
const code = res.data.code || 200;
// 获取错误信息
const msg = errorCode[code as keyof typeof errorCode] || res.data.msg || errorCode['default']
// 二进制数据则直接返回
if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') {
return res.data
}
if (code === 401) {
if (!isRelogin.show) {
isRelogin.show = true;
ElMessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' }).then(() => {
isRelogin.show = false;
useUserStore().logOut().then(() => {
location.href = '/index';
})
}).catch(() => {
isRelogin.show = false;
});
}
return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
} else if (code === 500) {
ElMessage({ message: msg, type: 'error' })
return Promise.reject(new Error(msg))
} else if (code === 601) {
ElMessage({ message: msg, type: 'warning' })
return Promise.reject(new Error(msg))
} else if (code !== 200&& code !== 400) {
ElNotification.error({ title: msg })
return Promise.reject('error')
} else {
return Promise.resolve(res.data)
}
},
error => {
console.log('err' + error)
let { message } = error;
if (message == "Network Error") {
message = "后端接口连接异常";
} else if (message.includes("timeout")) {
message = "系统接口请求超时";
} else if (message.includes("Request failed with status code")) {
message = "系统接口" + message.substr(message.length - 3) + "异常";
}
ElMessage({ message: message, type: 'error', duration: 5 * 1000 })
return Promise.reject(error)
}
)
// 通用下载方法
export function download(url: string, params: any, filename: string | undefined, config: AxiosRequestConfig<any> | undefined) {
downloadLoadingInstance = ElLoading.service({ text: "正在下载数据,请稍候", background: "rgba(0, 0, 0, 0.7)", })
return service.post(url, params, {
transformRequest: [(params) => { return tansParams(params) }],
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
responseType: 'blob',
...config
}).then(async (data:any) => {
const isBlob = blobValidate(data);
if (isBlob) {
const blob = new Blob([data])
saveAs(blob, filename)
} else {
const resText = await data.text();
const rspObj = JSON.parse(resText);
const errMsg = errorCode[rspObj.code as keyof typeof errorCode] || rspObj.msg || errorCode['default']
ElMessage.error(errMsg);
}
downloadLoadingInstance.close();
}).catch((r) => {
console.error(r)
ElMessage.error('下载文件出现错误,请联系管理员!')
downloadLoadingInstance.close();
})
}
export default service
2、创建src/utils/auth.ts
import Cookies from 'js-cookie'
const TokenKey = 'Admin-Token'
export function getToken() {
return Cookies.get(TokenKey)
}
export function setToken(token:any) {
return Cookies.set(TokenKey, token,{ expires: 1 })
}
export function removeToken() {
return Cookies.remove(TokenKey)
}
3、创建src/utils/mis.ts
/**
* 通用js方法封装处理
* Copyright (c) 2019 ruoyi
*/
// 日期格式化
// 日期格式化
// 日期格式化
export function parseTime(time: string | number | Date, pattern: string) {
if (arguments.length === 0 || !time) {
return null
}
const format = pattern || '{y}-{m}-{d} {h}:{i}:{s}'
let date
if (typeof time === 'object') {
date = time
} else {
if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) {
time = parseInt(time)
} else if (typeof time === 'string') {
time = time.replace(new RegExp(/-/gm), '/').replace('T', ' ').replace(new RegExp(/\.[\d]{3}/gm), '');
}
if ((typeof time === 'number') && (time.toString().length === 10)) {
time = time * 1000
}
date = new Date(time)
}
const formatObj = {
y: date.getFullYear(),
m: date.getMonth() + 1,
d: date.getDate(),
h: date.getHours(),
i: date.getMinutes(),
s: date.getSeconds(),
a: date.getDay()
}
const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result: string, key: string) => {
let value = key in formatObj ? formatObj[key as keyof typeof formatObj] : 0;
// Note: getDay() returns 0 on Sunday
if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value] }
if (result.length > 0 && value < 10) {
return '0' + value.toString(); // 将 value 转换为字符串并返回
}
return value.toString(); // 确保返回值是字符串
});
return time_str
}
// 添加日期范围
export function addDateRange(params: any, dateRange: any[], propName: string) {
let search = params;
search.params = typeof (search.params) === 'object' && search.params !== null && !Array.isArray(search.params) ? search.params : {};
dateRange = Array.isArray(dateRange) ? dateRange : [];
if (typeof (propName) === 'undefined') {
search.params['beginTime'] = dateRange[0];
search.params['endTime'] = dateRange[1];
} else {
search.params['begin' + propName] = dateRange[0];
search.params['end' + propName] = dateRange[1];
}
return search;
}
// 回显数据字典
export function selectDictLabel(datas: { [x: string]: { label: any, value: any } }, value: string | undefined) {
if (value === undefined) {
return "";
}
var actions = [];
Object.keys(datas).some((key) => {
if (datas[key].value == ('' + value)) {
actions.push(datas[key].label);
return true;
}
})
if (actions.length === 0) {
actions.push(value);
}
return actions.join('');
}
// 回显数据字典(字符串数组)
export function selectDictLabels(datas: { [key: string]: { label: any, value: any } }, value: string | undefined, separator: undefined) {
if (value === undefined || value.length ===0) {
return "";
}
if (Array.isArray(value)) {
value = value.join(",");
}
var actions: string[] = [];
var currentSeparator = undefined === separator ? "," : separator;
var temp = value.split(currentSeparator);
Object.keys(value.split(currentSeparator)).forEach((val) => {
var match = false;
Object.keys(datas).some((key) => {
if (datas[key].value == ('' + temp[parseInt(val)])) {
actions.push(datas[key].label + currentSeparator);
match = true;
}
})
if (!match) {
actions.push(temp[parseInt(val)] + currentSeparator);
}
})
return actions.join('').substring(0, actions.join('').length - 1);
}
// 字符串格式化(%s )
export function sprintf(str: string) {
var args = arguments, flag = true, i = 1;
str = str.replace(/%s/g, function () {
var arg = args[i++];
if (typeof arg === 'undefined') {
flag = false;
return '';
}
return arg;
});
return flag ? str : '';
}
// 转换字符串,undefined,null等转化为""
export function parseStrEmpty(str: string) {
if (!str || str == "undefined" || str == "null") {
return "";
}
return str;
}
// 数据合并
export function mergeRecursive(source: { [x: string]: any }, target: { [x: string]: any }) {
for (var p in target) {
try {
if (target[p].constructor == Object) {
source[p] = mergeRecursive(source[p], target[p]);
} else {
source[p] = target[p];
}
} catch (e) {
source[p] = target[p];
}
}
return source;
};
/**
* 构造树型结构数据
* @param {*} data 数据源
* @param {*} id id字段 默认 'id'
* @param {*} parentId 父节点字段 默认 'parentId'
* @param {*} children 孩子节点字段 默认 'children'
*/
export function handleTree(data: any, id: any, parentId: any, children: any) {
let config = {
id: id || 'id',
parentId: parentId || 'parentId',
childrenList: children || 'children'
};
const childrenListMap: { [key: string]: any[] } = {};
const nodeIds: { [key: string]: any } = {};
var tree = [];
for (let d of data) {
let parentId = d[config.parentId];
if (childrenListMap[parentId] == null) {
childrenListMap[parentId] = [];
}
nodeIds[d[config.id]] = d;
childrenListMap[parentId].push(d);
}
for (let d of data) {
let parentId = d[config.parentId];
if (nodeIds[parentId] == null) {
tree.push(d);
}
}
for (let t of tree) {
adaptToChildrenList(t);
}
function adaptToChildrenList(o: { [x: string]: any }) {
if (childrenListMap[o[config.id]] !== null) {
o[config.childrenList] = childrenListMap[o[config.id]];
}
if (o[config.childrenList]) {
for (let c of o[config.childrenList]) {
adaptToChildrenList(c);
}
}
}
return tree;
}
/**
* 参数处理
* @param {*} params 参数
*/
export function tansParams(params: { [x: string]: any }) {
let result = ''
for (const propName of Object.keys(params)) {
const value = params[propName];
var part = encodeURIComponent(propName) + "=";
if (value !== null && value !== "" && typeof (value) !== "undefined") {
if (typeof value === 'object') {
for (const key of Object.keys(value)) {
if (value[key] !== null && value[key] !== "" && typeof (value[key]) !== 'undefined') {
let params = propName + '[' + key + ']';
var subPart = encodeURIComponent(params) + "=";
result += subPart + encodeURIComponent(value[key]) + "&";
}
}
} else {
result += part + encodeURIComponent(value) + "&";
}
}
}
return result
}
// 返回项目路径
export function getNormalPath(p: string) {
if (p.length === 0 || !p || p == 'undefined') {
return p
};
let res = p.replace('//', '/')
if (res[res.length - 1] === '/') {
return res.slice(0, res.length - 1)
}
return res;
}
// 验证是否为blob格式
export function blobValidate(data: { type: string }) {
return data.type !== 'application/json'
}
4、创建src/plugins/cache.ts
const sessionCache = {
set (key: string | null, value: string | null) {
if (!sessionStorage) {
return
}
if (key != null && value != null) {
sessionStorage.setItem(key, value)
}
},
get (key: string | null) {
if (!sessionStorage) {
return null
}
if (key == null) {
return null
}
return sessionStorage.getItem(key)
},
setJSON (key: any, jsonValue: null) {
if (jsonValue != null) {
this.set(key, JSON.stringify(jsonValue))
}
},
getJSON (key: any) {
const value = this.get(key)
if (value != null) {
return JSON.parse(value)
}
},
remove (key: string) {
sessionStorage.removeItem(key);
}
}
const localCache = {
set (key: string | null, value: string | null) {
if (!localStorage) {
return
}
if (key != null && value != null) {
localStorage.setItem(key, value)
}
},
get (key: string | null) {
if (!localStorage) {
return null
}
if (key == null) {
return null
}
return localStorage.getItem(key)
},
setJSON (key: any, jsonValue: null) {
if (jsonValue != null) {
this.set(key, JSON.stringify(jsonValue))
}
},
getJSON (key: any) {
const value = this.get(key)
if (value != null) {
return JSON.parse(value)
}
},
remove (key: string) {
localStorage.removeItem(key);
}
}
export default {
/**
* 会话级缓存
*/
session: sessionCache,
/**
* 本地缓存
*/
local: localCache
}
5、创建src/plugins/cache.ts
const sessionCache = {
set (key: string | null, value: string | null) {
if (!sessionStorage) {
return
}
if (key != null && value != null) {
sessionStorage.setItem(key, value)
}
},
get (key: string | null) {
if (!sessionStorage) {
return null
}
if (key == null) {
return null
}
return sessionStorage.getItem(key)
},
setJSON (key: any, jsonValue: null) {
if (jsonValue != null) {
this.set(key, JSON.stringify(jsonValue))
}
},
getJSON (key: any) {
const value = this.get(key)
if (value != null) {
return JSON.parse(value)
}
},
remove (key: string) {
sessionStorage.removeItem(key);
}
}
const localCache = {
set (key: string | null, value: string | null) {
if (!localStorage) {
return
}
if (key != null && value != null) {
localStorage.setItem(key, value)
}
},
get (key: string | null) {
if (!localStorage) {
return null
}
if (key == null) {
return null
}
return localStorage.getItem(key)
},
setJSON (key: any, jsonValue: null) {
if (jsonValue != null) {
this.set(key, JSON.stringify(jsonValue))
}
},
getJSON (key: any) {
const value = this.get(key)
if (value != null) {
return JSON.parse(value)
}
},
remove (key: string) {
localStorage.removeItem(key);
}
}
export default {
/**
* 会话级缓存
*/
session: sessionCache,
/**
* 本地缓存
*/
local: localCache
}
6、创建src/stores/modules/user.ts
import { defineStore } from 'pinia'
import { login, logout, getInfo} from '@/api/login'
import { getToken, setToken, removeToken } from '@/utils/auth'
import defAva from '@/assets/vue.svg'
const useUserStore = defineStore(
'user',
{
state: () => ({
token: getToken(),
id: '',
name: '',
avatar: '',
roles: [] as string[],
permissions: [],
customList: [],
customer: {}
}),
actions: {
// 登录
login(userInfo: { username: string }) {
userInfo.username = userInfo.username.trim()
return new Promise<void>((resolve, reject) => {
login(userInfo).then((res: any) => {
console.log(res,'res111')
if (res.code === 200) { // 修改这里
setToken(res.data.access_token)
this.token = res.data.access_token
resolve(res)
} else {
reject(res.msg) // 修改这里
}
}).catch((error: any) => {
reject(error)
})
})
},
// 获取用户信息
getInfo() {
return new Promise((resolve, reject) => {
getInfo().then((res: any) => {
const user = res.data.user // 修改这里
const avatar = (user.avatar == "" || user.avatar == null) ? defAva : user.avatar;
if (res.data.roles && res.data.roles.length > 0) { // 修改这里
this.roles = res.data.roles
this.permissions = res.data.permissions
} else {
this.roles = ['ROLE_DEFAULT']
}
this.customer = res.data.customer // 修改这里
this.id = user.userId
this.name = user.userName
this.avatar = avatar
resolve(res)
}).catch((error: any) => {
reject(error)
})
})
},
// 退出系统
logOut() {
return new Promise<void>((resolve, reject) => {
logout().then(() => {
this.token = ''
this.roles = []
this.permissions = []
removeToken()
resolve()
}).catch((error: any) => {
reject(error)
})
})
}
}
})
export default useUserStore
7、在home.vue中测试应用
<template>
<div class="home">
<h1>{{ title }}</h1>
<!-- <el-icon :size="50"><House /></el-icon> -->
<!-- <el-button type="primary">点击</el-button> -->
<div v-for="article in articles" :key="article.id">
<h2>{{ article.title }}</h2>
<p>{{ article.content }}</p>
</div>
</div>
</template>
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useArticleStore } from '@/stores/article'
import useUserStore from '@/stores/modules/user'
const userStore = useUserStore()
const articleStore = useArticleStore()
const { articles } = storeToRefs(articleStore)
const title = ref('首页')
onMounted(() => {
articleStore.fetchArticles()
const params = {
username: "admin",
password: "******",
}
userStore.login(params).then((res) => {
console.log('res',res)
}).catch((err) => {
console.log('err',err)
});
})
</script>
<style lang="scss" scoped>
</style>
正常登录成功
8. mockjs & vite-plugin-mock
功能: 模拟接口数据
安装: npm install -D mockjs @types/mockjs vite-plugin-mock
1、在vite.config.ts引入配置mock
import { viteMockServe } from 'vite-plugin-mock'
export default defineConfig(({ mode }) => {
plugins: [
viteMockServe({
mockPath: './mock/',//设置模拟数据的存储文件夹
//@ts-ignore
supportTs: true,//是否读取ts文件模块
logger:true,//是否在控制台显示请求日志
localEnabled: true,//设置是否启用本地mock文件
prodEnabled:false//设置打包是否启用mock功能
})
]
})
2、在根目录新建mock/index.ts
import { MockMethod } from 'vite-plugin-mock'
import Mock from 'mockjs'
export default [
{
url: '/api/login',
method: 'post',
response: () => {
return {
code: 200,
data: Mock.mock({
access_token: "eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VyX2tleSI6Ij...",
customerName: "管理员账户",
expires_in: 28800,
}),
msg:'登录成功'
}
}
}
] as MockMethod[]
成功返回预设的返回值对象。
9. sass & sass-loader
功能: CSS预处理器
安装: npm install -D sass sass-loader
使用步骤:
1、新建src/assets/styles/index.scss
body {
--el-color-primary: #4F6EF7;
height: 100%;
margin: 0;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
font-family: SourceHanSansSC-Regular, SourceHanSansSC,Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
}
.el-select__wrapper{
height: 2.25rem;
}
label {
font-weight: 700;
}
html {
height: 100%;
box-sizing: border-box;
}
#app {
height: 100%;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
.no-padding {
padding: 0px !important;
}
.padding-content {
padding: 4px 0;
}
a:focus,
a:active {
outline: none;
}
a,
a:focus,
a:hover {
cursor: pointer;
color: inherit;
text-decoration: none;
}
ul,li{
list-style: none;
}
div:focus {
outline: none;
}
.fr {
float: right;
}
.fl {
float: left;
}
.pr-5 {
padding-right: 5px;
}
.pl-5 {
padding-left: 5px;
}
.block {
display: block;
}
.pointer {
cursor: pointer;
}
.inlineBlock {
display: block;
}
.clearfix {
&:after {
visibility: hidden;
display: block;
font-size: 0;
content: " ";
clear: both;
height: 0;
}
}
2、新建src/assets/styles/variables.module.scss
// base color
$blue: #324157;
$light-blue: #3A71A8;
$red: #C03639;
$pink: #E65D6E;
$green: #30B08F;
$tiffany: #4AB7BD;
$yellow: #FEC171;
$panGreen: #30B08F;
// 默认菜单主题风格
$base-menu-color: #bfcbd9;
$base-menu-color-active: #f4f4f5;
$base-menu-background: #304156;
$base-logo-title-color: #ffffff;
$base-menu-light-color: #606787;
$base-menu-light-background: #F2F6FB;
$base-logo-light-title-color: #fff;
$base-sub-menu-background: #1f2d3d;
$base-sub-menu-hover: #001528;
// 自定义暗色菜单风格
/**
$base-menu-color:hsla(0,0%,100%,.65);
$base-menu-color-active:#fff;
$base-menu-background:#001529;
$base-logo-title-color: #ffffff;
$base-menu-light-color:rgba(0,0,0,.70);
$base-menu-light-background:#ffffff;
$base-logo-light-title-color: #001529;
$base-sub-menu-background:#000c17;
$base-sub-menu-hover:#001528;
*/
$--color-primary: #4F6EF7;
$--color-success: #67C23A;
$--color-warning: #E6A23C;
$--color-danger: #F56C6C;
$--color-info: #909399;
$base-sidebar-width: 240px;
// the :export directive is the magic sauce for webpack
// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
:export {
menuColor: $base-menu-color;
menuLightColor: $base-menu-light-color;
menuColorActive: $base-menu-color-active;
menuBackground: $base-menu-background;
menuLightBackground: $base-menu-light-background;
subMenuBackground: $base-sub-menu-background;
subMenuHover: $base-sub-menu-hover;
sideBarWidth: $base-sidebar-width;
logoTitleColor: $base-logo-title-color;
logoLightTitleColor: $base-logo-light-title-color;
primaryColor: $--color-primary;
successColor: $--color-success;
dangerColor: $--color-danger;
infoColor: $--color-info;
warningColor: $--color-warning;
}
3、新建src/assets/styles/mixins.scss
@mixin clearfix {
&:after {
content: "";
display: table;
clear: both;
}
}
@mixin scrollBar {
&::-webkit-scrollbar-track-piece {
background: #d3dce6;
}
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background: #99a9bf;
border-radius: 20px;
}
}
@mixin relative {
position: relative;
width: 100%;
height: 100%;
}
@mixin pct($pct) {
width: #{$pct};
position: relative;
margin: 0 auto;
}
@mixin triangle($width, $height, $color, $direction) {
$width: $width/2;
$color-border-style: $height solid $color;
$transparent-border-style: $width solid transparent;
height: 0;
width: 0;
@if $direction==up {
border-bottom: $color-border-style;
border-left: $transparent-border-style;
border-right: $transparent-border-style;
}
@else if $direction==right {
border-left: $color-border-style;
border-top: $transparent-border-style;
border-bottom: $transparent-border-style;
}
@else if $direction==down {
border-top: $color-border-style;
border-left: $transparent-border-style;
border-right: $transparent-border-style;
}
@else if $direction==left {
border-right: $color-border-style;
border-top: $transparent-border-style;
border-bottom: $transparent-border-style;
}
}
4、在home组件中使用
<template>
<div class="home">
<h1>{{ title }}</h1>
<!-- <el-icon :size="50"><House /></el-icon> -->
<!-- <el-button type="primary">点击</el-button> -->
<div v-for="article in articles" :key="article.id">
<h2>{{ article.title }}</h2>
<p>{{ article.content }}</p>
</div>
</div>
</template>
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useArticleStore } from '@/stores/article'
import useUserStore from '@/stores/modules/user'
const userStore = useUserStore()
const articleStore = useArticleStore()
const { articles } = storeToRefs(articleStore)
const title = ref('首页')
onMounted(() => {
articleStore.fetchArticles()
const params = {
username: "admin",
password: "******",
}
userStore.login(params).then((res) => {
console.log('res',res)
}).catch((err) => {
console.log('err',err)
});
})
</script>
<style lang="scss" scoped>
@import '@/assets/styles/mixins.scss';
.container {
@include relative;
}
</style>
这里可以看出来样式已经生效赋值。
10. cross-env
功能: 跨平台设置环境变量
安装: npm install -D cross-env
使用步骤:
{
"scripts": {
"dev": "cross-env NODE_ENV=development vite",
"build:test": "cross-env NODE_ENV=test vue-tsc && vite build",
"build:prod": "cross-env NODE_ENV=production vue-tsc && vite build"
}
}
11. 配置环境变量
使用步骤:
根目录新建.env.development和.env.production
.env.development内容如下:
# 页面标题
VITE_APP_TITLE = '博客'
# 开发环境配置
VITE_APP_ENV = 'development'
# 小麒物联/开发环境
VITE_APP_BASE_API = '/api'
# 是否启用代理
VITE_HTTP_PROXY = true
# 端口
VITE_PORT = 80
# 本地环境接口地址
VITE_SERVE = 'http://******'
.env.production内容如下:
# 页面标题
VITE_APP_TITLE = '博客'
# 开发环境配置
VITE_APP_ENV = 'production'
# 小麒物联/开发环境
VITE_APP_BASE_API = '/api'
# 是否启用代理
VITE_HTTP_PROXY = false
# 端口
VITE_PORT = 80
# 本地环境接口地址
VITE_SERVE = 'http://******'
# 是否在打包时开启压缩,支持 gzip 和 brotli
VITE_BUILD_COMPRESS = gzip
12. 配置打包文件结构
build: {
rollupOptions: {
output: {
chunkFileNames: 'static/js/[name]-[hash].js',
entryFileNames: 'static/js/[name]-[hash].js',
assetFileNames: 'static/[ext]/[name]-[hash][extname]',
manualChunks(id) {
if (id.includes('element-plus')) {
return 'element-plus';
}
}
}
}
}
完整版vite.config.ts文件
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import { viteMockServe } from 'vite-plugin-mock'
// https://vite.dev/config/
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd())
const { VITE_APP_ENV, VITE_APP_BASE_API, VITE_SERVE, VITE_PORT } = env
const port = VITE_PORT ? parseInt(VITE_PORT, 10) : 8080
return {
base: VITE_APP_ENV === 'production' ? '/' : './',
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
}
},
plugins: [
vue(),
AutoImport({
imports: ["vue", "vue-router"],
dts: 'src/auto-import.d.ts', // 路径下自动生成文件夹存放全局指令
eslintrc: {
enabled: false, // 1、改为true用于生成eslint配置。2、生成后改回false,避免重复生成消耗
},
resolvers: [ElementPlusResolver()]
}),
Components({
dirs: ['src/components'], // 配置需要默认导入的自定义组件文件夹,该文件夹下的所有组件都会自动 import
resolvers: [ElementPlusResolver()],
}),
viteMockServe({
mockPath: './mock/',//设置模拟数据的存储文件夹
//@ts-ignore
supportTs: true,//是否读取ts文件模块
logger:true,//是否在控制台显示请求日志
localEnabled: true,//设置是否启用本地mock文件
prodEnabled:false//设置打包是否启用mock功能
})
],
// vite 相关配置
server: {
host: '0.0.0.0',
port: port,
open: true,
proxy: {
[VITE_APP_BASE_API]: {
target: VITE_SERVE,
changeOrigin: true,
rewrite: path => path.replace(RegExp(`^${VITE_APP_BASE_API}`), '')
}
},
disableHostCheck: true
},
css: {
postcss: {
plugins: [
{
postcssPlugin: 'internal:charset-removal',
AtRule: {
charset: (atRule) => {
if (atRule.name === 'charset') {
atRule.remove();
}
}
}
}
]
}
},
build: {
rollupOptions: {
output: {
chunkFileNames: 'static/js/[name]-[hash].js',
entryFileNames: 'static/js/[name]-[hash].js',
assetFileNames: 'static/[ext]/[name]-[hash][extname]',
manualChunks(id) {
if (id.includes('element-plus')) {
return 'element-plus';
}
}
}
}
}
}
})
完整版package.json文件
{
"name": "yf-blog",
"author": "nch",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"preview": "vite preview"
},
"repository": {
"type": "git",
"url": "https://gitee.com/niech_project/***.git"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"axios": "^1.7.9",
"element-plus": "^2.9.0",
"file-saver": "^2.0.5",
"highlight.js": "^11.10.0",
"js-cookie": "^3.0.5",
"marked": "^15.0.3",
"pinia": "^2.3.0",
"pinia-plugin-persistedstate": "^4.1.3",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@types/file-saver": "^2.0.7",
"@types/js-cookie": "^3.0.6",
"@types/mockjs": "^1.0.10",
"@types/node": "^22.10.2",
"@vitejs/plugin-vue": "^5.2.1",
"cross-env": "^7.0.3",
"mockjs": "^1.1.0",
"sass": "^1.83.0",
"sass-loader": "^16.0.4",
"typescript": "~5.6.2",
"unplugin-auto-import": "^0.18.6",
"unplugin-vue-components": "^0.27.5",
"vite": "^6.0.1",
"vite-plugin-mock": "^3.0.2",
"vue-tsc": "^2.1.10"
}
}
至此,前端项目基本架构完成。