2024年2月

整体

<template>
    <div class="project_main">
      <div class="alert_box">
        <el-alert v-if="alert.show" :title="alert.title" :type="alert.type" closable @close="handleAlertClose" />
      </div>
      <el-dialog v-model="confirmdialog" title="Warning" width="30%" center>
      <span>Confirm deleting this data row
      </span>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="confirmdialog = false">Cancel</el-button>
          <el-button type="primary" @click="confirmDelete">
            Confirm
          </el-button>
        </span>
      </template>
    </el-dialog>
      <div class="search_box">
        <el-form :inline="true" class="search_info">
            <el-form-item label="project:">
              <el-input v-model="projectname" placeholder="项目名" clearable />
            </el-form-item>
            <el-form-item label="owner:" >
              <el-input v-model="projectowner" placeholder="负责人" clearable />
            </el-form-item>
            <el-form-item label="rank:">
            <el-select
              v-model="projectrank"
              class="m-2"
              placeholder="Select"
              size="large"
              style="width: 240px"
              clearable 
            >
              <el-option
                v-for="item in options"
                :key="item.value"
                :label="item.label"
                :value="item.value"
              />
            </el-select>
          </el-form-item>
        <el-form-item>
            <el-button type="primary" @click="handleSubmit(1,10,projectname,projectowner,projectrank)">Query</el-button>
          </el-form-item>
          <el-form-item>
          <el-button type="primary" class="add_row" @click="handleAdd">Add</el-button>
        </el-form-item>
        </el-form>
      </div>
      <div class="project_table_box">
        <el-table :data="projectlist" style="width: 100%" :row-height="rowHeight">
          <el-table-column fixed prop="project_name" label="项目名" width="150">
            <template #default="{ row }">
              <span v-if="!row.editing && !row.addediting">{{ row.project_name }}</span>
              <el-input
                v-else
                v-model="row.editName"
              ></el-input>
            </template>
          </el-table-column>
          <el-table-column prop="project_rank" label="重要等级" width="140" >
            <template #default="{ row }">
              <span v-if="!row.editing && !row.addediting">{{ row.project_rank }}</span>
              <el-select
              v-else
              v-model="row.editRank"
              class="m-2"
              placeholder="Select"
              size="large"
              style="width: 110px"
              clearable 
            >
              <el-option
                v-for="item in options"
                :key="item.value"
                :label="item.label"
                :value="item.value"
              />
            </el-select>
            </template>
          </el-table-column>
          <el-table-column prop="project_owner_id" label="负责人" width="120">
            <template #default="{ row }">
              <span v-if="!row.editing && !row.addediting">{{ row.project_owner_id }}</span>
              <el-input
                v-else
                v-model="row.editOwner"
              ></el-input>
            </template>
          </el-table-column>
          <el-table-column prop="project_create_time" label="创建时间" width="320" />
          <el-table-column prop="project_last_mod" label="最后修改时间" width="600" />
          <el-table-column fixed="right" label="Operations" width="240">
            <template #default="{ row, $index }" class="operation_box">
              <el-button size="small" v-if="!row.editing && !row.addediting" @click="handleEdit(row, $index)">Edit</el-button>
              <el-button size="small" type="danger" v-if="row.editing && !row.addediting" @click="handleConfirm(row, $index)">Confirm</el-button>
              <el-button size="small" type="danger" v-if="!row.editing && !row.addediting" @click="handleDelete(row, $index)">Delete</el-button>
              <el-button size="small" v-if="row.editing" @click="handleCancel(row, $index)">Cancel</el-button>
              <el-button size="small" type="danger" v-if="row.addediting" @click="handleaddConfirm(row, $index)">Confirm2</el-button>
              <el-button size="small" v-if="row.addediting" @click="handleaddCancel(row, $index)">Cancel2</el-button>
            </template>
            </el-table-column>
        </el-table>
      </div>
      <div class="pagination">
        <div class="demo-pagination-block">
          <div class="demonstration">每页显示数</div>
          <el-pagination
            v-model:current-page="page"
            v-model:page-size="page_size"
            :page-sizes="[10, 20, 30, 40, 50]"
            :small="small"
            :disabled="disabled"
            :background="background"
            layout="sizes, prev, pager, next"
            :total="total_items"
            @size-change="handleSizeChange"
            @current-change="handleCurrentChange"
          />
  </div>
      </div>
    </div>
  </template>
  
  <script setup>
  import { ref, onMounted } from 'vue';
  import { projectrequest, projectupdate, projectdelete } from '../network/projectrequest';
  
  const projectlist = ref([]);
  const page = ref(1);
  const page_size = ref(10);
  const rowHeight = 40; 
  const small = ref(false);
  const background = ref(false);
  const disabled = ref(false);
  const projectname = ref('');
  const projectowner = ref('');
  const pagination_ctl = ref();
  const total_items = ref();
  const projectrank = ref('');
  const confirmdialog = ref(false);
  const options = [
    {
      value: 'P0',
      label: 'P0',
    },
    {
      value: 'P1',
      label: 'P1',
    },
    {
      value: 'P2',
      label: 'P2',
    },
    {
      value: 'P3',
      label: 'P3',
    },
    {
      value: 'P4',
      label: 'P4',
    },
    {
      value: 'P5',
      label: 'P5',
    },
  ];

  const alert = ref({
  show: false,
  type: 'success',
  title: 'Success alert',
});
function handleAlertClose() {
  alert.value.show = false;
}
  
  onMounted(() => {
    projectinfo(page.value, page_size.value)
  });
  function projectinfo(currentpage,currentpage_size,projectname,projectowner,projectrank){
    projectrequest(currentpage, currentpage_size,projectname,projectowner,projectrank).then((res) => {
      console.log(res.data)
      console.log(res)
      projectlist.value = res.data.data;
      pagination_ctl.value = res.data.pagination;
      total_items.value = pagination_ctl.value['total_items'];
    });
  }

  function handleSizeChange(pagesize_now){
    page_size.value = pagesize_now;
    page.value = 1;
    projectinfo(page.value, page_size.value, projectname.value, projectowner.value, projectrank.value);
  }

  function handleCurrentChange(page_now){
    page.value = page_now
    projectinfo(page.value, page_size.value, projectname.value, projectowner.value, projectrank.value);
  }

  function handleSubmit(currentpage,currentpage_size,projectname,projectowner,projectrank) {
  projectinfo(currentpage,currentpage_size,projectname,projectowner,projectrank);
  projectname = '';
  projectowner = '';
  }

  function handleEdit(row, index) {
  projectlist.value[index].editName = row.project_name;
  projectlist.value[index].editOwner = row.project_owner_id;
  projectlist.value[index].editRank = row.project_rank;
  projectlist.value[index].editing = true;
}

  function handleConfirm(row, index) {
    const editedData = {
    project_id: row.project_id,
    project_name: row.editName,
    project_rank: row.editRank,
    project_owner_id: row.editOwner,
  };
  //  console.log(editedData);
   projectupdate(editedData)
    .then((res) => {
      if (res.data.responsecode === 1000) {
        alert.value.type = 'success';
        alert.value.title = '更新成功';
        alert.value.show = true;
        // console.log(alert.value)
        projectlist.value[index].project_name = row.editName;
        projectlist.value[index].project_owner_id = row.editOwner;
        projectlist.value[index].editing = false;
      } else {
        alert.value.type = 'error';
        alert.value.title = `更新失败: ${res.data.errorMessage}`;
        alert.value.show = true;
        
        console.error('更新失败:', res.data.errorMessage);
      }
    })
    .catch((error) => {
      alert.value.type = 'error';
      alert.value.title = '更新失败';
      alert.value.show = true;

      console.error('更新失败:', error);
    });
}

  function handleCancel(row, index) {
  projectlist.value[index].editing = false;
}

  function handleDelete(row, index){
    confirmdialog.index = row.project_id;
    confirmdialog.value = true;
}

  function confirmDelete(){
    // console.log(confirmdialog.index);
    projectdelete(confirmdialog.index)
      .then((response) => {
      if (response.data.responsecode === 1000) {
        projectlist.value.splice(confirmdialog.index, 1);
        confirmdialog.value = false;
        alert.value.type = 'success';
        alert.value.title = '删除成功';
        alert.value.show = true;
      } else {
        alert.value.type = 'error';
        alert.value.title = `删除失败: ${response.data.data}`;
        alert.value.show = true;
        confirmdialog.value = false;
      }
    })
    .catch((error) => {
      alert.value.type = 'error';
      alert.value.title = `删除失败: ${error}`;
      alert.value.show = true;
      confirmdialog.value = false;
    });
  }

  function handleAdd() {
  projectlist.value.unshift({
    project_name: '',
    project_rank: '',
    project_owner_id: '',
    project_create_time: '',
    project_last_mod: '',
    addediting: true,
  });
}
  function handleaddConfirm(row, index) {
    const newData = {
    project_name: row.editName,
    project_rank: row.editRank,
    project_owner_id: row.editOwner,
  };
  projectupdate(newData)
    .then((res) => {
      if (res.data.responsecode === 1000) {
        alert.value.type = 'success';
        alert.value.title = '添加成功';
        alert.value.show = true;

        projectinfo(page.value, page_size.value);
      } else {
        alert.value.type = 'error';
        alert.value.title = `添加失败: ${res.data.errorMessage}`;
        alert.value.show = true;

        projectlist.value.splice(index, 1);
      }
    })
    .catch((error) => {
      alert.value.type = 'error';
      alert.value.title = `添加失败: ${error}`;
      alert.value.show = true;

      projectlist.value.splice(index, 1);
    });
  }

  function handleaddCancel(row, index) {
    projectlist.value.splice(index, 1);
  }
  </script>
  
  <style scoped>
  .el-alert {
    margin: 20px 0 0;
    z-index: 1000; 
  }
  .el-alert:first-child {
    margin: 0;
  }

  .operation_box {
    position: relative;
  }

  .operation_box .el-button {
    position: absolute;
    margin-top: 10px;
  }

  .operation_box .el-button:nth-child(2) {
    left: 70px; 
  }
  </style>

拆解

查询功能用表单实现,search_box部分,本质上就是用表单获取新参数,然后handleSubmit里面提交给后端,翻页也是类似的原理,获取参数-->传给后端
element本身表格不带增删改功能,主要难点在实现这块
CUD部分实现主要分两块内容:
1.表格切换可输入文本框:
需要从展示数据切换到可输入的文本框的功能有增和改,这块思路是给行(row)加一个状态属性,通过这个状态属性来切换前端展示的组件,具体看project_table_box这块
如下例,如果满足v-if="!row.editing && !row.addediting",那么这里就展示数据,如果不满足,那就变成input

  <el-table-column fixed prop="project_name" label="项目名" width="150">
    <template #default="{ row }">
      <span v-if="!row.editing && !row.addediting">{{ row.project_name }}</span>
      <el-input
        v-else
        v-model="row.editName"
      ></el-input>
    </template>
  </el-table-column>

控制状态切换的按钮放在了operation_box里面,增加功能的按钮为了美观放在了search_box部分
组件响应顺序:点击功能按钮--功能按钮响应函数修改前端--点击confirm--提交数据给后端,根据返回结果来改变前端/点击cancel--只改变前端,不提交数据
2.从当前行获取数据
主要利用了vue3的slot机制,也就是代码里看到的<template #default="{ row, $index }">这玩意,这种子template的作用就是从父template那边获取元素进行渲染,default定义了获取到的这些东西的变量名,在这里,row是当前行的数据,index表示当前行的索引(也就是projectlist这个数组里的索引),operation_box因为是project_table_box里面的子表,所以他的row和project_table_box的row是一样的。我们可以通过编辑row里面的属性来实现功能,或者通过index直接访问projectlist里面对应的数据。
需要注意的是,增加功能为了给用户提供一行可以用来编辑的空表,用projectlist.value.unshift直接在数组的最前面插入了一行空数据,这种插入是前端性质的,不涉及后端交互,又因为是插入在第一位,所以插入完成或者取消后,可以使用projectlist.value.splice(index, 1)从前端删除这条数据,index表示要移除的索引,1代表要移除1条数据
删除功能和增改实现思路差不多,不过把编辑换成了一个弹窗确认,依旧是operation按钮用于切换前端,在弹出来的弹窗上面的按钮才是真正绑定交互函数的按钮

