【转】 前端笔记之Vue(六)分页排序|酷表单实战&Vue-cli
【转】 前端笔记之Vue(六)分页排序|酷表单实战&Vue-cli
一、分页排序案例
后端负责提供接口(3000)
前端负责业务逻辑(8080)
接口地址:从8080跨域到3000拿数据
http://127.0.0.1:8080/api/shouji
分页排序接口: http://127.0.0.1:3000/shouji?page=1&pagesize=5&sortby=price&sortdirection=dao
代理跨域回来的数据接口地址: http://127.0.0.1:8080/api/shouji?page=1&pagesize=5&sortby=price&sortdirection=dao
后端app.js
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
var express = require("express"); var url = require("url"); var app = express(); var arr = [ {"id" : 1 , "title" : "苹果A" , "price" : 1699}, {"id" : 2 , "title" : "苹果B" , "price" : 1999}, ... {"id" : 14 , "title" : "苹果N" , "price" : 8888} ]; app.get("/shouji" , function(req,res){ var obj = url.parse(req.url, true).query; var page = obj.page; //页码 var pagesize = obj.pagesize; //每页显示的数量 var sortby = obj.sortby; //排序条件 var sortdirection = obj.sortdirection; //排序条件(正序或倒序) //按照id或价格排序 arr = arr.sort(function(a,b){ if(sortdirection == "zheng"){ return a[sortby] - b[sortby]; }else if(sortdirection == "dao"){ return b[sortby] - a[sortby]; } }) //提供数据给前端 res.json({ "number" : arr.length , //商品总数量 "results": arr.slice((page - 1) * pagesize, page * pagesize) //显示多少条数据 }) }); app.listen(3000);
前端main.js
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
import Vue from "vue"; import Vuex from "vuex"; import App from "./App.vue"; import store from "./store"; Vue.use(Vuex); new Vue({ el : "#app", store, render : (h) => h(App) })
新建taobao文件夹存放三要素,然后在文件夹的index.js中引入三要素:
state.js、action.js、mutations.js三个文件:
export default { ... }
store/index.js
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
import Vue from "vue"; import Vuex from "vuex"; import createLogger from "vuex/dist/logger"; import counterState from "./counter/state.js" import counterMutations from "./counter/mutations.js" import counterActions from "./counter/actions.js" import taobaoState from "./taobao/state.js" import taobaoMutations from "./taobao/mutations.js" import taobaoActions from "./taobao/actions.js" Vue.use(Vuex); //全局数据 const store = new Vuex.Store({ state : { counterState, taobaoState }, //同步的(commit) mutations : { ...counterMutations, ...taobaoMutations }, //异步的(dispatch) actions : { ...counterActions, ...taobaoActions }, plugins : [createLogger()] }); export default store;
state.js存储默认数据:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
export default { page : 1, pagesize: 5, sortby : "id", sortdirection:"zheng", number : 0, results :[] }
App.vue
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<template> <div> <table> <tr> <th>编号</th> <th>商品</th> <th>价格</th> </tr> </table> </div> </template> <script> export default { created(){ //生命周期,当组件被创建时触发,发出一个异步请求接口数据 this.$store.dispatch("init"); } } </script>
actions.js
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
export default { async init({commit,state}){ var page = state.taobaoState.page; var pagesize = state.taobaoState.pagesize; var sortby = state.taobaoState.sortby; var sortdirection = state.taobaoState.sortdirection; //发出异步的get请求拿数据 var {results,number} = await fetch(`/api/shouji?page=${page}&pagesize=${pagesize} &sortby=${sortby}&sortdirection=${sortdirection}`).then(data => data.json()); //数据从后端拿回来后,改变results和number //改变state只能通过mutations commit("changeResults", {results}) commit("changeNumber", {number}) } }
mutations.js,此时可以从控制台logger中查看请求数据成功,然后回到App.vue的结构显示数据。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
export default { changeResults(state , payload){ state.taobaoState.results = payload.results; }, changeNumber(state, payload) { state.taobaoState.number = payload.number; } }
回到App.vue显示数据和换页:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<template> <div> <table> <tr v-for="item in $store.state.taobaoState.results"> <td>{{item.id}}</td> <td>{{item.title}}</td> <td>{{item.price}}</td> </tr> </table> <button v-for="i in allPage()" @click="changePage(i)">{{i}}</button> </div> </template> <script> export default { created(){ //生命周期,当组件被创建的时候触发 this.$store.dispatch("init"); }, computed:{ allPage(){ //计算总页码数:总数量 / 每页数量,向上取整 var _state = this.$store.state.taobaoState; return Math.ceil(_state.number / _state.pagesize) } }, methods : { //分页页码跳转 changePage(page){ this.$store.dispatch("changePage", {page}); } } } </script>
actions.js
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
export default { async init({commit,state}){ ... }, //其他都和init一样,只改名字即可 async changePage({commit,state},{page}){ //改变page commit("changePage", {page}) //凑齐4个数据 var page = state.taobaoState.page; var pagesize = state.taobaoState.pagesize; var sortby = state.taobaoState.sortby; var sortdirection = state.taobaoState.sortdirection; //发出请求和init方法的一样 var {results, number} = await fetch(`/api/shouji?page=${page}&pagesize=${pagesize} &sortby=${sortby}&sortdirection=${sortdirection}`).then(data => data.json()); //改变state只能通过mutations commit("changeResults", {results}) commit("changeNumber", {number}) } }
mutations.js
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
export default { changeResult(state , payload){ state.taobaoState.results = payload.results; }, changeNumber(state, payload) { state.taobaoState.number = payload.number; }, //页码跳转 changePage(state , payload){ state.taobaoState.page = payload.page; } }
App.vue下面实现每页显示多少条:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<template> <div> <table> <tr v-for="item in $store.state.taobaoState.results"> <td>{{item.id}}</td> <td>{{item.title}}</td> <td>{{item.price}}</td> </tr> </table> <button v-for="i in allPage()" @click="changePage(i)">{{i}}</button> <select v-model="pagesize"> <option value="3">每页3条</option> <option value="5">每页5条</option> <option value="10">每页10条</option> </select> </div> </template> <script> export default { data(){ return { pagesize : this.$store.state.taobaoState.pagesize } }, created(){ //生命周期,当组件被创建的时候触发 this.$store.dispatch("init"); }, methods : { changePage(page){ this.$store.dispatch("changePage" , {page}); } }, //v-mode的值不能加圆括号,所以不能直接计算,先通过data(){}中计算,再用watch监听变化 //Vue提供一种更通用的方式来观察和响应Vue实例上的数据变动:侦听属性。 //以V-model绑定数据时使用的数据变化监测 //使用watch允许我们执行异步操作 watch : { pagesize(v){ //当pagesize改变,发出一个请求,去改变pagesize this.$store.dispatch("changePageSize" , {pagesize : v}); } } } </script>
actions.js
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
export default { async init({commit,state}){ ... }, async changePageSize({commit,state},{pagesize}){ //改变page commit("changePageSize", {pagesize:pagesize}) //凑齐4个数据 var page = state.taobaoState.page; var pagesize = state.taobaoState.pagesize; var sortby = state.taobaoState.sortby; var sortdirection = state.taobaoState.sortdirection; //发出请求 var {results,number} = await fetch(`/api/shouji?page=${page}&pagesize=${pagesize} &sortby=${sortby}&sortdirection=${sortdirection}`).then(data => data.json()); //改变state只能通过mutations commit("changeResults", {results}) commit("changeNumber", {number}) } }
mutations.js
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
export default { //...省略 changePage(state , payload){ state.taobaoState.page = payload.page; }, changePageSize(state, payload) { state.taobaoState.pagesize = payload.pagesize; } }
下面实现id和价格的排序
App.vue
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<template> <div> <table> <tr> <th> id: <button @click="changeSort('id','dao')">↓</button> <button @click="changeSort('id','zheng')">↑</button> </th> <th>商品:</th> <th> 价格: <button @click="changeSort('price','dao')">↓</button> <button @click="changeSort('price','zheng')">↑</button> </th> </tr> <tr v-for="item in $store.state.taobaoState.results"> <td>{{item.id}}</td> <td>{{item.title}}</td> <td>{{item.price}}</td> </tr> </table> <button v-for="i in allPage()" @click="changePage(i)" :class="{'cur':$store.state.taobaoState.page == i}"> {{i}} </button> </div> </template> <script> export default { methods : { changePage(page){ this.$store.dispatch("changePage", {page}); }, changeSort(sortby , sortdirection){ this.$store.dispatch("changeSort", {sortby , sortdirection}); } } } </script> <style> .cur{ background : orange;} </style>
actions.js封装成函数:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
async function load(commit, state){ //凑齐4个 var page = state.taobaoState.page; var pagesize = state.taobaoState.pagesize; var sortby = state.taobaoState.sortby; var sortdirection = state.taobaoState.sortdirection; //发出请求 var {results,number} = await fetch(`/api/shouji?page=${page}&pagesize=${pagesize}&sortby=${sortby}&sortdirection=${sortdirection}`).then(data => data.json()); //数据从后端拿回来后,改变results和number //改变state只能通过mutations commit("changeResults", { results}) commit("changeNumber", { number }) } export default { async init({commit , state}){ await load(commit , state); }, async changePage({ commit, state } , {page}) { //改变page commit("changePage", { page: page}) await load(commit, state); }, async changePageSize({ commit, state }, { pagesize }) { //改变pagesize commit("changePageSize", { pagesize: pagesize }) //页码归1 commit("changePage", { page: 1 }) await load(commit, state); }, //排序 async changeSort({commit, state}, {sortby, sortdirection}){ //改变pagesize commit("changeSort", { sortby, sortdirection}) //页码归1 commit("changePage", { page: 1 }) await load(commit, state); }, }
mutations.js
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
export default { //...省略 changePageSize(state, payload) { state.taobaoState.pagesize = payload.pagesize; }, changeSort(state , payload){ state.taobaoState.sortby = payload.sortby; state.taobaoState.sortdirection = payload.sortdirection; } }
二、Vue-cli
2.1 Vue-cli的安装
Vue 提供一个官方命令行工具(CLI),可用于快速搭建大型单页应用。该工具为现代化的前端开发工作流提供了开箱即用的构建配置。只需几分钟即可创建并启动一个带热重载、保存时静态检查以及可用于生产环境的构建配置的项目。
Vue-cli是Vue的快速起步工具(脚手架工具),再也不用手动配webpack。
Vue-loader的官网:
https://cn.vuejs.org/v2/guide/installation.html
https://vue-loader.vuejs.org/zh-cn/start/setup.html
在全局安装vue-cli:
npm install -g vue-cli
创建一个基于webpack模板的新项目文件夹,并初始化配置:
vue init webpack hello-vue
vue init webpack-simple hello-vue
vue init webpack-simple项目默认打包后只有一个html和js文件(适合小项目)
vue init webpack项目默认打包完之后,会有很标准的目录(适合中大型项目)
两种方式初始化Vue-cli项目的目录差别很大,你会发现vue init webpack的方式初始化项目,默认提供了很多webpack的配置,也更加方便你对代理(跨域)、最终打包资源放到服务器什么目录、以及js、css、img和项目在打包过程等优化的配置等。
vue init webpack |
vue init webpack-simple |
|
![]()
|
安装依赖:
npm install
启动项目:
npm run dev
2.2 Vue-cli的配置讲解
"scripts": { "dev": "cross-env NODE_ENV=development webpack-dev-server --open --hot", "build": "cross-env NODE_ENV=production webpack --progress --hide-modules" },
cross-env NODE_ENV=development 将环境变量设置成开发模式
cross-env NODE_ENV=production 将环境变量设置成生产模式
--open 自动开启浏览器
--hot 开启热更新, 热更新就是保存后进行局部刷新
打开项目以后 vue-cli给我们配置了很多东西。
.editorconfig对编辑器的统一配置,是让大家的代码有一个规范、代码缩进形式的统一,当大家提交代码后使用不同的编辑器打开时,显示的代码格式是一样的
root = true [*] charset = utf-8 indent_style = space //空格缩进 indent_size = 4 //统一缩进为4个 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true
关于生产模式的配置
开发过程不需要优化配置,只有在生产模式下,才需要优化、css压缩打包到一个文件,js合并压缩,关于性能优化,小图片会转成base64 减少http请求。
if (process.env.NODE_ENV === 'production') { module.exports.devtool = '#source-map' // http://vue-loader.vuejs.org/en/workflow/production.html module.exports.plugins = (module.exports.plugins || []).concat([ new webpack.DefinePlugin({ 'process.env': { NODE_ENV: '"production"' } }), new webpack.optimize.UglifyJsPlugin({ sourceMap: true, compress: { warnings: false } }), new webpack.LoaderOptionsPlugin({ minimize: true }) ]) }
将vue-cli提供的src文件夹中的assets图片文件夹,移动到根目录外面去,就是为了不让图片webpack编译打包。
vue中使用图片的两种方式:
第一种:传统方式
<img src="../assets/logo.png">
第二种:使用vue的v-bind指令来使用data数据中的图片:
<img :src="imgSrc"> <script> export default { data () { return { imgSrc : "../assets/logo.png" } } } </script>
webpack将png的图片变成base64,使用url-loader
{ test: /\.(png|jpg|gif|svg)$/, loader: 'url-loader', options: { limit: 8192 } }
limit是设置一个图片大小的临界点,值小于8192字节的图片就转成base64图片源码,好处就是能减少一个HTTP请求。
2.3项目打包上线
如果把自己的项目放到服务器运行,就需要使用npm run build将自己的项目打包出来。
然后在dist文件夹下面,就有打包、优化好的项目文件、打包好的项目文件可以放到服务器中运行。
注意:项目启动一定要在服务器环境下运行,在webpack服务器、node、phpnow服务器都可以。
三、酷表单项目
main.js
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
import Vue from "vue"; import Vuex from "vuex"; import App from "./App.vue"; Vue.use(Vuex); // 创建一个全局仓库 const store = new Vuex.Store({ state : { } }) new Vue({ el : "#app", store, render : (h) => h(App) })
第一步:写静态页面组件,App.vue
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<template> <div> <div class="warp"> <div class="leftPart">左侧部分</div> <div class="centerPart"> <div class="outerBox onedit"> <div class="cbox"> <div class="qtitle"><em>*</em> 我是新的多选题目,请编辑</div> <div class="qoption"> <label><input type="checkbox" />新的项目A</label> <label><input type="checkbox" />新的项目B</label> </div> </div> <span class="edit"></span> <span class="up"></span> <span class="down"></span> </div> <div class="outerBox"> <div class="cbox"> <div class="qtitle"><em>*</em> 我是新的单选题目,请编辑</div> <div class="qoption"> <label><input type="radio" />新的项目A</label> <label><input type="radio" />新的项目B</label> </div> </div> <span class="edit"></span> <span class="up"></span> <span class="down"></span> </div> <div class="outerBox"> <div class="cbox"> <div class="qtitle"><em>*</em> 我是新的下拉选项题目,请编辑</div> <div class="qoption"> <select> <option>新的项目A</option> <option>新的项目B</option> </select> </div> </div> <span class="edit"></span> <span class="up"></span> <span class="down"></span> </div> </div> <div class="rightPart">右侧部分</div> </div> </div> </template> <style lang='stylus'> .warp{ width:1300px;min-height:500px; margin:50px auto;overflow:hidden; .leftPart,.rightPart{ float:left; width:350px;min-height:500px;background-color:#ccc; } .centerPart{ float:left; width:600px;min-height:500px;padding:20px; overflow:hidden;box-sizing:border-box;background: #fff; .outerBox{ width: 500px;position: relative; .cbox{ width:500px; padding:10px 0px; border-bottom: 1px solid #eee;position: relative; .qtitle{ font-size:18px;font-weight:bold;margin-bottom:10px; } label{margin-right: 10px;cursor: pointer;} input[type=checkbox], input[type=radio]{margin-right: 5px;} select{ width:300px;height:30px; border: 1px solid #bdbdbd;border-radius:6px; } } .edit{ position:absolute;right:20px;top:16px; width:20px;height:20px; background:url(/images/bianji.svg); background-size:cover; display:none; } .down{ position:absolute;right:50px;top:18px; width:16px;height:16px; background:url(/images/down.svg); background-size:cover;display:none; } .up{ position:absolute;right:80px;top:18px; width:16px;height:16px; background:url(/images/up.svg); background-size:cover;display:none; } &:hover .edit, &:hover .up, &:hover .down{display:block;} &.onedit{animation:donghua .5s linear infinite alternate;} @-webkit-keyframes donghua{ 0%{box-shadow:0px 0px 0px red;} 100%{box-shadow:0px 0px 20px red;} } } } } </style>
第二步:拆分组件App.vue
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<template> <div> <div class="warp"> <div class="leftPart"> <typeTestArea></typeTestArea> </div> <div class="centerPart"> <div class="outerBox"> <singleOption></singleOption> <span class="edit"></span> <span class="up"></span> <span class="down"></span> </div> <div class="outerBox"> ... </div> </div> <div class="rightPart"> <setArea></setArea> </div> </div> </div> </template> <script> import singleOption from "./components/singleOption.vue"; import multipleOption from "./components/multipleOption.vue"; import menuOption from "./components/menuOption.vue"; import setArea from "./components/setArea.vue"; import typeTestArea from "./components/typeTestArea.vue"; export default{ components:{ singleOption, multipleOption, menuOption, setArea, typeTestArea } } </script>
把单选、多选、下拉分别拆分到singleOption.vue、multipleOption.vue、menuOption.vue中。
组件中的.cbox类名可以不用写了,后面会在App.vue中添加。
第三步:设置题目默认数据和显示到视图(App.vue)
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<template> <div> <div class="warp"> <div class="leftPart"> <typeTestArea></typeTestArea> </div> <div class="centerPart" > <div class="outerBox" v-for="(item,index) in q"> <!-- <singleOption></singleOption> --> <!-- :is="item.type" 表示要显示的选项类型 --> <div :is="item.type" :item="item" :index="index" class="cbox"></div> <span class="edit" :data-index="index"></span> <span class="up" :data-index="index"></span> <span class="down" :data-index="index"></span> </div> </div> <div class="rightPart"> <setArea></setArea> </div> </div> </div> </template> <script> export default{ data(){ return { q:[ { "title":"你觉得下面哪个学历最牛叉?", "type":"singleOption", "option":[ {"v":"家里蹲大学"}, {"v":"英国贱桥大学"}, {"v":"美国麻绳礼工"}, {"v":"蓝翔技工学校"} ], "required":false //是否为必填 }, { "title":"你喜欢吃的食物? ", "type":"multipleOption", "option":[ {"v":"榴莲"}, {"v":"香蕉"}, {"v":"葡萄"}, {"v":"梨子"} ], "required":false }, { "title":"治疗失眠最有效的方法是?", "type":"menuOption", "option":[ {"v":"吃安眠药"}, {"v":"看国产电视剧"}, {"v":"催眠术"}, {"v":"用大锤打晕"} ], "required":false } ] } } } </script>
下面把数据显示在视图
单选组件singleoption.vue:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<template> <div> <div class="qtitle"> <em v-show="item.required"> * </em> {{index+1}}、{{item.title}} </div> <div class="qoption"> <label v-for="option in item.option"> <input type="radio" :name="'q'+(index+1)" /> {{option.v}} </label> </div> </div> </template> <script> export default{ props:["item","index"] } </script>
多选组件multipleoption.vue:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<template> <div> <div class="qtitle"> <em v-show="item.required"> * </em> {{index+1}}、{{item.title}} </div> <div class="qoption"> <label v-for="option in item.option"> <input type="checkbox" :name="'q'+(index+1)" /> {{option.v}} </label> </div> </div> </template> <script> export default{ props:["item","index"] } </script>
下拉组件menuoption.vue:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<template> <div> <div class="qtitle"> <em v-show="item.required">*</em> {{index+1}}、{{item.title}} </div> <div class="qoption"> <select> <option v-for="option in item.option" :value="option.v"> {{option.v}} </option> </select> </div> </div> </template> <script> export default{ props:["item","index"] } </script>
第四步:拖拽
App.vue
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<script> export default{ data(){ return { ... }, components:{ ... }, //组件上树之后的生命周期 mounted:function(){ var self = this; //draggable(拖拽)和sortable(拖拽排序)结合使用 //拖拽 $('.typeTestBox li').draggable({ connectToSortable:".centerPart", //可拖拽到什么位置 helper:"clone", //克隆拖拽 revert: "invalid",//拖拽停止时,归位的动画 }); //拖拽排序 $('.centerPart').sortable({ cancel:".cbox,span", //禁止从匹配的元素上拖拽排序。 //当排序停止时触发该事件。 stop:function(event,ui){ //获取拖拽后的排序编号和data-titletype属性值 var index = $(ui.item[0]).index(); var titleType = $(ui.item[0]).data("titletype"); //拖拽后题目名称消失 $(ui.item[0]).remove(); //然后从index开始,不删除,添加新项 self.q.splice(index,0,{ "title":"一个新的题目,请编辑", "type":titleType, "option":[ {"v":"新选项A"}, .. {"v":"新选项D"} ], "required":false }); } }) //事件委托,上箭头、下箭头 //向上排序交互位置 $(".centerPart").on('click','.up', function(event){ var index = $(this).data("index"); //获取题目编号 if(index > 0){//如果大于0即可交换位置 //尾删头插 //temp是要添加的新项,即删除的那项(即当前点击的项) var temp = self.q.splice(index,1)[0]; //从当前的上一题开始,删除0项,从后面添加新项 self.q.splice(index-1,0,temp); }; }); $(".centerPart").on('click','.down', function(event){ var index = $(this).data("index"); var temp = self.q.splice(index,1)[0]; self.q.splice(index+1,0,temp) }); } } </script>
第五步:题目编辑
main.js
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
import Vue from "vue";
import Vuex from "vuex";
import App from "./App.vue";
Vue.use(Vuex);
// 创建一个全局仓库
const store = new Vuex.Store({
state : {
nowedit : 1 //当前编辑的题号
},
mutations: {
// 修改全局的nowedit
changeNowEdit(state,{nowedit}){
state.nowedit = nowedit
}
}
})
App.vue:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<template> <div> <div class="wrap"> <div class="leftPart"> <typeTestArea></typeTestArea> </div> <div class="centerPart"> <div class="outerBox" v-for="(item,index) in q"> <div :is="item.type" :item="item" :index="index" class="cbox"></div> <span class="edit" :data-index="index"></span> <span class="up" :data-index="index"></span> <span class="down" :data-index="index"></span> </div> </div> <div class="rightPart"> <setArea v-if="$store.state.nowedit != 0" :item="q[$store.state.nowedit - 1]"> </setArea> </div> </div> </div> </template>
setarea.vue右侧组件布局:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<template> <div class="typeTestArea"> <h3>设置题目</h3> <div class="con"> 标题:<input type="text" v-model="item.title" /> </div> <div class="con"> 是否必填:<input type="checkbox" v-model="item.required"> </div> <div class="con"> 题型: <input type="radio" value="singleoption" v-model="item.type" />单选 <input type="radio" value="multipleoption" v-model="item.type" />多选 <input type="radio" value="menuoption" v-model="item.type" />下拉 </div> <div class="con"> <!-- 题目选项们(更改之后,鼠标离开后双向修改) --> <div class="options"> <p v-for="(option,index) in item.option" :key="option.v"> <input type="text" v-model="option.v"> <span class="del"></span> <span class="changeOrder"></span> </p> </div> <p class="addoption" >添加新的选项</p> </div> </div> </template> <script> export default{ props:["item"], } </script> <style scoped lang='stylus'> .typeTestArea{ padding:20px; .con{ line-height:150%;padding:10px 0; } input[type="text"]{ width:230px;height:30px;color: #495060; border-radius:4px; border: 1px solid #dddee1;padding-left:5px; } .addoption{ width:230px;height:35px;background: #2db7f5;border-radius:5px; } .addoption:hover{background:#18b566;} .options input{ margin-bottom:10px; } .del,.changeOrder{ display:inline-block;width: 16px;height:16px;padding:2px; background:url(/images/del.svg);background-size:cover; position:relative;top:6px;left:5px;border-radius:5px; } .changeOrder{ background:url(/images/order.svg);cursor:move; } .del:hover,.changeOrder:hover{animation:donghua 0.3s linear 0s alternate;} @-webkit-keyframes donghua{ 0%{transform:rotate(0deg) scale(1);} 50%{transform:rotate(180deg) scale(1.3);} 100%{transform:rotate(360deg) scale(1);} } } </style>
setarea.vue右侧组件功能实现:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<template> <div class="typeTestArea"> <h3>设置题目</h3> <div class="con"> 标题:<input type="text" v-model="item.title" /> </div> <div class="con"> 是否必填:<input type="checkbox" v-model="item.required"> </div> <div class="con"> 题型: <input type="radio" value="singleoption" v-model="item.type" />单选 <input type="radio" value="multipleoption" v-model="item.type" />多选 <input type="radio" value="menuoption" v-model="item.type" />下拉 </div> <div class="con"> <!-- 题目选项们(更改之后,鼠标离开后双向修改) --> <div class="options" ref="option"> <p v-for="(option,index) in item.option" :key="option.v"> <input type="text" v-model.lazy="option.v"> <span class="del" @click="delBtn(index)"></span> <span class="changeOrder"></span> </p> </div> <p class="addoption" @click="addoption">添加新的选项</p> </div> </div> </template> <script> export default{ props:["item"], methods:{ addoption(){ this.item.option.push({"v":""}); }, delBtn(index){ this.item.option.splice(index,1); } }, mounted:function(){ var startIndex = 0; //全局变量 var self =this; $(this.$refs.option).sortable({ handle:".changeOrder", //限制拖拽的对象 //拖拽开始 start:function(e,ui){ //获取当前拖拽的编号 startIndex = $(ui.item).index(); console.log(startIndex) }, //拖拽结束后 stop:function(e,ui){ //拖拽结束后的编号 var endIndex = $(ui.item).index(); //视图中题目的选项也要跟着变化(前删后插) //从startIndex删除1项 var delOption = self.item.option.splice(startIndex,1)[0]; //从endIndex的位置添加之前删除的项 self.item.option.splice(endIndex,0,delOption); } }) } } </script>
App.vue 编辑题目按钮:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<template> <div> <div class="warp"> <div class="leftPart"> <typetestarea></typetestarea> </div> <div class="centerPart" > <div :class="{'outerBox':true,'onedit':$store.state.nowedit==index+1}" v-for="(item,index) in q"> <div :is="item.type" :item="item" :index="index" class="cbox"></div> <span class="edit" :data-index="index" @click="$store.commit('changeNowEdit',{'nowedit':index+1})"> </span> <span class="up" :data-index="index" @click="$store.commit('changeNowEdit',{'nowedit':index+1})"> </span> <span class="down" :data-index="index" @click="$store.commit('changeNowEdit',{'nowedit':index+1})"> </span> </div> </div> <div class="rightPart"> <setarea v-if="$store.state.nowedit != 0" :item="q[$store.state.nowedit-1]"> </setarea> </div> </div> </div> </template> <script> export default{ data(){ return { ... } }, mounted:function(){ var self = this; //拖拽排序 $('.centerPart').sortable({ cancel:".cbox,span", // 当排序停止时触发该事件。 stop:function(event,ui){ ... self.q.splice(index,0,{ ... }); //拖拽添加题目完成后,让新的题目变成当前编辑状态 self.$store.commit('changeNowEdit',{'nowedit':index+1}); } }); } } </script>
v-model的问题:
1)我们的数据放在全局,全局的数据理论上讲只能有commit()来更改!
2)vue中视图更新的原理和React完全不同,vue使用数据劫持,只要数据变化,视图一定变化。
v-model可以直接和全局的store中的数据进行双向绑定!但是绑定了,就违背了commit()更改数据的原则,管他呢!!
我们可以在computed中写set()和get()方法,让get从store中取值,set发出commit命令。
官网:https://vuex.vuejs.org/zh-cn/forms.html