实验十一 Vuex 实现王者荣耀后台比赛管理系统

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')
相关推荐
庖丁解牛1 小时前
第二课:把代码保存成网页 + 认识变量 (2)
前端
代码猎人1 小时前
怎么开发一个Vue3项目
前端
梦里寻码1 小时前
从 0 到 1 用 NestJS + LangChain 搭一套可售卖的 SaaS AI 客服:挂件一行代码接入,知识库秒变 SEO 帮助中心
前端·后端·github
三十而立洋1 小时前
前端模块化的演进:从全局变量到 Import Maps
前端·javascript·前端工程化
橘猫走江湖1 小时前
浮动塌陷复现失败:父元素为什么一直不塌
前端·css
用户1733598075371 小时前
纯前端 PDF 按书签拆分避坑指南:拆分后章节对不上?
前端·javascript·vue.js
мо仙堡杠把子ご灬1 小时前
Flex 自适应布局:告别 calc,用一个 height-auto 搞定
前端·javascript
环境栈笔记1 小时前
指纹浏览器检查代理失败怎么排查?先分清代理、网络和检测服务
前端·人工智能·后端·自动化
ShineWinsu2 小时前
对于HTML:HTML常见标签的解析—下
前端·css·面试·html·标签·表单·emmet