2.png

下载安装node.js
创建项目目录,在里面npm init vite@latest,可以直接选框架语言快速生成,也可以选完框架后选择自定义,按提示安装相关vue组件,最后进去npm install安装依赖(根据package.json安装),npm run dev运行环境,vscode安装volar
目录中的 index.html 是项目的入口;package.json 是管理项目依赖和配置的文件;public 目录放置静态资源,比如 logo 等图片;vite.config.js 就是和 Vite 相关所有工程化的配置;src 就是工作的重点,我们大部分的代码都会在 src 目录下管理和书写,后面我们也会在 src 目录下细化项目规范。
访问提示里给的本地路径,可以看到vite的界面
Vue 负责核心,Vuex 负责管理数据,vue-router 负责管理路由
npm install vuex@next
npm install vue-router@next
如果在npm i的时候使用了--save,则该包会被安装到所有的环境
如果--save-dev,则只保存在dev环境
package.json里面,各个版本号的前面,^代表大版本号不动,后两位找当前最新,~代表前两位不动,最后一位最新
vue3基于proxy实现了响应机制

语法

风格

把vue.app删光光后,里面就剩下这两块东西
一块放js,一块放html
另外需要注意的是,默认情况下app.vue是程序的入口,也就是这里的东西会出现在所有的程序中,后面开发时,除非你有这方面的需求,如固定的侧边栏导航栏,不然不要在这里写页面代码(除了路由)

<template>
这里放html
</template>

<script>
这里放js
</script>

vue有两种风格,分别是选项式(vue2)和组合式(vue3)
选项式

<script>
export default {
  // data() 返回的属性将会成为响应式的状态
  // 并且暴露在 `this` 上
  data() {
    return {
      count: 0
    }
  },

  // methods 是一些用来更改状态与触发更新的函数
  // 它们可以在模板中作为事件处理器绑定
  methods: {
    increment() {
      this.count++
    }
  },

  // 生命周期钩子会在组件生命周期的各个不同阶段被调用
  // 例如这个函数就会在组件挂载完成后被调用
  mounted() {
    console.log(`The initial count is ${this.count}.`)
  }
}
</script>

<template>
  <button @click="increment">Count is: {{ count }}</button>
</template>

