Vue3电影中后台开发纪实(四):增删改查

项目源码:电影中台+后台

@删除单个热映数据

准备删除接口

api/movieApi.js

/* 删除影片 */
// async函数的返回值是Promise对象
export async function deletePlaying(id) {
  const {
    msg,
    data: { deletedCount },
  } = await doDelete(`/film/${id}`);

  // return相当于Promise在履约
  return { msg, deletedCount };
}

触发删除操作

views/film/Detail.vue

<!-- 从作用域插槽数据中解构出当前行id -->
<template #default="{ row: { _id } }">
  <!-- 点击Edit按钮 携带id跳转详情页 -->
  <el-button
    @click="$router.push(`/film/${_id}`)"
    type="primary"
    :icon="Edit"
    circle
    size="small"
  />

  <!-- 触发单个影片删除 -->
  <el-button
    @click="deleteItem(_id)"
    type="danger"
    :icon="Delete"
    circle
    size="small"
  />
</template>

调用接口执行删除

views/film/Detail.vue

import { deletePlaying } from "@api/movieApi";
  // 调用服务端API执行删除
  const { msg, deletedCount } = await deletePlaying(deleteId.value);
  console.log("msg=", msg);

删除后反馈+刷新页面

import { ElMessage } from "element-plus";
  // 调用服务端API执行删除
  const { msg, deletedCount } = await deletePlaying(deleteId.value);
  console.log("msg=", msg);

  /* 使用Message提示信息 */
  ElMessage({
    message: msg,
    type: deletedCount ? "success" : "error",
  });

  if (deletedCount) {
    // 暴力重绘整个页面(性能低下,体验垃圾,横批——你个渣渣)
    // window.location.reload();

    /* 修改响应式数据,让数据去驱动视图做【差量渲染】 */
    // 找出要删除的那条数据 arr.find(item=>item._id===xxid)
    tableData.value.find((item, index) => {
      if (item._id === deleteId.value) {
        /* 等find正常返回后 再执行真正的删除动作 */
        setTimeout(() => {
          tableData.value.splice(index, 1);
        });

        return item;
      }
    });
  }

@删除前使用弹窗确认

界面上准备好弹窗,并先行隐藏

<!-- 默认隐藏的对话框 -->
<el-dialog
  v-model="dialogVisible"
  title="操作确认"
  width="30%"
  :before-close="handleClose"
>
  <span>{{ dialogMode.msg }}</span>
  <template #footer>
    <span class="dialog-footer">
      <el-button @click="dialogVisible = false">取消</el-button>
      <el-button type="primary" @click="dialogMode.callback">
        确认
      </el-button>
    </span>
  </template>
</el-dialog>

弹窗的关联数据

  • 使用一个ref控制弹窗的显隐
// 对话框显隐控制
const dialogVisible = ref(false);
  • 使用一组弹窗模式控制弹窗的提示文字+确认回调
/* 对话框模式 */
const dialogModes = {
  // 单个删除模式
  deleteItem: {
    msg: "确认删除影片吗?",
    callback: doDeleteItem,
  },

  // 批量删除模式
  patchDelete: {
    msg: "确认执行批量删除吗?",
    callback: doPatchDelete,
  },
};

// 默认使用单个删除模式
let dialogMode = ref(dialogModes.deleteItem);

完整的删除流程

  • 用户点击删除
  • 暂存要删除的影片ID
  • 设置好弹窗模式:内置提示文字 + 确认时的回调函数
  • 用户点击确认时调出先前暂存好的影片ID并执行删除
  • 提示删除结果 + 刷新页面

开始删除

/* 删除单个 */
const deleteId = ref(0);
const deleteItem = (id) => {
  // 先将要删除的id暂存起来
  deleteId.value = id;

  // 确认删除对话框显示粗来
  dialogMode.value = dialogModes.deleteItem;
  dialogVisible.value = true;
};

用户点击确认

<!-- 用户点击确认时执行【当前模式对应的回调】 -->
<el-button type="primary" @click="dialogMode.callback">
    确认
