1、AdminLayout.vue
html
<template>
<div class="admin-layout">
<!--侧边菜单-->
<div class="sidebar">
<div class="logo-area">
<img src="/images/admin/logo.png" alt="logo">
<h4>王者比赛后台</h4>
</div>
<div class="menu-list">
<router-link to="/dashboard" class="menu-item" active-class="menu-active">
📊 仪表盘
</router-link>
<router-link to="/playerManage" class="menu-item" active-class="menu-active">
👤 选手管理
</router-link>
<router-link to="/scheduleManage" class="menu-item" active-class="menu-active">
📅 赛程管理
</router-link>
<router-link to="/teamManage" class="menu-item" active-class="menu-active">
🏆 战队管理
</router-link>
<router-link to="/profile" class="menu-item" active-class="menu-active">
⚙️ 个人中心
</router-link>
</div>
</div>
<!--右侧区域:顶部头部 +内容-->
<div class="main-wrap">
<div class="admin-header">
<span>欢迎您:{{ $store.getters.username }}</span>
<button class="logout-btn" @click="handleLogout">退出登录</button>
</div>
<div class="content-box">
<router-view></router-view>
</div>
</div>
</div>
</template>
<script>
export default {
name: "AdminLayout",
methods: {
handleLogout() {
this.$store.dispatch("logoutUser")
this.$router.push("/login")
}
}
}
</script>
<style scoped>
.admin-layout {
display: flex;
width: 100%;
height: 100vh;
}
.sidebar {
width: 220px;
background: #001529;
color: #fff;
}
.logo-area {
height: 60px;
display: flex;
align-items: center;
padding: 0 16px;
border-bottom: 1px solid #222;
}
.logo-area img {
width: 32px;
height: 32px;
margin-right: 10px;
}
.menu-list {
margin-top: 10px;
}
.menu-item {
display: block;
height: 44px;
line-height: 44px;
padding: 0 20px;
color: #a0a0a0;
text-decoration: none;
}
.menu-active {
background: #1890ff;
color: #ffffff;
}
.main-wrap {
flex: 1;
display: flex;
flex-direction: column;
}
.admin-header {
height: 60px;
background: #fff;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 24px;
font-size: 15px;
}
.logout-btn {
padding: 6px 14px;
border: none;
background: #ff4d4f;
color: white;
border-radius: 4px;
}
.content-box {
flex: 1;
padding: 20px;
overflow-y: auto;
}
</style>
2、Register.vue
html
<template>
<div class="register-page">
<div class="register-card">
<h2>后台账号注册</h2>
<div class="form-item">
<label>账号</label>
<input v-model="form.username" placeholder="请设置登录账号">
</div>
<div class="form-item">
<label>密码</label>
<input v-model="form.password" type="password" placeholder="设置密码">
</div>
<div class="form-item">
<label>昵称</label>
<input v-model="form.nickname" placeholder="后台显示昵称">
</div>
<button class="reg-btn" @click="doRegister">注册账号</button>
<div class="tip">已有账号?<span class="link" @click="$router.push('/login')">前往登录</span></div>
</div>
</div>
</template>
<script>
export default {
name: "Register",
data() {
return {
form: {
username: "",
password: "",
nickname: ""
}
}
},
methods: {
doRegister() {
if (!this.form.username || !this.form.password || !this.form.nickname) {
alert("账号、密码、昵称不能为空!")
return
}
//注册信息保存本地,模拟注册数据库
localStorage.setItem("regUser", JSON.stringify(this.form))
alert("注册成功,请前往登录!")
this.$router.push("/login")
}
}
}
</script>
<style scoped>
.register-page {
width: 100%;
height: 100vh;
background: #f0f2f5;
display: flex;
justify-content: center;
align-items: center;
}
.register-card {
width: 420px;
background: #fff;
padding: 32px;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12);
}
.register-card h2 {
text-align: center;
margin-bottom: 24px;
color: #333;
}
.form-item {
margin-bottom: 16px;
}
.form-item label {
display: block;
margin-bottom: 6px;
font-size: 14px;
}
.form-item input {
width: 100%;
height: 42px;
border: 1px solid #dcdcdc;
border-radius: 6px;
padding: 0 10px;
}
.reg-btn {
width: 100%;
height: 44px;
background: #1890ff;
color: #fff;
border: none;
border-radius: 6px;
margin-top: 8px;
}
.tip {
margin-top: 16px;
text-align: center;
font-size: 14px;
}
.link {
color: #1890ff;
cursor: pointer;
}
</style>
3、Login.vue
html
<template>
<div class="login-page">
<div class="login-card">
<h2>比赛后台登录</h2>
<div class="form-item">
<label>账号</label>
<input v-model="loginForm.username" placeholder="输入注册账号">
</div>
<div class="form-item">
<label>密码</label>
<input v-model="loginForm.password" type="password" placeholder="输入密码">
</div>
<button class="login-btn" @click="doLogin">登录系统</button>
<div class="tip">还没有账号?<span class="link" @click="$router.push('/register')">去注册</span></div>
</div>
</div>
</template>
<script>
export default {
name: "Login",
data() {
return {
loginForm: {
username: "",
password: ""
}
}
},
methods: {
doLogin() {
//读取本地注册的用户
const regInfo = JSON.parse(localStorage.getItem("regUser"))
if (!regInfo) {
alert("尚未注册,请先注册账号!")
return
}
if (regInfo.username === this.loginForm.username && regInfo.password === this.loginForm.password) {
//登录成功,调用action把用户信息存入vuex
this.$store.dispatch("loginUser", {
username: regInfo.username,
nickname: regInfo.nickname
})
alert("登录成功!")
this.$router.push("/dashboard")
} else {
alert("账号密码错误!")
}
}
}
}
</script>
<style scoped>
.login-page {
width: 100%;
height: 100vh;
background: #f0f2f5;
display: flex;
justify-content: center;
align-items: center;
}
.login-card {
width: 420px;
background: #fff;
padding: 32px;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12);
}
.login-card h2 {
text-align: center;
margin-bottom: 24px;
}
.form-item {
margin-bottom: 16px;
}
.form-item label {
display: block;
margin-bottom: 6px;
font-size: 14px;
}
.form-item input {
width: 100%;
height: 42px;
border: 1px solid #dcdcdc;
border-radius: 6px;
padding: 0 10px;
}
.login-btn {
width: 100%;
height: 44px;
background: #1890ff;
color: #fff;
border: none;
border-radius: 6px;
}
.tip {
margin-top: 16px;
text-align: center;
font-size: 14px;
}
.link {
color: #1890ff;
cursor: pointer;
}
</style>
4、Dashboard.vue
html
<template>
<div class="dashboard">
<h2>📊 王者荣耀比赛后台仪表盘</h2>
<div class="card-wrap">
<div class="stat-card">
<h3>参赛战队总数</h3>
<p class="num">8</p>
</div>
<div class="stat-card">
<h3>登记选手</h3>
<p class="num">42</p>
</div>
<div class="stat-card">
<h3>待进行比赛</h3>
<p class="num">12</p>
</div>
<div class="stat-card">
<h3>已完成比赛</h3>
<p class="num">26</p>
</div>
</div>
<div class="info-card">
<h4>系统提示</h4>
<p>当前登录管理员:{{ $store.getters.username }}</p>
<p>数据来源Vuex全局状态仓库;用户信息本地持久化存储</p>
</div>
</div>
</template>
<script>
export default {
name: "Dashboard"
}
</script>
<style scoped>
.dashboard h2 {
margin-bottom: 20px;
}
.card-wrap {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 20px;
}
.stat-card {
background: #fff;
padding: 20px;
border-radius: 10px;
box-shadow: 0 1px 8px rgba(0, 0, 0, 0.08);
}
.num {
font-size: 30px;
color: #1890ff;
font-weight: bold;
margin-top: 8px;
}
.info-card {
background: #fff;
padding: 20px;
border-radius: 10px;
box-shadow: 0 1px 8px rgba(0, 0, 0, 0.08);
}
</style>
5、PlayerManage.vue
html
<template>
<div class="player-manage">
<h2>👤 选手管理</h2>
<div class="operate-bar">
<button class="add-btn" @click="handleAdd">新增选手</button>
</div>
<div class="table-card">
<table>
<thead>
<tr>
<th>ID</th>
<th>头像</th>
<th>选手昵称</th>
<th>所属战队</th>
<th>位置</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in playerList" :key="item.id">
<td>{{ item.id }}</td>
<td>
<img :src="item.avatar" class="avatar-img" />
</td>
<td>{{ item.name }}</td>
<td>{{ item.team }}</td>
<td>{{ item.position }}</td>
<td>
<button class="btn-edit" @click="handleEdit(index)">编辑</button>
<button class="btn-del" @click="handleDel(index)">删除</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 新增/编辑弹窗 -->
<div v-if="showDialog" class="dialog">
<div class="dialog-inner">
<h3>{{ isEdit ? '编辑选手' : '新增选手' }}</h3>
<div class="row-item">
<label>选手昵称:</label>
<input v-model="form.name" />
</div>
<div class="row-item">
<label>所属战队:</label>
<input v-model="form.team" />
</div>
<div class="row-item">
<label>位置:</label>
<input v-model="form.position" />
</div>
<!-- 头像上传 -->
<div class="row-item">
<label>选手头像:</label>
<input type="file" accept="image/*" @change="onFileChange" />
</div>
<!--头像预览-->
<div class="preview-box" v-if="form.avatar">
<span>预览:</span>
<img :src="form.avatar" class="preview-avatar" />
</div>
<div class="dialog-btns">
<button @click="showDialog = false">取消</button>
<button @click="confirmSave">确定保存</button>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "PlayerManage",
data() {
return {
showDialog: false,
isEdit: false,
editIndex: -1,
form: {
id: "",
name: "",
team: "",
position: "",
avatar: "/images/admin/default-avatar.png"
},
defaultList: [
{
id: 1,
name: "一诺",
avatar: "/images/admin/avatar1.png",
team: "AG超玩会",
position: "发育路"
},
{
id: 2,
name: "暖阳",
avatar: "/images/admin/avatar2.png",
team: "北京WB",
position: "打野"
}
],
playerList: []
}
},
created() {
const localStr = localStorage.getItem("playerListData")
if (localStr) {
this.playerList = JSON.parse(localStr)
} else {
this.playerList = [...this.defaultList]
this.saveLocal()
}
},
methods: {
saveLocal() {
localStorage.setItem("playerListData", JSON.stringify(this.playerList))
},
//生成自增简短ID
getNewId() {
if (this.playerList.length === 0) {
return 1
}
const maxId = Math.max(...this.playerList.map(p => p.id))
return maxId + 1
},
handleAdd() {
this.isEdit = false
this.form = {
id: this.getNewId(),
name: "",
team: "",
position: "",
avatar: "/images/admin/default-avatar.png"
}
this.showDialog = true
},
handleEdit(index) {
this.isEdit = true
this.editIndex = index
this.form = { ...this.playerList[index] }
this.showDialog = true
},
handleDel(index) {
if (window.confirm("确定删除该选手?")) {
this.playerList.splice(index, 1)
this.saveLocal()
}
},
onFileChange(e) {
const file = e.target.files[0]
if (!file) return
const reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = (res) => {
this.form.avatar = res.target.result
}
},
confirmSave() {
if (!this.form.name || !this.form.team || !this.form.position) {
alert("昵称、战队、位置不能为空!")
return
}
if (this.isEdit) {
//编辑,id保持不变,不重新生成
this.playerList[this.editIndex] = { ...this.form }
} else {
this.playerList.push({ ...this.form })
}
this.saveLocal()
this.showDialog = false
}
}
}
</script>
<style scoped>
.player-manage h2 {
margin-bottom: 16px;
}
.operate-bar {
margin-bottom: 12px;
}
.add-btn {
padding: 6px 14px;
background: #1890ff;
color: #fff;
border: none;
border-radius: 4px;
}
.table-card {
background: #fff;
border-radius: 10px;
box-shadow: 0 1px 8px rgba(0, 0, 0, 0.08);
padding: 16px;
}
table {
width: 100%;
border-collapse: collapse;
}
th,
td {
border: 1px solid #eee;
padding: 10px;
text-align: center;
}
th {
background: #f7f8fa;
}
.avatar-img {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
}
.btn-edit {
margin: 0 4px;
padding: 3px 8px;
background: #4096ff;
color: #fff;
border: none;
border-radius: 3px;
}
.btn-del {
padding: 3px 8px;
background: #ff4d4f;
color: #fff;
border: none;
border-radius: 3px;
}
.dialog {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.4);
display: flex;
justify-content: center;
align-items: center;
z-index: 999;
}
.dialog-inner {
background: #fff;
padding: 24px;
border-radius: 8px;
min-width: 420px;
}
.row-item {
margin: 12px 0;
}
.row-item input {
margin-left: 8px;
padding: 4px;
}
.preview-box {
margin: 10px 0;
}
.preview-avatar {
width: 60px;
height: 60px;
border-radius: 50%;
margin-left: 8px;
object-fit: cover;
}
.dialog-btns {
margin-top: 20px;
display: flex;
gap: 12px;
justify-content: center;
}
.dialog-btns button {
padding: 5px 16px;
}
</style>
6、ScheduleManage.vue
html
<template>
<div class="schedule-manage">
<h2>📅 比赛赛程管理</h2>
<div class="operate-bar">
<button class="add-btn" @click="handleAdd">新增赛程</button>
</div>
<div class="table-card">
<table>
<thead>
<tr>
<th>赛程ID</th>
<th>比赛名称</th>
<th>对战双方</th>
<th>比赛时间</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(s, index) in scheduleList" :key="s.id">
<td>{{ s.id }}</td>
<td>{{ s.title }}</td>
<td>{{ s.vs }}</td>
<td>{{ s.time }}</td>
<td>{{ s.status }}</td>
<td>
<button class="btn-detail" @click="$router.push(`/matchDetail/${s.id}`)">查看详情</button>
<button class="btn-edit" @click="handleEdit(index)">编辑</button>
<button class="btn-del" @click="handleDel(index)">删除</button>
</td>
</tr>
</tbody>
</table>
</div>
<!--新增编辑弹窗-->
<div v-if="showDialog" class="dialog">
<div class="dialog-inner">
<h3>{{ isEdit ? '编辑赛程' : '新增赛程' }}</h3>
<div class="row-item">
<label>比赛名称:</label>
<input v-model="form.title" />
</div>
<div class="row-item">
<label>对战双方:</label>
<input v-model="form.vs" />
</div>
<div class="row-item">
<label>比赛时间:</label>
<input v-model="form.time" />
</div>
<div class="row-item">
<label>比赛状态:</label>
<input v-model="form.status" />
</div>
<div class="dialog-btns">
<button @click="showDialog = false">取消</button>
<button @click="confirmSave">确定保存</button>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "ScheduleManage",
data() {
return {
showDialog: false,
isEdit: false,
editIndex: -1,
form: {
id: "",
title: "",
vs: "",
time: "",
status: ""
},
defaultList: [
{ id: 1, title: "春季赛小组赛", vs: "AG超玩会 VS WB", time: "2026‑10‑20 19:00", status: "待开赛" },
{ id: 2, title: "春季赛小组赛", vs: "Hero VS TES", time: "2026‑10‑21 17:00", status: "待开赛" }
],
scheduleList: []
}
},
created() {
const local = localStorage.getItem("scheduleListData")
if (local) {
this.scheduleList = JSON.parse(local)
} else {
this.scheduleList = [...this.defaultList]
this.saveLocal()
}
},
methods: {
saveLocal() {
localStorage.setItem("scheduleListData", JSON.stringify(this.scheduleList))
},
getNewId() {
if (this.scheduleList.length === 0) return 1
const maxId = Math.max(...this.scheduleList.map(item => item.id))
return maxId + 1
},
handleAdd() {
this.isEdit = false
this.form = {
id: this.getNewId(),
title: "",
vs: "",
time: "",
status: ""
}
this.showDialog = true
},
handleEdit(index) {
this.isEdit = true
this.editIndex = index
this.form = { ...this.scheduleList[index] }
this.showDialog = true
},
handleDel(index) {
if (window.confirm("确定删除该赛程?")) {
this.scheduleList.splice(index, 1)
this.saveLocal()
}
},
confirmSave() {
if (!this.form.title || !this.form.vs || !this.form.time || !this.form.status) {
alert("所有字段不能为空!")
return
}
if (this.isEdit) {
this.scheduleList[this.editIndex] = { ...this.form }
} else {
this.scheduleList.push({ ...this.form })
}
this.saveLocal()
this.showDialog = false
}
}
}
</script>
<style scoped>
.schedule-manage h2 {
margin-bottom: 16px;
}
.operate-bar {
margin-bottom: 12px;
}
.add-btn {
padding: 6px 14px;
background: #1890ff;
color: #fff;
border: none;
border-radius: 4px;
}
.table-card {
background: #fff;
border-radius: 10px;
box-shadow: 0 1px 8px rgba(0, 0, 0, 0.08);
padding: 16px;
}
table {
width: 100%;
border-collapse: collapse;
}
th,
td {
border: 1px solid #eee;
padding: 10px;
text-align: center;
}
th {
background: #f7f8fa;
}
.btn-detail {
padding: 3px 8px;
background: #1890ff;
color: #fff;
border: none;
border-radius: 3px;
margin: 0 2px;
}
.btn-edit {
margin: 0 2px;
padding: 3px 8px;
background: #4096ff;
color: #fff;
border: none;
border-radius: 3px;
}
.btn-del {
padding: 3px 8px;
background: #ff4d4f;
color: #fff;
border: none;
border-radius: 3px;
}
.dialog {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.4);
display: flex;
justify-content: center;
align-items: center;
z-index: 999;
}
.dialog-inner {
background: #fff;
padding: 24px;
border-radius: 8px;
min-width: 420px;
}
.row-item {
margin: 12px 0;
}
.row-item input {
margin-left: 8px;
padding: 4px;
width: 260px;
}
.dialog-btns {
margin-top: 20px;
display: flex;
gap: 12px;
justify-content: center;
}
.dialog-btns button {
padding: 5px 16px;
}
</style>
7、TeamManage.vue
html
<template>
<div class="team-manage">
<h2>🏆 参赛战队管理</h2>
<div class="operate-bar">
<button class="add-btn" @click="handleAdd">新增战队</button>
</div>
<div class="team-card-wrap">
<div class="team-card" v-for="(t, index) in teamList" :key="t.id">
<img :src="t.logo" alt="">
<h4>{{ t.teamName }}</h4>
<p>赛区:{{ t.area }}</p>
<p>积分:{{ t.score }}</p>
<div class="btns">
<button class="edit" @click="handleEdit(index)">编辑</button>
<button class="del" @click="handleDel(index)">删除</button>
</div>
</div>
</div>
<!--新增编辑弹窗-->
<div v-if="showDialog" class="dialog">
<div class="dialog-inner">
<h3>{{ isEdit ? '编辑战队' : '新增战队' }}</h3>
<div class="row-item">
<label>战队名称:</label>
<input v-model="form.teamName" />
</div>
<div class="row-item">
<label>赛区:</label>
<input v-model="form.area" />
</div>
<div class="row-item">
<label>积分:</label>
<input v-model.number="form.score" type="number" />
</div>
<div class="row-item">
<label>战队Logo:</label>
<input type="file" accept="image/*" @change="onFileChange" />
</div>
<div class="preview-box" v-if="form.logo">
<span>Logo预览:</span>
<img :src="form.logo" class="preview-logo" />
</div>
<div class="dialog-btns">
<button @click="showDialog = false">取消</button>
<button @click="confirmSave">确定保存</button>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "TeamManage",
data() {
return {
showDialog: false,
isEdit: false,
editIndex: -1,
form: {
id: "",
teamName: "",
area: "",
score: 0,
logo: "/images/admin/team1.png"
},
defaultList: [
{ id: 1, teamName: "AG超玩会", logo: "/images/admin/team1.png", area: "KPL", score: 12 },
{ id: 2, teamName: "北京WB", logo: "/images/admin/team2.png", area: "KPL", score: 10 }
],
teamList: []
}
},
created() {
const local = localStorage.getItem("teamListData")
if (local) {
this.teamList = JSON.parse(local)
} else {
this.teamList = [...this.defaultList]
this.saveLocal()
}
},
methods: {
saveLocal() {
localStorage.setItem("teamListData", JSON.stringify(this.teamList))
},
getNewId() {
if (this.teamList.length === 0) return 1
const maxId = Math.max(...this.teamList.map(item => item.id))
return maxId + 1
},
handleAdd() {
this.isEdit = false
this.form = {
id: this.getNewId(),
teamName: "",
area: "",
score: 0,
logo: "/images/admin/team1.png"
}
this.showDialog = true
},
handleEdit(index) {
this.isEdit = true
this.editIndex = index
this.form = { ...this.teamList[index] }
this.showDialog = true
},
handleDel(index) {
if (window.confirm("确定删除该战队?")) {
this.teamList.splice(index, 1)
this.saveLocal()
}
},
onFileChange(e) {
const file = e.target.files[0]
if (!file) return
const reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = res => {
this.form.logo = res.target.result
}
},
confirmSave() {
if (!this.form.teamName || !this.form.area) {
alert("战队名称、赛区不能为空!")
return
}
if (this.isEdit) {
this.teamList[this.editIndex] = { ...this.form }
} else {
this.teamList.push({ ...this.form })
}
this.saveLocal()
this.showDialog = false
}
}
}
</script>
<style scoped>
.team-manage h2 {
margin-bottom: 16px;
}
.operate-bar {
margin-bottom: 12px;
}
.add-btn {
padding: 6px 14px;
background: #1890ff;
color: #fff;
border: none;
border-radius: 4px;
}
.team-card-wrap {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 18px;
}
.team-card {
background: #fff;
padding: 16px;
border-radius: 10px;
box-shadow: 0 1px 8px rgba(0, 0, 0, 0.08);
text-align: center;
}
.team-card img {
width: 80px;
height: 80px;
border-radius: 8px;
margin-bottom: 8px;
object-fit: cover;
}
.btns {
margin-top: 10px;
}
.edit {
padding: 4px 10px;
background: #4096ff;
color: #fff;
border: none;
border-radius: 4px;
margin-right: 6px;
}
.del {
padding: 4px 10px;
background: #ff4d4f;
color: #fff;
border: none;
border-radius: 4px;
}
.dialog {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.4);
display: flex;
justify-content: center;
align-items: center;
z-index: 999;
}
.dialog-inner {
background: #fff;
padding: 24px;
border-radius: 8px;
min-width: 440px;
}
.row-item {
margin: 12px 0;
}
.row-item input {
margin-left: 8px;
padding: 4px;
width: 240px;
}
.preview-box {
margin: 10px 0;
}
.preview-logo {
width: 70px;
height: 70px;
border-radius: 6px;
margin-left: 8px;
object-fit: cover;
}
.dialog-btns {
margin-top: 20px;
display: flex;
gap: 12px;
justify-content: center;
}
.dialog-btns button {
padding: 5px 16px;
}
</style>
8、Profile.vue
html
<template>
<div class="profile">
<h2>⚙️个人中心</h2>
<div class="profile-card">
<h3>管理员信息(数据来自Vuex仓库)</h3>
<div class="row">
<label>登录账号:</label>
<span>{{ $store.state.userInfo.username }}</span>
</div>
<div class="row">
<label>管理员昵称:</label>
<span>{{ $store.state.userInfo.nickname }}</span>
</div>
<div class="row">
<label>登录状态:</label>
<span style="color:green">已登录</span>
</div>
<p style="margin-top:12px;color:#666">用户信息同时保存在localStorage本地存储,刷新页面不会丢失。</p>
</div>
</div>
</template>
<script>
export default {
name: "Profile"
}
</script>
<style scoped>
.profile h2 {
margin-bottom: 16px;
}
.profile-card {
background: #fff;
padding: 24px;
border-radius: 10px;
box-shadow: 0 1px 8px rgba(0, 0, 0, 0.08);
}
.row {
margin: 12px 0;
font-size: 15px;
}
.row label {
width: 100px;
display: inline-block;
color: #666;
}
</style>
9、MatchDetail.vue
html
<template>
<div class="match-detail">
<h2>比赛详情</h2>
<div class="detail-card">
<p>接收动态路由matchId:{{ matchId }}</p>
<hr />
<h4>春季赛 · 小组赛</h4>
<p>对战:AG超玩会 VS 北京WB</p>
<p>开赛时间:2026‑10‑20 19:00</p>
<p>比赛状态:待开赛</p>
<p>比赛地点:KPL电竞中心</p>
</div>
</div>
</template>
<script>
export default {
name: "MatchDetail",
props: ["matchId"]
}
</script>
<style scoped>
.match-detail h2 {
margin-bottom: 16px;
}
.detail-card {
background: #fff;
padding: 24px;
border-radius: 10px;
box-shadow: 0 1px 8px rgba(0, 0, 0, 0.08);
}
.detail-card p {
margin: 8px 0;
}
</style>
10、store/index.js
javascript
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
//登录用户信息
userInfo: JSON.parse(localStorage.getItem('userInfo')) || null
},
getters: {
isLogin(state) {
return !!state.userInfo
},
username(state) {
return state.userInfo ? state.userInfo.nickname : "未登录"
}
},
mutations: {
SET_USERINFO(state, payload) {
state.userInfo = payload
//同步保存到本地存储,实现持久化
localStorage.setItem("userInfo", JSON.stringify(payload))
},
CLEAR_USERINFO(state) {
state.userInfo = null
localStorage.removeItem("userInfo")
}
},
actions: {
//登录action,提交mutation保存用户
loginUser({ commit }, userObj) {
commit("SET_USERINFO", userObj)
},
logoutUser({ commit }) {
commit("CLEAR_USERINFO")
}
}
})
11、router/index.js
javascript
import Vue from 'vue'
import VueRouter from 'vue-router'
import store from '@/store'
import AdminLayout from '@/components/AdminLayout.vue'
import Register from '@/views/Register.vue'
import Login from '@/views/Login.vue'
import Dashboard from '@/views/Dashboard.vue'
import PlayerManage from '@/views/PlayerManage.vue'
import ScheduleManage from '@/views/ScheduleManage.vue'
import TeamManage from '@/views/TeamManage.vue'
import Profile from '@/views/Profile.vue'
import MatchDetail from '@/views/MatchDetail.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/register',
component: Register
},
{
path: '/login',
component: Login
},
{
path: '/',
component: AdminLayout,
redirect: '/dashboard',
children: [
{ path: 'dashboard', component: Dashboard },
{ path: 'playerManage', component: PlayerManage },
{ path: 'scheduleManage', component: ScheduleManage },
{ path: 'teamManage', component: TeamManage },
{ path: 'profile', component: Profile }
]
},
//动态路由:比赛详情,接收matchId
{
path: '/matchDetail/:matchId',
component: MatchDetail,
props: true
}
]
const router = new VueRouter({
mode: 'hash',
routes
})
//全局前置守卫
router.beforeEach((to, from, next) => {
const isLogin = store.getters.isLogin
//注册、登录页面直接放行
if (to.path === '/register' || to.path === '/login') {
next()
} else {
if (isLogin) {
next()
} else {
//未登录跳转到注册页面
next('/register')
}
}
})
export default router
12、App.vue
javascript
<template>
<div id="app">
<router-view></router-view>
</div>
</template>
<script>
export default {
name: "App"
}
</script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
background-color: #f0f2f5;
}
</style>
13、main.js
javascript
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
Vue.config.productionTip = false
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')