组合式,以下笔记都将由组合式编写(为什么网上教程全是选项式的啊,怒了

<script setup>
import { ref, onMounted } from 'vue'

// 响应式状态
const count = ref(0)

// 用来修改状态、触发更新的函数
function increment() {
  count.value++
}

// 生命周期钩子
onMounted(() => {
  console.log(`The initial count is ${count.value}.`)
})
</script>

<template>
  <button @click="increment">Count is: {{ count }}</button>
</template>

组合式的风格可以把一组相关的数据和方法放在一起,以便更好地管理

function useTodos() {
  let title = ref("");
  let todos = ref([{ title: "学习Vue", done: false }]);
  function addTodo() {
    todos.value.push({
      title: title.value,
      done: false,
    });
    title.value = "";
  }
  function clear() {
    todos.value = todos.value.filter((v) => !v.done);
  }
  let active = computed(() => {
    return todos.value.filter((v) => !v.done).length;
  });
  let all = computed(() => todos.value.length);
  let allDone = computed({
    get: function () {
      return active.value === 0;
    },
    set: function (value) {
      todos.value.forEach((todo) => {
        todo.done = value;
      });
    },
  });
  return { title, todos, addTodo, clear, active, all, allDone };
}

这段代码定义了一个名为 useTodos 的函数,它返回一个对象,该对象包含了一些属性和方法,用于管理一个 todo 列表。

let title = ref("");: 定义了一个响应式数据 title,初始值为空字符串。

let todos = ref([{ title: "学习Vue", done: false }]);: 定义了一个响应式数据 todos,初始值是一个包含一个 todo 对象的数组,该对象有一个 title 属性和一个 done 属性。

function addTodo() { ... }: 定义了一个名为 addTodo 的方法,用于向 todos 数组中添加一个新的 todo 对象。该方法将新的 todo 对象 push 到 todos.value 中,并将 title.value 设置为空字符串。

function clear() { ... }: 定义了一个名为 clear 的方法,用于清除已完成的 todo 对象。该方法使用 filter 方法从 todos.value 数组中筛选出未完成的 todo 对象,然后将结果赋值给 todos.value。

let active = computed(() => { ... }): 定义了一个计算属性 active,它返回未完成的 todo 对象的数量。计算属性使用 todos.value 数组进行计算,并返回结果。

let all = computed(() => todos.value.length);: 定义了一个计算属性 all,它返回 todos.value 数组的长度。

let allDone = computed({ ... }): 定义了一个计算属性 allDone,它既可以读取也可以设置。当读取 allDone 时,它会判断 active.value 是否等于 0,并返回判断结果。当设置 allDone 时,它会遍历 todos.value 数组,并将每个 todo 对象的 done 属性设置为传入的值。

return { title, todos, addTodo, clear, active, all, allDone };: 返回一个包含 title、todos、addTodo、clear、active、all 和 allDone 属性的对象。这些属性和方法可以在组件中使用。
比如这样

<script>
import { useTodos } from './useTodos';
const { title, todos, addTodo, clear, active, all, allDone } = useTodos();
</script>
  <template>
  <div>
    <h1>{{ title }}</h1>
    <ul>
      <li v-for="todo in todos" :key="todo.id">{{ todo.text }}</li>
    </ul>
    <button @click="addTodo">Add Todo</button>
    <button @click="clear">Clear Todos</button>
  </div>
</template>

基础

值/属性绑定

文本插值,会根据变量的值动态更新,使用双大括号加载,被加载的值会被当做普通文本输出而不是html处理

<span>Message: {{ msg }}</span>

双大括号的写法不能用于html的属性,所以提供了另外一种写法

<div v-bind:id="dynamicId"></div>
或者简写成
<div :id="dynamicId"></div>

如果想一次性绑多个,可以这样写
const objectOfAttrs = {
  id: 'container',
  class: 'wrapper'
}
//不指定具体的属性
<div v-bind="objectOfAttrs"></div>

//动态的属性名
<a v-bind:[attributeName]="url"> ... </a>

<!-- 简写 -->
<a :[attributeName]="url"> ... </a>

当属性为真值或者true时,这个属性是显性的,如果属性值为false,那么这个属性将不被启用

js表达式

vue支持完整的js表达式,{{}}或者所有的v-指令中都能被识别
但是vue只支持能够返回值的语句,类似赋值语句或者判断语句都无法生效

{{ number + 1 }}

{{ ok ? 'YES' : 'NO' }}

{{ message.split('').reverse().join('') }}

<div :id="`list-${id}`"></div>

ref,reactive

响应式的创建变量、对象
注意是ref,不是Ref,ref用于创建一个响应式变量,而Ref是ts中的ref调用后返回的类型

<script>
import {reactive,computed,toRefs} from 'vue'
interface DataProps {
  count: number;
  double: number;
  increase: () => void;
}
const data: DataProps = reactive({
  count: 0,
  increase: () => {data.count++},
  double: computed(() => data.count * 2)
})
//const refdata = toRefs(data)
</script>

      <p>{{ data.count }}</p>
      <p>{{ data.double }}</p>
      <button @click="data.increase">clickhere</button>

//如果是用torefs转的响应式,increase要写成refdata.count.increase

示例:搜索框

<div class="content">
  <input type="text" placeholder="搜索" ref="searchWord">
  <span class="iconfont icon-fangdajing" @click="search(this.$refs.searchWord.value)"></span>
</div>

要注意的是,ref在函数中引用赋值时,需要指定访问xxx.value,但是从template传递过来时(包括从函数参数传过来时),可以直接访问
示例:翻页

<template>
    <div class="project_main">
      <div class="search_box"></div>
      <div class="project_table_box">
        <el-table :data="projectlist" style="width: 100%" :row-height="rowHeight">
          <el-table-column fixed prop="project_name" label="项目名" width="150" />
          <el-table-column prop="project_rank" label="重要等级" width="120" />
          <el-table-column prop="project_owner_id" label="负责人" width="120" />
          <el-table-column prop="project_create_time" label="创建时间" width="320" />
          <el-table-column prop="project_last_mod" label="最后修改时间" width="600" />
        </el-table>
      </div>
      <div class="pagination">
        <div class="demo-pagination-block">
          <div class="demonstration">每页显示数</div>
          <el-pagination
            v-model:current-page="page"
            v-model:page-size="page_size"
            :page-sizes="[10, 20, 30, 40, 50]"
            :small="small"
            :disabled="disabled"
            :background="background"
            layout="sizes, prev, pager, next"
            :total="100"
            @size-change="handleSizeChange"
            @current-change="handleCurrentChange"
          />
  </div>
      </div>
    </div>
  </template>
  
  <script setup>
  import { ref, onMounted } from 'vue';
  import { projectrequest } from '../network/projectrequest';

  
  const projectlist = ref([]);
  const page = ref(1);
  const page_size = ref(10);
  const rowHeight = 40; 
  const small = ref(false)
  const background = ref(false)
  const disabled = ref(false)
  
  onMounted(() => {
    projectinfo(page.value, page_size.value)
  });
  function projectinfo(currentpage,currentpage_size){
    projectrequest(currentpage, currentpage_size).then((res) => {
      projectlist.value = res.data.data;
      console.log(projectlist.value)
      console.log(currentpage,currentpage_size)
    });
  }

  function handleSizeChange(pagesize_now){
    page_size.value = pagesize_now;
    page.value = 1;
    projectinfo(page.value, page_size.value)
  };

  function handleCurrentChange(page_now){
    page.value = page_now
    projectinfo(page.value, page_size.value)
  }
  </script>

计算属性

利用现有定义的值来计算出一个新的值,好处是计算过一次后就会缓存

<script setup>
import { reactive, computed } from 'vue'

const author = reactive({
  name: 'John Doe',
  books: [
    'Vue 2 - Advanced Guide',
    'Vue 3 - Basic Guide',
    'Vue 4 - The Mystery'
  ]
})

// 一个计算属性 ref
const publishedBooksMessage = computed(() => {
  return author.books.length > 0 ? 'Yes' : 'No'
})
</script>

<template>
  <p>Has published books:</p>
  <span>{{ publishedBooksMessage }}</span>
</template>

v-if/show

v-i会根据值的真假来判断是否插入/移除该节点

<p v-if="seen">Now you see me</p>
<div v-if="type === 'A'">
  A
</div>
<div v-else-if="type === 'B'">
  B
</div>
<div v-else-if="type === 'C'">
  C
</div>
<div v-else>
  Not A/B/C
</div>

示例

<template>
  <div><button @click="onclickup" id="show">click here</button></div>
  <div v-if!="flag">U.N is her?</div>
  <div v-else>WOW!</div>
</template>

<script setup>
  import { ref } from 'vue';

  const flag = ref(false)

  function onclickup() {
    if (flag.value === false){
      flag.value = true
    }
    else if(flag.value === true){
      flag.value = false
    }
  }
</script>

v-show和v-if功能类似,都是控制节点是否展示,但是show是在样式添加css--display:none,隐藏后dom仍然存在,if则是增加删除对应的dom

v-on

监听dom事件,用于对事件的反应,可以用于函数的调用

<a v-on:click="counter += 1"> ... </a>

<!-- 简写 -->
<a @click="doSomething"> ... </a>

<a v-on:[eventName]="doSomething"> ... </a>

<!-- 简写 -->
<a @[eventName]="doSomething">

<!--带参-->
<button @click="say('hello')">Say hello</button>

function say(message) {
  alert(message)
}

v-for

遍历取值,绑定key属性可以让vue根据该属性所对应的值来绑定节点的身份,以方便其进行排序,如果没有可绑的可以用index

<template>
<--值only-->
<li v-for="item in agelist">{{ item.message }}</li>
<--值和索引-->
<li v-for="(item,index) in agelist">{{index}} : {{ item.message }}</li>
<--用第二个位置代表属性,用第三个位置代表索引-->
<li v-for="(value,key,index) in character" :key="key|index">{{ index }}:{{ key }}:{{ value }}</li>
<--范围内取值,从1开始-->
<span v-for="n in 10">{{ n }}</span>
</template>

<script setup>
import { ref } from 'vue';

const flag = ref(false)
const agelist = ref([{message:17},{message:20}])
const character = ref({
  yukari : 17,
  marisa : 20
})

</script>

<!-- 17
20
0 : 17
1 : 20
0:yukari:17
1:marisa:20
2:erin:9999
12345678910 -->

如果要同时使用v-for和v-if,不要放在同一级,if优先级比for更高,会影响代码功能
正确示范

<template v-for="todo in todos">
  <li v-if="!todo.isComplete">
    {{ todo.name }}
  </li>
</template>

v-model

表单绑定

<p>Message is: {{ message }}</p>
  <input v-model="message" placeholder="edit me" />

  <input type="checkbox" id="checkbox" v-model="checked" />
  <label for="checkbox">{{ checked }}</label>

  //复选框的值放入列表,列表中的值为选项的value
  <template>
  <div>choice list:{{ checklist }}</div>
  <form>
  <input type="checkbox" v-model="checklist" id="zhangsan" value="zhangsan">
  <label for="zhangsan">zhangsan</label>
  <input type="checkbox" v-model="checklist" id="lisi" value="lisi">
  <label for="lisi">lisi</label>
  <input type="checkbox" v-model="checklist" id="wangwu" value="wangwu">
  <label for="wangwu">wangwu</label>
  </form>
  </template>
  <script>
  const checklist = ref([])
</script>

  //自定义值选择器
  <select v-model="selected">
    <option v-for="option in options" :key="option.value" :value="option.value">
    {{ option.text }}
</option>
  </select>

  <div>Selected: {{ selected }}</div>
  const options = ref([
    { text: 'One', value: 'A' },
    { text: 'Two', value: 'B' },
    { text: 'Three', value: 'C' }
  ])

修饰符

<!-- 在 "change" 事件后同步更新而不是 "input" -->
<input v-model.lazy="msg" />

//自动转数字
<input v-model.number="age" />

//去除两端空格
<input v-model.trim="msg" />

watch

监听某个值的变化并对其进行操作
例:监听count的变化

<script setup>
import { ref, watch } from 'vue';
const count = ref(0)
watch(count,(newValue, oldValue)=>{
  if(newValue >=3){
    console.log("too much click",newValue)
  }
}
)
function clickup(){
  count.value++
}
</script>

<template>
  <button @click="clickup">clickhere:{{ count }}</button>

</template>

如果有多个变化的值,可以从newvalue中取对应的值,值名可在控制台看到

v-slot

插槽绑定
假如某模板如下

<template>
  <form class="validate-form-container">
    <slot name="default"></slot>
    <div class="submit-area" @click.prevent="submitForm">
      <slot name="submit">//当没有绑定时会显示默认的提交按钮
        <button type="submit" class="btn btn-primary">提交</button>
      </slot>
    </div>
  </form>
</template>

现对其使用

<template>
  <div class="login-page mx-auto p-3 w-330">
    <h5 class="my-4 text-center">登录到xx</h5>
    <validate-form @form-submit="onFormSubmit">
      <div class="mb-3">
        <label class="form-label">邮箱地址</label>
        <validate-input //上面模板的代码块
          :rules="emailRules" v-model="emailVal"
          placeholder="请输入邮箱地址"
          type="text"
          ref="inputRef"
        />
      </div>
      <div class="mb-3">
        <label class="form-label">密码</label>
        <validate-input
          type="password"
          placeholder="请输入密码"
          :rules="passwordRules"
          v-model="passwordVal"
        />
      </div>
      <template #submit>//v-slot的简写,指定name="submit"
        <button type="submit" class="btn btn-primary btn-block btn-large">登录</button>
      </template>
    </validate-form>
  </div>
</template>

响应式

下面使用组合式表达
想创建一个变量,用ref来进行声明,其值通过var.value输出
当你在模板中使用了一个 ref,然后改变了这个 ref 的值时,Vue 会自动检测到这个变化,并且相应地更新 DOM。这是通过一个基于依赖追踪的响应式系统实现的。当一个组件首次渲染时,Vue 会追踪在渲染过程中使用的每一个 ref。然后,当一个 ref 被修改时,它会触发追踪它的组件的一次重新渲染。<script setup> SFC可以避免我们手动在setup()中输出的繁琐

<template>
  <div>count:<button @click="onclickup">{{ count }}</button></div>
  </template>

  <script setup>
  import { ref } from 'vue';

const count = ref(111)

function onclickup() {
  count.value++
}
</script>

直接输出count和count.value的区别

RefImpl {__v_isShallow: false, dep: undefined, __v_isRef: true, _rawValue: 111, _value: 111}dep: undefined__v_isRef: true__v_isShallow: false_rawValue: 111_value: 111value: (...)[[Prototype]]: Object
                                                                                                                                                                              App.vue:9 111

在其他组件模板中使用ref的方式,无setup的情况下

import { ref } from 'vue'

export default {
  setup() {
    const count = ref(0)

    function increment() {
      // 在 JavaScript 中需要 .value
      count.value++
    }

    // 不要忘记同时暴露 increment 函数
    return {
      count,
      increment
    }
  }
}

引用模板

<template>
<HelloWorld />
</template>

<script setup>
import HelloWorld from './components/HelloWorld.vue';
</script>

如果需要父组件给子组件传递一些值,需要用到props
props传过来的值,不能在setup作为变量使用,但是可以在template里面使用

<template>
  <p>title:{{ title }}</p>
  <p>id:{{ paperid }}</p>
</template>

<script setup>
defineProps(['paperid','title'])
</script>
<template>
<HelloWorld v-for="paper in paperlist" :paperid="paper.id" :title="paper.title"/>
</template>

<script setup>
import HelloWorld from './components/HelloWorld.vue';
const paperlist = ref([
  {id: 1, title: 'hello on board'},
  {id: 2, title: 'guide for new blood'},
  {id: 3, title: '100 way to learn vue'}
])
</script>

更全能的写法

const props = defineProps({
  isOpen: Boolean,
  count: {
    type: Number, //类型
    default: 0 //默认值
  },
  title: {
    type: String,
    required: true //是否必须
  }
});

如果想要使用复杂的类型,比如接口数组,得用PropType包裹

export interface Columntype{
    id: number;
    title: string;
    avatar: string;
    abstract: string;
}
const props = defineProps({
    columnlist: {
        type: Array as PropType<Columntype[]>,
        required: true
    }
})

子调用父的方法,可以用emit
https://zhuanlan.zhihu.com/p/581646269?utm_id=0
emit有点像订阅发布的意思,子组件在自己身上注册了一个方法,然后告诉父组件我这里要调用一个名为xxx的方法,父组件真实定义xxx后,再把xxx放入子组件注册这个方法的地方真实调用。也就是子组件负责注册,父组件负责定义和调用

<template>
  <HelloWorld v-for="paper in paperlist" :paperid="paper.id" :title="paper.title" @printpaper="printpaper(paper.title)"/>
</template>
<script setup>
import HelloWorld from './components/HelloWorld.vue';
function printpaper(papername) {
  console.log("already printed paper ",papername)
}
</script>
<template>
  <p>title:{{ title }}</p>
  <p>id:{{ paperid }}</p>
  <button @click="$emit('printpaper')">Print</button>
</template>

<script setup>
defineProps(['paperid','title'])
defineEmits(['printpaper'])
</script>

页面切换,通过点击按钮来切换对应加载的组件,使用component

<template>
<button v-for="(_,tab) in tabs" @click="currentpage = tab" :key="tab">{{ tab }}</button>
<component :is="tabs[currentpage]"></component>
</template>

<script setup>
import { ref } from 'vue';
import HelloWorld from './components/HelloWorld.vue';
import shop from './components/shop.vue'
const currentpage = ref()
</script>

创建一个应用

在默认文件里有个main.js
createApp的参数为根组件,在这里根组件为App

import { createApp } from 'vue'
import './style.css'
import App from './App.vue'

createApp(App).mount('#app')

监听

监听变量的值,当发生变化时执行操作
如果想要同时监控多个值,把第一个参数换成数组

import { watch } from 'vue'
watch(data,()=>{
  document.title = 'data updated'
})

如果想监控一个对象里面的值,需要把那个值函数化

const data: DataProps = reactive({
  count: 0,
  increase: () => {data.count++},
  double: computed(() => data.count * 2)
})
watch(()=>data.count,()=>{  //不能直接data.count
  document.title = 'data updated'
})

插槽

在子组件上提供一个插槽,以便于父组件传入的东西能够替代插槽里面的默认设置
https://zhuanlan.zhihu.com/p/529152853

子组件FancyButton
<button class="fancy-btn">
  <slot></slot> <!-- 插槽出口 -->
</button>

父组件中使用
<FancyButton>
  Click me! <!-- 插槽内容 -->
</FancyButton>

传送

传送可以让你把你的html挂载到其他的dom节点下
某个模组

<template>
    <Teleport to="#modal">//指定要传送的节点的id,被包裹的部分会给传送走
        <div id="centermodal" v-if="isOpen">
            <h2><slot>Modal window</slot></h2>
            <button @click="$emit('onModalclose')">close</button>
        </div>
    </Teleport>
</template>

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <link rel="icon" href="/favicon.ico">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vite App</title>
  </head>
  <body>
    <div id="app"></div>
    <div id="modal"></div>//会被挂载在这里
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>

加载后按f12查看,发现窗口在modal这个div下,和app是平级,成功实现了传送
可以用来避免过深的嵌套

Promise、Suspense

异步执行
示例:两秒后打印42

<template>
    <h1>{{ result }}</h1>
    <span></span>
</template>

<script setup lang="ts">
import {ref} from 'vue'

const result = ref<number | null>(null);

new Promise<number>((resolve) => {
  setTimeout(() => {
    resolve(42);
  }, 2000);
}).then((value) => {
  result.value = value;
});
</script>

app.vue

  <Suspense>
    <template #default>//加载出来后再显示这块
      <Asyncshow />
    </template>
    <template #fallback> //加载失败时显示
      <h1>Loading...</h1>
    </template>
  </Suspense>

provide,inject

在根组件中一次申明,就可以在任意组件中取用

app.vue
const location = ref('HZ')
const changelocation = (input: string) => {
  location.value = input
}
provide('location',location)

      <button @click="changelocation('Tokyo')">changeyourlocation</button>
//点击按钮就能修改
xxx.vue
import {inject} from 'vue'
const location = inject('location')

获取节点

vue3移除了this指针,但是依旧有方法可以获得节点

<template>
  <div class="demo-dropdown-wrap" ref="dropdownRef">
</template>
<script setup lang="ts">
const dropdownRef = ref<null | HTMLElement>(null) //起名一定要和上面一样
const handleMenuClick = (e: MouseEvent) => {
    console.log('click', e);
    if(dropdownRef.value){
        if(dropdownRef.value.contains(e.target as HTMLElement)){
            console.log(dropdownRef.value)
        }
    }
};
</script>

$attrs

好像是阻止vue把子节点的属性合并到父节点上面去的,暂时没用到就先不写了

路由

https://router.vuejs.org/zh/guide
在vue中使用来显示对应的路由界面

基本路由

npm install vue-router@4 --save
当然也可以在创建项目的时候用手脚架快速搭建
在src/router下创建index.js
默认文件

import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  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('../views/AboutView.vue')
    }
  ]
})

export default router

在views下创建个shop,添加到路由里面,访问路径/shop,即可看到对应的页面