</el-button>

执行删除

/* 执行单个删除 */
const doDeleteItem = async () => {
  // 先读入要删除的id
  console.log("doDeleteItem", deleteId.value);

  // 将对话框隐藏
  dialogVisible.value = false;

  // 调用服务端API执行删除
  const { msg, deletedCount } = await deletePlaying(deleteId.value);
  console.log("msg=", msg);

  /* 使用Message提示信息 */
  ElMessage({
    message: msg,
    type: deletedCount ? "success" : "error",
  });

  if (deletedCount) {
    // 暴力重绘整个页面(性能低下,体验垃圾,横批——你个渣渣)
    // window.location.reload();

    /* 修改响应式数据,让数据去驱动视图做【差量渲染】 */
    // 找出要删除的那条数据 arr.find(item=>item._id===xxid)
    tableData.value.find((item, index) => {
      if (item._id === deleteId.value) {
        /* 等find正常返回后 再执行真正的删除动作 */
        setTimeout(() => {
          tableData.value.splice(index, 1);
        });

        return item;
      }
    });
  }
};

@批量删除

获取用户批量选择的数组

<!-- 
//多选变化时的处理函数,载荷为勾选上的数据集
@selection-change="handleSelectionChange" 
-->
<el-table
  :data="computedData"
  stripe
  class="middle"
  style="width: 100%"
  :default-sort="{ prop: 'date', order: 'ascending' }"
  @selection-change="handleSelectionChange"
>
...
</el-table>
/* 多选时此处能拿到选中的子数组 */
const selectedItems = ref([]);
const handleSelectionChange = (val) => {
  console.log("handleSelectionChange", val);
  selectedItems.value = val;
};

触发批量删除

  <el-button class="opBtn" type="danger" @click="patchDelete">
    <el-icon><Close /></el-icon>&nbsp;删除
  </el-button>

先弹窗确认

const patchDelete = () => {
  // 确认删除对话框显示粗来
  dialogMode.value = dialogModes.patchDelete;
  dialogVisible.value = true;
};

用户确认执行批量删除

<!-- 用户点击确认时执行【当前模式对应的回调】 -->
<el-button type="primary" @click="dialogMode.callback">
    确认
</el-button>

执行批量删除,反馈结果,刷新页面

/* 执行批量删除 */
const doPatchDelete = () => {
  dialogVisible.value = false;
  console.log("doPatchDelete");

  Promise.all(
    /* 将选中的【电影数组】映射为【执行删除的Promise数组】 */
    selectedItems.value.map((film) => deletePlaying(film._id))
  )
    .then((results) => {
      ElMessage({
        message: "批量删除成功",
        type: "success",
      });

      // 依然应该使用数据驱动视图 这里做了简单处理
      window.location.reload()
    })
    .catch((err) => {
      ElMessage({
        message: err,
        type: "error",
      });
    });
};

@更新模块数据

用户点击进入详情页

  <!-- 右侧固定的操作按钮区 -->
  <el-table-column fixed="right" label="操作" width="90">
    <!-- 从作用域插槽数据中解构出当前行id -->
    <template #default="{ row: { _id } }">
      <!-- 点击Edit按钮 携带id跳转详情页 -->
      <el-button
        @click="$router.push(`/film/${_id}`)"
        type="primary"
        :icon="Edit"
        circle
        size="small"
      />
      ...
    </template>
  </el-table-column>

用户修改表单数据

所有的表单元素都使用了v-model双向数据绑定,将数据实时同步给响应式对象form

