本项目分为二部分
1、后台管理系统(用户管理,角色管理,视频管理等)
2、客户端(登录注册、发布视频)
在前端方面我们使用的技术栈包括
TypeScript
Vue3
ant Design Vue
axios
echarts
highcharts
mockjs
pinia
vue-router
npm create vite@latest bilibili-vue3-ts -- --template vue
将生成的js文件都修改为ts文件
npm install
执行成功
安装TypeScript
npm install -g typescript
安装完成后,在控制台运行如下命令,检查安装是否成功(3.x):
tsc -v
设置自动编译
$FileNameWithoutExtension$.js:$FileNameWithoutExtension$.js.map
$FileDir$
作为前端项目我们使用一些场景的依赖
这里我们只需要将以下依赖复制到package.json,重新运行npm install
将package-lock.json文件夹删除
在package.json当中
"dependencies": {"ant-design-vue": "^3.3.0-beta.4","axios": "^0.27.2","echarts": "^5.3.3","echarts-gl": "^2.0.9","highcharts": "^10.2.1","pinia": "^2.0.23","pinia-plugin-persist": "^1.0.0","sass": "^1.54.9","swiper": "^8.4.5","vue": "^3.2.37","vue-router": "^4.1.5","vue3-audio-player": "^1.0.5","vue3-seamless-scroll": "^2.0.1"},"devDependencies": {"less": "^4.1.3","unplugin-auto-import": "^0.11.2","unplugin-vue-components": "^0.22.4","@vitejs/plugin-vue": "^4.0.0","vite": "^4.0.0"}
执行npm install
除了以上安装方式以外,
你也可以自己找到对应依赖的官方网站,
一个一个手动安装
创建router文件夹
import { createRouter,createWebHashHistory } from 'vue-router';
import Home from "../views/Home.vue";
import About from "../views/About.vue";
//2、定义一些路由
//每个路由都需要映射到一个组件
//我们后面再讨论嵌套路由
const routes = [{path:"/",component:Home,name:"Home"},{path:"/About",component:About,name:"About"},
];
//3、创建路由实例并传递‘routes’配置
//你可以在这里输入更多的配置,但是我们在这里
const router = createRouter({//4、内部提供了 history 模式的实现。为了简单起见,我们在这里使用hash模式history:createWebHashHistory(),routes, //routes:routes 的缩写
})
export default router
创建Home.vue和About.vue
Home
修改App.vue
import { createPinia } from 'pinia'
import piniaPluginPersist from 'pinia-plugin-persist'
const store = createPinia()
store.use(piniaPluginPersist)
export default store
import { defineStore } from 'pinia'
export const userStore = defineStore({id: 'user',state: () => {return {title: '',token:''}},getters: {getTitle: (state) => state.title,},actions: {setTitle(title:string) {this.title= title}},// 开启数据缓存// @ts-ignorepersist: { //数据默认存在 sessionStorage 里,并且会以 store 的 id 作为 keyenabled: true}
})
在main.ts当中引入如上内容
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from './router/index'
import store from './store/index'
import { createPinia } from 'pinia'
import * as echarts from 'echarts'let app = createApp(App)
app.config.globalProperties.$echarts = echarts
app.use(router)
app.use(store)
app.use(createPinia)
app.mount('#app')
//vite.config.js
import { defineConfig } from 'vite'
import {resolve} from 'path'
import vue from '@vitejs/plugin-vue'
import Components from "unplugin-vue-components/vite"
import AutoImport from "unplugin-auto-import/vite"
export default defineConfig({plugins: [vue(),AutoImport({}),Components({}),],// ...resolve: {alias: {'@': resolve(__dirname, './src')}},server: {port: 80,host: true,open: true,proxy: {'/api': {target: 'http://api.cpengx.cn/metashop/api',changeOrigin: true,rewrite: (p) => p.replace(/^\/api/, '')},}},// 开启less支持css: {preprocessorOptions: {less: {javascriptEnabled: true}}}
})
运行测试
npm run dev
//vite.config.js
import { defineConfig } from 'vite'
import {resolve} from 'path'
import vue from '@vitejs/plugin-vue'
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'
import Components from "unplugin-vue-components/vite"
import AutoImport from "unplugin-auto-import/vite"
export default defineConfig({plugins: [vue(),AutoImport({resolvers: [AntDesignVueResolver() ],}),Components({resolvers: [AntDesignVueResolver({importStyle: 'less', // 一定要开启这个配置项}),],}),],// ...resolve: {alias: {'@': resolve(__dirname, './src')}},server: {port: 80,host: true,open: true,proxy: {'/api': {target: 'http://api.cpengx.cn/metashop/api',changeOrigin: true,rewrite: (p) => p.replace(/^\/api/, '')},}},// 开启less支持css: {preprocessorOptions: {less: {modifyVars: { // 在这里自定义主题色等样式'primary-color': '#fb7299','link-color': '#fb7299','border-radius-base': '2px',},javascriptEnabled: true,}}}
})
在Home当中放置一个按钮
Home
Primary Button
重新运行并访问
安装axios:一个基于promise的HTTP库,类ajax
npm install axios
import axios from 'axios'
// @ts-ignore
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
// 创建axios实例const service = axios.create({// axios中请求配置有baseURL选项,表示请求URL公共部分baseURL: "/bilibili-api",//baseURL: "/",// 超时timeout: 10000
})
export default service
配置请求
import request from '../utils/request'
/* 有参 */
export const getXqInfo = (params:any) => {return request({method: "GET",url: "/grid/openApi/screen/getXqInfo",params,});
};
/* 无参 */
export const getCommunityOverview = ( ) => {return request({method: "GET",url: "/grid/openApi/screen/getCommunityOverview",});
};
删除页面的自动创建好的页面
设置路由
import { createRouter,createWebHashHistory } from 'vue-router';
import Home from "../views/Home.vue";
import Login from "../views/Login.vue";
//2、定义一些路由
//每个路由都需要映射到一个组件
//我们后面再讨论嵌套路由
const routes = [{path:"/",component:Home,name:"Home"},{path:"/login",component:Login,name:"Login"},
];
//3、创建路由实例并传递‘routes’配置
//你可以在这里输入更多的配置,但是我们在这里
const router = createRouter({//4、内部提供了 history 模式的实现。为了简单起见,我们在这里使用hash模式history:createWebHashHistory(),routes, //routes:routes 的缩写
})
export default router
我们找到From表单的内容
https://www.antdv.com/components/form-cn
复制上述代码,但是我们并不会直接使用期内容
Submit Reset
访问页面http://localhost/#/login
删除style.css当中样式
调整一下页面
登录
import request from '@/utils/request'
/* 无参 */
export const getCaptchaImg = ( ) => {return request({method: "GET",url: "/captcha",});
};
安装qs
qs:查询参数序列化和解析库
npm install qs
安装mockjs
mockjs:为我们生成随机数据的工具库
npm install mockjs
在main.ts当中引入mock.ts
import "@/mock"
完善mock.ts
// @ts-ignore
import Mock from "mockjs";const Random = Mock.Randomlet Result = {code: 200,msg: '操作成功',data: null
}Mock.mock('/bilibili-api/captcha','get',()=>{// @ts-ignoreResult.data = {token: Random.string(32),captchaImg:Random.dataImage('120x40','p7n5w')}return Result;
})
完善request.ts
,设置发起请求的内容
import axios from 'axios'
// @ts-ignore
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
// 创建axios实例const service = axios.create({// axios中请求配置有baseURL选项,表示请求URL公共部分baseURL: "/bilibili-api",//baseURL: "/",// 超时timeout: 10000
})
export default service
完善vite.config.ts
,设置发起请求的路径和地址
proxy: {'/bilibili-api': {target: 'http://localhost:8081',changeOrigin: true,rewrite: (p) => p.replace(/^\/bilibili-api/, '')},
}
完善src/api/index.ts设置请求
import request from '@/utils/request'
/*无参*/
export const getCaptchaImg = () => {return request({method: "GET",url: "/captcha",});
};
设置登录页面完善请求内容
import {getCaptchaImg} from "@/api";
const getCaptcha = () => {getCaptchaImg().then(res => {formState.token = res.data.data.token;captchaImg.value = res.data.data.captchaImg;})
}
onMounted(()=>{getCaptcha();
})
访问http://localhost/#/login
SET_TOKEN(token:string ){this.token = tokenlocalStorage.setItem("token",token)},
export const userLogin = (data:any) => {return request({url: '/login',method: 'post',data: data})
};
const router = useRouter();
import { useRouter } from "vue-router";
const user = userStore()const router = useRouter();
const onFinish = (values: any) => {userLogin(formState).then(res => {const jwt = res.headers['authorization']user.SET_TOKEN(jwt);router.push("/");})
};
完善mock.ts
Mock.mock('/bilibili-api/login','post',()=>{Result.code = 404Result.msg = "验证码错误"return Result;
})
import 'ant-design-vue/dist/antd.css';
import axios from 'axios'
import { message as Message, notification } from 'ant-design-vue';
import { useRouter } from "vue-router";
// @ts-ignore
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
// 创建axios实例const service = axios.create({// axios中请求配置有baseURL选项,表示请求URL公共部分baseURL: "/bilibili-api",// 超时timeout: 10000
})
service.interceptors.request.use(config => {// @ts-ignoreconfig.headers['Authorization'] = localStorage.getItem("token")return config;
});
service.interceptors.response.use(response => {let res = response.dataif (res.code === 200) {return response} else {Message.error(!res.msg ? '系统异常' : res.msg)return Promise.reject(response.data.msg)}}, error => {if (error.response.data) {error.message = error.response.data.msg}if (error.response.status === 401) {useRouter().push("/login")}Message.error(error.message)return Promise.reject(error)}
)
export default service
运行测试
http://localhost/#/login
一般来说,管理系统的页面我们都是头部是一个简单的信息展示系统名称和登录用户信息,然后中间的左边是菜单导航栏,右边是内容,对应到ant Design Vue的组件中,我们可以找到这个Layout 布局容器用于布局,方便快速搭建页面的基本结构。
而我们采用这个布局:
Sider Header Content Footer
router.push("/index");
设置一下状态码使其跳转成功
bilibili后台管理系统 主页 系统管理用户管理 角色管理 菜单管理 系统工具数字字典 、 (collapsed = !collapsed)"/> (collapsed = !collapsed)" /> adminOption 1 Option 2 Option 3 Option 4 Home List App Content
后台管理系统 主页 系统管理用户管理 角色管理 菜单管理 系统工具数字字典
import SideMenu from "./inc/SideMenu.vue"
将index.vue的内容全部抽取到Home
、 (collapsed = !collapsed)"/> (collapsed = !collapsed)" /> adminOption 1 Option 2 Option 3 Option 4 Home List App Content
children:[{path:'/index',name:'Index',component:Index}]
在Home.vue当中设置路由
访问页面:http://localhost/#/index
防止手残贴上全部代码
import { createRouter,createWebHashHistory } from 'vue-router';
import Home from "../views/Home.vue";
import Index from "../views/Index.vue";
import Login from "../views/Login.vue";
import Menu from '../views/sys/Menu.vue'
import Role from '../views/sys/Role.vue'
import User from '../views/sys/User.vue'
//2、定义一些路由
//每个路由都需要映射到一个组件
//我们后面再讨论嵌套路由
const routes = [{path:"/",component:Home,name:"Home",children:[{path:'',name:'Index',component:Index},{path:'/index',name:'Index',component:Index},{path:'/users',name:'SysUser',component:User},{path:'/roles',name:'SysRole',component:Role},{path:'/menus',name:'SysMenu',component:Menu}]},{path:"/login",component:Login,name:"Login"},
];
//3、创建路由实例并传递‘routes’配置
//你可以在这里输入更多的配置,但是我们在这里
const router = createRouter({//4、内部提供了 history 模式的实现。为了简单起见,我们在这里使用hash模式history:createWebHashHistory(),routes, //routes:routes 的缩写
})
export default router
访问:http://localhost/#/roles
访问: http://localhost/#/users
访问:http://localhost/#/menus
主页 系统管理 用户管理 角色管理 菜单管理 系统工具 数字字典
点击测试
管理界面的右上角的用户信息现在是写死的,
因为我们现在已经登录成功,所以我们可以通过接口去请求获取到当前的用户信息了,
这样我们就可以动态显示用户的信息,这个接口比较简单,然后退出登录的链接也一起完成,
就请求接口同时把浏览器中的缓存删除就退出了哈。
export const getUserInfo = () => {return request({url: '/sys/userInfo',method: 'get',})
};
Mock.mock('/bilibili-api/sys/userInfo','get',()=>{// @ts-ignoreResult.data = {id:"1",username:"itbluebox",avatar:"https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png"}return Result
})
个人中心
import UserCenter from '../views/UserCenter.vue'
{path:'/userCenter',name:'UserCenter',component:UserCenter
},
创建对应的页面
修改 重置
export const logout = () => {return request({url: '/logout',method: 'get',})
};
退出
import { userStore} from '@/store/store'
const user = userStore()
const logOut = () => {logout().then(response => {user.resetState()localStorage.clear();sessionStorage.clear();router.push("/login");});
}
设置Store的状态
resetState(){this.token = ""},
设置mock
Mock.mock('/bilibili-api/logout','get',()=>{return Result;
})
{path:'/sys/users',name:'SysUser',component:User},{path:'/sys/roles',name:'SysRole',component:Role},{path:'/sys/menus',name:'SysMenu',component:Menu}
目前先这样。后期会对其进行优化
后台管理系统 主页 {{menu.title}} {{item.title}}
刷新并访问页面
menuList:[],authoritys:[]setMenuList(menuList:any) {this.menuList = menuList},setAuthoritys(authoritys:any) {this.authoritys = authoritys},
发送获取菜单的请求
export const nav = () => {return request({url: '/sys/menu/nav',method: 'get',})
};
在路由当中获取拿到menuList,
import {nav} from "@/api";
import { userStore} from '@/store/store'
设置在路由加载前拿到前,拿到菜单的内容并添加到Store
router.beforeEach((to,from,next)=>{nav().then(res => {//拿到menuListuserStore().setMenuList(res.data.data.nav)userStore().setAuthoritys(res.data.data.authoritys)})next()
})
Mock.mock('/bilibili-api/sys/menu/nav', 'get', () => {let nav = [{key:101,title: '系统管理',name: 'SysMange',icon: 'setting-outlined',path: '',children: [{key:102,title: '用户管理',name: 'SysUser',icon: 'user-outlined',path: '/sys/users',children: []},{key:103,title: '角色管理',name: 'SysUser',icon: 'user-group-add-outlined',path: '/sys/roles',children: []},{key:104,title: '菜单管理',name: 'SysMenu',icon: 'menu-outlined',path: '/sys/menus',children: []}]},{key:201,title: '系统工具',name: 'SysTools',icon: 'menu-outlined',path: '',children: [{title: '数字字典',name: 'SysDict',icon: 'container-outlined',path: '/sys/dicts',children: []}]}];// @ts-ignorelet authoritys = [];// @ts-ignoreResult.data = {nav: nav,// @ts-ignoreauthoritys: authoritys}return Result;
})
import {ref,reactive,onMounted} from "vue";
import {AppstoreOutlined,MailOutlined,SettingOutlined,TeamOutlined,UserOutlined,MenuOutlined,ContainerOutlined
} from '@ant-design/icons-vue';
import Icon from "@/components/Icon.vue"
import { userStore} from '@/store/store'
let selectedKeys =ref(['1'])
let selectedKeysTop =ref(['1'])
let collapsed =ref(false)
let menuList = reactive({menus: []
})
onMounted(()=>{menuList.menus = userStore().getMenuList
})
hasRoutes:falsegetHasRoutes: (state) => state.hasRoutes,changeRouteStatus(hasRoutes:any){this.hasRoutes = hasRoutes;
}
router.beforeEach((to,from,next)=>{let hasRoutes = userStore().getHasRoutes;if(!hasRoutes){nav().then(res => {//拿到menuListuserStore().setMenuList(res.data.data.nav)userStore().setAuthoritys(res.data.data.authoritys)hasRoutes = trueuserStore().changeRouteStatus(hasRoutes)})}next()
})
查看效果
在Home当中引入该内容
import Tabs from "@/views/inc/Tabs.vue"
editableTabsValue: 0,editableTabs: [{title: '首页',content: '/index',key: 0,closable: false,}],getEditableTabsValue: (state) => state.editableTabsValue,getEditableTabs: (state) => state.editableTabs,
addTab(tab:any) {this.editableTabs.push({title: tab.title,content: tab.path,key: tab.key,closable: true,});},setEditableTabs(tab:any){this.editableTabs = tab;},setEditableTabsIndex0(){this.editableTabsValue = 0;},setEditableTabsIndexClearALL(){this.editableTabs = [{title: '首页',content: '/index',key: 0,closable: false,}]},setEditableTabsValue(tabValue:number){this.editableTabsValue = tabValue;}
主页
{{item.title}}
onMounted(()=>{var menus = userStore().getEditableTabsValue;//设置高亮同步selectedKeys.value.length = 0;selectedKeys.value.push(menus+"")menuList.menus = userStore().getMenuList;
});
const selectMenu = (item:any) => {userStore().addTab(item)
}
const selectMenuIndex0 = () => {userStore().setEditableTabsIndex0()
}
addTab(tab:any) {let index = this.editableTabs.findIndex(e => e.title === tab.title )if(index === -1){this.editableTabs.push({title: tab.title,content: tab.path,key: tab.key,closable: true,});}this.editableTabsValue = tab.key;},
多次点击以后不会出现
const onTabClick = (targetKey: string) => {let jsonArray = userStore().getEditableTabslet path = "";for(let i =0;i < jsonArray.length;i++){if(targetKey == jsonArray[i].key){path = jsonArray[i].content;}}userStore().setEditableTabsValue(targetKey)router.push(path);
}
{{editableTabsValue}}后台管理系统
let editableTabsValue = computed({get(){let key = userStore().getEditableTabsValue;selectedKeys.value.length = 0;selectedKeys.value.push(key)return key;},set(val:any){userStore().setMenuList(val);}
});
完善清除功能
const removeAll = () => {activeKey.value = `newTab${++newTabIndex.value}`;userStore().setEditableTabsIndexClearALL()userStore().setEditableTabsValue("1")router.push("/index");
};
设置通过ip路径访问的时候,设置对应tabs和menu
访问:http://localhost/#/sys/menus
设置退出登录后清除tab
退出
const logOut = () => {logout().then(response => {user.resetState()user.setEditableTabsIndexClearALL()user.setEditableTabsIndex0()localStorage.clear();sessionStorage.clear();router.push("/login");});
}
新建 {{ record.type }} {{ record.type }} {{ record.type }} {{ record.statu }} {{ record.statu }} 编辑 删除
上面当面数据超出页面的时候页面跟着滚动这样不太好我们优化一下,设置侧边栏和头部不懂,设置内容区域滚动
、 (collapsed = !collapsed)"/> (collapsed = !collapsed)" /> {{userInfo.username}} 个人中心 退出
内容滚动头部底部不滚
新建 {{ record.type }} {{ record.type }} {{ record.type }} {{ record.statu }} {{ record.statu }} 编辑 删除 目录 菜单 按钮 禁用 正常 提交 重置
新建 {{ record.type }} {{ record.type }} {{ record.type }} {{ record.statu }} {{ record.statu }} 编辑 删除 目录 菜单 按钮 禁用 正常 提交 重置
用户的增删改查以及对应的权限
新建 {{ text }} Name{{ tag.toUpperCase() }} 分配角色 重置密码 编辑 删除![]()
Upload男 女 正常 停止 注销 提交 重置