import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import shop from '../views/shop.vue'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  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('../views/AboutView.vue')
    },
    {
      path: '/shop',
      name: 'shop',
      component: shop//指定对应的组件名,如果没放在同级目录或者views下要像上面import,上面展示的是动态写法
    },
  ]
})

export default router

动态路由

动态路径,可以动态的识别参数,我这里把User.vue放在了/src/news/

import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import shop from '../views/shop.vue'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'home',
      component: HomeView
    },
    {
      path: '/User/:username/home/:id',
      name: 'User',
      component: () => import('../news/User.vue')
    }
  ]
})

export default router

User.vue

<template>
    <h1>User</h1>
    <p>welcome {{ $route.params.username }},your id is {{ $route.params.id }}</p>
</template>

//访问http://localhost:5173/User/alice/home/123
//显示User
//welcome alice,your id is 123

跳转

如果想要通过链接的形式跳转,使用RouterLink

<RouterLink to="/shop">Shop</RouterLink>

如果是要用变量做跳转,记得直接绑定

<RouterLink :to="'/column/' + item.id" @click="console.log(item.id)">

当你想使用函数做跳转,你就需要push

// const goToHomePage = () => {
//     routerhome.push('/login');
// };

重定向

重定向

    {
      path: '/buy',
      redirect: '/shop'
    }

嵌套路由

嵌套路由

const routes = [
  {
    path: '/user/:id',
    component: User,
    children: [
      {
        // 当 /user/:id/profile 匹配成功
        // UserProfile 将被渲染到 User 的 <router-view> 内部
        path: 'profile',
        component: UserProfile,
      },
      {
        // 当 /user/:id/posts 匹配成功
        // UserPosts 将被渲染到 User 的 <router-view> 内部
        path: 'posts',
        component: UserPosts,
      },
    ],
  },
]

嵌套示例

import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import shop from '../views/shop.vue'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  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('../views/AboutView.vue')
    },
    {
      path: '/shop',
      name: 'shop',
      component: shop
    },
    {
      path: '/User/:username/home/:id',
      name: 'User',
      component: () => import('../news/User.vue'),
      children: [
        {
          path: '',
          component: () => import('../news/Userhome.vue')
        },
        {
          path: 'profile',
          component: () => import('../news/Userprofile.vue')
        },
      ],
    },
    {
      path: '/buy',
      redirect: '/shop'
    }
  ]
})

export default router
import { RouterView } from 'vue-router';
<template>
    <h1>User</h1>
    <p>welcome {{ $route.params.username }},your id is {{ $route.params.id }}</p>
    <RouterView></RouterView> //必须有这行来显示子组件
</template>

Userhome.vue
<template>
    <p>here is home</p>
</template>

Userprofile.vue
<template>
    <p>here is profile</p>
</template>

懒加载

懒加载可以让你访问到对应路由的时候再加载对应的资源,以减少资源开销和加载时间

// 将
// import UserDetails from './views/UserDetails.vue'
// 替换成
const UserDetails = () => import('./views/UserDetails.vue')

const router = createRouter({
  // ...
  routes: [{ path: '/users/:id', component: UserDetails }],
})

props传参

URL /search?q=vue 将传递 {query: 'vue'} 作为 props 传给 SearchUser 组件

const routes = [
  {
    path: '/search',
    component: SearchUser,
    props: route => ({ query: route.query.q })
  }
]

在对应组件使用$route.query.q来获取值
另外一种params的方式在上面动态路由里面展示过了

回退

退回上n页

router.go(n)

push

import { useRouter } from 'vue-router';
const routerhome = useRouter()
const goToHomePage = () => {
    routerhome.push('/');
};

守卫

声明式api里面有这样三个路由守卫

const UserDetails = {
  template: `...`,
  beforeRouteEnter(to, from) {
    // 在渲染该组件的对应路由被验证前调用
    // 不能获取组件实例 `this` !
    // 因为当守卫执行时,组件实例还没被创建!
  },
  beforeRouteUpdate(to, from) {
    // 在当前路由改变,但是该组件被复用时调用
    // 举例来说,对于一个带有动态参数的路径 `/users/:id`,在 `/users/1` 和 `/users/2` 之间跳转的时候,
    // 由于会渲染同样的 `UserDetails` 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
    // 因为在这种情况发生的时候,组件已经挂载好了,导航守卫可以访问组件实例 `this`
  },
  beforeRouteLeave(to, from) {
    // 在导航离开渲染该组件的对应路由时调用
    // 与 `beforeRouteUpdate` 一样,它可以访问组件实例 `this`
  },
}

换到setup的组合式里面,由于setup中路由已经创建完毕,所以只有leave和update两个可用
如果你想使用enter,可以参考这篇,其实原理就是除了setup以外另写个script,在里面用export default
https://blog.richex.cn/vue3-how-to-use-beforerouteenter-in-script-setup-syntactic-sugar.html

import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
import { ref } from 'vue'

export default {
  setup() {
    // 与 beforeRouteLeave 相同,无法访问 `this`
    onBeforeRouteLeave((to, from) => {
      const answer = window.confirm(
        'Do you really want to leave? you have unsaved changes!'
      )
      // 取消导航并停留在同一页面上
      if (!answer) return false
    })

    const userData = ref()

    // 与 beforeRouteUpdate 相同,无法访问 `this`
    onBeforeRouteUpdate(async (to, from) => {
      //仅当 id 更改时才获取用户,例如仅 query 或 hash 值已更改
      if (to.params.id !== from.params.id) {
        userData.value = await fetchUser(to.params.id)
      }
    })
  },
}

当然你也可以用用看路由独享的守卫,不过这个得写在router里面

const routes = [
  {
    path: '/users/:id',
    component: UserDetails,
    beforeEnter: (to, from) => {
      // reject the navigation
      return false
    },
  },
]

完整的导航解析流程

  1. 导航被触发。
  2. 在失活的组件里调用 beforeRouteLeave 守卫。
  3. 调用全局的 beforeEach 守卫。
  4. 在重用的组件里调用 beforeRouteUpdate 守卫(2.2+)。
  5. 在路由配置里调用 beforeEnter。
  6. 解析异步路由组件。
  7. 在被激活的组件里调用 beforeRouteEnter。
  8. 调用全局的 beforeResolve 守卫(2.5+)。
  9. 导航被确认。
  10. 调用全局的 afterEach 钩子。
  11. 触发 DOM 更新。
  12. 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。

示例:检测用户是否登录,否则通通逐一发送login(老牛皮藓网址了

import { globalFlagsStore } from '@/stores/store'
router.beforeEach((to, from, next) => {
  const { user } = globalFlagsStore().flags;
  
  // 如果用户未登录且访问的不是登录页面,则导航至登录页面
  if (!user.isLoging && to.path !== '/login') {
    next('/login');
  } else {
    next();
  }
});

meta

可以在路由上面带元信息,用来配合守卫之类的功能

import ColumnListVue from '@/hooks/ColumnList.vue'
import ColumnDetailVue from '@/views/ColumnDetail.vue'
import HomeVue from '@/views/Home.vue'
import LoginVue from '@/views/Login.vue'
import PostpageVue from '@/views/Postpage.vue'
import { createRouter, createWebHistory } from 'vue-router'
import { globalFlagsStore } from '@/stores/store'


const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'home',
      component: HomeVue
    },
    {
      path: '/login',
      name: 'login',
      component: LoginVue,
      meta: {requirenologin: true}
    },
    {
      path: '/column/',
      name: 'column',
      component: ColumnListVue
    },
    {
      path: '/column/:id',
      name: 'columndetail',
      component: ColumnDetailVue
    },
    {
      path: '/create/',
      name: 'createpost',
      component: PostpageVue,
      meta: {requirelogin: true},
    }

  ]
})

router.beforeEach((to, from, next) => {
  const { user } = globalFlagsStore().flags;
  //登录的不给访问login,没登陆的不给访问create
  if (!user.isLoging && to.meta.requirelogin) {
    next('/login');
  }else if (to.meta.requirenologin && user.isLoging){
    next('/');
  }else {
    next()
  }
});


export default router

promise

const test = new Promise((resolve,rejects) =>{
    setTimeout(() => {
        const data = { name: "John", age: 30 };
        // 如果成功获取到数据,调用 resolve 并传递数据
        resolve(data);
        // 如果获取数据失败,调用 reject 并传递错误信息
        rejects("Error fetching data");
      }, 2000);  
}).then((data)=>{
    if(true){
        console.log(data)
    }
}).catch((error) => {
    console.error(error)
})

async await

async表明一个函数是个异步函数,在async内部可以使用await,await表面这个调用是个promise调用,如果调用成功会返回调用结果,如果失败则会返回报错

async function getData() {
  try {
    const response = await fetch('https://api.example.com/data'); // 等待fetch请求的Promise对象完成
    const data = await response.json(); // 等待解析响应的Promise对象完成
    console.log(data);
  } catch (error) {
    console.log('Error:', error);
  }
}

getData();

pinia

状态管理工具,对标vuex,更轻量
vite自定义安装的时候可以直接勾选,如果快速安装则需要另外再装下
npm install pinia
状态是啥,说白了就是当前一些值,比如说我当前在查询3369这个单号的工单,然后我需要切换到另外一个组件继续处理,我希望再切换过去的时候依旧能保持工单号,这样可以减少再操作的麻烦,并且这种情况可能会出现多次,于是我就需要一个状态保持工具
pinia中的store是全局的,不跟随组件的加载卸载改变,并且每个组件都可以读或者写,它有三个概念,state、getter 和 action,相当于组件中的 data、 computed 和 function
(选项式真的太丑了,我宁愿写组合式)

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const pinia = createPinia()
const app = createApp(App)

app.use(pinia)
app.mount('#app')


如果你选的是自定义安装,那么自带的示例长这样,放在stores目录下(不过好像没被使用

import { ref, computed } from 'vue'
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0) //代表变量
  const doubleCount = computed(() => count.value * 2) //代表计算属性
  function increment() { //代表函数
    count.value++
  }

  return { count, doubleCount, increment }
})

让我们写个vue调用一下他

<template>
    <h3>hello,here is shop</h3>
    <p>here is your point:{{ store.count }}</p>
    <button @click="store.increment">click</button>
</template>

<script setup>
import { useCounterStore } from '@/stores/counter'
const store = useCounterStore()
</script>

钩子


使用不同的钩子函数可以在程序的不同生命周期来调用功能
vue3中beforeDestory变成beforeUnmount,destoryed变成unmounted,beforecreate和created合成了setup
另外新增两个调试用的

renderTracked -> onRenderTracked,状态跟踪,vue3新引入的钩子函数,只有在开发环境有用,用于跟踪所有响应式变量和方法,一旦页面有update,就会跟踪他们并返回一个event对象

renderTriggered -> onRenderTriggered,状态触发,同样是vue3新引入的钩子函数,只有在开发环境有效,与onRenderTracked的效果类似,但不会跟踪所有的响应式变量方法,只会定点追踪发生改变的数据,同样返回一个event对象


使用示例

import {onMounted, onUpdated,onRenderTriggered} from 'vue'

onMounted(()=> {
  console.log('mounted')
})
onUpdated(()=>{
  console.log('updated')
})
onRenderTriggered(()=>{
  console.log(event)
})

监听鼠标坐标

const x = ref(0)
const y = ref(0)
function updateMouse(e: MouseEvent){
  x.value = e.pageX
  y.value = e.pageY
}
onMounted(()=>{
  document.addEventListener('click',updateMouse)
})
onUnmounted(()=>{
  document.removeEventListener('click',updateMouse)
})

打包

npm run build
npm run preview
vite自带rollup打包,不用另外去折腾webpack啥的
rollup带tree shaking,也就是摇树机制,会自动把没用到的代码删除掉

  "scripts": {
    "dev": "vite",
    "build": "run-p type-check \"build-only {@}\" --", //打包语句,@会被替换成build-only
    "preview": "vite preview",
    "test:unit": "vitest",
    "build-only": "vite build",
    "type-check": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false"
  },

第三方

axios

