第一个完整的Web项目!
WalkThrough 自定义内容:开发静态分支并部署,定制ai角色通过post请求回复用户。
前端 配置环境 官网安装nodejs,命令行安装vue: npm install -g @vue/cli ;
创建项目vue create yike 选择默认Vue3+Eslint(Vue CLI v5.0.8). 更推荐使用Vite构建。
进入工作目录 cd yike,预览 npm run serve
在根目录创建static,mock目录存放静态文件和无后端时的测试数据;新建src/assets/fonts/目录存放字体,src/api/存放接口,src/router/存放路由,src/store/保存状态,utils/工具包,styles/, views/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 yike 主要文件目录 ├─public ├─mock ├─src │ ├─api │ ├─assets │ │ └─fonts │ ├─components │ ├─router │ ├─store │ ├─styles | ├─views │ └─utils └─static
安装路由插件 npm install vue-router --save
新建文件 router/index.js, views/YikeIndex.vue
移除App.vue中所有的默认内容并删除helloworld.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <template> <router-view > </router-view > </template> <script > export default { name : 'App' , components : { } } </script > ...
更新index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import { createRouter, createWebHashHistory} from "vue-router" ;const routes = [ { path : '/' , name : 'index' , component : () => import ('../views/YikeIndex' ) } ]const router = createRouter ({ history : createWebHashHistory (), routes, })export default router;
疑问:为什么需要路由?它周围的函数是做什么的?
单页面应用通常只有一个html文件,切换不同页面的功能其实是利用路由切换显示的组件,并非页面的跳转。
在mainjs引入路由
1 2 3 4 5 6 7 8 9 10 import { createApp } from 'vue' import App from './App.vue' import router from '@/router/index' const app = createApp (App ) app.use (router) app.mount ('#app' )
实现了根页面是YikeIndex的内容。
提前引入Vuex npm install vuex@next --save,按照官网实例写一个index文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import { createStore } from "vuex" ;const store = createStore ({ state ( ) { return { count : 0 } }, mutations : { increment (state ) { state.count ++ } } })export default store;
main引入
关于样式,安装less插件 npm install less less-loader --save
1 2 3 4 5 6 <style lang="less" scoped> .title { font-size : 24px; } </style>
在创建styles/common.less 文件备用。
引入axios npm install axios --save,npm install vue-axios --save
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 import axios from 'axios' import VueAxios from 'vue-axios' ... app.use (VueAxios , axios) <script>export default { data ( ) { return { aaa : '' , } }, components : { }, computed : { }, created ( ) { this .getUser (); }, methods : { getUser ( ) { this .axios .post ("http://182.61.20.123:3001/singin/match" , { data : 'aaa' , pwd : 'bbb' }) .then ((res ) => { console .log (res) }) } } } </script>
引入文件 修改网页图标和标题,替换favicon.ico文件
public/index中的
xxx
编辑公共样式commons.less
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 // Colors whats the @ means? // @ declear the following string is a mutable value @primary-color : #3 b73f0;@link-color : #1890 ff;@success-color : #22 bf87;@warning-color : #f67770;@error-color : #f35248; // 中性色@gray-0 : #202020 ;@gray-1 : #5 b5b5b;@gray-2 : #949494 ;@gray-9 : #f6f6f8;@gray-10 : #ffffff; // font style@size-12 : 12px ;@size-14 : 14px ;@size-16 : 16px ; // 间距@padding-4 : 4px ;@padding-8 : 8px ;@padding-12 : 12px ;@padding-20 : 20px ;body { padding :0px ; margin :0px ; line-height : 1.5 ; color : @gray-0 ;font-size : @size-14 ;background-color : @gray-10 ;transition : all 0.3s ; // all css 0.3s 平滑过渡font-family : Arial, Helvetica, sans-serif; -webkit-font-smoothing : antialiased; // 抗锯齿 -moz-osx-font-smoothing : grayscale; }p { padding : 0 ; margin : 0 ; }li { list-style : none; // no point in list }img { padding : 0 ; margin : 0 ; display : block; // better }a { text-decoration : none; &:hover { text-decoration : underline; } }input , textarea { outline : none; }
单页面引入
1 2 3 4 5 6 7 <style lang="less">@import url(./styles/commons.less);.ab { font-size : @size-16 ; color : @warning-color; } </style>
此方法不能实现全局引入
安装插件npm i style-resources-loader --save-dev,npm i vue-cli-plugin-style-resources-loader --save-dev
全局导入
1 2 3 4 5 6 7 8 9 10 11 12 13 // vue.config .js // include less const path = require("path"); const { prependListener } = require('process'); module.exports = { pluginOptions: { "style-resources-loader": { preProcessor: "less" , patterns: [path.resolve (__dirname, "./src/styles/commons.less" )], } } }
此后可无@import url(./styles/commons.less);直接使用less-styles
导入矢量图,在fonticon找一些图放进购物车保存到项目,下载解压的文件夹放入assets/font/下,在Appvue的style部分导入@import './assets/fonts/basic_icon/iconfont.css';,参照文件夹中的demo网页使用矢量图,例如<span class="iconfont icon-zan1"></span>
页面编写 设计页面结构
新建views/WallMessage.vue,更新路由重定向到wall
1 2 3 4 5 6 7 8 9 10 11 12 13 // YikeIndex.vue <template> <div > <p >index of views</p > <p class="title"> Rouderls board</p > <router-view></router-view> // new add </div > </template> // router/index.js const routes = [ { path: '/' , redirect: '/wall' , name: 'index' , component: () => import('../views/YikeIndex' ), children: [ { path: 'wall' , component: () => import('../views/WallMassage' ) } ] , } ]
编写top部分组件
在YikeIndex导入,注册,使用组件
1 2 3 4 5 6 7 8 9 10 11 12 13 <template> <div > <video class="bg-video "></video > <topBar></topBar> <router-view></router-view> </div > </template> ... import topBar from '@/components/TopBar.vue' ... components : { topBar, },
还可以放一个背景(z-index: -1 使其永远在底下)视频效果作为可选项。(跳过P5 20min)
设计topbar样式,使其接近可用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 <template> <div class="top -bar"> <div class="logo"> <img src ="@/assets/logo.png" class="logo-img" /> <p class="logo-name" >Rouderls Board</p> </div> <div class="menu" >some menu</div> <div class="user" > <div class="user-head" ></div> </div> </div> </template> <script> export default { } </script> <style lang="less" scoped>.top-bar { width : 100% ; height : 52px ; background-color : rgba (255 ,255 ,255 ,0.80 ); box-shadow : 0 0 4px 0 rgba (0 ,0 ,0 ,0.1 ); backdrop-filter : blur (10px ); padding : 0 30px ; box-sizing : border-box; // 便于计算padding position : fixed; // bar永远在最上层 top : 0 ; left : 0 ; z-index : 9999 ; display : flex; // 居中 三等分 justify-content : space-between; align-items : center; .logo { width : 200px ; display : flex; align-items : center; .logo-name { font-size : 20px ; color : @gray-0 ; letter-spacing : 0 ; text-align : justify; font-weight : 600 ; padding-right : 15px ; } } .user { width : 200px ; .user-head { float : right; // 靠右 // 用户头像 圆形渐变色 border-radius : 50% ; height : 36px ; width : 36px ; background-image : linear-gradient (180deg , #7be7ff 2% ,#1e85e2 100% ); } } }.logo-img { height : 30px ; } </style>
创建button组件,同样在conponents下创建,在topBar内引入,注册
疑问 引入名是YkButtonVue,为什么html标签使用一样效果?
A: vue自动将大驼峰改成了小写连字符的格式的html标签
在button中使用标签能将传入的文字写在按钮中,防止button内部将文字定死。
定义按钮样式
疑问 :class 是什么? 变量?
《button class未确定》
开始涉及vue语法
速通了footbar部分。为了使其保持在底部,将主题部分min-height: 100vh填满整个页面。
编写主体 utils/data.js 设置假数据
1 2 3 4 5 export const wallType = [ { name: '留言板' , slogan: '这片大地上可否有你不能忘却之事?' }, { name: '照片墙' , slogan: '很多事情值得记录,当然也值得回味。' }] ; export const label = [ ['留言' , '目标' , '理想' , '过去' , '将来' , '爱情' , '亲情' , '友情' , '秘密' , '信条' , '无题' ] , ['me' , 'ta' ,'like' ,'life' ,'sky' ,'city' ,'sea' ] , ]
将列表内容用循环展示出来
<p class="label-list" v-for="(e, index) in label[id]" :key="index">{{ e }}</p>
疑问 什么意思?
在html里面写js需要{}包裹,这里不能用-连字符,html标签会使用连字符,为什么?
设置nlabel变量记录当前选择的标签,默认-1对应All。关注第2-7行。
编写对应方法设置index,@click 识别鼠标事件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <div class="label"> <p class="label-list" :class="{lselected:nlabel==-1}" @click="selectNode(-1)" >All</p> <p class="label-list" v-for="(e, index) in label[id]" :key="index" :class="{lselected:nlabel==index}" @click="selectNode(index)" >{{ e }}</p> </div> <script> import { wallType, label } from '@/utils/data'; export default { data() { return { wallType, label, id: 0, // 切换留言板和照片墙 nlabel: -1, // 选中的标签,默认-1 all } }, methods: { selectNode(e) { this.nlabel = e; } }, } </script>
创建note卡片,引入
编写css,处理hover情况,考虑将其纳入common.less库
引入字体
1 2 3 4 5 6 <style > @font-face { font-family: fa; src: url("@/assets/fonts/xxx.tty"); } </style>
引入后端模拟数据npm i mockjs --save
./mock/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 let Mock = require('mockjs') export const node = Mock.mock({ "data|20" : [ { "moment" : new Date(), "id|+1" : 1 , "userId|+1" : 10 , "message|24-96" : "@cword" , "label|0-10" : 0 , "name" : "@cname" , "like|0-120" : 0 , "favor|0-120" : 0 , "comment|0|120" : 0 , "imgurl|0-4" : 0 , "revoke|0-20" : 0 , "report|0-20" : 0 , } ] } )
疑问? 芝士神魔: 按照一定规则设计的假数据
card展示, :表示变量,传入组件内部的props中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <div class ="card" > <node-card-vue v-for ="(e, index) in node" :key ="index" :note ="e" class ="card-inner" > </node-card-vue > </div> <script > export default { props : { width : { default : '288px' , }, note : { default : { }, } } } </script >
现在我们尝试把mock生成的模拟数据显示到我们的卡片中。在。 WallMessage里面导入node。将其中的数据作为参数传入到Card组件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import {node} from '../../mock/index' export default { data ( ) { return { wallType, label, id : 0 , nlabel : -1 , note : node.data , } },<node-card-vue v-for ="(e, index) in note" :key ="index" :note ="e" class ="card-inner" > </node-card-vue >
这一块。今天遇到了很多次莫名其妙的崩溃。然后莫名其妙的排查注意到是main.js路由引入的时候使用了@(默认代指src/),尝试改成点号再去导入就莫名其妙的修好了。我也不知道为什么。但是用点号写是最保险的所以还是尽量点号吧。 浏览器缓存可能不会及时更新,刷新后才是真正最新的代码,和@的使用无关。
最后我是一步一步通过log调试,用method属性返回label的这个标签,然后在网页HTML里边通过调用getlabel方法来成功显示。
接下来处理一下time的显示,源数据太长了,需要优化。创建utils/yksg.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 export const dateOne = (e ) => { let d = new Date (e); let y = d.getFullYear (); let m = d.getMonth () + 1 ; let D = d.getDate (); if (D < 10 ) { D = '0' + D; } if (m < 10 ) { m = '0' + m; } let dates = y + '.' + m + '.' + D; return dates; }
添加 add按钮,并使其固定在页面右下角,但不进入footbar(可选,动画效果P12t10min)
同理创建model组件用于新建留言。
1 2 // 毛玻璃效果 backdrop-filter: blur(10px);
因为写留言这个标题会变成发照片之类的变量,尝试setup语法糖,创建title参数,经组件导入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 <yk-model :model-title=title></yk-model> data ( ) { return { wallType, label, id : 0 , nlabel : -1 , note : node.data , title : '写留言' , } },<script setup > import { defineProps } from 'vue' ;const props = defineProps ({ modelTitle : { default : '标题' , }, }) console .log (props);</script > <p class ="model-name" > {{ modelTitle }}</p >
为了实现model组件的开关,创建isModel的bool值,使用v-if语法管理model是否显示。该值交由父级控制:引入defineEmits
……
跳过弹出动画P15
……
newCard组件,插入到ykModel内的中。
选中便签颜色的CSS为colorselected,在v-for中有变量index, colorSelected(默认为0,有点击方法将其设置为index),可选颜色标签中有是否启动CSS的判断语句:class="{colorselected: index==colorSelected}",判断为true时加上该CSS表示选中该颜色。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <p class ="color-li" v-for ="(e, index) in cardColor1" :key="index" :style="{background: e}" :class ="{colorselected:index==colorSelected}" @click="changeColor(index)" ></p>data ( ) { return { cardColor1, cardColor, colorSelected : 0 , } },methods : { changeColor (index ) { this .colorSelected = index; } } .colorselected { border : 1px solid rgba (59 ,115 ,240 ,1 ); }
简单动画效果P16t25min。
v-model绑定textarea,input变量
@click=“functionName” 函数可以不用加括号,也可以加括号,两种写法都可以。
关于留言板和照片墙的切换,涉及路由/wall?id=0
1 2 3 4 5 computed: { id() { return this.$route.query.id; // current page id } },
根据id的变化切换按钮的颜色,nom加冒号转为变量?
点击事件修改id
1 2 3 4 5 6 7 methods: { changeWall(newid) { this.$router.push({ query: {id: newid}, }) } },
进入wallMessage.vue,创建上文同样的id(),并删去data()中的id,此时id由两个按钮控制,title和label可以动态响应。(记得给card组件加上v-show=”id==0”)
疑问 v-show 和 v-if 有什么区别? 似乎都管理是否显示
A: show通过display样式管理元素的显示,比较快,if则为true才渲染DOM元素,比较慢。
路由完全看不懂,什么原理?
v-for到底是怎么运作的,为什么传入e?挖个坑,深入Vue时探究。
后端 在根目录同级创建server/
cd server && npm i express --save
推荐使用nodemon插件热编译
配置环境 在wsl中用docker起mysql
docker run --name yikeSql -e MYSQL_ROOT_PASSWORD=123456 -e MYSQL_DATABASE=WALL -p 3306:3306 -d mysql:8.0
注:HOST还是填localhost
修改mysql权限允许密码登录
docker exec -it yikeSql mysql -uroot -p进入mysq cli
ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY '123456'; FLUSH PRIVILEGES;
一些mysql命令(全部以分号结尾)
1 2 3 4 show databases; use databasename;show tables;select * from tablename;
再启动 docker start name
2025/5/27可用docker镜像
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 { "builder" : { "gc" : { "defaultKeepStorage" : "20GB" , "enabled" : true } }, "experimental" : false , "registry-mirrors" : [ "https://docker.hpcloud.cloud" , "https://docker.m.daocloud.io" , "https://docker.unsee.tech" , "https://docker.1panel.live" , "http://mirrors.ustc.edu.cn" , "https://docker.chenby.cn" , "http://mirror.azure.cn" , "https://dockerpull.org" , "https://dockerhub.icu" , "https://hub.rat.dev" , "https://proxy.1panel.live" , "https://docker.1panel.top" , "https://docker.m.daocloud.io" , "https://docker.1ms.run" , "https://docker.ketches.cn" ] }
https://cloud.tencent.com/developer/article/2483548
在server文件夹下创建index.js,引入一些库,启动express,
1 2 3 4 5 6 7 const express = require ('express' )const path = require ('path' )let ejs = require ('ejs' )let config = require ('./config/default' )const app = express ()
设置跨域(允许浏览器访问),
1 2 3 4 5 6 7 8 9 10 11 app.use ((req, res, next ) => { res.header ("Access-Control-Allow-Origin" , "*" ) res.header ("Access-Control-Allow-Methods" , "GET,POST,PUT,DELETE,OPTIONS" ) res.header ("Access-Control-Allow-Headers" , "Content-Type,Authorization" ) if (req.method === 'OPTIONS' ) { res.sendStatus (200 ) } else { next () } })
映射静态文件,
1 2 3 4 5 6 7 app.use (express.static (__dirname + '/dist' )) app.use (express.static (__dirname + '/data' )) app.use (express.static (__dirname + '/views' ))
设置html视图? 疑问
1 2 app.engine ('html' , ejs.__express ) app.set ('view engine' , 'html' )
引入路由: require('./routes/index')(app)创建server/routes/index.js
1 2 3 4 5 6 7 8 module .exports = function (app ) { app.get ('/test' , (req, res ) => { res.type ('html' ); res.render ('test' ); }); }
监听端口3000
将一些基础配置保存至server/config/default.js
1 2 3 4 5 6 7 8 9 10 11 const config = { port : 3000 , database : { HOST : 'localhost' , USER : 'root' , PASSWORD : "123456" , WALL : 'WALL' , } }module .exports = config;
数据库操作 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 const mysql = require ('mysql' )let config = require ('../config/default' )const db = mysql.createConnection ({ host : config.database .HOST , user : config.database .USER , password : config.database .PASSWORD , })const pool = mysql.createPool ({ host : config.database .HOST , user : config.database .USER , password : config.database .PASSWORD , database :config.database .WALL , })let bdbs = (sql, values, usePool = false ) => { return new Promise ((resolve, reject ) => { const conn = usePool ? pool : db; conn.query (sql, values, (err, reslut ) => { if (err) { reject (err) } else { resolve (reslut) } }) }) }let WALL = `create database if not exists WALL default charset utf8 collate utf8_general_ci;` let createDatabase = (db ) => { return bdbs (db, [], false ) }let walls = `create table if not exists walls( id INT NOT NULL AUTO_INCREMENT, type INT NOT NULL COMMENT '0NOTE1PIC', message VARCHAR(1000) COMMENT '留言', name VARCHAR(100) NOT NULL, userId VARCHAR(100) NOT NULL, moment VARCHAR(100) NOT NULL, label INT NOT NULL, color INT COMMENT '', imgurl VARCHAR(100) COMMENT '', PRIMARY KEY ( id ) );` let feedbacks = ` create table if not exists feedbacks( id INT NOT NULL AUTO_INCREMENT, wallId INT NOT NULL, userId VARCHAR(100) NOT NULL, type INT NOT NULL, moment VARCHAR(100) NOT NULL, PRIMARY KEY (id) ); ` let comments = ` create table if not exists comments( id INT NOT NULL AUTO_INCREMENT, wallId INT NOT NULL, userId VARCHAR(100) NOT NULL, imgurl VARCHAR(100), comment VARCHAR(1000), name VARCHAR(100) NOT NULL, moment VARCHAR(100) NOT NULL, PRIMARY KEY (id) ); ` let createTable = (sql ) => { return bdbs (sql, [], true ) }async function create ( ) { await createDatabase (WALL ); createTable (walls); createTable (feedbacks); createTable (comments); }create ();
编写插入note语句
1 2 3 4 5 exports .insertWall = (value ) => { let _sql = "insert into walls set type=?,message=?,name=?,userId=?,moment=?,label=?,color=?,imgurl=?;" return query (_sql, value); }
在server/controller/dbServe.js引入上述文件,拿到前端数据插入数据库,
1 2 3 4 5 6 7 8 9 10 11 12 const db = require ('../lib/db' )exports .insertWall = async (req, res) => { let i = req.body ; await db.insertWall ([i.type , i.message , i.name , i.userId , i.moment , i.label , i.color , i.imgurl ]) .then (result => { res.send ({ code : 200 , message : result, }) }) }
在路由引入dbServe,负责调用插入函数
1 2 3 4 app.post ('/insertwall' , (req, res ) => { controller.insertWall (req, res); })
去前端测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 / / newcard methods apiTest() { let data = { type: 0 , message: 'hello world' , name: 'doctor' , userId: '3' , moment: new Date (), label: 0 , color: 3 , imgurl: 3 , } this.axios .post("http://localhost:3000/insertwall", data) .then ((res) = > { console.log (res); }) }/ /
暂时注释掉后端insert部分,保留send,查看控制台的log
路由记得带斜杠!我忘了!再试试插入数据库。
前后端连起来了!
联调 对生产和上线环境做区分,前端创建utils/env.js,当node_env 为dev时url为localhost,否则为实际的域名地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 let baseUrl = '' ;let routerMode = 'hash' ;let baseImgPath;if (process.env .NODE_ENV === 'development' ) { baseUrl = 'http://localhost:3000' ; baseImgPath = 'http://localhost:3000' ; } else { baseUrl = 'http://localhost:3000' ; baseImgPath = 'http://localhost:3000' ; }export { baseUrl, routerMode, baseImgPath }
对接后端
接下来使用axios,同样创建utils/axios.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 import { baseUrl } from "./env" ;import axios from "axios" ;const service = axios.create ({ baseURL : baseUrl, timeout : 5000 }); service.interceptors .request .use ( config => { return config; }, error => { console .log (error); return Promise .reject (); } ); service.interceptors .response .use ( response => { if (response.status === 200 ) { return response.data ; } else { Promise .reject (); } }, error => { console .log (error); return Promise .reject (); } );
为了前后端能统一id的值,使用先前安装的vuex来管理(相当于从后端拿id做前端的全局变量,省的props层层传递)。
在后端创建接收访客ip的路由
前端创建对应api
疑问:this? () or :?
methods下的函数之间调用需要加上this,否则看不到其他函数,
1 2 3 4 5 6 const obj = { foo ( ) { this .bar (); }, bar ( ) { } } obj.foo (); foo ();
data() {} 和 setup() {} 都是函数,需要返回数据对象。
Vue 会在组件实例化时调用它们,获取返回值作为组件的数据源。
props: {}、methods: {}、computed: {} 这些是对象,用来声明属性、方法、计算属性等。
Vue 直接读取对象里的内容,不需要执行。
git gc(全称:git garbage collect,垃圾回收)是 Git 提供的一个优化和清理本地仓库的命令。它会自动清理和压缩仓库中的无用数据,使仓库体积变小、操作更流畅。
新建一个全局提醒组件文件夹components/sub/message/
把自己的组件在main中导入P34
注销mock的note数据,当note为空时展示“暂无note”的信息和图片,通过require导入图片或直接cdn使用。
何时展示?
isOk: -1, // -1 真实数据准备中, 0 数据为空, 2 没有更多
加载中可以使用lottie显示加载动画P35half
bug:like和comment的计数的显示错误,前后端数据接口对不上,暂改为count
{{ note.comment[0].count }}
向后端请求数据时请求1个pagesize的card数据,可以在后续添加加载更多的选项再请求。
现在处理点击创建按钮时将card插入到首位的功能。
区分显示“没有card”和“没有更多”的逻辑
两者都有page==0,isOk记录了两者的区别,根据这两个变量做判断。
项目基本功能完成。
[仓库地址]:
参考