项目学习记录 (一)_successfully created project sku-info-chart. ge-程序员宅基地

技术标签: 学习  # Express基础  # Vue基础  

Node.js + Vue.js 全栈开发王者荣耀手机端官网和管理后台

创建项目

创建 npm 项目 server

新建文件夹 node-vue-moba,在此文件夹新建 server 文件夹

node-vue-moba % mkdir server

进入 server 目录,创建 npm 项目

node-vue-moba % cd server
server % npm init -y
Wrote to /node-vue-moba/server/package.json:

{
    
  "name": "server",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

创建 vue 项目 admin

node-vue-moba % vue create admin


Vue CLI v5.0.8
? Please pick a preset: Default ([Vue 3] babel, eslint)


Vue CLI v5.0.8
  Creating project in /node-vue-moba/admin.
  Initializing git repository...
️  Installing CLI plugins. This might take a while...


added 858 packages, and audited 859 packages in 3m

94 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
  Invoking generators...
  Installing additional dependencies...


added 92 packages, and audited 951 packages in 13s

107 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
  Running completion hooks...

  Generating README.md...

  Successfully created project admin.
  Get started with the following commands:

 $ cd admin
 $ npm run serve

创建 vue 项目 web

node-vue-moba % vue create web

Vue CLI v5.0.8
? Please pick a preset: Default ([Vue 3] babel, eslint)


Vue CLI v5.0.8
  Creating project in /node-vue-moba/web.
  Initializing git repository...
️  Installing CLI plugins. This might take a while...


added 858 packages, and audited 859 packages in 2m

94 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
  Invoking generators...
  Installing additional dependencies...


added 92 packages, and audited 951 packages in 15s

107 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
  Running completion hooks...

  Generating README.md...

  Successfully created project web.
  Get started with the following commands:

 $ cd web
 $ npm run serve

server package.json 修改

{
    
  "name": "server",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    
    "serve": "nodemon index.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

若是没有安装过nodemon ,先全局安装 nodemon

npm i -g nodemon

server 安装 express, mongoose, cors

node-vue-moba % cd server
server % npm i express@next mongoose cors

added 79 packages, and audited 80 packages in 26s

3 packages are looking for funding
  run `npm fund` for details

3 high severity vulnerabilities

To address issues that do not require attention, run:
  npm audit fix

To address all issues (including breaking changes), run:
  npm audit fix --force

Run `npm audit` for details.

在 server 新建文件 index.js

const express = require("express")

const app = express()

app.use(require('cors')())
app.use(express.json())

app.use('/uploads', express.static(__dirname + '/uploads'))

app.listen(3000, ()=>{
    
    console.log('http://localhost:3000');
});

启用服务

npm run serve

在浏览器打开 http://localhost:3000/
在这里插入图片描述

启动 admin

node-vue-moba % cd admin
admin % npm run serve

> [email protected] serve
> vue-cli-service serve

 INFO  Starting development server...


 DONE  Compiled successfully in 29989ms                                                                                                                                                                                   10:03:20


  App running at:
  - Local:   http://localhost:8080/ 
  - Network: http://192.168.50.81:8080/

  Note that the development build is not optimized.
  To create a production build, run npm run build.

在浏览器打开 http://localhost:8080/
在这里插入图片描述

admin 安装 element-plus

node-vue-moba % cd admin
admin % vue add element-plus

  Installing vue-cli-plugin-element-plus...


added 1 package, and audited 952 packages in 11s

107 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
  Successfully installed plugin: vue-cli-plugin-element-plus

? How do you want to import Element Plus? Fully import
? Do you want to overwrite the SCSS variables of Element Plus? No
? Choose the locale you want to load, the default locale is English (en) zh-cn

  Invoking generator for vue-cli-plugin-element-plus...
  Installing additional dependencies...


added 8 packages, and audited 960 packages in 24s

108 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
  Running completion hooks...

  Successfully invoked generator for plugin: vue-cli-plugin-element-plus

在浏览器打开 http://localhost:8080/ 发现页面发生了变化
在这里插入图片描述

admin 添加 router

admin % vue add router
 WARN  There are uncommitted changes in the current repository, it's recommended to commit or stash them first.
? Still proceed? Yes

  Installing @vue/cli-plugin-router...


up to date, audited 960 packages in 4s

108 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
  Successfully installed plugin: @vue/cli-plugin-router

? Use history mode for router? (Requires proper server setup for index fallback 
in production) No

  Invoking generator for @vue/cli-plugin-router...
  Installing additional dependencies...


added 2 packages, and audited 962 packages in 4s

109 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
  Running completion hooks...

  Successfully invoked generator for plugin: @vue/cli-plugin-router

在浏览器打开 http://localhost:8080/ 发现页面发生了变化
在这里插入图片描述

在 admin/src/views 新建文件 MainVue.vue

<template>
    <el-container style="height: 100vh;">
        <el-aside width="200px" style="background-color: rgb(238, 241, 246)">
            <el-menu router :default-openeds="['1']" unique-opened :default-active="$route.path">
                <el-submenu index="1">
                    <template #title>
                        <i class="el-icon-message"></i>内容管理
                    </template>
                    <el-menu-item-group>
                        <template #title>物品</template>
                        <el-menu-item index="/items/create">新建物品</el-menu-item>
                        <el-menu-item index="/items/list">物品列表</el-menu-item>
                    </el-menu-item-group>
                    <el-menu-item-group>
                        <template #title>英雄</template>
                        <el-menu-item index="/heroes/create">新建英雄</el-menu-item>
                        <el-menu-item index="/heroes/list">英雄列表</el-menu-item>
                    </el-menu-item-group>
                    <el-menu-item-group>
                        <template #title>文章</template>
                        <el-menu-item index="/articles/create">新建文章</el-menu-item>
                        <el-menu-item index="/articles/list">文章列表</el-menu-item>
                    </el-menu-item-group>
                </el-submenu>

                <el-submenu index="2">
                    <template #title>
                        <i class="el-icon-message"></i>运营管理
                    </template>
                    <el-menu-item-group>
                        <template #title>广告位</template>
                        <el-menu-item index="/ads/create">新建广告位</el-menu-item>
                        <el-menu-item index="/ads/list">广告位列表</el-menu-item>
                    </el-menu-item-group>
                </el-submenu>

                <el-submenu index="3">
                    <template #title>
                        <i class="el-icon-message"></i>系统设置
                    </template>
                    <el-menu-item-group>
                        <template #title>分类</template>
                        <el-menu-item index="/categories/create">新建分类</el-menu-item>
                        <el-menu-item index="/categories/list">分类列表</el-menu-item>
                    </el-menu-item-group>
                    <el-menu-item-group>
                        <template #title>管理员</template>
                        <el-menu-item index="/admin_users/create">新建管理员</el-menu-item>
                        <el-menu-item index="/admin_users/list">管理员列表</el-menu-item>
                    </el-menu-item-group>
                </el-submenu>
            </el-menu>
        </el-aside>

        <el-container>
            <el-header style="text-align: right; font-size: 12px">
                <el-dropdown>
                    <i class="el-icon-setting" style="margin-right: 15px"></i>
                    <template v-slot:dropdown>
                        <el-dropdown-menu>
                            <el-dropdown-item>查看</el-dropdown-item>
                            <el-dropdown-item>新增</el-dropdown-item>
                            <el-dropdown-item>删除</el-dropdown-item>
                        </el-dropdown-menu>
                    </template>
                </el-dropdown>
                <span>王小虎</span>
            </el-header>

            <el-main>
                <router-view :key="$route.path"></router-view>

            </el-main>
        </el-container>
    </el-container>
</template>
  
  
<style>
.el-header {
    
    background-color: #b3c0d1;
    color: #333;
    line-height: 60px;
}

.el-aside {
    
    color: #333;
}
</style>
  
<script>
export default {
    
    data() {
    
        const item = {
    
            date: "2016-05-02",
            name: "王小虎",
            address: "上海市普陀区金沙江路 1518 弄"
        };
        return {
    
            tableData: Array(20).fill(item)
        };
    }
};
</script>

修改 admin/src/router/index.js

import {
     createRouter, createWebHashHistory } from 'vue-router'
// import HomeView from '../views/HomeView.vue'
import MainView from '../views/MainVue.vue'

const routes = [
  // {
    
  //   path: '/',
  //   name: 'home',
  //   component: HomeView
  // },
  // {
    
  //   path: '/about',
  //   name: 'about',
  //   // route level code-splitting
  //   // this generates a separate chunk (about.[hash].js) for this route
  //   // which is lazy-loaded when the route is visited.
  //   component: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue')
  // }

  {
    
    path: '/',
    name: 'main',
    component: MainView
  }
]

const router = createRouter({
    
  history: createWebHashHistory(),
  routes
})

export default router

修改 admin/src/App.vue

<template>
  <!-- <nav>
    <router-link to="/">Home</router-link> |
    <router-link to="/about">About</router-link>
  </nav> -->
  <router-view/>
</template>

<style>
html,body{
    
  margin: 0;
  padding: 0;
}
/* #app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}

nav {
  padding: 30px;
}

nav a {
  font-weight: bold;
  color: #2c3e50;
}

nav a.router-link-exact-active {
  color: #42b983;
} */
</style>

在浏览器打开 http://localhost:8080/ 发现页面发生了变化
![在这里插入图片描述](https://img-blog.csdnimg.cn/910b2d4e21be4c2bb8e950cf95306a4d.png

在这里插入图片描述

新建文件

在 admin/src/views 新建文件 AdEdit.vue, AdList.vue, AdminUserEdit.vue, AdminUserList.vue, ArticleEdit.vue, ArticleList.vue, CategoryEdit.vue, CategoryList.vue, HeroEdit.vue, HeroList.vue, ItemEdit.vue, ItemList.vue, Login.vue

<template>
     <h1>This is an Category Edit page</h1>
</template>

修改 admin/src/router/index.js

import {
     createRouter, createWebHashHistory } from 'vue-router'
// import HomeView from '../views/HomeView.vue'
import MainView from '../views/MainVue.vue'

import LoginView from '../views/LoginView.vue'

import CategoryEdit from '../views/CategoryEdit.vue'
import CategoryList from '../views/CategoryList.vue'

import ItemEdit from '../views/ItemEdit.vue'
import ItemList from '../views/ItemList.vue'

import HeroEdit from '../views/HeroEdit.vue'
import HeroList from '../views/HeroList.vue'

import ArticleEdit from '../views/ArticleEdit.vue'
import ArticleList from '../views/ArticleList.vue'

import AdEdit from '../views/AdEdit.vue'
import AdList from '../views/AdList.vue'

import AdminUserEdit from '../views/AdminUserEdit.vue'
import AdminUserList from '../views/AdminUserList.vue'


const routes = [
  // {
    
  //   path: '/',
  //   name: 'home',
  //   component: HomeView
  // },
  // {
    
  //   path: '/about',
  //   name: 'about',
  //   // route level code-splitting
  //   // this generates a separate chunk (about.[hash].js) for this route
  //   // which is lazy-loaded when the route is visited.
  //   component: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue')
  // }
  {
     path: '/login', name: 'login', component: LoginView, meta: {
     isPublic: true } },
  {
    
    path: '/',
    name: 'main',
    component: MainView,
    children: [
      {
     path: '/categories/create', component: CategoryEdit },
      {
     path: '/categories/edit/:id', component: CategoryEdit, props: true },
      {
     path: '/categories/list', component: CategoryList },

      {
     path: '/items/create', component: ItemEdit },
      {
     path: '/items/edit/:id', component: ItemEdit, props: true },
      {
     path: '/items/list', component: ItemList },

      {
     path: '/heroes/create', component: HeroEdit },
      {
     path: '/heroes/edit/:id', component: HeroEdit, props: true },
      {
     path: '/heroes/list', component: HeroList },

      {
     path: '/articles/create', component: ArticleEdit },
      {
     path: '/articles/edit/:id', component: ArticleEdit, props: true },
      {
     path: '/articles/list', component: ArticleList },

      {
     path: '/ads/create', component: AdEdit },
      {
     path: '/ads/edit/:id', component: AdEdit, props: true },
      {
     path: '/ads/list', component: AdList },

      {
     path: '/admin_users/create', component: AdminUserEdit },
      {
     path: '/admin_users/edit/:id', component: AdminUserEdit, props: true },
      {
     path: '/admin_users/list', component: AdminUserList },

    ]

  }
]

const router = createRouter({
    
  history: createWebHashHistory(),
  routes
})

router.beforeEach((to, from ,next) => {
    
  if (!to.meta.isPublic && !localStorage.token) {
    
    return next('/login')
  }
  next()
})

export default router

admin 添加 axios

npm i axios --legacy-peer-deps

added 6 packages, and audited 968 packages in 9s

109 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

在 admin 新建文件 .env.development

VUE_APP_API_URL=http://localhost:3000/admin/api
  • .env 全局默认配置文件

  • .env.development 开发环境下的配置文件

  • .env.production 生产环境下的配置文件

如果我们运行 npm run serve 就会先加载 .env 文件,之后加载 .env.development 文件,如果两个文件有同一项,则后加载的文件就会覆盖掉第一个文件,即 .env.development 文件覆盖掉了 .env 文件的选项

同理,如果执行了 npm run build ,则就是加载了 .env 和 .env.production 文件

属性名必须以 VUE_APP 开头,比如 VUE_APP_API_URL

直接调用 process.env 属性(全局属性,任何地方都可以使用)比如 process.env.VUE_APP_API_URL

在 admin 新建文件 http.js

import axios from 'axios'
import router from './router'

const http = axios.create({
    
  baseURL: process.env.VUE_APP_API_URL || '/admin/api'
  // baseURL: 'http://localhost:3000/admin/api'
})

http.interceptors.request.use(function (config) {
    
  // Do something before request is sent
  if (localStorage.token) {
    
    config.headers.Authorization = 'Bearer ' + localStorage.token
  }
  return config;
}, function (error) {
    
  // Do something with request error
  return Promise.reject(error);
});

http.interceptors.response.use(res => {
    
  return res
}, err => {
    
  if (err.response.data.message) {
    
    this.$message({
    
      type: 'error',
      message: err.response.data.message
    })

    if (err.response.status === 401) {
    
      router.push('/login')
    }
  }

  return Promise.reject(err)
})

export default http

$ 是在 Vue 所有实例中都可用的 property 的一个简单约定。这样做会避免和已被定义的数据、方法、计算属性产生冲突。

  • 如果 vue 原型参数和组件中定义的参数相同,则会被覆盖,有冲突,建议使用 $ 定义原型参数
  • 如果 vue 原型参数和组件中定义的参数不相同,那么可以不使用 $ 定义

修改 admin/src/main.js

import {
     createApp } from 'vue'
import App from './App.vue'
import installElementPlus from './plugins/element'
import router from './router'

import http from './http'
Vue.prototype.$http = http

const app = createApp(App).use(router)
installElementPlus(app)
app.mount('#app')

vue3.x vs vue2.x

//=======vue3.x
//使用createApp函数来实例化vue,
//该函数接收一个根组件选项对象作为第一个参数
//使用第二个参数,我们可以将根 prop 传递给应用程序
import {
     createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

createApp(App,{
     userName: "blackLieo" })
.use(store)
.use(router)
.mount('#app')  
//由于 createApp 方法返回应用实例本身,因此可以在其后链式调用其它方法,这些方法可以在以下部分中找到。

//=======vue2.x
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

 Vue({
    
  router,
  store,
  render: h => h(App)
}).$mount('#app')

server 添加 bcrypt, http-assert, inflection, jsonwebtoken, multer, require-all

npm i http-assert inflection jsonwebtoken multer require-all     

added 32 packages, and audited 112 packages in 23s

4 packages are looking for funding
  run `npm fund` for details

3 high severity vulnerabilities

To address issues that do not require attention, run:
  npm audit fix

To address all issues (including breaking changes), run:
  npm audit fix --force

Run `npm audit` for details.
npm i bcrypt                                         

added 53 packages, and audited 165 packages in 32s

7 packages are looking for funding
  run `npm fund` for details

3 high severity vulnerabilities

To address issues that do not require attention, run:
  npm audit fix

To address all issues (including breaking changes), run:
  npm audit fix --force

Run `npm audit` for details.

在 server 新建文件夹 models, plugins, routes, middleware

在 plugins 新建 db.js 文件

module.exports = app => {
    
  const mongoose = require("mongoose")
  mongoose.connect('mongodb://127.0.0.1:27017/node-vue-moba', {
    
    useNewUrlParser: true
  })

  require('require-all')(__dirname + '/../models')
}

在 models 新建 Ad.js 文件

const mongoose = require('mongoose')

const schema = new mongoose.Schema({
    
  name: {
     type: String },
  items: [{
    
    image: {
     type: String },
    url: {
     type: String },
  }]
})

module.exports = mongoose.model('Ad', schema)

在 models 新建 AdminUser.js 文件

const mongoose = require('mongoose')

const schema = new mongoose.Schema({
    
  username: {
     type: String },
  password: {
    
    type: String,
    select: false,
    set(val) {
    
      return require('bcrypt').hashSync(val, 10)
    }
  },
})

module.exports = mongoose.model('AdminUser', schema)

在 models 新建 Article.js 文件

const mongoose = require('mongoose')

const schema = new mongoose.Schema({
    
  categories: [{
     type: mongoose.SchemaTypes.ObjectId, ref: 'Category' }],
  title: {
     type: String },
  body: {
     type: String },
}, {
    
  timestamps: true
})

module.exports = mongoose.model('Article', schema)

在 models 新建 Category.js 文件

const mongoose = require('mongoose')

const schema = new mongoose.Schema({
    
    name: {
     type: String },
    parent: {
     type: mongoose.SchemaTypes.ObjectId, ref: 'Category' },
})

schema.virtual('children', {
    
    localField: '_id',
    foreignField: 'parent',
    justOne: false,
    ref: 'Category'
})

schema.virtual('newsList', {
    
    localField: '_id',
    foreignField: 'categories',
    justOne: false,
    ref: 'Article'
})

module.exports = mongoose.model('Category', schema)

在 models 新建 Hero.js 文件

const mongoose = require('mongoose')

const schema = new mongoose.Schema({
    
  name: {
     type: String },
  avatar: {
     type: String },
  banner: {
     type: String },
  title: {
     type: String },
  categories: [{
     type: mongoose.SchemaTypes.ObjectId, ref: 'Category' }],
  scores: {
    
    difficult: {
     type: Number },
    skills: {
     type: Number },
    attack: {
     type: Number },
    survive: {
     type: Number },
  },
  skills: [{
    
    icon: {
     type: String },
    name: {
     type: String },
    delay: {
     type: String },
    cost: {
     type: String },
    description: {
     type: String },
    tips: {
     type: String },
  }],
  items1: [{
     type: mongoose.SchemaTypes.ObjectId, ref: 'Item' }],
  items2: [{
     type: mongoose.SchemaTypes.ObjectId, ref: 'Item' }],
  usageTips: {
     type: String },
  battleTips: {
     type: String },
  teamTips: {
     type: String },
  partners: [{
    
    hero: {
     type: mongoose.SchemaTypes.ObjectId, ref: 'Hero' },
    description: {
     type: String },
  }],
})

module.exports = mongoose.model('Hero', schema, 'heroes')

在 models 新建 Item.js 文件

const mongoose = require('mongoose')

const schema = new mongoose.Schema({
    
  name: {
     type: String },
  icon: {
     type: String },
})

module.exports = mongoose.model('Item', schema)

在 middleware 新建 auth.js 文件

module.exports = options => {
    
  const assert = require('http-assert')
  const jwt = require('jsonwebtoken')
  const AdminUser = require('../models/AdminUser')

  return async (req, res, next) => {
    
    const token = String(req.headers.authorization || '').split(' ').pop()
    assert(token, 401, '请先登录')
    const {
     id } = jwt.verify(token, req.app.get('secret'))
    assert(id, 401, '请先登录')
    req.user = await AdminUser.findById(id)
    assert(req.user, 401, '请先登录')
    await next()
  }
}

在 middleware 新建 resource.js 文件

module.exports = options => {
    
  return async (req, res, next) => {
    
    const modelName = require('inflection').classify(req.params.resource)
    req.Model = require(`../models/${
      modelName}`)
    next()
  }
}

在 routes 新建文件夹 admin, web

在 routes/admin 新建 index.js 文件

module.exports = app => {
    
    const express = require('express')
    const assert = require('http-assert')
    const jwt = require('jsonwebtoken')
    const AdminUser = require('../../models/AdminUser')
    const router = express.Router({
    
      mergeParams: true
    })
  
    // 创建资源
    router.post('/', async (req, res) => {
    
      const model = await req.Model.create(req.body)
      res.send(model)
    })
    // 更新资源
    router.put('/:id', async (req, res) => {
    
      const model = await req.Model.findByIdAndUpdate(req.params.id, req.body)
      res.send(model)
    })
    // 删除资源
    router.delete('/:id', async (req, res) => {
    
      await req.Model.findByIdAndDelete(req.params.id)
      res.send({
    
        success: true
      })
    })
    // 资源列表
    router.get('/', async (req, res) => {
    
      const queryOptions = {
    }
      if (req.Model.modelName === 'Category') {
    
        queryOptions.populate = 'parent'
      }
      const items = await req.Model.find().setOptions(queryOptions).limit(100)
      res.send(items)
    })
    // 资源详情
    router.get('/:id', async (req, res) => {
    
      const model = await req.Model.findById(req.params.id)
      res.send(model)
    })
    // 登录校验中间件
    const authMiddleware = require('../../middleware/auth')
    const resourceMiddleware = require('../../middleware/resource')
    app.use('/admin/api/rest/:resource', authMiddleware(), resourceMiddleware(), router)
  
    const multer = require('multer')
    // const MAO = require('multer-aliyun-oss');
    const upload = multer({
    
      dest: __dirname + '/../../uploads',
    //   storage: MAO({
    
    //     config: {
    
    //       region: 'oss-cn-zhangjiakou',
    //       accessKeyId: '替换为你的真实id',
    //       accessKeySecret: '替换为你的真实secret',
    //       bucket: 'node-vue-moba'
    //     }
    //   })
    })
    app.post('/admin/api/upload', authMiddleware(), upload.single('file'), async (req, res) => {
    
      const file = req.file
      // file.url = `http://test.topfullstack.com/uploads/${file.filename}`
      file.url = `http://localhost:3000/uploads/${
      file.filename}`
      res.send(file)
    })
  
    app.post('/admin/api/login', async (req, res) => {
    
      const {
     username, password } = req.body
      // 1.根据用户名找用户
  
      const user = await AdminUser.findOne({
     username }).select('+password')
      assert(user, 422, '用户不存在')
      // 2.校验密码
      const isValid = require('bcrypt').compareSync(password, user.password)
      assert(isValid, 422, '密码错误')
      // 3.返回token
      const token = jwt.sign({
     id: user._id }, app.get('secret'))
      res.send({
     token })
    })
  
    // 错误处理函数
    app.use(async (err, req, res, next) => {
    
      // console.log(err)
      res.status(err.statusCode || 500).send({
    
        message: err.message
      })
    })
  }

在 routes/web 新建 index.js 文件

module.exports = app => {
    
  const router = require('express').Router()
  const mongoose = require('mongoose')
  // const Article = require('../../models/Article')
  const Category = mongoose.model('Category')
  const Article = mongoose.model('Article')
  const Hero = mongoose.model('Hero')

  // 导入新闻数据
  router.get('/news/init', async (req, res) => {
    
    const parent = await Category.findOne({
    
      name: '新闻分类'
    })
    const cats = await Category.find().where({
    
      parent: parent
    }).lean()
    const newsTitles = ["夏日新版本“稷下星之队”即将6月上线", "王者荣耀携手两大博物馆 走进稷下学宫", "王者大陆第一学院【稷下】档案", "跨界合作丨控油神装登场,唤醒无限护肤力量!", "像素游戏时代“老四强”重聚《魂斗罗:归来》,新版本、新英雄燃爆两周年庆", "6月11日全服不停机更新公告", "【已修复】王者大陆的端午宝藏活动页面异常问题说明", "6月7日体验服停机更新公告", "6月4日全服不停机更新公告", "关于2019年KPL春季赛总决赛 RNG.M vs eStarPro 补赛、赛果及世界冠军杯安排公告", "活力夏日活动周 王者峡谷好礼多", "王者大陆的端午宝藏活动公告", "峡谷庆端午 惊喜礼不断", "【场里场外,一起开黑】感恩礼包放送", "KPL总决赛来临之际 场里场外一起开黑/观赛活动开启!", "【6月15日 再战西安 · 2019年KPL春季赛总决赛重启公告】", "王者荣耀世界冠军杯荣耀来袭,KPL赛区选拔赛谁能突围而出?", "【关于2019年KPL春季赛总决赛门票退换及异地用户现场观赛补贴公告】", "KRKPL:还在用庄周打辅助?JY边路庄周带你越塔莽!", "世冠KPL赛区战队出征名单公布 王者,无惧挑战!"]
    const newsList = newsTitles.map(title => {
    
      const randomCats = cats.slice(0).sort((a, b) => Math.random() - 0.5)
      return {
    
        categories: randomCats.slice(0, 2),
        title: title
      }
    })
    await Article.deleteMany({
    })
    await Article.insertMany(newsList)
    res.send(newsList)
  })

  // 新闻列表接口
  router.get('/news/list', async (req, res) => {
    
    // const parent = await Category.findOne({
    
    //   name: '新闻分类'
    // }).populate({
    
    //   path: 'children',
    //   populate: {
    
    //     path: 'newsList'
    //   }
    // }).lean()
    const parent = await Category.findOne({
    
      name: '新闻分类'
    })
    const cats = await Category.aggregate([
      {
     $match: {
     parent: parent._id } },
      {
    
        $lookup: {
    
          from: 'articles',
          localField: '_id',
          foreignField: 'categories',
          as: 'newsList'
        }
      },
      {
    
        $addFields: {
    
          newsList: {
     $slice: ['$newsList', 5] }
        }
      }
    ])
    const subCats = cats.map(v => v._id)
    cats.unshift({
    
      name: '热门',
      newsList: await Article.find().where({
    
        categories: {
     $in: subCats }
      }).populate('categories').limit(5).lean()
    })

    cats.map(cat => {
    
      cat.newsList.map(news => {
    
        news.categoryName = (cat.name === '热门')
          ? news.categories[0].name : cat.name
        return news
      })
      return cat
    })
    res.send(cats)

  })

  // 导入英雄数据
  router.get('/heroes/init', async (req, res) => {
    
    await Hero.deleteMany({
    })
    const rawData = [{
     "name": "热门", "heroes": [{
     "name": "后羿", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/169/169.jpg" }, {
     "name": "孙悟空", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/167/167.jpg" }, {
     "name": "铠", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/193/193.jpg" }, {
     "name": "鲁班七号", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/112/112.jpg" }, {
     "name": "亚瑟", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/166/166.jpg" }, {
     "name": "甄姬", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/127/127.jpg" }, {
     "name": "孙尚香", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/111/111.jpg" }, {
     "name": "典韦", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/129/129.jpg" }, {
     "name": "韩信", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/150/150.jpg" }, {
     "name": "庄周", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/113/113.jpg" }] }, {
     "name": "战士", "heroes": [{
     "name": "赵云", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/107/107.jpg" }, {
     "name": "钟无艳", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/117/117.jpg" }, {
     "name": "吕布", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/123/123.jpg" }, {
     "name": "曹操", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/128/128.jpg" }, {
     "name": "典韦", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/129/129.jpg" }, {
     "name": "宫本武藏", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/130/130.jpg" }, {
     "name": "达摩", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/134/134.jpg" }, {
     "name": "老夫子", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/139/139.jpg" }, {
     "name": "关羽", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/140/140.jpg" }, {
     "name": "露娜", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/146/146.jpg" }, {
     "name": "花木兰", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/154/154.jpg" }, {
     "name": "亚瑟", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/166/166.jpg" }, {
     "name": "孙悟空", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/167/167.jpg" }, {
     "name": "刘备", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/170/170.jpg" }, {
     "name": "杨戬", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/178/178.jpg" }, {
     "name": "雅典娜", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/183/183.jpg" }, {
     "name": "哪吒", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/180/180.jpg" }, {
     "name": "铠", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/193/193.jpg" }, {
     "name": "狂铁", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/503/503.jpg" }, {
     "name": "李信", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/507/507.jpg" }, {
     "name": "盘古", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/529/529.jpg" }] }, {
     "name": "法师", "heroes": [{
     "name": "小乔", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/106/106.jpg" }, {
     "name": "墨子", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/108/108.jpg" }, {
     "name": "妲己", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/109/109.jpg" }, {
     "name": "嬴政", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/110/110.jpg" }, {
     "name": "高渐离", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/115/115.jpg" }, {
     "name": "扁鹊", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/119/119.jpg" }, {
     "name": "芈月", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/121/121.jpg" }, {
     "name": "周瑜", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/124/124.jpg" }, {
     "name": "甄姬", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/127/127.jpg" }, {
     "name": "武则天", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/136/136.jpg" }, {
     "name": "貂蝉", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/141/141.jpg" }, {
     "name": "安琪拉", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/142/142.jpg" }, {
     "name": "姜子牙", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/148/148.jpg" }, {
     "name": "王昭君", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/152/152.jpg" }, {
     "name": "张良", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/156/156.jpg" }, {
     "name": "不知火舞", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/157/157.jpg" }, {
     "name": "钟馗", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/175/175.jpg" }, {
     "name": "诸葛亮", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/190/190.jpg" }, {
     "name": "干将莫邪", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/182/182.jpg" }, {
     "name": "女娲", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/179/179.jpg" }, {
     "name": "杨玉环", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/176/176.jpg" }, {
     "name": "弈星", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/197/197.jpg" }, {
     "name": "米莱狄", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/504/504.jpg" }, {
     "name": "沈梦溪", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/312/312.jpg" }, {
     "name": "上官婉儿", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/513/513.jpg" }, {
     "name": "嫦娥", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/515/515.jpg" }] }, {
     "name": "坦克", "heroes": [{
     "name": "廉颇", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/105/105.jpg" }, {
     "name": "刘禅", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/114/114.jpg" }, {
     "name": "白起", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/120/120.jpg" }, {
     "name": "夏侯惇", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/126/126.jpg" }, {
     "name": "项羽", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/135/135.jpg" }, {
     "name": "程咬金", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/144/144.jpg" }, {
     "name": "刘邦", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/149/149.jpg" }, {
     "name": "牛魔", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/168/168.jpg" }, {
     "name": "张飞", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/171/171.jpg" }, {
     "name": "东皇太一", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/187/187.jpg" }, {
     "name": "苏烈", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/194/194.jpg" }, {
     "name": "梦奇", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/198/198.jpg" }, {
     "name": "孙策", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/510/510.jpg" }, {
     "name": "猪八戒", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/511/511.jpg" }] }, {
     "name": "刺客", "heroes": [{
     "name": "阿轲", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/116/116.jpg" }, {
     "name": "李白", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/131/131.jpg" }, {
     "name": "韩信", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/150/150.jpg" }, {
     "name": "兰陵王", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/153/153.jpg" }, {
     "name": "娜可露露", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/162/162.jpg" }, {
     "name": "橘右京", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/163/163.jpg" }, {
     "name": "百里玄策", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/195/195.jpg" }, {
     "name": "裴擒虎", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/502/502.jpg" }, {
     "name": "元歌", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/125/125.jpg" }, {
     "name": "司马懿", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/137/137.jpg" }, {
     "name": "云中君", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/506/506.jpg" }] }, {
     "name": "射手", "heroes": [{
     "name": "孙尚香", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/111/111.jpg" }, {
     "name": "鲁班七号", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/112/112.jpg" }, {
     "name": "马可波罗", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/132/132.jpg" }, {
     "name": "狄仁杰", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/133/133.jpg" }, {
     "name": "后羿", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/169/169.jpg" }, {
     "name": "李元芳", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/173/173.jpg" }, {
     "name": "虞姬", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/174/174.jpg" }, {
     "name": "成吉思汗", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/177/177.jpg" }, {
     "name": "黄忠", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/192/192.jpg" }, {
     "name": "百里守约", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/196/196.jpg" }, {
     "name": "公孙离", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/199/199.jpg" }, {
     "name": "伽罗", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/508/508.jpg" }] }, {
     "name": "辅助", "heroes": [{
     "name": "庄周", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/113/113.jpg" }, {
     "name": "孙膑", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/118/118.jpg" }, {
     "name": "蔡文姬", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/184/184.jpg" }, {
     "name": "太乙真人", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/186/186.jpg" }, {
     "name": "大乔", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/191/191.jpg" }, {
     "name": "鬼谷子", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/189/189.jpg" }, {
     "name": "明世隐", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/501/501.jpg" }, {
     "name": "盾山", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/509/509.jpg" }, {
     "name": "瑶", "avatar": "https://game.gtimg.cn/images/yxzj/img201606/heroimg/505/505.jpg" }] }]
    for (let cat of rawData) {
    
      if (cat.name === '热门') {
    
        continue
      }
      // 找到当前分类在数据库中对应的数据
      const category = await Category.findOne({
    
        name: cat.name
      })
      cat.heroes = cat.heroes.map(hero => {
    
        hero.categories = [category]
        return hero
      })
      // 录入英雄
      await Hero.insertMany(cat.heroes)
    }

    res.send(await Hero.find())
  })

  // 英雄列表接口
  router.get('/heroes/list', async (req, res) => {
    
    const parent = await Category.findOne({
    
      name: '英雄分类'
    })
    const cats = await Category.aggregate([
      {
     $match: {
     parent: parent._id } },
      {
    
        $lookup: {
    
          from: 'heroes',
          localField: '_id',
          foreignField: 'categories',
          as: 'heroList'
        }
      }
    ])
    const subCats = cats.map(v => v._id)
    cats.unshift({
    
      name: '热门',
      heroList: await Hero.find().where({
    
        categories: {
     $in: subCats }
      }).limit(10).lean()
    })

    res.send(cats)

  });

  // 文章详情
  router.get('/articles/:id', async (req, res) => {
    
    const data = await Article.findById(req.params.id).lean()
    data.related = await Article.find().where({
    
      categories: {
     $in: data.categories }
    }).limit(2)
    res.send(data)
  })

  router.get('/heroes/:id', async (req, res) => {
    
    const data = await Hero
      .findById(req.params.id)
      .populate('categories items1 items2 partners.hero')
      .lean()
    res.send(data)
  })

  app.use('/web/api', router)
}

修改 server/index.js

const express = require("express")

const app = express()

app.set('secret', 'i2u34y12oi3u4y8')

app.use(require('cors')())
app.use(express.json())

app.use('/', express.static(__dirname + '/web'))
app.use('/admin', express.static(__dirname + '/admin'))

app.use('/uploads', express.static(__dirname + '/uploads'))

require('./plugins/db')(app)
require('./routes/admin')(app)
require('./routes/web')(app)

app.listen(3000, ()=>{
    
    console.log('http://localhost:3000');
});

修改 server/routes/admin/index.js

  app.post('/admin/api/login', async (req, res) => {
    
      const {
     username, password } = req.body
      // 1.根据用户名找用户
      AdminUser.create(req.body)
  
      const user = await AdminUser.findOne({
     username }).select('+password')
      assert(user, 422, '用户不存在')
      // 2.校验密码
      const isValid = require('bcrypt').compareSync(password, user.password)
      assert(isValid, 422, '密码错误')
      // 3.返回token
      const token = jwt.sign({
     id: user._id }, app.get('secret'))
      res.send({
     token })
    })
  
    // 错误处理函数
    app.use(async (err, req, res, next) => {
    
      // console.log(err)
      res.status(err.statusCode || 500).send({
    
        message: err.message
      })
    })

通过 AdminUser.create(req.body) 添加用户,以便登陆,添加需要的用户名密码后删除该行

修改 admin/src/http.js

import {
    ElMessage} from 'element-plus'

 // this.$message({
    
    //   type: 'error',
    //   message: err.response.data.message
    // })
    
    ElMessage({
    
      type: 'error',
      message: err.response.data.message
    })
    

在 admin/src 新建 style.css 文件

.avatar-uploader .el-upload {
    
    border: 1px dashed #d9d9d9;
    border-radius: 6px;
    cursor: pointer;
    position: relative;
    overflow: hidden;
  }
  .avatar-uploader .el-upload:hover {
    
    border-color: #409eff;
  }
  .avatar-uploader-icon {
    
    font-size: 28px;
    color: #8c939d;
    min-width: 5rem;
    height: 5rem;
    line-height: 5rem;
    text-align: center;
  }
  .avatar {
    
    min-width: 5rem;
    height: 5rem;
    display: block;
  }

修改 admin/src/main.js

import './style.css'



app.mixin({
    
    computed: {
    
      uploadUrl(){
    
        return this.$http.defaults.baseURL + '/upload'
      }
    },
    methods: {
    
      getAuthHeaders(){
    
        return {
    
          Authorization: `Bearer ${
      localStorage.token || ''}`
        }
      }
    }
  })

admin 添加 vue3-editor

npm i vue3-editor --legacy-peer-deps

added 16 packages, and audited 984 packages in 17s

117 packages are looking for funding
  run `npm fund` for details

2 moderate severity vulnerabilities

Some issues need review, and may require choosing
a different dependency.

Run `npm audit` for details.

修改 admin/src/views

AdEdit.vue

<template>
     <div class="about">
          <h1>{
    {
     id ? '编辑' : '新建' }}广告位</h1>
          <el-form label-width="120px" @submit.prevent="save">
               <el-form-item label="名称">
                    <el-input v-model="model.name"></el-input>
               </el-form-item>
               <el-form-item label="广告">
                    <el-button size="small" @click="model.items.push({})">
                         <i class="el-icon-plus"></i> 添加广告
                    </el-button>
                    <el-row type="flex" style="flex-wrap: wrap">
                         <el-col :md="24" v-for="(item, i) in model.items" :key="i">
                              <el-form-item label="跳转链接 (URL)">
                                   <el-input v-model="item.url"></el-input>
                              </el-form-item>
                              <el-form-item label="图片" style="margin-top: 0.5rem;">
                                   <el-upload class="avatar-uploader" :action="uploadUrl" :headers="getAuthHeaders()"
                                        :show-file-list="false" :on-success="res => item.image = res.url">
                                        <img v-if="item.image" :src="item.image" class="avatar">
                                        <i v-else class="el-icon-plus avatar-uploader-icon"></i>
                                   </el-upload>
                              </el-form-item>

                              <el-form-item>
                                   <el-button size="small" type="danger" @click="model.items.splice(i, 1)">删除</el-button>
                              </el-form-item>
                         </el-col>
                    </el-row>
               </el-form-item>
               <el-form-item>
                    <el-button type="primary" native-type="submit">保存</el-button>
               </el-form-item>
          </el-form>
     </div>
</template>
   
<script>
export default {
    
     props: {
    
          id: {
    }
     },
     data() {
    
          return {
    
               model: {
    
                    items: []
               }
          };
     },
     methods: {
    
          async save() {
    
               //     let res;
               if (this.id) {
    
                    await this.$http.put(`rest/ads/${
      this.id}`, this.model);//res = 
               } else {
    
                    await this.$http.post("rest/ads", this.model);//res = 
               }
               this.$router.push("/ads/list");
               this.$message({
    
                    type: "success",
                    message: "保存成功"
               });
          },
          async fetch() {
    
               const res = await this.$http.get(`rest/ads/${
      this.id}`);
               this.model = Object.assign({
    }, this.model, res.data);
          }
     },
     created() {
    
          this.id && this.fetch();
     }
};
</script>

AdList.vue

<template>
     <div>
       <h1>广告位列表</h1>
       <el-table :data="items">
         <el-table-column prop="_id" label="ID" width="240"></el-table-column>
         <el-table-column prop="name" label="名称"></el-table-column>
         <el-table-column fixed="right" label="操作" width="180">
           <template v-slot="scope">
             <el-button
               type="text"
               size="small"
               @click="$router.push(`/ads/edit/${scope.row._id}`)"
             >编辑</el-button>
             <el-button type="text" size="small" @click="remove(scope.row)">删除</el-button>
           </template>
         </el-table-column>
       </el-table>
     </div>
   </template>
   
   <script>
   export default {
    
     data() {
    
       return {
    
         items: []
       };
     },
     methods: {
    
       async fetch() {
    
         const res = await this.$http.get("rest/ads");
         this.items = res.data;
       },
       remove(row) {
    
         this.$confirm(`是否确定要删除 "${
      row.name}"`, "提示", {
    
           confirmButtonText: "确定",
           cancelButtonText: "取消",
           type: "warning"
         }).then(async () => {
    
           await this.$http.delete(`rest/ads/${
      row._id}`);//const res = 
           this.$message({
    
             type: "success",
             message: "删除成功!"
           });
           this.fetch();
         });
       }
     },
     created() {
    
       this.fetch();
     }
   };
   </script>

AdminUserEdit.vue

<template>
     <div class="about">
       <h1>{
    {
    id ? '编辑' : '新建'}}管理员</h1>
       <el-form label-width="120px" @submit.prevent="save">
         
         <el-form-item label="用户名">
           <el-input v-model="model.username"></el-input>
         </el-form-item>
         <el-form-item label="密码">
           <el-input type="text" v-model="model.password"></el-input>
         </el-form-item>
         <el-form-item>
           <el-button type="primary" native-type="submit">保存</el-button>
         </el-form-item>
       </el-form>
     </div>
   </template>
   
   <script>
   export default {
    
     props: {
    
       id: {
    }
     },
     data(){
    
       return {
    
         model: {
    },
         
       }
     },
     methods: {
    
       async save(){
    
     //     let res
         if (this.id) {
    
           await this.$http.put(`rest/admin_users/${
      this.id}`, this.model)//res = 
         } else {
    
           await this.$http.post('rest/admin_users', this.model)//res = 
         }
         this.$router.push('/admin_users/list')
         this.$message({
    
           type: 'success',
           message: '保存成功'
         })
       },
       async fetch(){
    
         const res = await this.$http.get(`rest/admin_users/${
      this.id}`)
         this.model = res.data
       },
       
       
     },
     created(){
    
       this.id && this.fetch()
     }
   }
   </script>
   

AdminUserList.vue

<template>
     <div>
       <h1>管理员列表</h1>
       <el-table :data="items">
         <el-table-column prop="_id" label="ID" width="240"></el-table-column>
         <el-table-column prop="username" label="用户名"></el-table-column>
         <el-table-column fixed="right" label="操作" width="180">
           <template v-slot="scope">
             <el-button
               type="text"
               size="small"
               @click="$router.push(`/admin_users/edit/${scope.row._id}`)"
             >编辑</el-button>
             <el-button type="text" size="small" @click="remove(scope.row)">删除</el-button>
           </template>
         </el-table-column>
       </el-table>
     </div>
   </template>
   
   <script>
   export default {
    
     data() {
    
       return {
    
         items: []
       };
     },
     methods: {
    
       async fetch() {
    
         const res = await this.$http.get("rest/admin_users");
         this.items = res.data;
       },
       remove(row) {
    
         this.$confirm(`是否确定要删除 "${
      row.name}"`, "提示", {
    
           confirmButtonText: "确定",
           cancelButtonText: "取消",
           type: "warning"
         }).then(async () => {
    
           await this.$http.delete(`rest/admin_users/${
      row._id}`);//const res = 
           this.$message({
    
             type: "success",
             message: "删除成功!"
           });
           this.fetch();
         });
       }
     },
     created() {
    
       this.fetch();
     }
   };
   </script>

ArticleEdit.vue

<template>
     <div class="about">
       <h1>{
    {
    id ? '编辑' : '新建'}}文章</h1>
       <el-form label-width="120px" @submit.prevent="save">
         <el-form-item label="所属分类">
           <el-select v-model="model.categories" multiple>
             <el-option
               v-for="item in categories"
               :key="item._id"
               :label="item.name"
               :value="item._id"
             ></el-option>
           </el-select>
         </el-form-item>
         <el-form-item label="标题">
           <el-input v-model="model.title"></el-input>
         </el-form-item>
         <el-form-item label="详情">
           <vue-editor v-model="model.body" useCustomImageHandler @imageAdded="handleImageAdded"></vue-editor>
         </el-form-item>
         <el-form-item>
           <el-button type="primary" native-type="submit">保存</el-button>
         </el-form-item>
       </el-form>
     </div>
   </template>
   
   <script>
   import {
     VueEditor } from "vue3-editor";
   
   export default {
    
     props: {
    
       id: {
    }
     },
     components: {
    
       VueEditor
     },
     data() {
    
       return {
    
         model: {
    },
         categories: []
       };
     },
     methods: {
    
       async handleImageAdded(file, Editor, cursorLocation, resetUploader) {
    
         const formData = new FormData();
         formData.append("file", file);
         const res = await this.$http.post("upload", formData);
         Editor.insertEmbed(cursorLocation, "image", res.data.url);
         resetUploader();
       },
       async save() {
    
     //     let res;
         if (this.id) {
    
           await this.$http.put(`rest/articles/${
      this.id}`, this.model);//res = 
         } else {
    
           await this.$http.post("rest/articles", this.model);//res = 
         }
         this.$router.push("/articles/list");
         this.$message({
    
           type: "success",
           message: "保存成功"
         });
       },
       async fetch() {
    
         const res = await this.$http.get(`rest/articles/${
      this.id}`);
         this.model = res.data;
       },
       async fetchCatgories() {
    
         const res = await this.$http.get(`rest/categories`);
         this.categories = res.data;
       }
     },
     created() {
    
       this.fetchCatgories();
       this.id && this.fetch();
     }
   };
   </script>
   

ArticleList.vue

<template>
     <div>
       <h1>文章列表</h1>
       <el-table :data="items">
         <el-table-column prop="_id" label="ID" width="240"></el-table-column>
         <el-table-column prop="title" label="标题"></el-table-column>
         <el-table-column fixed="right" label="操作" width="180">
           <template v-slot="scope">
             <el-button
               type="text"
               size="small"
               @click="$router.push(`/articles/edit/${scope.row._id}`)"
             >编辑</el-button>
             <el-button type="text" size="small" @click="remove(scope.row)">删除</el-button>
           </template>
         </el-table-column>
       </el-table>
     </div>
   </template>
   
   <script>
   export default {
    
     data() {
    
       return {
    
         items: []
       };
     },
     methods: {
    
       async fetch() {
    
         const res = await this.$http.get("rest/articles");
         this.items = res.data;
       },
       remove(row) {
    
         this.$confirm(`是否确定要删除文章 "${
      row.title}"`, "提示", {
    
           confirmButtonText: "确定",
           cancelButtonText: "取消",
           type: "warning"
         }).then(async () => {
    
           await this.$http.delete(`rest/articles/${
      row._id}`);//const res = 
           this.$message({
    
             type: "success",
             message: "删除成功!"
           });
           this.fetch();
         });
       }
     },
     created() {
    
       this.fetch();
     }
   };
   </script>

CategoryEdit.vue

<template>
     <div class="about">
       <h1>{
    {
    id ? '编辑' : '新建'}}分类</h1>
       <el-form label-width="120px" @submit.prevent="save">
         <el-form-item label="上级分类">
           <el-select v-model="model.parent">
             <el-option v-for="item in parents" :key="item._id"
             :label="item.name" :value="item._id"></el-option>
           </el-select>
         </el-form-item>
         <el-form-item label="名称">
           <el-input v-model="model.name"></el-input>
         </el-form-item>
         <el-form-item>
           <el-button type="primary" native-type="submit">保存</el-button>
         </el-form-item>
       </el-form>
     </div>
   </template>
   
   <script>
   export default {
    
     props: {
    
       id: {
    }
     },
     data(){
    
       return {
    
         model: {
    },
         parents: [],
       }
     },
     methods: {
    
       async save(){
    
     //     let res
         if (this.id) {
    
           await this.$http.put(`rest/categories/${
      this.id}`, this.model)//res = 
         } else {
    
           await this.$http.post('rest/categories', this.model)//res = 
         }
         this.$router.push('/categories/list')
         this.$message({
    
           type: 'success',
           message: '保存成功'
         })
       },
       async fetch(){
    
         const res = await this.$http.get(`rest/categories/${
      this.id}`)
         this.model = res.data
       },
       async fetchParents(){
    
         const res = await this.$http.get(`rest/categories`)
         this.parents = res.data
       },
       
     },
     created(){
    
       this.fetchParents()
       this.id && this.fetch()
     }
   }
   </script>
   

CategoryList.vue

<template>
     <div>
       <h1>分类列表</h1>
       <el-table :data="items">
         <el-table-column prop="_id" label="ID" width="240"></el-table-column>
         <el-table-column prop="parent.name" label="上级分类"></el-table-column>
         <el-table-column prop="name" label="分类名称"></el-table-column>
         <el-table-column fixed="right" label="操作" width="180">
           <template v-slot="scope">
             <el-button
               type="text"
               size="small"
               @click="$router.push(`/categories/edit/${scope.row._id}`)"
             >编辑</el-button>
             <el-button type="text" size="small" @click="remove(scope.row)">删除</el-button>
           </template>
         </el-table-column>
       </el-table>
     </div>
   </template>
   
   <script>
   export default {
    
     data() {
    
       return {
    
         items: []
       };
     },
     methods: {
    
       async fetch() {
    
         const res = await this.$http.get("rest/categories");
         this.items = res.data;
       },
       remove(row) {
    
         this.$confirm(`是否确定要删除分类 "${
      row.name}"`, "提示", {
    
           confirmButtonText: "确定",
           cancelButtonText: "取消",
           type: "warning"
         }).then(async () => {
    
           await this.$http.delete(`rest/categories/${
      row._id}`);//const res = 
           this.$message({
    
             type: "success",
             message: "删除成功!"
           });
           this.fetch();
         });
       }
     },
     created() {
    
       this.fetch();
     }
   };
   </script>

HeroEdit.vue

<template>
     <div class="about">
       <h1>{
    {
    id ? '编辑' : '新建'}}英雄</h1>
       <el-form label-width="120px" @submit.prevent="save">
         <el-tabs value="basic" type="border-card">
           <el-tab-pane label="基础信息" name="basic">
             <el-form-item label="名称">
               <el-input v-model="model.name"></el-input>
             </el-form-item>
             <el-form-item label="称号">
               <el-input v-model="model.title"></el-input>
             </el-form-item>
             <el-form-item label="头像">
               <el-upload
                 class="avatar-uploader"
                 :action="uploadUrl"
                 :headers="getAuthHeaders()"
                 :show-file-list="false"
                 :on-success="res => model.avatar=res.url"
               >
                 <img v-if="model.avatar" :src="model.avatar" class="avatar">
                 <i v-else class="el-icon-plus avatar-uploader-icon"></i>
               </el-upload>
             </el-form-item>
   
             <el-form-item label="Banner">
               <el-upload
                 class="avatar-uploader"
                 :action="uploadUrl"
                 :headers="getAuthHeaders()"
                 :show-file-list="false"
                 :on-success="res => model.banner=res.url"
               >
                 <img v-if="model.banner" :src="model.banner" class="avatar">
                 <i v-else class="el-icon-plus avatar-uploader-icon"></i>
               </el-upload>
             </el-form-item>
   
             <el-form-item label="类型">
               <el-select v-model="model.categories" multiple>
                 <el-option
                   v-for="item of categories"
                   :key="item._id"
                   :label="item.name"
                   :value="item._id"
                 ></el-option>
               </el-select>
             </el-form-item>
             <el-form-item label="难度">
               <el-rate style="margin-top:0.6rem" :max="9" show-score v-model="model.scores.difficult"></el-rate>
             </el-form-item>
             <el-form-item label="技能">
               <el-rate style="margin-top:0.6rem" :max="9" show-score v-model="model.scores.skills"></el-rate>
             </el-form-item>
             <el-form-item label="攻击">
               <el-rate style="margin-top:0.6rem" :max="9" show-score v-model="model.scores.attack"></el-rate>
             </el-form-item>
             <el-form-item label="生存">
               <el-rate style="margin-top:0.6rem" :max="9" show-score v-model="model.scores.survive"></el-rate>
             </el-form-item>
   
             <el-form-item label="顺风出装">
               <el-select v-model="model.items1" multiple>
                 <el-option v-for="item of items" :key="item._id" :label="item.name" :value="item._id"></el-option>
               </el-select>
             </el-form-item>
             <el-form-item label="逆风出装">
               <el-select v-model="model.items2" multiple>
                 <el-option v-for="item of items" :key="item._id" :label="item.name" :value="item._id"></el-option>
               </el-select>
             </el-form-item>
             <el-form-item label="使用技巧">
               <el-input type="textarea" v-model="model.usageTips"></el-input>
             </el-form-item>
             <el-form-item label="对抗技巧">
               <el-input type="textarea" v-model="model.battleTips"></el-input>
             </el-form-item>
             <el-form-item label="团战思路">
               <el-input type="textarea" v-model="model.teamTips"></el-input>
             </el-form-item>
           </el-tab-pane>
           <el-tab-pane label="技能" name="skills">
             <el-button size="small" @click="model.skills.push({})">
               <i class="el-icon-plus"></i> 添加技能
             </el-button>
             <el-row type="flex" style="flex-wrap: wrap">
               <el-col :md="12" v-for="(item, i) in model.skills" :key="i">
                 <el-form-item label="名称">
                   <el-input v-model="item.name"></el-input>
                 </el-form-item>
                 <el-form-item label="图标">
                   <el-upload
                     class="avatar-uploader"
                     :action="uploadUrl"
                     :headers="getAuthHeaders()"
                     :show-file-list="false"
                     :on-success="res => item.icon=res.url"
                   >
                     <img v-if="item.icon" :src="item.icon" class="avatar">
                     <i v-else class="el-icon-plus avatar-uploader-icon"></i>
                   </el-upload>
                 </el-form-item>
                 <el-form-item label="冷却值">
                   <el-input v-model="item.delay"></el-input>
                 </el-form-item>
                 <el-form-item label="消耗">
                   <el-input v-model="item.cost"></el-input>
                 </el-form-item>
                 <el-form-item label="描述">
                   <el-input v-model="item.description" type="textarea"></el-input>
                 </el-form-item>
                 <el-form-item label="小提示">
                   <el-input v-model="item.tips" type="textarea"></el-input>
                 </el-form-item>
                 <el-form-item>
                   <el-button size="small" type="danger" @click="model.skills.splice(i, 1)">删除</el-button>
                 </el-form-item>
               </el-col>
             </el-row>
           </el-tab-pane>
   
           <el-tab-pane label="最佳搭档" name="partners">
             <el-button size="small" @click="model.partners.push({})">
               <i class="el-icon-plus"></i> 添加英雄
             </el-button>
             <el-row type="flex" style="flex-wrap: wrap">
               <el-col :md="12" v-for="(item, i) in model.partners" :key="i">
                 <el-form-item label="英雄">
                   <el-select filterable v-model="item.hero">
                     <el-option 
                     v-for="hero in heroes"
                     :key="hero._id"
                     :value="hero._id"
                     :label="hero.name"
                     ></el-option>
                   </el-select>
                 </el-form-item>
                 <el-form-item label="描述">
                   <el-input v-model="item.description" type="textarea"></el-input>
                 </el-form-item>
                 <el-form-item>
                   <el-button size="small" type="danger" @click="model.partners.splice(i, 1)">删除</el-button>
                 </el-form-item>
               </el-col>
             </el-row>
           </el-tab-pane>
         </el-tabs>
         <el-form-item style="margin-top: 1rem;">
           <el-button type="primary" native-type="submit">保存</el-button>
         </el-form-item>
       </el-form>
     </div>
   </template>
   
   <script>
   export default {
    
     props: {
    
       id: {
    }
     },
     data() {
    
       return {
    
         categories: [],
         items: [],
         heroes: [],
         model: {
    
           name: "",
           avatar: "",
           skills: [],
           partners: [],
           scores: {
    
             difficult: 0
           }
         }
       };
     },
     methods: {
    
       async save() {
    
     //     let res;
         if (this.id) {
    
           await this.$http.put(`rest/heroes/${
      this.id}`, this.model);//res = 
         } else {
    
           await this.$http.post("rest/heroes", this.model);//res = 
         }
         // this.$router.push("/heroes/list");
         this.$message({
    
           type: "success",
           message: "保存成功"
         });
       },
       async fetch() {
    
         const res = await this.$http.get(`rest/heroes/${
      this.id}`);
         this.model = Object.assign({
    }, this.model, res.data);
       },
       async fetchCategories() {
    
         const res = await this.$http.get(`rest/categories`);
         this.categories = res.data;
       },
       async fetchItems() {
    
         const res = await this.$http.get(`rest/items`);
         this.items = res.data;
       },
       async fetchHeroes() {
    
         const res = await this.$http.get(`rest/heroes`);
         this.heroes = res.data;
       }
     },
     created() {
    
       this.fetchItems();
       this.fetchCategories();
       this.fetchHeroes();
       this.id && this.fetch();
     }
   };
   </script>
   
   <style>
   </style>
   

HeroList.vue

<template>
     <div>
       <h1>英雄列表</h1>
       <el-table :data="items">
         <el-table-column prop="_id" label="ID" width="240"></el-table-column>
         <el-table-column prop="name" label="英雄名称"></el-table-column>
         <el-table-column prop="title" label="称号"></el-table-column>
         <el-table-column prop="avatar" label="头像">
           <template v-slot="scope">
             <img :src="scope.row.avatar" style="height:3rem;">
           </template>
         </el-table-column>
         <el-table-column fixed="right" label="操作" width="180">
           <template v-slot="scope">
             <el-button
               type="text"
               size="small"
               @click="$router.push(`/heroes/edit/${scope.row._id}`)"
             >编辑</el-button>
             <el-button type="text" size="small" @click="remove(scope.row)">删除</el-button>
           </template>
         </el-table-column>
       </el-table>
     </div>
   </template>
   
   <script>
   export default {
    
     data() {
    
       return {
    
         items: []
       };
     },
     methods: {
    
       async fetch() {
    
         const res = await this.$http.get("rest/heroes");
         this.items = res.data;
       },
       remove(row) {
    
         this.$confirm(`是否确定要删除分类 "${
      row.name}"`, "提示", {
    
           confirmButtonText: "确定",
           cancelButtonText: "取消",
           type: "warning"
         }).then(async () => {
    
           await this.$http.delete(`rest/heroes/${
      row._id}`);//const res = 
           this.$message({
    
             type: "success",
             message: "删除成功!"
           });
           this.fetch();
         });
       }
     },
     created() {
    
       this.fetch();
     }
   };
   </script>

ItemEdit.vue

<template>
     <div class="about">
       <h1>{
    {
    id ? '编辑' : '新建'}}物品</h1>
       <el-form label-width="120px" @submit.prevent="save">
         <el-form-item label="名称">
           <el-input v-model="model.name"></el-input>
         </el-form-item>
         <el-form-item label="图标">
           <el-upload
             class="avatar-uploader"
             :action="uploadUrl"
             :headers="getAuthHeaders()"
             :show-file-list="false"
             :on-success="afterUpload"
           >
             <img v-if="model.icon" :src="model.icon" class="avatar">
             <i v-else class="el-icon-plus avatar-uploader-icon"></i>
           </el-upload>
         </el-form-item>
         <el-form-item>
           <el-button type="primary" native-type="submit">保存</el-button>
         </el-form-item>
       </el-form>
     </div>
   </template>
   
   <script>
   export default {
    
     props: {
    
       id: {
    }
     },
     data() {
    
       return {
    
         model: {
    }
       };
     },
     methods: {
    
       afterUpload(res){
    
     //     this.$set(this.model, 'icon', res.url)
         this.model.icon = res.url
       },
       async save() {
    
     //     let res;
         if (this.id) {
    
           await this.$http.put(`rest/items/${
      this.id}`, this.model);//res = 
         } else {
    
           await this.$http.post("rest/items", this.model);//res = 
         }
         this.$router.push("/items/list");
         this.$message({
    
           type: "success",
           message: "保存成功"
         });
       },
       async fetch() {
    
         const res = await this.$http.get(`rest/items/${
      this.id}`);
         this.model = res.data;
       }
     },
     created() {
    
       this.id && this.fetch();
     }
   };
   </script>

ItemList.vue

<template>
     <div>
          <h1>物品列表</h1>
          <el-table :data="items">
               <el-table-column prop="_id" label="ID" width="240"></el-table-column>
               <el-table-column prop="name" label="物品名称"></el-table-column>
               <el-table-column prop="icon" label="图标">
                    <template v-slot="scope">
                         <img :src="scope.row.icon" style="height:3rem;">
                    </template>
               </el-table-column>
               <el-table-column fixed="right" label="操作" width="180">
                    <template v-slot="scope">
                         <el-button type="text" size="small"
                              @click="$router.push(`/items/edit/${scope.row._id}`)">编辑</el-button>
                         <el-button type="text" size="small" @click="remove(scope.row)">删除</el-button>
                    </template>
               </el-table-column>
          </el-table>
     </div>
</template>
   
<script>
export default {
    
     data() {
    
          return {
    
               items: []
          };
     },
     methods: {
    
          async fetch() {
    
               const res = await this.$http.get("rest/items");
               this.items = res.data;
          },
          remove(row) {
    
               this.$confirm(`是否确定要删除分类 "${
      row.name}"`, "提示", {
    
                    confirmButtonText: "确定",
                    cancelButtonText: "取消",
                    type: "warning"
               }).then(async () => {
    
                    await this.$http.delete(`rest/items/${
      row._id}`);//const res = 
                    this.$message({
    
                         type: "success",
                         message: "删除成功!"
                    });
                    this.fetch();
               });
          }
     },
     created() {
    
          this.fetch();
     }
};
</script>

Login.vue

<template>
     <div class="login-container">
          <el-card header="请先登录" class="login-card">
               <el-form @submit.prevent="login">
                    <el-form-item label="用户名">
                         <el-input v-model="model.username"></el-input>
                    </el-form-item>
                    <el-form-item label="密码">
                         <el-input type="password" v-model="model.password"></el-input>
                    </el-form-item>
                    <el-form-item>
                         <el-button type="primary" native-type="submit">登录</el-button>
                    </el-form-item>

               </el-form>
          </el-card>
     </div>
</template>
   
<script>
export default {
    
     data() {
    
          return {
    
               model: {
    }
          }
     },
     methods: {
    
          async login() {
    
               const res = await this.$http.post('login', this.model)
               // sessionStorage.token = res.data.token
               localStorage.token = res.data.token
               this.$router.push('/')
               this.$message({
    
                    type: 'success',
                    message: '登录成功'
               })
               
          }
     }
}
</script>
   
<style>
.login-card {
    
     width: 25rem;
     margin: 5rem auto;
}
</style>

web 添加 axios, dayjs, vue-router,swiper, node-sass, sass-loader

web % vue add router

  Installing @vue/cli-plugin-router...


up to date, audited 951 packages in 7s

107 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
  Successfully installed plugin: @vue/cli-plugin-router

? Use history mode for router? (Requires proper server setup for index fallback in production) No

  Invoking generator for @vue/cli-plugin-router...
  Installing additional dependencies...


added 2 packages, and audited 953 packages in 3s

108 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
  Running completion hooks...

  Successfully invoked generator for plugin: @vue/cli-plugin-router

npm i axios dayjs swiper

added 8 packages, and audited 961 packages in 13s

109 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

npm install --save-dev node-sass sass-loader
npm WARN deprecated @npmcli/[email protected]: This functionality has been moved to @npmcli/fs
npm WARN deprecated @npmcli/[email protected]: This functionality has been moved to @npmcli/fs

added 133 packages, and audited 1094 packages in 29m

118 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

web 新建 .env.development

VUE_APP_API_URL=http://localhost:3000/web/api

web/src/assets 添加 iconfont, images, scss

在这里插入图片描述

修改 web/src/components

新建 Card.vue

<template>
    <div class="card bg-white p-3 mt-3">
      <div class="card-header d-flex ai-center" 
      :class="{'border-bottom': !plain, 'pb-3': !plain}">
        <i class="iconfont" :class="`icon-${icon}`"></i>
        <div class="fs-xl flex-1 px-2">
          <strong>{
    {
    title}}</strong>
        </div>
        <i class="iconfont icon-menu" v-if="!plain"></i>
      </div>
      <div class="card-body pt-3">
        <slot></slot>
      </div>
    </div>
  </template>
  
  <script>
  export default {
    
    props: {
    
      title: {
     type: String, required: true },
      icon: {
     type: String, required: true },
      plain: {
     type: Boolean }
    }
  };
  </script>
  
  <style lang="scss">
  @import "../assets/scss/_variables.scss";
  .card {
    
    border-bottom: 1px solid $border-color;
  }
  </style>

新建 ListCard.vue

<template>
    <m-card :icon="icon" :title="title">
        <div class="nav jc-between">
            <!-- @click="$refs.list.swiper.slideTo(i)" -->
            <div class="nav-item" :class="{ active: active === i }" v-for="(category, i) in categories" :key="i">
                <div class="nav-link">{
    {
     category.name }}</div>
            </div>
        </div>
        <div class="pt-3">
            <swiper ref="list" :options="{ autoHeight: true }" @slide-change="() => active = $refs.list.swiper.realIndex">
                <swiper-slide v-for="(category, i) in categories" :key="i">
                    <slot name="items" :category="category"></slot>
                </swiper-slide>
            </swiper>
        </div>
    </m-card>
</template>
  
<script>

// Import Swiper Vue.js components
import {
     Swiper, SwiperSlide } from 'swiper/vue';

// Import Swiper styles
import 'swiper/css';
import 'swiper/css/pagination';
import 'swiper/css/navigation';

// import './style.css';

// import required modules
import {
     Autoplay, Pagination, Navigation } from 'swiper/modules';

export default {
    
    components: {
    
        Swiper,
        SwiperSlide,
    },
    setup() {
    
        return {
    
            modules: [Autoplay, Pagination, Navigation],
        };
    },
    props: {
    
        icon: {
     type: String, required: true },
        title: {
     type: String, required: true },
        categories: {
     type: Array, required: true }
    },
    data() {
    
        return {
    
            active: 0
        }
    }
};
</script>
  
<style></style>

修改 web/src/views

新建 ArticleView.vue

<template>
    <div class="page-article" v-if="model">
        <div class="d-flex py-3 px-2 border-bottom">
            <div class="iconfont icon-Back text-blue"></div>
            <strong class="flex-1 text-blue pl-2">{
    {
     model.title }}</strong>
            <div class="text-grey fs-xs">2019-06-19</div>
        </div>
        <div v-html="model.body" class="px-3 body fs-lg"></div>
        <div class="px-3 border-top py-3">
            <div class="d-flex ai-center">
                <i class="iconfont icon-menu1"></i>
                <strong class="text-blue fs-lg ml-1">相关资讯</strong>
            </div>
            <div class="pt-2">
                <!-- <router-link class="py-1" tag="div" :to="`/articles/${item._id}`" v-for="item in model.related"
                    :key="item._id">{
    {
     item.title }}</router-link> -->
                <router-link class="py-1" custom v-slot="{ navigate }" :to="`/articles/${item._id}`"
                    v-for="item in model.related" :key="item._id">
                    <div @click="navigate" @keypress.enter="navigate" role="link">{
    {
     item.title }}</div>
                </router-link>
            </div>
        </div>
    </div>
</template>
  
<script>
export default {
    
    props: {
    
        id: {
     required: true }
    },
    data() {
    
        return {
    
            model: null
        };
    },
    watch: {
    
        id: 'fetch',
        // id(){
    
        //   this.fetch()
        // }
    },
    methods: {
    
        async fetch() {
    
            const res = await this.$http.get(`articles/${
      this.id}`);
            this.model = res.data;
        }
    },
    created() {
    
        this.fetch();
    }
};
</script>
  
<style lang="scss">
.page-article {
    
    .icon-Back {
    
        font-size: 1.6923rem;
    }

    .body {
    
        img {
    
            max-width: 100%;
            height: auto;
        }

        iframe {
    
            width: 100%;
            height: auto;
        }
    }
}
</style>

新建 HeroView.vue

<template>
    <div class="page-hero" v-if="model">
        <div class="topbar bg-black py-2 px-3 d-flex ai-center text-white">
            <img src="../assets/logo.png" height="30" />
            <div class="px-2 flex-1">
                <span class="text-white">王者荣耀</span>
                <span class="ml-2">攻略站</span>
            </div>
            <!-- <router-link to="/" tag="div">更多英雄 &gt;</router-link> -->
            <router-link to="/" custom v-slot="{ navigate }">
                <div @click="navigate" @keypress.enter="navigate" role="link">更多英雄 &gt;</div>
            </router-link>
        </div>
        <div class="top" :style="{ 'background-image': `url(${model.banner})` }">
            <div class="info text-white p-3 h-100 d-flex flex-column jc-end">
                <div class="fs-sm">{
    {
     model.title }}</div>
                <h2 class="my-2">{
    {
     model.name }}</h2>
                <div class="fs-sm">{
    {
     model.categories.map(v => v.name).join('/') }}</div>
                <div class="d-flex jc-between pt-2">
                    <div class="scores d-flex ai-center" v-if="model.scores">
                        <span>难度</span>
                        <span class="badge bg-primary">{
    {
     model.scores.difficult }}</span>
                        <span>技能</span>
                        <span class="badge bg-blue-1">{
    {
     model.scores.skills }}</span>
                        <span>攻击</span>
                        <span class="badge bg-danger">{
    {
     model.scores.attack }}</span>
                        <span>生存</span>
                        <span class="badge bg-dark">{
    {
     model.scores.survive }}</span>
                    </div>
                    <!-- <router-link to="/" tag="span" class="text-grey fs-sm">皮肤: 2 &gt;</router-link> -->
                    <router-link to="/" class="text-grey fs-sm" custom v-slot="{ navigate }">
                        <span @click="navigate" @keypress.enter="navigate" role="link">皮肤: 2 &gt;</span>
                    </router-link>
                </div>
            </div>
        </div>
        <!-- end of top -->
        <div>
            <div class="bg-white px-3">
                <div class="nav d-flex jc-around pt-3 pb-2 border-bottom">
                    <div class="nav-item active">
                        <div class="nav-link">英雄初识</div>
                    </div>
                    <div class="nav-item">
                        <div class="nav-link">进阶攻略</div>
                    </div>
                </div>
            </div>
            <swiper>
                <swiper-slide>
                    <div>
                        <div class="p-3 bg-white border-bottom">
                            <div class="d-flex">
                                <!-- <router-link tag="button" to="/" class="btn btn-lg flex-1">
                                    <i class="iconfont icon-menu1"></i>
                                    英雄介绍视频
                                </router-link> -->
                                <router-link to="/" class="btn btn-lg flex-1" custom v-slot="{ navigate }">
                                    <button @click="navigate" @keypress.enter="navigate" role="link">
                                        <i class="iconfont icon-menu1"></i>
                                        英雄介绍视频
                                    </button>
                                </router-link>
                                <!-- <router-link tag="button" to="/" class="btn btn-lg flex-1 ml-2">
                                    <i class="iconfont icon-menu1"></i>
                                    英雄介绍视频
                                </router-link> -->
                                <router-link to="/" class="btn btn-lg flex-1 ml-2" custom v-slot="{ navigate }">
                                    <button @click="navigate" @keypress.enter="navigate" role="link">
                                        <i class="iconfont icon-menu1"></i>
                                        英雄介绍视频
                                    </button>
                                </router-link>
                            </div>

                            <!-- skills -->
                            <div class="skills bg-white mt-4">
                                <div class="d-flex jc-around">
                                    <img class="icon" @click="currentSkillIndex = i"
                                        :class="{ active: currentSkillIndex === i }" :src="item.icon"
                                        v-for="(item, i) in model.skills" :key="item.name" />
                                </div>
                                <div v-if="currentSkill">
                                    <div class="d-flex pt-4 pb-3">
                                        <h3 class="m-0">{
    {
     currentSkill.name }}</h3>
                                        <span class="text-grey-1 ml-4">
                                            (冷却值: {
    {
     currentSkill.delay }}
                                            消耗: {
    {
     currentSkill.cost }})
                                        </span>
                                    </div>
                                    <p>{
    {
     currentSkill.description }}</p>
                                    <div class="border-bottom"></div>
                                    <p class="text-grey-1">小提示: {
    {
     currentSkill.tips }}</p>
                                </div>
                            </div>
                        </div>

                        <m-card plain icon="menu1" title="出装推荐" class="hero-items">
                            <div class="fs-xl">顺风出装</div>
                            <div class="d-flex jc-around text-center mt-3">
                                <div v-for="item in model.items1" :key="item.name">
                                    <img :src="item.icon" class="icon">
                                    <div class="fs-xs">{
    {
     item.name }}</div>
                                </div>
                            </div>
                            <div class="border-bottom mt-3"></div>
                            <div class="fs-xl mt-3">逆风出装</div>
                            <div class="d-flex jc-around text-center mt-3">
                                <div v-for="item in model.items2" :key="item.name">
                                    <img :src="item.icon" class="icon">
                                    <div class="fs-xs">{
    {
     item.name }}</div>
                                </div>
                            </div>
                        </m-card>
                        <m-card plain icon="menu1" title="使用技巧">
                            <p class="m-0">{
    {
     model.usageTips }}</p>
                        </m-card>
                        <m-card plain icon="menu1" title="对抗技巧">
                            <p class="m-0">{
    {
     model.battleTips }}</p>
                        </m-card>
                        <m-card plain icon="menu1" title="团战思路">
                            <p class="m-0">{
    {
     model.teamTips }}</p>
                        </m-card>
                        <m-card plain icon="menu1" title="英雄关系">
                            <div class="fs-xl">最佳搭档</div>
                            <div v-for="item in model.partners" :key="item.name" class="d-flex pt-3">
                                <img :src="item.hero.avatar" alt="" height="50">
                                <p class="flex-1 m-0 ml-3">
                                    {
    {
     item.description }}
                                </p>
                            </div>
                            <div class="border-bottom mt-3"></div>
                        </m-card>

                    </div>
                </swiper-slide>
                <swiper-slide></swiper-slide>
            </swiper>
        </div>
    </div>
</template>
  
<script>
// Import Swiper Vue.js components
import {
     Swiper, SwiperSlide } from 'swiper/vue';

// Import Swiper styles
import 'swiper/css';
import 'swiper/css/pagination';
import 'swiper/css/navigation';

// import './style.css';

// import required modules
import {
     Autoplay, Pagination, Navigation } from 'swiper/modules';

export default {
    
    components: {
    
    Swiper,
    SwiperSlide,
  },
  setup() {
    
    return {
    
      modules: [Autoplay, Pagination, Navigation],
    };
  },
    props: {
    
        id: {
     required: true }
    },
    data() {
    
        return {
    
            model: null,
            currentSkillIndex: 0
        };
    },
    computed: {
    
        currentSkill() {
    
            return this.model.skills[this.currentSkillIndex];
        }
    },
    methods: {
    
        async fetch() {
    
            const res = await this.$http.get(`heroes/${
      this.id}`);
            this.model = res.data;
        }
    },
    created() {
    
        this.fetch();
    }
};
</script>
  
<style lang="scss">
@import '../assets/scss/_variables.scss';

.page-hero {
    
    .top {
    
        height: 50vw;
        background: #fff no-repeat top center;
        background-size: auto 100%;
    }

    .info {
    
        background: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 1));

        .scores {
    
            .badge {
    
                margin: 0 0.25rem;
                display: inline-block;
                width: 1rem;
                height: 1rem;
                line-height: 0.9rem;
                text-align: center;
                border-radius: 50%;
                font-size: 0.6rem;
                border: 1px solid rgba(255, 255, 255, 0.2);
            }
        }
    }

    .skills {
    
        img.icon {
    
            width: 70px;
            height: 70px;
            border: 3px solid map-get($colors, 'white');

            &.active {
    
                border-color: map-get($colors, 'primary');
            }

            border-radius: 50%;
        }
    }

    .hero-items {
    
        img.icon {
    
            width: 45px;
            height: 45px;
            border-radius: 50%;
        }
    }
}
</style>
  

新建 HomeView.vue

<!-- <template>
  <div class="home">
    <img alt="Vue logo" src="../assets/logo.png">
    <HelloWorld msg="Welcome to Your Vue.js App"/>
  </div>
</template>

<script>
// @ is an alias to /src
import HelloWorld from '@/components/HelloWorld.vue'

export default {
    
  name: 'HomeView',
  components: {
    
    HelloWorld
  }
}
</script> -->
<template>
  <div>
    <swiper :spaceBetween="30" :centeredSlides="true" :autoplay="{ delay: 2500, disableOnInteraction: false, }" :pagination="{ clickable: true, }" :navigation="true" :modules="modules" class="mySwiper" :options="swiperOption">
      <swiper-slide>
        <img class="w-100" src="../assets/images/210794580bb9303653804bb7b482f2a4.jpeg" alt>
      </swiper-slide>
      <swiper-slide>
        <img class="w-100" src="../assets/images/210794580bb9303653804bb7b482f2a4.jpeg" alt>
      </swiper-slide>
      <swiper-slide>
        <img class="w-100" src="../assets/images/210794580bb9303653804bb7b482f2a4.jpeg" alt>
      </swiper-slide>
      <template #pagination>
        <div class="swiper-pagination pagination-home text-right px-3 pb-1"></div>
      </template>
    </swiper>
    <!-- end of swiper -->

    <div class="nav-icons bg-white mt-3 text-center pt-3 text-dark-1">
      <div class="d-flex flex-wrap">
        <div class="nav-item mb-3" v-for="n in 10" :key="n">
          <i class="sprite sprite-news"></i>
          <div class="py-2">爆料站</div>
        </div>
      </div>
      <div class="bg-light py-2 fs-sm">
        <i class="sprite sprite-arrow mr-1"></i>
        <span>收起</span>
      </div>
    </div>
    <!-- end of nav icons -->

    <m-list-card icon="menu1" title="新闻资讯" :categories="newsCats">
      <template #items="{ category }">
        <!-- <router-link tag="div" :to="`/articles/${news._id}`" class="py-2 fs-lg d-flex"
          v-for="(news, i) in category.newsList" :key="i">
          <span class="text-info">[{
    {
     news.categoryName }}]</span>
          <span class="px-2">|</span>
          <span class="flex-1 text-dark-1 text-ellipsis pr-2">{
    {
     news.title }}</span>
          <span class="text-grey-1 fs-sm">{
    {
     news.createdAt | date }}</span>
        </router-link> -->
        <router-link :to="`/articles/${news._id}`" class="py-2 fs-lg d-flex" v-for="(news, i) in category.newsList"
          :key="i" custom v-slot="{ navigate }">
          <div @click="navigate" @keypress.enter="navigate" role="link">
            <span class="text-info">[{
    {
     news.categoryName }}]</span>
            <span class="px-2">|</span>
            <span class="flex-1 text-dark-1 text-ellipsis pr-2">{
    {
     news.title }}</span>
            <!-- news.createdAt | date -->
            <span class="text-grey-1 fs-sm">{
    {
     (news.createdAt) }}</span>
          </div>
        </router-link>
      </template>
    </m-list-card>

    <m-list-card icon="card-hero" title="英雄列表" :categories="heroCats">
      <template #items="{ category }">
        <div class="d-flex flex-wrap" style="margin: 0 -0.5rem;">
          <!-- <router-link tag="div" :to="`/heroes/${hero._id}`" class="p-2 text-center" style="width: 20%;"
            v-for="(hero, i) in category.heroList" :key="i">
            <img :src="hero.avatar" class="w-100">
            <div>{
    {
     hero.name }}</div>
          </router-link> -->
          <router-link :to="`/heroes/${hero._id}`" class="p-2 text-center" style="width: 20%;" custom
            v-slot="{ navigate }" v-for="(hero, i) in category.heroList" :key="i">
            <div @click="navigate" @keypress.enter="navigate" role="link">
              <img :src="hero.avatar" class="w-100">
              <div>{
    {
     hero.name }}</div>
            </div>
          </router-link>
        </div>
      </template>
    </m-list-card>

    <m-card icon="menu1" title="精彩视频"></m-card>
    <m-card icon="menu1" title="图文攻略"></m-card>
  </div>
</template>

<script>
import dayjs from "dayjs";

// Import Swiper Vue.js components
import {
     Swiper, SwiperSlide } from 'swiper/vue';

// Import Swiper styles
import 'swiper/css';
import 'swiper/css/pagination';
import 'swiper/css/navigation';

// import './style.css';

// import required modules
import {
     Autoplay, Pagination, Navigation } from 'swiper/modules';

export default {
    
  // filters: {
    
  //   date(val) {
    
  //     return dayjs(val).format("MM/DD");
  //   }
  // },
  components: {
    
    Swiper,
    SwiperSlide,
  },
  setup() {
    
    return {
    
      modules: [Autoplay, Pagination, Navigation],
    };
  },
  data() {
    
    return {
    
      swiperOption: {
    
        pagination: {
    
          el: ".pagination-home"
        }
      },
      newsCats: [],
      heroCats: []
    };
  },
  methods: {
    
    async fetchNewsCats() {
    
      const res = await this.$http.get("news/list");
      this.newsCats = res.data;
    },
    async fetchHeroCats() {
    
      const res = await this.$http.get("heroes/list");
      this.heroCats = res.data;
    }
  }, computed: {
    
    newsDate(theNewsDate) {
    
      if (theNewsDate) {
    
        // return date(theNewsDate)
        return dayjs(theNewsDate).format("MM/DD");
      }
      return theNewsDate
    }
  },
  created() {
    
    this.fetchNewsCats();
    this.fetchHeroCats();
  }
};
</script>

<style lang="scss">
@import "../assets/scss/variables";

.pagination-home {
    
  .swiper-pagination-bullet {
    
    opacity: 1;
    border-radius: 0.1538rem;
    background: map-get($colors, "white");

    &.swiper-pagination-bullet-active {
    
      background: map-get($colors, "info");
    }
  }
}

.nav-icons {
    
  border-top: 1px solid $border-color;
  border-bottom: 1px solid $border-color;

  .nav-item {
    
    width: 25%;
    border-right: 1px solid $border-color;

    &:nth-child(4n) {
    
      border-right: none;
    }
  }
}
</style>

新建 MainView.vue

<template>
    <div>
      <div class="topbar bg-black py-2 px-3 d-flex ai-center">
        <img src="../assets/logo.png" height="30">
        <div class="px-2 flex-1">
          <div class="text-white">王者荣耀</div>
          <div class="text-grey-1 fs-xxs">团队成就更多</div>
        </div>
        <button type="button" class="btn bg-primary">立即下载</button>
      </div>
      <div class="bg-primary pt-3 pb-2">
        <div class="nav nav-inverse pb-1 jc-around">
          <div class="nav-item active">
            <!-- <router-link class="nav-link" tag="div" to="/">首页</router-link> -->
            <router-link to="/" class="nav-link" custom v-slot="{ navigate }">
                <div @click="navigate" @keypress.enter="navigate" role="link">首页</div>
            </router-link>
          </div>
          <div class="nav-item">
            <!-- <router-link class="nav-link" tag="div" to="/">攻略中心</router-link> -->
            <router-link to="/" class="nav-link" custom v-slot="{ navigate }">
                <div @click="navigate" @keypress.enter="navigate" role="link">攻略中心</div>
            </router-link>
          </div>
          <div class="nav-item">
            <!-- <router-link class="nav-link" tag="div" to="/">赛事中心</router-link> -->
            <router-link to="/" class="nav-link" custom v-slot="{ navigate }">
                <div @click="navigate" @keypress.enter="navigate" role="link">赛事中心</div>
            </router-link>
          </div>
          
        </div>
      </div>
  
      <router-view></router-view>
    </div>
  </template>
  
  <script>
  export default {
    
  
  }
  </script>
  
  <style lang="scss">
  .topbar {
    
    position: sticky;
    top: 0;
    z-index: 999;
  }
  </style>
  

修改 web/src/App.vue

<template>
  <!-- <nav>
    <router-link to="/">Home</router-link> |
    <router-link to="/about">About</router-link>
  </nav>
  <router-view/> -->
  <div id="app">
    <router-view/>
  </div>
</template>

<style>
#app {
    

  width: 50%;
  margin:0 auto;
}
/* #app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}

nav {
  padding: 30px;
}

nav a {
  font-weight: bold;
  color: #2c3e50;
}

nav a.router-link-exact-active {
  color: #42b983;
} */
</style>

修改 web/src/main.js

import {
     createApp } from 'vue'
import App from './App.vue'
import router from './router'

const app = createApp(App).use(router)
app.mount('#app')

// createApp(App).use(router).mount('#app')

import './assets/iconfont/iconfont.css'
import './assets/scss/style.scss'

import Card from './components/CardView.vue'
app.component('m-card', Card)

import ListCard from './components/ListCard.vue'
app.component('m-list-card', ListCard)

import axios from 'axios'
app.config.globalProperties.$http = axios.create({
    
  baseURL: process.env.VUE_APP_API_URL || '/web/api'
  // baseURL: 'http://localhost:3000/web/api'
})

修改 web/src/router/index.js

import {
     createRouter, createWebHashHistory } from 'vue-router'
// import HomeView from '../views/HomeView.vue'
import Main from '../views/MainView.vue'
import Home from '../views/HomeView.vue'
import Article from '../views/ArticleView.vue'
import Hero from '../views/HeroView.vue'

const routes = [
  // {
    
  //   path: '/',
  //   name: 'home',
  //   component: HomeView
  // },
  {
    
    path: '/',
    component: Main,
    children: [
      {
     path: '/', name: 'home', component: Home },
      {
     path: '/articles/:id', name: 'article', component: Article, props: true }
    ]
  },
  {
    path: '/heroes/:id', name: 'hero', component: Hero, props: true},
  {
    
    path: '/about',
    name: 'about',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue')
  }
]

const router = createRouter({
    
  history: createWebHashHistory(),
  routes
})

export default router

web 显示
在这里插入图片描述

代码资源

代码资源: https://download.csdn.net/download/weixin_42350100/88048998

错误处理

ERROR  Failed to compile with 1 error                                                                                                                                                                                    10:59:33

[eslint] 
/Users/jwdmac2/Desktop/2023/web-projects/node-vue-moba/admin/src/views/MainVue.vue
  59:39  error  Named slots must use '<template>' on a custom element  vue/valid-v-slot

 1 problem (1 error, 0 warnings)


You may use special comments to disable some warnings.
Use // eslint-disable-next-line to ignore the next line.
Use /* eslint-disable */ to ignore all warnings in a file.
ERROR in [eslint] 
/Users/jwdmac2/Desktop/2023/web-projects/node-vue-moba/admin/src/views/MainVue.vue
  59:39  error  Named slots must use '<template>' on a custom element  vue/valid-v-slot

 1 problem (1 error, 0 warnings)


webpack compiled with 1 error

v-slot 应位于 <template> 标签上:

   <el-dropdown-menu v-slot:dropdown>
   	<el-dropdown-item>查看</el-dropdown-item>
  	<el-dropdown-item>新增</el-dropdown-item>
   	<el-dropdown-item>删除</el-dropdown-item>
   </el-dropdown-menu>

==>

 <template v-slot:dropdown>
 	<el-dropdown-menu>
    	<el-dropdown-item>查看</el-dropdown-item>
        <el-dropdown-item>新增</el-dropdown-item>
        <el-dropdown-item>删除</el-dropdown-item>
    </el-dropdown-menu>
 </template>

===================================================================================

node-vue-moba\admin\src\views\MainVue.vue
   6:21  error  `slot` attributes are deprecated  vue/no-deprecated-slot-attribute
   8:23  error  `slot` attributes are deprecated  vue/no-deprecated-slot-attribute
  16:23  error  `slot` attributes are deprecated  vue/no-deprecated-slot-attribute
  21:21  error  `slot` attributes are deprecated  vue/no-deprecated-slot-attribute
  23:23  error  `slot` attributes are deprecated  vue/no-deprecated-slot-attribute
  31:23  error  `slot` attributes are deprecated  vue/no-deprecated-slot-attribute
  36:21  error  `slot` attributes are deprecated  vue/no-deprecated-slot-attribute
  38:23  error  `slot` attributes are deprecated  vue/no-deprecated-slot-attribute
  46:23  error  `slot` attributes are deprecated  vue/no-deprecated-slot-attribute
  57:29  error  `slot` attributes are deprecated  vue/no-deprecated-slot-attribute

 10 problems (10 errors, 0 warnings)
  9 errors and 0 warnings potentially fixable with the `--fix` option.

webpack compiled with 1 error

vue 3.x 增加了v-slot的指令,去掉了原来的slot,slot-scope属性。

  • slot=“title” ==> #title

  • slot-scope=“scope” ==> v-slot=“scope”

  • slot-scope ==> v-slot

===================================================================================

 ERROR  Failed to compile with 1 error                                                                                                                                                                                    13:27:40

[eslint] 
/Users/jwdmac2/Desktop/2023/web-projects/node-vue-moba/admin/src/views/Login.vue
  1:1  error  Component name "Login" should always be multi-word  vue/multi-word-component-names

 1 problem (1 error, 0 warnings)


You may use special comments to disable some warnings.
Use // eslint-disable-next-line to ignore the next line.
Use /* eslint-disable */ to ignore all warnings in a file.
ERROR in [eslint] 
/Users/jwdmac2/Desktop/2023/web-projects/node-vue-moba/admin/src/views/Login.vue
  1:1  error  Component name "Login" should always be multi-word  vue/multi-word-component-names

 1 problem (1 error, 0 warnings)


webpack compiled with 1 error

更改组件名, 使其符合命名规范, 如: StudentName 或者 student-name

===================================================================================

admin % npm i axios
npm ERR! code ERESOLVE
npm ERR! ERESOLVE could not resolve
npm ERR! 
npm ERR! While resolving: element-plus@1.0.2-beta.71
npm ERR! Found: vue@3.3.4
npm ERR! node_modules/vue
npm ERR!   peerOptional vue@"^2 || ^3.2.13" from @vue/babel-preset-app@5.0.8
npm ERR!   node_modules/@vue/babel-preset-app
npm ERR!     @vue/babel-preset-app@"^5.0.8" from @vue/cli-plugin-babel@5.0.8
npm ERR!     node_modules/@vue/cli-plugin-babel
npm ERR!       dev @vue/cli-plugin-babel@"~5.0.0" from the root project
npm ERR!   peerOptional vue@"*" from @vue/babel-preset-jsx@1.4.0
npm ERR!   node_modules/@vue/babel-preset-jsx
npm ERR!     @vue/babel-preset-jsx@"^1.1.2" from @vue/babel-preset-app@5.0.8
npm ERR!     node_modules/@vue/babel-preset-app
npm ERR!       @vue/babel-preset-app@"^5.0.8" from @vue/cli-plugin-babel@5.0.8
npm ERR!       node_modules/@vue/cli-plugin-babel
npm ERR!         dev @vue/cli-plugin-babel@"~5.0.0" from the root project
npm ERR!   3 more (@vue/server-renderer, vue-router, the root project)
npm ERR! 
npm ERR! Could not resolve dependency:
npm ERR! peer vue@"3.1.x" from element-plus@1.0.2-beta.71
npm ERR! node_modules/element-plus
npm ERR!   element-plus@"^1.0.2-beta.28" from the root project
npm ERR! 
npm ERR! Conflicting peer dependency: vue@3.1.5
npm ERR! node_modules/vue
npm ERR!   peer vue@"3.1.x" from element-plus@1.0.2-beta.71
npm ERR!   node_modules/element-plus
npm ERR!     element-plus@"^1.0.2-beta.28" from the root project
npm ERR! 
npm ERR! Fix the upstream dependency conflict, or retry
npm ERR! this command with --force or --legacy-peer-deps
npm ERR! to accept an incorrect (and potentially broken) dependency resolution.
npm ERR! 
npm ERR! 
npm ERR! For a full report see:
npm ERR! /.npm/_logs/2023-07-11T05_47_34_901Z-eresolve-report.txt

npm ERR! A complete log of this run can be found in:
npm ERR!     /.npm/_logs/2023-07-11T05_47_34_901Z-debug-0.log

在新版本的npm中,默认情况下,npm install遇到冲突的peerDependencies时将失败。

使用 --force 或 --legacy-peer-deps 可解决这种情况。

–force 会无视冲突,并强制获取远端npm库资源,当有资源冲突时覆盖掉原先的版本。

–legacy-peer-deps:安装时忽略所有peerDependencies,忽视依赖冲突,采用npm版本4到版本6的样式去安装依赖,已有的依赖不会覆盖。

建议用–legacy-peer-deps 比较保险一点

npm install --legacy-peer-deps

===================================================================================

[eslint] 
/Users/jwdmac2/Desktop/2023/web-projects/node-vue-moba/admin/src/main.js
  7:1  error  'Vue' is not defined  no-undef

 1 problem (1 error, 0 warnings)

Vue 2.x 有许多全局 API 和配置。Vue3.0中对这些API做出了调整:

将全局的API,即:Vue.xxx调整到应用实例(app)上

2.x 全局 API 3.x全局 API
Vue.config app.config
Vue.config.productionTip 移除
Vue.config.ignoredElements app.config.isCustomElement
Vue.component app.component
Vue.directive app.directive
Vue.mixin app.mixin
Vue.use app.use
Vue.prototype app.config.globalProperties

productionTip:在开发环境下,Vue.js 会在控制台输出一些有用的提示信息,可以通过将其设置为 false 来禁用这些提示,默认为 true。在 Vue 3.x 中移除。

===================================================================================

 error  in ./src/main.js

Module not found: Error: Can't resolve './http' in '/Users/jwdmac2/Desktop/2023/web-projects/node-vue-moba/admin/src'

ERROR in ./src/main.js 5:0-26
Module not found: Error: Can't resolve './http' in '/Users/jwdmac2/Desktop/2023/web-projects/node-vue-moba/admin/src'

webpack compiled with 1 error

检查 http.js 文件路径

===================================================================================

ERROR
_ctx.$set is not a function
TypeError: _ctx.$set is not a function
    at on-success (webpack-internal:///./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[3]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/views/AdEdit.vue?vue&type=template&id=099dedf2:80:43)
    at Proxy.handleSuccess (webpack-internal:///./node_modules/element-plus/es/el-upload/index.js:605:13)
    at Object.onSuccess (webpack-internal:///./node_modules/element-plus/es/el-upload/index.js:481:17)
    at XMLHttpRequest.onload (webpack-internal:///./node_modules/element-plus/es/el-upload/index.js:92:12)

:on-success="res => $set(item, ‘image’, res.url)
==>
:on-success="res => item.image=res.url

===================================================================================

Module not found: Error: Can't resolve 'sass-loader' in '/Users/jwdmac2/Desktop/2023/web-projects/node-vue-moba/web'
 @ ./src/main.js 11:0-41 12:24-28

确认项目中是否已安装 sass-loader 包。可以在项目根目录下运行以下命令进行确认:
npm ls sass-loader

npm ls sass-loader
[email protected] /Users/jwdmac2/Desktop/2023/web-projects/node-vue-moba/web
└── (empty)

确认项目中是否已安装 node-sass 包。sass-loader 是依赖于 node-sass 包的,如果没有安装 node-sass 包,也会导致无法找到 sass-loader 包。可以在项目根目录下运行以下命令进行确认:
npm ls node-sass

npm ls node-sass
[email protected] /Users/jwdmac2/Desktop/2023/web-projects/node-vue-moba/web
└── (empty)

安装 sass-loader
npm install --save-dev sass-loader

安装 node-sass
npm install --save-dev node-sass

npm install --save-dev node-sass sass-loader
npm WARN deprecated @npmcli/[email protected]: This functionality has been moved to @npmcli/fs
npm WARN deprecated @npmcli/[email protected]: This functionality has been moved to @npmcli/fs

added 133 packages, and audited 1094 packages in 29m

118 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

===================================================================================

'tag' property on 'router-link' component is deprecated. Use scoped slots instead

vue-routerv3.1.x以上版本,新增“v-slot”,推荐使用‘custom v-slot’代替‘tag=“li”’

Vue Router3.1.0以下 以前
 <router-link tag='li' to='/about' class="customer">联系客服</router-link>
 
Vue Router3.1.0以上 现在
<router-link to="/about" custom v-slot="{ navigate }">
  <li @click="navigate" @keypress.enter="navigate" role="link">联系客服</li>
</router-link>

===================================================================================

error  Filters are deprecated  vue/no-deprecated-filter

从vue3.0开始,过滤器就被移除了

//vue2.x中的过滤器:
<span>{
    {
     curTime | Time }}</span>

 filters: {
    
        Time(time) {
    
            //如果time存在
            if(time) {
    
                var m = Math.floor(time / 60)
                var s = Math.round(time % 60)
                return `${
      m < 10 ? "0" + m : m}:${
      s < 10 ? "0" + s : s}`
            }
        }
    }


//vue.3.x把它写到计算属性里:
<span>{
    {
     TimeCurTime }}</span>

 computed: {
    
        TimeCurTime() {
    
            if(this.curTime) {
    
                var m = Math.floor(this.curTime / 60)
                var s = Math.round(this.curTime % 60)
                return `${
      m < 10 ? "0" + m : m}:${
      s < 10 ? "0" + s : s}`
            }
        }
    }

用于自定义组件时, v-model prop和事件默认名称已更改:

  • prop : value -> modelValue
  • 事件: input -> update: modelValue;

v-bind 的 .sync 修饰符和组件的 model 选项已移除,可在 v-model 上加一个参数代替

新增:

现在可以在同一个组件上使用多个 v-model 绑定;
现在可以自定义 v-mdoel 修饰符;

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_42350100/article/details/131652342

智能推荐

c# 调用c++ lib静态库_c#调用lib-程序员宅基地

文章浏览阅读2w次,点赞7次,收藏51次。四个步骤1.创建C++ Win32项目动态库dll 2.在Win32项目动态库中添加 外部依赖项 lib头文件和lib库3.导出C接口4.c#调用c++动态库开始你的表演...①创建一个空白的解决方案,在解决方案中添加 Visual C++ , Win32 项目空白解决方案的创建:添加Visual C++ , Win32 项目这......_c#调用lib

deepin/ubuntu安装苹方字体-程序员宅基地

文章浏览阅读4.6k次。苹方字体是苹果系统上的黑体,挺好看的。注重颜值的网站都会使用,例如知乎:font-family: -apple-system, BlinkMacSystemFont, Helvetica Neue, PingFang SC, Microsoft YaHei, Source Han Sans SC, Noto Sans CJK SC, W..._ubuntu pingfang

html表单常见操作汇总_html表单的处理程序有那些-程序员宅基地

文章浏览阅读159次。表单表单概述表单标签表单域按钮控件demo表单标签表单标签基本语法结构<form action="处理数据程序的url地址“ method=”get|post“ name="表单名称”></form><!--action,当提交表单时,向何处发送表单中的数据,地址可以是相对地址也可以是绝对地址--><!--method将表单中的数据传送给服务器处理,get方式直接显示在url地址中,数据可以被缓存,且长度有限制;而post方式数据隐藏传输,_html表单的处理程序有那些

PHP设置谷歌验证器(Google Authenticator)实现操作二步验证_php otp 验证器-程序员宅基地

文章浏览阅读1.2k次。使用说明:开启Google的登陆二步验证(即Google Authenticator服务)后用户登陆时需要输入额外由手机客户端生成的一次性密码。实现Google Authenticator功能需要服务器端和客户端的支持。服务器端负责密钥的生成、验证一次性密码是否正确。客户端记录密钥后生成一次性密码。下载谷歌验证类库文件放到项目合适位置(我这边放在项目Vender下面)https://github.com/PHPGangsta/GoogleAuthenticatorPHP代码示例://引入谷_php otp 验证器

【Python】matplotlib.plot画图横坐标混乱及间隔处理_matplotlib更改横轴间距-程序员宅基地

文章浏览阅读4.3k次,点赞5次,收藏11次。matplotlib.plot画图横坐标混乱及间隔处理_matplotlib更改横轴间距

docker — 容器存储_docker 保存容器-程序员宅基地

文章浏览阅读2.2k次。①Storage driver 处理各镜像层及容器层的处理细节,实现了多层数据的堆叠,为用户 提供了多层数据合并后的统一视图②所有 Storage driver 都使用可堆叠图像层和写时复制(CoW)策略③docker info 命令可查看当系统上的 storage driver主要用于测试目的,不建议用于生成环境。_docker 保存容器

随便推点

网络拓扑结构_网络拓扑csdn-程序员宅基地

文章浏览阅读834次,点赞27次,收藏13次。网络拓扑结构是指计算机网络中各组件(如计算机、服务器、打印机、路由器、交换机等设备)及其连接线路在物理布局或逻辑构型上的排列形式。这种布局不仅描述了设备间的实际物理连接方式,也决定了数据在网络中流动的路径和方式。不同的网络拓扑结构影响着网络的性能、可靠性、可扩展性及管理维护的难易程度。_网络拓扑csdn

JS重写Date函数,兼容IOS系统_date.prototype 将所有 ios-程序员宅基地

文章浏览阅读1.8k次,点赞5次,收藏8次。IOS系统Date的坑要创建一个指定时间的new Date对象时,通常的做法是:new Date("2020-09-21 11:11:00")这行代码在 PC 端和安卓端都是正常的,而在 iOS 端则会提示 Invalid Date 无效日期。在IOS年月日中间的横岗许换成斜杠,也就是new Date("2020/09/21 11:11:00")通常为了兼容IOS的这个坑,需要做一些额外的特殊处理,笔者在开发的时候经常会忘了兼容IOS系统。所以就想试着重写Date函数,一劳永逸,避免每次ne_date.prototype 将所有 ios

如何将EXCEL表导入plsql数据库中-程序员宅基地

文章浏览阅读5.3k次。方法一:用PLSQL Developer工具。 1 在PLSQL Developer的sql window里输入select * from test for update; 2 按F8执行 3 打开锁, 再按一下加号. 鼠标点到第一列的列头,使全列成选中状态,然后粘贴,最后commit提交即可。(前提..._excel导入pl/sql

Git常用命令速查手册-程序员宅基地

文章浏览阅读83次。Git常用命令速查手册1、初始化仓库git init2、将文件添加到仓库git add 文件名 # 将工作区的某个文件添加到暂存区 git add -u # 添加所有被tracked文件中被修改或删除的文件信息到暂存区,不处理untracked的文件git add -A # 添加所有被tracked文件中被修改或删除的文件信息到暂存区,包括untracked的文件...

分享119个ASP.NET源码总有一个是你想要的_千博二手车源码v2023 build 1120-程序员宅基地

文章浏览阅读202次。分享119个ASP.NET源码总有一个是你想要的_千博二手车源码v2023 build 1120

【C++缺省函数】 空类默认产生的6个类成员函数_空类默认产生哪些类成员函数-程序员宅基地

文章浏览阅读1.8k次。版权声明:转载请注明出处 http://blog.csdn.net/irean_lau。目录(?)[+]1、缺省构造函数。2、缺省拷贝构造函数。3、 缺省析构函数。4、缺省赋值运算符。5、缺省取址运算符。6、 缺省取址运算符 const。[cpp] view plain copy_空类默认产生哪些类成员函数

推荐文章

热门文章

相关标签