npm install --save axios
npm install --save querystring
基于promise
(这里不知道为啥全局引用失败,先单个引用先)
如果安装了报但是还是报找不到这个模块,那么看下这个处理方法
https://blog.csdn.net/qq_22841387/article/details/123433223?spm=1001.2014.3001.5501

基础

get

axios({
    method: "get",
    url: "xxxx"
}).then(res => {
    console.log(res.data);
})

简写
axios.get("xxxx")
    .then(res =>{
      console.log(res.data);
    })

post

axios({
    method:"post",
    url:"xxx",
    data:qs.stringify({
        param1:"xxx",
        param2:"xxxx",
        verification_code:"xxxxx"
    })
}).then(res =>{
    console.log(res.data);
})

简写
axios.post("xxx", qs.stringify({
      param: "xxx",
  ......
    }))
      .then(res => {
        console.log(res.data);
      })

跨域问题
在vue/vite.config.js中加上

devServer: {
    proxy: {
      '/api': {
        target: '<url>',
        changeOrigin: true
      }
    }
}

ts例子

结合TS使用

import { ref } from 'vue'
import axios from 'axios'

function useURLLoader (url: string){
    const result = ref(null)
    const loading = ref(true)
    const loaded = ref(false)
    const error = ref(null)

    axios.get(url).then((rawData) => {
        loading.value = false
        loaded.value = true
        result.value = rawData.data
    }).catch(e => {
        error.value = e
        loading.value = false
    })
    return {
        result,
        loading,
        loaded,
        error
    }
}

export default useURLLoader
import useURLLoader from './hooks/useURLLoader'
const {result,loading,loaded} = useURLLoader("https://mock.apifox.cn/m1/3372030-0-default/pet/1")
      <h1 v-if="loading">Loading</h1>
      <p v-if="loaded">reslut:{{ result }}</p>

如果想要使用的调用都在某个网站上的不同路径下,可以在main直接设置根目录,然后调用api的时候只要写相对目录就行了

axios.defaults.baseURL = 'https://根目录'
axios.get('/users')

拦截器

说白了就是把包拦下来改点东西再继续传,分成请求和响应两种
请求拦截器

axios.interceptors.request.use(
  (config) => {
    // 在发送请求之前,对请求进行处理
    // 比如添加请求头,验证身份信息等
    config.headers.Authorization = 'Bearer token';

    return config;//必须返回,不能只拦不放
  },
  (error) => {
    // 请求错误时的处理
    return Promise.reject(error);
  }
);

在 Axios 中,请求拦截器的回调函数中的 config 参数是一个包含请求配置的对象。这个对象具有以下常用属性:

url:请求的 URL 地址。
method:请求的 HTTP 方法,例如 GET、POST。
baseURL:基础 URL 地址,会被添加到 url 前面。
headers:请求的头部信息,是一个对象,可以设置请求头的内容,例如设置认证信息。
params:请求的 URL 参数,也是一个对象。这个对象会被自动转换为 URL 查询字符串的形式,并添加到 URL 的末尾。
data:请求的主体数据,通常用于 POST、PUT、PATCH 等请求方法。
timeout:请求的超时时间,单位是毫秒。
transformRequest:请求数据的转换函数,可以用来转换请求数据的格式。
transformResponse:响应数据的转换函数,可以用来转换响应数据的格式。
paramsSerializer:URL 参数的序列化函数,可以自定义参数的序列化方式。
responseType:响应的数据类型,例如 ‘json’、‘text’ 等。
withCredentials:是否允许携带跨域请求的凭证。
auth:用于 HTTP 基础认证的用户名和密码。
onUploadProgress:上传进度的回调函数。
onDownloadProgress:下载进度的回调函数。

响应拦截器

axios.interceptors.response.use(
  (response) => {
    // 在接收到响应之前,对响应进行处理
    // 比如解析数据,统一处理错误等
    const data = response.data;
    if (data.code !== 200) {
      // 处理错误
      console.log('请求出错');
    }

    return response;
  },
  (error) => {
    // 响应错误时的处理
    return Promise.reject(error);
  }
);

封装、并发

很多时候肯定要用的网址不止一个,单纯封一个默认的网址有点不够用,可以用create来创建独立的axios示例
axios.all可以处理并发请求,他的参数为一个包含多个请求的数组
axios本身也是个promise函数,也可以用then catch来处理结果

import axios from 'axios'

const userRequest = axios.create({
  baseURL:"xx"
}
)
const goodsRequest = axios.create({
  baseURL: "xxxx"
})

axios.all([
  userRequest.get(),
  goodsRequest.post({})
])
.then(axios.spread((userResponse, goodsResponse) => {
  console.log('User Response:', userResponse);
  console.log('Goods Response:', goodsResponse);
}))
.catch(error => {
  console.log('Error:', error);
});

环境隔离

不同环境需要封装的地址是不同的,可以通过配置环境文件,来做到在启动的时候就进行自动读取对应环境下的配置,以实现环境隔离封装
https://cn.vitejs.dev/guide/env-and-mode.html
示例:
在项目根目录下创建.env.development

VITE_MODE_NAME=development
VITE_APP_BASE_URL=http://127.0.0.1:8000/

配置vite.config.ts

export default defineConfig({
  plugins: [
    vue(),
    vueJsx(),
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  },
  envDir: './'//按照实际的路径来
})

封装使用

import axios, { type InternalAxiosRequestConfig, type AxiosResponse } from 'axios';

function request(config) {
    const instance = axios.create({
        baseURL: import.meta.env.VITE_APP_BASE_URL,
        timeout: 5000
    });
    return instance(config);
}

quillEditor

富文本编辑器
https://blog.csdn.net/weixin_42232622/article/details/126317622
https://www.kancloud.cn/liuwave/quill/1434140

npm install @vueup/vue-quill@alpha --save

无缝滚动vue3-seamless-scroll

https://doc.wssio.com/opensource/vue3-seamless-scroll/

webpack

npm install webpack-dev-server;

UI

antd vue

npm install ant-design-vue@next --save
https://2x.antdv.com/docs/vue/getting-started-cn

import Antd from 'ant-design-vue';
app.use(Antd)

element-plus

npm install element-plus --save
默认源下不了可以用国内源--registry=https://registry.npmmirror.com

import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'

const app = createApp(App)

app.use(ElementPlus)
app.mount('#app')

iconfont

一个前端图表素材库
https://www.iconfont.cn/
选择图表放入购物车,结算到项目,选择下载到本地,解压后放到本地目录下
打开里面的html文件,根据提示引用对应的组件,也可直接在main引用css

以django一文中写的demo为例子,写一下前端调用后端api的方式
后端接口/menu/sapi

前端axios封装
封装处理器(关于环境隔离这块参考vue篇)

import axios, { type InternalAxiosRequestConfig, type AxiosResponse } from 'axios';

function request(config: any) {
    const instance = axios.create({
        baseURL: import.meta.env.VITE_APP_BASE_URL,
        timeout: 5000
    });

    instance.interceptors.request.use((config: any) => {
        //响应拦截,处理身份验证
        return config;
    }, (err) => {
        console.log(err);
    });

    instance.interceptors.response.use((res: AxiosResponse) => {
      //响应拦截,用于处理错误
        return res.data ? res.data : res;//返回响应的 data 字段,如果 data 字段不存在,则返回完整的响应。
    }, (err) => {
        console.log(err);
    });

    return instance(config);
}

export default request

封装api

import request from "./requestconfig"


function getMainMenu(){
  return request({
    url:"/menu/sapi",
  })
}

在app.vue中尝试调用

<script setup lang="ts">
import getMainMenu from '@/network/home'
import {type AxiosResponse } from 'axios';
getMainMenu().then((res: AxiosResponse)=>{
  console.log(res) //因为这里直接返回了res.data,所以不需要手动写.data
})
</script>

在写一个自动化处理工具,用zipfile解压文件的时候,发现解压出来的中文文件名全变成了乱码
windows中文默认编码gbk,与zipfile默认cp437冲突导致的
解决方案:
python3.11版本以下:
修改zipfile源码

if flags & 0x800:
                # UTF-8 file names extension
                filename = filename.decode('utf-8')
            else:
                # Historical ZIP filename encoding
                filename = filename.decode('cp437')
                filename = filename.encode('cp437').decode('gbk') #第一处


if fheader[_FH_GENERAL_PURPOSE_FLAG_BITS] & 0x800:
                # UTF-8 filename
                fname_str = fname.decode("utf-8")
            else:
                fname_str = fname.decode("cp437")
                fname_str = fname_str.encode("cp437").decode('gbk') #第二处

3.11以上
zipfile.ZipFile(file_path, 'r',metadata_encoding='gbk')

参考https://www.jb51.net/python/2957879by.htm

下面例子都使用pipenv虚拟环境
基本以DRF这个二次封装的Django框架来实现功能

安装与初始化

pip3 install django -i http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com
官网是djangoproject.com

如果是用pipenv安装
pipenv install --pypi-mirror <mirror_url> <package_name>

在放源代码的目录下运行,创建项目的配置管理程序
django-admin startproject mysite
默认创建的一些文件都是项目的配置,manage不用动,控制程序用的,urls路由,settings配置项目诸如数据库连接之类的配置,asgi\wgsi在服务器上部署会用到

如果配置没问题,可以来创建你的后端程序目录,注意新建的app是在源代码目录下,和上面的配置管理程序同级
python manage.py startapp demo
创建完后的目录里面,views是目录,models是数据库模型,但是最好还是写sql生成表,test是测试,apps是配置
如果你的要用需要把程序拆分成多个app,可以用上面这条命令分别创建app对应的目录

可以使用这条命令来运行你的程序
python manage.py runserver

当你创建好了你项目的程序目录、数据库等,需要配置django的配置项来使其生效
编辑setting.py
没提到的配置项可以参考
https://blog.csdn.net/zhouruifu2015/article/details/129646086

BASE_DIR = Path(__file__).resolve().parent.parent
sys.path.insert(0,BASE_DIR)//代码目录
sys.path.insert(0,os.path.join(BASE_DIR,"demo")) //app目录

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'demo', //有几个app就注册几个
]
STATICFILES_DIRS = [os.path.join(BASE_DIR,'static')] //放置静态文件

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'webdb',
        'USER': 'root',
        'PASSWORD': '123456',
        'HOST': '127.0.0.1' ,
        'PORT': '3306',
    }
}

安装mysql依赖
pipenv install --pypi-mirror http://mirrors.aliyun.com/pypi/simple/ mysqlclient
建个库试一下

use webdb;
create table USER (
id int not null auto_increment comment '用户id',
name varchar(255) collate utf8mb4_general_ci not null comment '用户名',
birthday datetime default null,
mobile varchar(255)  collate utf8mb4_general_ci default null,
email varchar(255) collate utf8mb4_general_ci not null,
password varchar(255) collate utf8mb4_general_ci not null,
create_time datetime default current_timestamp,
primary key (id)
)ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

python manage.py runserver

你也可以通过写model类来建表,写完后先运行python manage.py makemigrations app名,然后再运行python manage.py migrate,就会给你创建出表,但不灵活,还是推荐写sql
反过来也可以把已有的表直接导成model类,python manage.py inspectdb --database default(你配置里面配的数据库) xxx(你要搞的表名) > backend/appname/models.py
如果要导出多个表可以直接写多个,空格隔开,如果你非得写成多个语句导入,记得用>>而不是>

示例:python .\mysite\manage.py inspectdb default USER > .\mysite\demo\models.py
注意这个很容易出现编码问题,导完看看是不是utf-8的

安装个跨域用的pipenv install --pypi-mirror http://mirrors.aliyun.com/pypi/simple/ django-cors-headers

INSTALLED_APPS  =  [ 
    ... , 
    "corsheaders" , 
    ... , 
]