// 表单数据存储器
const form = reactive({});
<!-- 详情表单 -->
<!-- form没有数据时不渲染form -->
<el-form v-if="form[`name`]" :model="form" label-width="120px">
  <!-- 片名关联form.name -->
  <el-form-item label="片名">
    <el-input v-model="form.name" />
  </el-form-item>

  <!-- 片长关联form.runtime -->
  <el-form-item label="片长">
    <el-col :span="7">
      <el-input v-model="form.runtime" type="number" />
    </el-col>
  </el-form-item>

  <!-- 评分关联form.grade -->
  <el-form-item label="评分">
    <el-col :span="7">
      <el-input v-model="form.grade" />
    </el-col>
  </el-form-item>

  <!-- 影片类型关联form.filmType.name -->
  <!-- filmType?.name 如果filmType不为null/undeined 就继续读取filmType?.name 否则直接返回null/undeined -->
  <el-form-item label="影片类型">
    <el-select
      v-model="form.filmType.name"
      placeholder="please select your zone"
    >
      <el-option label="2D" value="2D" />
      <el-option label="3D" value="3D" />
      <el-option label="4D" value="4D" />
    </el-select>
  </el-form-item>

  <!-- 影片类型关联form.premiereAt -->
  <el-form-item label="首映日期">
    <el-col :span="8">
      <el-date-picker
        v-model="form.premiereAt"
        type="date"
        placeholder="Pick a date"
        style="width: 100%"
      />
    </el-col>
  </el-form-item>

  <!-- 在映关联form.isPresale -->
  <el-form-item label="在映">
    <el-switch v-model="form.isPresale" />
  </el-form-item>

  <!-- 影片类型关联form.category -->
  <el-form-item label="影片类型">
    <el-checkbox-group v-model="form.category">
      <el-checkbox label="爱情" name="category" />
      <el-checkbox label="动作" name="category" />
      <el-checkbox label="科幻" name="category" />
      <el-checkbox label="历史" name="category" />
      <el-checkbox label="悬疑" name="category" />
      <el-checkbox label="喜剧" name="category" />
      <el-checkbox label="战争" name="category" />
      <el-checkbox label="剧情" name="category" />
      <el-checkbox label="犯罪" name="category" />
      <el-checkbox label="纪录片" name="category" />
    </el-checkbox-group>
  </el-form-item>

  <!-- 国家关联form.nation -->
  <el-form-item label="国家">
    <el-radio-group v-model="form.nation">
      <el-radio label="中国大陆" />
      <el-radio label="欧美" />
      <el-radio label="日韩" />
      <el-radio label="其它" />
    </el-radio-group>
  </el-form-item>

  <!-- 演职人员关联form.actors -->
  <!-- 
  :on-preview="handlePreview"
  :on-remove="handleRemove"
   -->
  <el-form-item label="使用照片墙">
    <el-switch v-model="usePictureCard" />
    {{ usePictureCard }}
  </el-form-item>

  <el-form-item label="演职人员">
    <el-upload
      v-model:file-list="form.actors"
      class="upload-demo"
      :list-type="usePictureCard ? 'picture-card' : 'picture'"
      :auto-upload="true"
      action="/api/file/upload"
      :on-success="onActorUploadSuccess"
    >
      <el-icon><Plus /></el-icon>
    </el-upload>
  </el-form-item>

  <!-- 海报关联form.poster -->
  <el-form-item label="海报">
    <el-upload
      v-model:file-list="form.poster"
      action="/api/file/upload"
      list-type="picture-card"
      :on-success="onPosterUploadSuccess"
    >
      <el-icon><Plus /></el-icon>
    </el-upload>
  </el-form-item>

  <!-- 剧情摘要form.synopsis -->
  <el-form-item label="剧情摘要">
    <el-input v-model="form.synopsis" autosize type="textarea" />
  </el-form-item>

  <!-- 提交与重置 -->
  <el-form-item>
    <el-button class="opBtn" type="primary" @click="onSubmit"
      >更新</el-button
    >
    <el-button class="opBtn">重置</el-button>
  </el-form-item>
</el-form>

用户点击提交

  • 拿到实时表单数据
const onSubmit = async () => {
  console.log("submit!", form);
  ...
};

去格式化表单数据

  • 对数据执行【去格式化】动作,使表单数据匹配回后台需要的格式
  • 这个动作和加载到后台数据时将修改为表单所需的【格式化】动作是互逆操作