MIDDLEWARE  =  [ 
#'django.middleware.csrf.CsrfViewMiddleware',
    "corsheaders.middleware.CorsMiddleware" , 
    ... , 
]
# CORS_ORIGIN_ALLOW_ALL为True, 指定所有域名(ip)都可以访问后端接口, 默认为False
CORS_ORIGIN_ALLOW_ALL = True
#允许cookie
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_HEADERS  =  [ 
    "accept" , 
    "accept-encoding" , 
    "authorization" , 
    "content-type" , 
    "dnt" , 
    "origin" , 
    "user-agent" , 
    "x-csrftoken" , 
    "x-requested-with" , 
]

题外话:windows上面启动mysql的方法
SERVICES.MSC找到对应的服务启动
如果运行的时候出现ValueError: source code string cannot contain null bytes这个错误,把你的文件看一下,看看是不是有文件编码方式变成了utf-16,改回utf-8就行了

基础

类视图与model

建一个宝宝程序
新建一个menu目录,在views里面编写

from django.shortcuts import render
from django.views import View
from django.http import HttpResponse

# Create your views here.

class GoodsMainMenu(View):
    def get(self,request):
        print("get!")
        return HttpResponse("get it!")

    def post(self,request):
        print("post coming~")
        return HttpResponse("post it!")

配置settingsINSTALLED_APPS
配置urls

from django.contrib import admin
from django.urls import path
from menu.views import GoodsMainMenu

urlpatterns = [
    path('admin/', admin.site.urls),
    path('main_menu',GoodsMainMenu.as_view())
]

让我们用前面教的方法,从数据库中把一张表导成model

from django.db import models


class User(models.Model):
    name = models.CharField(max_length=255, db_comment='用户名')
    birthday = models.DateTimeField(blank=True, null=True)
    mobile = models.CharField(max_length=255, blank=True, null=True)
    email = models.CharField(max_length=255)
    password = models.CharField(max_length=255)
    create_time = models.DateTimeField(blank=True, null=True, auto_now=True)

    class Meta:
        managed = False
        db_table = 'user'

    def to_dict(self): //写个数据处理函数
        return {
            'name': self.name,
            'email': self.email
        }

然后让我们用从数据库里面取到的数据作为返回的内容,当然,得先进行序列化处理

from django.core import serializers
from django.shortcuts import render
from django.views import View
from django.http import HttpResponse
from menu.models import User
from django.http import JsonResponse
import json

# Create your views here.

class GoodsMainMenu(View):
    def get(self,request):
        users = User.objects.all()
        print("get!")
        #序列化,这里不能用json.dumps
        main_menu = serializers.serialize('json', users) #获取所有数据
        #取部分数据,需要在models中预先返回
        data = [user.to_dict() for user in users]
        result = {}
        result["status"] = '1000'
        result["data"] = data
        #jsonresponse会自动返回json格式的结果
        return JsonResponse(result)
        # return HttpResponse(json.dumps(result))
        # return HttpResponse(main_menu)
   
    def post(self,request):
        print("post coming~")
        return HttpResponse("post it!")

带参数查询,附只获取部分列的方法

class ProjectView(View):
    def get(self,request):
        param_id = request.GET['manager_id'] #注意这里是[]
        project = Project.objects.filter(manager_id=param_id).values('project_id', 'project_name', 'create_time')
        # project_list = serializers.serialize('json',project)  #转化为列表
        project_list = list(project)
        result = {
            'status': 900,
            'data': project_list,
        }
        #return HttpResponse(project_list)
        return JsonResponse(result)

上面的表结构

class Project(models.Model):
    project_id = models.AutoField(primary_key=True, db_comment='项目id')
    manager_id = models.IntegerField(db_comment='经理id来自user的id')
    project_name = models.CharField(max_length=255)
    create_time = models.DateTimeField(blank=True, null=True)

    class Meta:
        managed = False
        db_table = 'project'

让我们来抽象一点,把返回结果抽象到一个模块里面

class ViewResponse():

    @staticmethod
    def success(data):
        result = {
            'status': 1000,
            'data': data
        }
        return(result)

然后改造前面那两个api

class GoodsMainMenu(View):
    def get(self,request):
        users = User.objects.all()
        #序列化,这里不能用json.dumps
        main_menu = serializers.serialize('json', users) #获取所有数据
        #取部分数据,需要在models中预先返回
        data = [user.to_dict() for user in users]
        #jsonresponse会自动返回json格式的结果
        return JsonResponse(response.ViewResponse.success(data))


class ProjectView(View):
    def get(self,request):
        param_id = request.GET['manager_id']
        project = Project.objects.filter(manager_id=param_id).values('project_id', 'project_name', 'create_time')
        # project_list = serializers.serialize('json',project)  #转化为列表
        project_list = list(project)
        return JsonResponse(response.ViewResponse.success(project_list))

合并url

假设我们现在已经写了一堆功能,有一堆app,自然也会有一堆路由
把所有的路由都放在总的urls.py里面总归看起来会很乱,为了方便管理,我们可以把app的路由放在自己的目录下面
例:menu和demo两个app(需要注册

#menu  urls.py
from django.urls import path, re_path

from menu.views import GoodsMainMenu,ProjectView, UserresigterView, UsersearchView, UsergenericAPIvie, UsergenericmixAPIView

urlpatterns = [
    path('main_menu',GoodsMainMenu.as_view()),
    path('project',ProjectView.as_view()),
    path('register',UsergenericmixAPIView.as_view()),
    re_path("register/(?P<pk>.*)", UsergenericmixAPIView.as_view()),
    path("search",UsersearchView.as_view())
]
#demo   urls.py
from django.urls import path, re_path
from demo.views import UserApiview

urlpatterns = [
    path('usertype/<int:type>/<int:page>',UserApiview.as_view()),
]

总,访问的时候把总入口跟具体的路由拼接访问就行了

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('menu/',include('menu.urls')),
    path('demo/',include('demo.urls'))
]
#例:http://127.0.0.1:8000/menu/register/1

跨站

pip install django-cors-headers
在你的Django项目的settings.py文件中,做如下配置:

INSTALLED_APPS = [
    ...
    'corsheaders',
    ...
]
MIDDLEWARE = [
    ...
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.common.CommonMiddleware',
    ...
]
# 允许所有源
CORS_ORIGIN_ALLOW_ALL = True

# 或者,只允许特定的源
CORS_ORIGIN_WHITELIST = [
    'http://localhost:5173',
]

修改时区

默认存储时间为utc时间,修改时区方法:
修改settings.py

TIME_ZONE = 'Asia/Shanghai'
#如果只改上面没生效,把下面也改了
USE_TZ = False

中间件

django可以在settings里面配置中间件MIDDLEWARE
当请求从外部传入时,先按顺序从上到下走一遍中间件里面的处理方式,然后才到视图,视图返回结果后,同样自底向上的再走一遍处理,最后才返回给接收端。利用这种特性可以配置一些玩意在里面,比如统一的response处理器
例:

class CustomResponseMiddleware(MiddlewareMixin):

    def process_response(self, request, response):
        if isinstance(response, Response):
            result = {}
            print(response.data)
            data = response.data.get('data')
            pagination = response.data.get('pagination', None)

            status_code = response.status_code
            if status_code >= 400:
                result['responsecode'] = 1001
                result['data'] = data
            elif status_code == 200 or status_code == 201:
                result['responsecode'] = 1000
                result['data'] = data
                if pagination:
                    result['pagination'] = pagination['pagination']
            else:
                return response
            
            return JsonResponse(result)

        return response

视图只要正常用Response来返回结果就可以了,这个处理器会自动处理成json
另外就是DRF框架里面也有个配置在settings里的配置,比如配置个分页处理器

REST_FRAMEWORK = {
  'DEFAULT_PAGINATION_CLASS': 'utils.custompagination.CustomPagination',
}
from rest_framework.pagination import PageNumberPagination
class CustomPagination(PageNumberPagination):
    page_size = 10
    page_size_query_param = 'page_size'
    max_page_size = 50

    def get_page_size(self, request):
        page_size = super().get_page_size(request)
        return max(page_size, 10)

    def get_paginated_response(self, data):
        return {
            'pagination': {
                'total_items': self.page.paginator.count,
                'page_size': self.get_page_size(self.request),
                'page': int(self.request.query_params.get('page', 1)),
                'total_pages': self.page.paginator.num_pages,
            }
        }

这东西的处理步骤在中间件跟视图的中间,也就是请求流入处理过程是中间件--DRF--视图,返回亦然,另外亲测这里重写的get_paginated_response方法貌似不会被自动触发,需要手动调用,get_page_size和上面配置的最大size那些倒是可以生效,怪耶
一个视图被上面两玩意处理后,前端调用获得的数据结构大概长这样

data: (10) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}]
pagination: {total_items: 105, page_size: 10, page: 1, total_pages: 11}
responsecode: 1000

django rest framework

https://www.django-rest-framework.org/
一个用于开发restful框架的封装,看了下还支持oauth2,不错
pipenv install --pypi-mirror http://mirrors.aliyun.com/pypi/simple/ djangorestframework
然后

INSTALLED_APPS = [
...
    'rest_framework',
]

获取参数方法

get:
request.query_params.get(xxx)
post:
request.data.get(xxx)

视图

类视图APIview

简单来讲,从数据库到api的流程是这样的

  1. 建库建表
  2. 建model
  3. 建与之对应的序列化器
  4. 建立视图,在视图里面调用model进行查询(从api调用中获得参数),用序列化器对查询结果进行处理(反序列化save或者序列化返回给前端)
  5. url暴露api

https://blog.csdn.net/weixin_43865008/article/details/128851985

api类视图,在查询功能的同时实现了翻页功能

from rest_framework.views import APIView
from menu.models import User
from django.http import JsonResponse
from utils import response
#main_menu/type/1
class UserApiview(APIView):
    def get(self,request,type,page):
        current_page = (page - 1)*2
        end_page = page*2
        userdata = User.objects.filter(type=type).values('name', 'email', 'create_time')[current_page:end_page]
        return JsonResponse(response.ViewResponse.success(list(userdata)))
#path('usertype/<int:type>/<int:page>',UserApiview.as_view())

查询经理名下的项目,post更新或者新建项目
注意这里涉及到时区转换,需要先在settings里面配置TIME_ZONE = 'Asia/Shanghai',django默认存放的为UTC时间,如果想要根据你配置的时区来转换显示的数据,需要用timezone.localtime来处理一下

class ProjectView(APIView):
    def get(self,request):
        param_id = request.GET['manager_id']
        projects = Project.objects.filter(manager_id=param_id).values('project_id', 'project_name', 'create_time')
        # project_list = serializers.serialize('json',project)  #转化为列表
        project_list = []
        for project in projects:
            project_list.append({
                'project_id': project["project_id"],
                'project_name': project["project_name"],
                'create_time': timezone.localtime(project["create_time"]).isoformat()
            })
        return JsonResponse(response.ViewResponse.success(project_list))
    
    def post(self, request, *args, **kwargs):
        data = request.data
        manager_id = data.get('manager_id') #这个是APIView取数据的写法,django原生没有data
        project_name = data.get('project_name')
        if not manager_id or not isinstance(manager_id, int):
            return JsonResponse({'message': 'manager_id必须是数字'})

        # 对project_name进行数据校验
        if not project_name or not isinstance(project_name, str):
            return JsonResponse({'message': 'project_name必须是字符串'})
        project, created = Project.objects.update_or_create(
            manager_id=manager_id,
            project_name=project_name,
            defaults={'create_time': timezone.now()}
        )
        if created:
            return JsonResponse({'message': '新项目创建成功'})
        else:
            return JsonResponse({'message': '项目已存在,更新成功'})

使用原生sql,实现关键字查询、多种排序,连表查询,翻页

class GoodsSearchAPIView(APIView):
    def get(self,request,keyword,page,order_by):
        """
        select r.comment_count,g.image,g.name,g.p_price,g.shop_name,g.sku_id from goods g
            left join
            (
            select count(c.sku_id) as comment_count,c.sku_id from comment c group by c.sku_id
            ) r
            on g.sku_id=r.sku_id
            where g.name like "%手机%"
            order by  r.comment_count desc limit 15,15
        """
        order_dict = {
            1:"r.comment_count",
            2:"g.p_price"
        }


        limit_page = (page-1)*15
#         执行原生sql
        from django.db import connection
        from django.conf import settings

        sql = """
            select r.comment_count,concat('{}',g.image) as image,g.name,g.p_price,g.shop_name,g.sku_id from goods g
                left join
                (
                select count(c.sku_id) as comment_count,c.sku_id from comment c group by c.sku_id
                ) r
                on g.sku_id=r.sku_id
                where g.name like "%{}%"
                order by {} desc limit {},15
        """.format(settings.IMAGE_URL,keyword,order_dict[order_by],limit_page)
        cursor = connection.cursor()
        cursor.execute(sql)
        res = self.dict_fetchall(cursor)
        final_list = []
        for i in res:
            res_json = json.dumps(i,cls=DecimalEncoder,ensure_ascii=False)
            final_list.append(res_json)
        return ResponseMessage.GoodsResponse.success(final_list)

    def dict_fetchall(self, cursor):
        desc = cursor.description #返回元组,包含了查询结果的字段信息,[0]是字段名,如[('name', ...), ('age', ...)]
        return [dict(zip([col[0] for col in desc], row)) for row in cursor.fetchall()]
        #fetchall返回查询的所有结果,元组,元组中的元素顺序和字段的顺序是对应的。例如,[('Tom', 20), ('Jerry', 22)]

model与序列化器

经过上面的例子,我们已经知道django里面数据库的表是以一种叫做model的object存在的,而数据一般以json格式转换,那么从获取数据到导入数据,需要进行反序列化处理
https://www.django-rest-framework.org/api-guide/serializers/
写个user的序列化器,他可以把json数据和user model之间进行转换

from menu.models import User
from rest_framework import serializers
from rest_framework.validators import UniqueValidator

class UserSerializer(serializers.ModelSerializer):
    email = serializers.EmailField( #EmailField 字段类型可以验证数据是否为有效的电子邮件地址
        required=True,allow_blank=False,
        validators=[UniqueValidator(queryset=User.objects.all(),message="用户已存在")]#检查值是否唯一
    )

    class Meta:
        model = User #对标的model
        fields = "__all__" #序列化所有的字段

这是视图类

class UserresigterView(APIView):
    def post(self,request):
        user_data_serializer = UserSerializer(data=request.data)
        if user_data_serializer.is_valid():
            user_data = User.objects.create(**user_data_serializer.validated_data)
            user_front = UserSerializer(user_data)#这里因为要返回给前端看,所以得序列化一下
            #这里用前面的数据注册了一个user后,返回的完整的user数据作格式化
            return JsonResponse(response.ViewResponse.success(user_front.data))
        else:
            return JsonResponse(user_data_serializer.errors, status=400)

# post
# {
#     "name": "uuko",
#     "email": "[email protected]",
#     "password": "123456",
#     "type": 1
# }
# return
# {
#     "status": 1000,
#     "data": {
#         "id": 11,
#         "email": "[email protected]",
#         "name": "uuko",
#         "birthday": null,
#         "mobile": null,
#         "password": "123456",
#         "create_time": null, #这里没自动生成是因为user moderl没加auto_now=True
#           也可以试试直接把model里面时间的语句删掉,避免覆盖数据库的自动生成语句
#         "type": 1
#     }
# }

当然明文存储密码是不安全的,我们可以加个md5,这里使用save(),他可以调用我们二次封装的create函数

class UserSerializer(serializers.ModelSerializer):
    email = serializers.EmailField(
        required=True,allow_blank=False,
        validators=[UniqueValidator(queryset=User.objects.all(),message="用户已存在")]
    )

    def create(self,validated_data): #第二个参数就代表外面传进来的数据
        validated_data["password"] = md5(validated_data["password"].encode()).hexdigest()
        result = User.objects.create(**validated_data)
        return result

    class Meta:
        model = User
        fields = "__all__"

class UserresigterView(APIView):
    def post(self,request):
        user_data_serializer = UserSerializer(data=request.data)
        if user_data_serializer.is_valid():
            user_data = user_data_serializer.save()
            user_front = UserSerializer(user_data)
            return JsonResponse(response.ViewResponse.success(user_front.data))
        else:
            return JsonResponse(user_data_serializer.errors, status=400)

当然还可以再进一步,禁止password被序列化,也就是这东西不会出现在api的响应里面

class UserSerializer(serializers.ModelSerializer):
    ···
    password = serializers.CharField(write_only=True)

依葫芦画瓢再写个查询用户的功能

class UsersearchView(APIView):
    def get(self,request):
        email = request.GET['email']
        try:
            user_data = User.objects.get(email=email)
            user_front = UserSerializer(user_data)
            return JsonResponse(response.ViewResponse.success(user_front.data))
        except Exception as e:
            print(e)
            return JsonResponse({"msg": "failed to get user info"})

GenericAPIView

经过上面的例子,我们可以看出,视图类里面有很多重复代码,尤其是model、序列化器这一块,使用GenericAPIView可以帮助我们统一配置这些功能,当然还有其他能力,比如配置分页器
from rest_framework.generics import GenericAPIView
让我们来重新封装上面注册用户信息的api

class UsergenericAPIvie(GenericAPIView):
    queryset = User.objects #封装了对象查询
    serializer_class = UserSerializer #封装了序列化器

    def post(self, request):
        result = self.get_serializer(data = request.data) #调用序列化器
        try:
            result.is_valid()
            result.save()
            return JsonResponse(response.ViewResponse.success(result.data))
        except Exception as e:
            return JsonResponse(result.errors, status=400)

让我们再丰富下查询功能,如果不带参则查询所有,带参则查询对应的参数,而且可以支持不同的/复数的参数

class UsergenericAPIvie(GenericAPIView):
    #queryset = User.objects
    serializer_class = UserSerializer
    def get_queryset(self):
        queryset = User.objects.all()
        email = self.request.query_params.get('email', None)
        type = self.request.query_params.get('type', None)
        if email is not None:
            queryset = queryset.filter(email=email)
            #return User.objects.filter(email=email)
        if type is not None:
            queryset = queryset.filter(type=type)
        return queryset

    def post(self, request):
        result = self.get_serializer(data = request.data)
        try:
            result.is_valid()
            result.save()
            return JsonResponse(response.ViewResponse.success(result.data))
        except Exception as e:
            return JsonResponse(result.errors, status=400)
        
    def get(self, request):
        return JsonResponse(self.get_serializer(instance = self.get_queryset(),many = True).data,safe=False)
        #当 safe=True 时,JsonResponse 要求传递给它的数据必须是一个字典,这里可能是一个包含多个字典的列表
    

Mixin

mixin是个扩展类,最大的特点是可以和别的类一起使用,快速实现某些功能
from rest_framework.mixins import CreateModelMixin
快速调用create,无需再手动调is_valid,save
等于post

还是重写上面注册功能

class UsergenericmixAPIView(GenericAPIView,CreateModelMixin):
    serializer_class = UserSerializer
    queryset = User.objects.all()
    
    def post(self, request):
        return self.create(request)

此外还有
ListModelMixin=get全部
RetrieveModelMixin=get具体对象,默认用pk主键,等于lookup_field
UpdateModelMixin=put更新
DestroyModelMixin=delete销毁

用主键查询对应信息

class UsergenericmixAPIView(GenericAPIView, CreateModelMixin, RetrieveModelMixin):
    serializer_class = UserSerializer
    queryset = User.objects.all()
    
    def post(self, request):
        return self.create(request)
    
    def get(self, request, pk):
        return self.retrieve(request, pk)

#re_path("register/(?P<pk>.*)", UsergenericmixAPIView.as_view()),

viewsets视图集

上面我们对于相同动作带不带参的处理方式是另外写一个get_queryset来处理参数,但其实也可以使用viewsets来处理不同情况下调用的函数
ViewSetMixin

from rest_framework.viewsets import ViewSetMixin
class UserMixApiView(ViewSetMixin,GenericAPIView,RetrieveModelMixin,ListModelMixin,UpdateModelMixin,DestroyModelMixin):
    //注意这里因为py继承优先级的问题,必须让ViewSetMixin在前面
    serializer_class = UserSerializer
    queryset = User.objects.all()

    def single(self,request,pk):
        return self.retrieve(request, pk)
    
    def alldata(self,request):
        return self.list(request)
    
    def save(self,request):
        return self.create(request)
    
    def update_user(self,request,pk):
        return self.update(request,pk)
    
    def delete(self,request):
        return self.destroy(request)


path("sapi",UserMixApiView.as_view({
        'get': 'alldata',
        'post': 'save'
    })),//没参来这
    re_path('sapi/(?P<pk>.*)',UserMixApiView.as_view({
        'get': 'single',
        'post': 'update'
    }))//有参来这

当然,这样写就要写一大堆的继承,看起来很麻烦,通过查看源码,我们发现这一堆玩意都可以用一个东西替代
ModelViewSet
然后就只要继承一个玩意就行了

class UserMixApiView(ModelViewSet):

#从源码可以看出这玩意包罗万象
# class ModelViewSet(mixins.CreateModelMixin,
#                    mixins.RetrieveModelMixin,
#                    mixins.UpdateModelMixin,
#                    mixins.DestroyModelMixin,
#                    mixins.ListModelMixin,
#                    GenericViewSet):

model

数据库表在django中的模型叫做Model Class,继承自django.db.models.Model
这个类定义了一些对数据的操作方法
objects是每个 Django 模型的默认管理器,它提供了对数据库的查询操作。可以通过模型类的 objects 属性来进行数据查询
YourModel.objects.all() 返回一个包含所有 YourModel 对象的 QuerySet
YourModel.objects.get(field1='some_value')返回满足查询条件的单个对象,不存在或者多结果会引起错误

try:
    obj = YourModel.objects.get(field1='some_value')
except YourModel.DoesNotExist:
    # 处理查询结果不存在的情况
except YourModel.MultipleObjectsReturned:
    # 处理查询结果多于一个对象的情况

YourModel.objects.filter(field1='some_value')返回一个满足筛选条件的queryset
YourModel.objects.exclude(field1='some_value') 返回一个包含不符合条件的对象的 QuerySet。
save(): save() 方法用于保存或更新对象到数据库。如果对象是新创建的,则插入新记录;如果对象已存在(根据主键判断),则更新相应的记录。
delete(): delete() 方法用于删除对象。obj.delete() 将会从数据库中删除与 obj 对象对应的记录。
count(): count() 方法返回 QuerySet 中对象的数量。
queryset是一个用于表示数据库查询的对象。它允许你执行数据库查询,并得到符合条件的一组数据。如果你相对查到的数据进行序列化并返回

from django.http import JsonResponse
from .models import YourModel
from .serializers import YourModelSerializer

def get_data(request, field_value):
    # 根据字段的值获取数据
    queryset = YourModel.objects.filter(your_field=field_value)

    # 使用序列化器将数据序列化为 JSON 格式,这里有多组数据,需要 many=True
    serializer = YourModelSerializer(queryset, many=True)
    serialized_data = serializer.data

    # 返回 JSON 响应
    return JsonResponse({'data': serialized_data})

翻页器

原版
https://docs.djangoproject.com/en/5.0/topics/pagination/
drf的翻页器
https://www.django-rest-framework.org/api-guide/pagination/
如果需要全局配置

REST_FRAMEWORK = {
  # 全局分页
  'DEFAULT_PAGINATION_CLASS': 'xxxxx',
}

示例:带翻页和页码选择的翻页器

from rest_framework.pagination import PageNumberPagination
class CustomPagination(PageNumberPagination):
    page_size = 10
    page_size_query_param = 'page_size'
    max_page_size = 50
    
    def get_page_size(self, request):
        page_size = super().get_page_size(request)
        return max(page_size, 10)