const onSubmit = async () => {
  console.log("submit!", form);

  /* 对数据做必要的【去格式化】的动作 */
  const deformatedForm = Object.assign({}, form, {
    // 影片类型: 数组转字符串
    category: form.category.join("|"),

    // 首映日期: Date转时间戳
    premiereAt: form.premiereAt / 1000,

    // 海报:[{name,url},...]数组转字符串
    poster: form.poster.map((picture) => picture.url).join(""),

    // 演员表: [{name,url}]=>[{name,role,avatarAddress}]
    actors: form.actors.map(({ name, url }) => {
      let [_name, _role] = name.split("-");
      let avatarAddress = url;
      return {
        name: _name,
        role: _role,
        avatarAddress,
      };
    }),
  });

  // 删除deformatedForm中的id(修改id对于数据库来说是非法操作)
  delete deformatedForm._id;
  console.log("after deformat", deformatedForm);
  
  /* 向服务端发起请求 */
  ...
};

执行数据更新

  /* 向服务端发起请求 */
  const { msg, modifiedCount } = await updatePlaying(form._id, deformatedForm);

  /* 反馈用户更新信息结果 */
  ElMessage({
    message: msg,
    type: modifiedCount ? "success" : "error",
  });

  // 暴力刷新查看更新结果
  setTimeout(() => {
    window.location.reload();
  }, 500);

@图片上传

配置服务端上传接口

  • 注意:ElementPlus的Upload组件上传文件的input框的name字段 的值为file,服务端接收的时候需要注意;
  • action 即服务端上传接口,用户选择好文件后会自动向服务端上传,服务端配置好接收接口即可;
  • 由于跨域问题的存在,action="/api/file/upload"中的/api代表本地路由,这个前缀在执行网络上传时会被覆盖为服务端的baseUrl
  • on-success为文件上传成功后的回调函数,稍后我们需要在表单数据中将图片的地址修改为服务端地址;
<!-- 演职人员关联form.actors -->
<el-form-item label="演职人员">
    <el-upload
      v-model:file-list="form.actors"
      class="upload-demo"
      :list-type="usePictureCard ? 'picture-card' : 'picture'"
      :auto-upload="true"
      action="/api/file/upload"
      :on-success="onActorUploadSuccess"
    >
      <el-icon><Plus /></el-icon>
    </el-upload>
    </el-form-item>

    <!-- 海报关联form.poster -->
    <el-form-item label="海报">
    <el-upload
      v-model:file-list="form.poster"
      action="/api/file/upload"
      list-type="picture-card"
      :on-success="onPosterUploadSuccess"
    >
      <el-icon><Plus /></el-icon>
    </el-upload>
</el-form-item>

文件上传成功的数据加工

  • 文件上传成功,其url只是一个临时url,我们需要修改其目标地址为服务器端文件的访问地址
/* 海报上传成功回调 */
const onPosterUploadSuccess = (response, uploadFile, uploadFiles) => {
  console.log(
    "response, uploadFile, uploadFiles=",
    response,
    uploadFile,
    uploadFiles
  );

  // 将海报的信息中的poster字段做修改
  form.poster = [
    {
      name: "",
      url: `http://localhost:8173/upload/${uploadFile.name}`,
    },
  ];
};

/* 演员照片上传成功回调 */
const onActorUploadSuccess = (response, uploadFile, uploadFiles) => {
  console.log(
    "response, uploadFile, uploadFiles=",
    response,
    uploadFile,
    uploadFiles
  );

  // 新上传的文件位于actors列表的末尾,更正这个数据
  let lastActor = form.actors[form.actors.length - 1];
  let [name, role] = uploadFile.name.split("-");
  console.log("name,role", name, role);

  role = role.split(".")[0]; //去掉文件名后缀

  /* 更新表单数据 */
  Object.assign(lastActor, {
    name: `${name}-${role}`,
    url: `http://localhost:8173/upload/${uploadFile.name}`,
  });
};

三连了没就走???

image.png

watch,follow,fork!!!

祝大家撸码愉快~

© 版权声明
THE END
喜欢就支持一下吧
点赞5 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容