class ProjectView(GenericAPIView, ListModelMixin, CreateModelMixin):
    queryset = Project.objects.all()
    serializer_class = ProjectSerializer
    pagination_class = CustomPagination

    def get(self, request, *args, **kwargs):
        return self.list(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        return self.create(request, *args, **kwargs)
#GET /your-api-endpoint/?page=2&page_size=20

数据

#http://127.0.0.1:8000/project/projects?page=2&page_size=20
{
    "count": 115,
    "next": "http://127.0.0.1:8000/project/projects?page=3&page_size=20",
    "previous": "http://127.0.0.1:8000/project/projects?page_size=20",
    "results": [
        {...}
    ]
}

JWT

cookie分为会话性和存储性,会话性窗口关闭就会失效,存储性则会落盘,时效较长
session保存在服务端,至于具体是内存redis还是数据库看设计
token:令牌,用户访问api时会附带上使用,由服务端生成,客户端保存

用户在网页登录,服务端验证账户密码等信息无误后,生成一个token返回,用户接收token,并使用这个token来重新发起请求,服务端接收到token,解密后校验一些用户的数据,无误后把数据返回给用户,如果不通过则返回错误信息

https://jwt.io/

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.cThIIoDvwdueQB468K5xDc5633seEFoqwxjF_xSJyQQ

{
  "alg": "HS256",
  "typ": "JWT"
}

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

一个jwt外观看起来由.连接的三段加密字符组成
第一段字符是header,包含了加密算法的信息,使用base64处理
第二段是payload,包含存储着的用户的一些信息,使用base64处理
第三段是经过加盐和base64处理的验证信息,将第一段和第二段做以上加密处理后得到的base64字符串和本段内容进行对比,如果一致则通过校验

pip install pyjwt
在项目的setting里面有SECRET_KEY这个字段,就代表项目的盐(你可以修改这个字段)

import jwt
import datetime

SECRET_KEY = 'uoaoiscnzxo&(^(*h9hnhuhga6^E%))'

def create_token():
    header = {
    "alg": "HS256",
    "typ": "JWT"
    }

    payload = {
        'user_id': 1,
        'user_name': 'zhangsan',
        'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=1)#过期时间,别改exp这个字段名
    }

    result = jwt.encode(headers=header,payload=payload,key=SECRET_KEY,algorithm='HS256')
    return result

def token_decode(token):
    try:
        return jwt.decode(token,SECRET_KEY,algorithms=["HS256"])
    except jwt.exceptions.ExpiredSignatureError:
        return("Failed to decode jwt token:expired")

token = create_token()
print(token)
print(token_decode("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJ1c2VyX25hbWUiOiJ6aGFuZ3NhbiIsImV4cCI6MTcwMDYzNTc3N30.mlJQ3vVN_gRa-_jFHP-UhLVa6b1PTF6qYZooo3HAoCc"))

通过url方式进行token的验证

import jwt
import datetime
from rest_framework.authentication import BaseAuthentication
from django.http import JsonResponse
from rest_framework.exceptions import AuthenticationFailed

SECRET_KEY = 'uoaoiscnzxo&(^(*h9hnhuhga6^E%))'
#创建token
def create_token(payload,minutes):
    header = {
    "alg": "HS256",
    "typ": "JWT"
    }
    payload['exp'] = datetime.datetime.utcnow() + datetime.timedelta(minutes=minutes)

    result = jwt.encode(headers=header,payload=payload,key=SECRET_KEY,algorithm='HS256')
    return result
#解密token
def token_decode(token):
    result = {'status': False, 'content': '', 'error': None}
    try:
        result['content'] = jwt.decode(token,SECRET_KEY,algorithms=["HS256"])
        result['status'] = True
    except jwt.exceptions.ExpiredSignatureError:
        result['error'] = "Failed to decode jwt token:expired"
    except:
        result['error'] = "Failed to decode jwt token"
    return(result)
#token验证器,直接自定义验证规则
# class tokenauth(BaseAuthentication):
#     def authenticate(self,request):
#         token = request.GET.get('token')
#         if token:
#             result_token = token_decode(token)
#             if result_token['status'] != True:
#                 raise AuthenticationFailed({"code": "A00002", "data": None, "msg": "token已失效", "success": False})
#             else:
#                 return(result_token,token)
#         else:
#             raise AuthenticationFailed({"code": "A00003", "data": None, "msg": "缺少token", "success": False})
#                raise AuthenticationFailed错误会被django捕获变成40x,不会导致程序中断
#token验证,不对token内容做处理
class tokenauth(BaseAuthentication):
    def authenticate(self,request):
        token = request.GET.get('token')
        result_token = token_decode(token)
        return(result_token,token) #必须返回两个
from hashlib import md5
from rest_framework.views import APIView
from demo.serializers import UserSerializer
from menu.models import User
from django.http import JsonResponse
from utils import response,mysite_jwt
from rest_framework.generics import GenericAPIView
from rest_framework.mixins import ListModelMixin
#main_menu/type/1
# class UserApiview(APIView):
#     def get(self,request,type,page):
#         current_page = (page - 1)*2
#         end_page = page*2
#         userdata = User.objects.filter(type=type).values('name', 'email', 'create_time')[current_page:end_page]
#         return JsonResponse(response.ViewResponse.success(list(userdata)))

class Userloginview(GenericAPIView):
    serializer_class = UserSerializer
    def post(self,request):
        request_data = request.data
        email = request_data['email']
        user_data = User.objects.get(email = email)
        if user_data:
            user = self.get_serializer(instance = user_data)
            request_pw = md5(request_data['password'].encode()).hexdigest()
            user_pw = user.data['password']
            if request_pw != user_pw:
                return JsonResponse({'error':'用户名或密码错误'})
            else:
                payload = {
                    'User_name': user.data['name'],
                    'User_type': user.data['type']
                }
                return JsonResponse({'token':mysite_jwt.create_token(payload=payload,minutes=1)})
        else:
            return JsonResponse({'error':"用户不存在"})
        
class Userlist(GenericAPIView, ListModelMixin):
    serializer_class = UserSerializer
    queryset = User.objects.all()
    authentication_classes = [mysite_jwt.tokenauth]#单个配置
    def get(self,request):
        print(request.user) #固定用法,返回token验证函数的result_token部分
        print(request.auth) #固定用法,返回token验证函数的token部分
        if not request.user['status']: #自行定义对token有效性的处理,比直接在验证函数写灵活
            return JsonResponse(request.user,safe=False)
        return self.list(request)
#使用示例
# http://127.0.0.1:8000/demo/alluser?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VyX25hbWUiOiJvcmluIiwiVXNlcl90eXBlIjoxLCJleHAiOjE3MDEwNjQyODN9.y4sS4xksOjcwcS8ogE4M-QCcnK8gS2armjTEnMjahNk

通过header的方式传token

把token放在http header里面

class tokenauthheader(BaseAuthentication):
    def authenticate(self,request):
        token = request.META.get('HTTP_TOKEN')#header字段会被处理成大写加HTTP_,如token变成HTTP_TOKEN
        print(token)
        result_token = token_decode(token)
        return(result_token,token)

全局配置token解析器

在settings中配置

REST_FRAMEWORK = {"DEFAULT_AUTHENTICATION_CLASSES":['utils.mysite_jwt.tokenauthheader']}
#实际路径根据自己项目来

示例

查询/创建/更新api

get:查询所有,带翻页器
post:自动分辨创建还是更新,时间字段自动创建
model

class ResServer(models.Model):
    res_server_type = models.CharField(max_length=8, db_comment='类型')
    res_server_platform = models.CharField(max_length=20, db_comment='平台')
    res_server_code = models.CharField(max_length=100, blank=True, null=True, db_comment='资源在平台上的编号')
    res_server_public_ip = models.CharField(max_length=45, db_comment='公网ip')
    res_server_internal_ip = models.CharField(max_length=45, db_comment='内网ip')
    res_server_env = models.CharField(max_length=4, blank=True, null=True, db_comment='环境')
    res_server_project = models.CharField(max_length=50, db_comment='所属项目')
    res_server_create_time = models.DateTimeField(blank=True, null=True)
    res_server_update_time = models.DateTimeField(blank=True, null=True)

    class Meta:
        managed = False
        db_table = 'res_server'

序列化器

class ResServerSerializer(serializers.ModelSerializer):
    def create(self, validated_data):
        validated_data['res_server_create_time'] = timezone.now()
        validated_data['res_server_update_time'] = timezone.now()
        return super().create(validated_data)
    
    def update(self, instance, validated_data):
        instance.res_server_type = validated_data.get('res_server_type', instance.res_server_type)
        instance.res_server_platform = validated_data.get('res_server_platform', instance.res_server_platform)
        instance.res_server_code = validated_data.get('res_server_code', instance.res_server_code)
        instance.res_server_public_ip = validated_data.get('res_server_public_ip', instance.res_server_public_ip)
        instance.res_server_internal_ip = validated_data.get('res_server_internal_ip', instance.res_server_internal_ip)
        instance.res_server_env = validated_data.get('res_server_env', instance.res_server_env)
        instance.res_server_project = validated_data.get('res_server_project', instance.res_server_project)
        instance.res_server_update_time = timezone.now()

        instance.save() 
        return instance       
     
    class Meta:
        model = ResServer
        fields = '__all__'
        read_only_fields = ['res_server_create_time', 'res_server_update_time']

视图

class CustomPagination(PageNumberPagination):
    page_size = 10
    page_size_query_param = 'page_size'
    max_page_size = 50

    def get_page_size(self, request):
        page_size = super().get_page_size(request)
        return max(page_size, 10)
class ResServerView(GenericAPIView):
    queryset = ResServer.objects.all()
    serializer_class = ResServerSerializer
    pagination_class = CustomPagination


    def get(self, request):
        resservers = self.paginate_queryset(self.get_queryset())
        serializer = self.serializer_class(instance=resservers, many=True)
        return ProjectInfoResponse.success(serializer.data)

    def post(self, request):
            data = request.data
            res_server_id = data.get('id')
            try:
                if res_server_id and ResServer.objects.filter(id=res_server_id).exists():
                    instance = ResServer.objects.get(id=res_server_id)
                    serializer = self.get_serializer(instance, data=data)
                else:
                    serializer = self.serializer_class(data=data)

                if serializer.is_valid():
                    serializer.save()
                    return ProjectInfoResponse.success(serializer.data)
                else:
                    return ProjectInfoResponse.failed(serializer.errors)
            except ResServer.DoesNotExist:
                return ProjectInfoResponse.failed("Instance does not exist.")
            except Exception as e:
                return ProjectInfoResponse.failed(str(e))

复杂查询

部分匹配,非原生sql
封装翻页器返回

from django.db.models import Q
from rest_framework.response import Response
class CustomPagination(PageNumberPagination):
    page_size = 10
    page_size_query_param = 'page_size'
    max_page_size = 50

    def get_page_size(self, request):
        page_size = super().get_page_size(request)
        return max(page_size, 10)

    def get_paginated_response(self, data):
        return Response({
            'pagination': {
                'total_items': self.page.paginator.count,
                'page_size': self.get_page_size(self.request),
                'page': int(self.request.query_params.get('page', 1)),
                'total_pages': self.page.paginator.num_pages,
            },
            'data': data,
        })

class ProjectView(GenericAPIView):
    queryset = Project.objects.all()
    serializer_class = ProjectSerializer
    pagination_class = CustomPagination
    def get(self, request):
        project_name = request.query_params.get('project_name', None)
        project_owner_id = request.query_params.get('project_owner_id', None)

        query_filter = Q()
        if project_name:
            query_filter |= Q(project_name__icontains=project_name)
        if project_owner_id:
            query_filter |= Q(project_owner_id__icontains=project_owner_id)

        projects = self.paginate_queryset(self.queryset.filter(query_filter))
        serializer = self.get_serializer(projects, many=True)
        pagination = self.get_paginated_response(serializer.data)

        return ProjectInfoResponse.success(serializer.data,pagination.data)

from django.http import JsonResponse

class ProjectInfoResponse:

    @staticmethod
    def success(data, pagination=None):
        result = {'responsecode': 1000, 'data': data}
        if pagination:
            result['pagination'] = pagination['pagination']
        return JsonResponse(result)