[vue2]智慧商城app

news2024/11/24 18:33:19

项目演示

查看项目效果, 明确功能模块

项目收获

通过该项目的学习, 可以收获的内容

创建项目

  1. 创建命令: vue create hm-shopping-app
  2. 清理项目多余文件
  3. 清理 路由配置文件 和 App.vue文件
  4. 新增 API接口目录 和 utils工具方法目录

vant组件库

第三方封装好了很多的组件, 整合起来就是一个组件库

官网: Vant 2 - Mobile UI Components built on Vue

使用

  1. 安装: yarn add vant@latest-v2 -S
  2. 如果遇到安装报错, 可能是版本冲突导致, 尝试 npm i vant@latest-v2 -S --legacy-peer-deps 命令
  3. 全部导入与按需导入

  1. 根据官网(快速上手)配置按需导入
  2. 统一管理Vant组件
// 按需导入vant组件
import Vue from 'vue'
import {  Button, Switch } from 'vant'

Vue.use(Button)
Vue.use(Switch)
  1. 引入配置文件
...
import '@/utils/vant-ui'
  1. 验证按需加载是否成功

其他组件库

PC端: element-ui (element-plus)(饿了么) ant-design-vue(阿里)

移动端 vant-ui(有赞) Mint UI(饿了么) Cibe UI(滴滴)

vw适配

开发移动端,就要解决屏幕适配问题, 在脚手架环境中,目前主流的解决方案,就是使用postcss插件, 实现px单位自动换算成vw单位

  1. 安装: yarn add -D postcss-px-to-viewport@1.1.1
  2. 说明: postcss插件不同版本的配置略有差异, 本项目统一使用1.1.1版本
  3. 根目录新建 postcss.config.js文件
module.exports = {
  plugins: {
    'postcss-px-to-viewport': {
      // vw适配的标准屏幕宽度 iphoneX
      // 设计图750时, 使用1倍图=>适配375的标准屏幕(最常用)
      // 设计图640时, 使用1倍图=>适配320的标准屏幕
      viewportWidth: 375
    }
  }
}
  1. 测试: css书写px单位, 编译后检查元素单位是否是vw

配置路由

但凡是单个页面, 独立展示的, 都是一级路由

一级路由

  1. 配置路由规则
... ...

const routes = [
  { path: '/', redirect: '/layout' },
  { path: '/login', component: () => import('../views/login/index.vue') },
  { path: '/myorder', component: () => import('../views/myorder/index.vue') },
  { path: '/pay', component: () => import('../views/pay/index.vue') },
  { path: '/search', component: () => import('../views/search/index.vue') },
  { path: '/searchlist', component: () => import('../views/search/list.vue') },
  { path: '/prodetail/:id', component: () => import('../views/prodetail/index.vue') },
  { path: '/search', component: () => import('../views/search/index.vue') },
  { path: '/address', component: () => import('../views/address/index.vue') }
]

.. ...
  1. 一级路由出口
<template>
  <div id="app">
    <router-view></router-view>
  </div>
</template>

二级路由

  1. 配置二级路由规则
... ...

const routes = [
  {
    path: '/layout',
    component: () => import('../views/layout/index.vue'),
    redirect: '/home',
    children: [
      { path: '/home', component: () => import('../views/layout/home.vue') },
      { path: '/cart', component: () => import('../views/layout/cart.vue') },
      { path: '/user', component: () => import('../views/layout/user.vue') },
      { path: '/category', component: () => import('../views/layout/category.vue') }
    ]
  }
]

... ...
  1. 配置导航链接/二级路由出口
<template>
  <div class="continer">

    <router-view></router-view>

    <van-tabbar route active-color="#ee0a24" inactive-color="#000">
      <van-tabbar-item to="/home" icon="wap-home-o">首页</van-tabbar-item>
      <van-tabbar-item to="/category" icon="apps-o">分类</van-tabbar-item>
      <van-tabbar-item to="/cart" icon="shopping-cart-o">购物车</van-tabbar-item>
      <van-tabbar-item to="/user" icon="user-o">我的</van-tabbar-item>
    </van-tabbar>
  </div>
</template>

<script>
export default {
  name: 'LayoutIndex'
}
</script>

<style>
</style>

登录页

1.样式初始化
  1. 新建style/common.less, 重置默认样式
// 重置默认样式
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    font-family: "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif;
    font-size: 14px;
    // color: #333;
    -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
    -webkit-tap-highlight-color: transparent;
}

// 文字溢出省略号
.text-ellipsis-2 {
    overflow: hidden;
    -webkit-line-clamp: 2;
    text-overflow: ellipsis;
    display: -webkit-box;
    -webkit-box-orient: vertical;
}
  1. main.js导入common.less
... ...
import '@/style/common.less'
... ...
  1. 把图片等素材拷贝到assets目录

2.静态布局
  1. 使用vant-nav-bar组件
<template>
  <div class="login">
    <!-- 注意要按需引入 -->
    <van-nav-bar title="会员登录" left-arrow @click-left="$router.go(-1)" />
    ... ...
  </div>
</template>
  1. 通过样式覆盖修改箭头的颜色
// 自定义van-nav-bar左侧箭头颜色
.van-nav-bar {
    .van-nav-bar__arrow {
        color: #333;
    }
}
  1. 其他静态结构的编写
<template>
  <div class="login">
    <van-nav-bar title="会员登录" left-arrow @click-left="$router.go(-1)" />

    <div class="container">
      <div class="title">
        <h3>手机号登录</h3>
        <p>未注册的手机号登录后将自动注册</p>
      </div>

      <div class="form">
        <div class="form-item">
          <input v-model="mobile" class="inp" maxlength="11" placeholder="请输入手机号码" type="text">
        </div>
        <div class="form-item">
          <input v-model="picCode" class="inp" maxlength="5" placeholder="请输入图形验证码" type="text">
          <img v-if="picUrl" :src="picUrl" @click="getPicCode" alt="">
        </div>
        <div class="form-item">
          <input v-model="msgCode" class="inp" placeholder="请输入短信验证码" type="text">
          <button @click="getMagCode">
            {{ second === totalSecond ? '获取验证码' : second + '秒后重新发送'}}
          </button>
        </div>
      </div>

      <div class="login-btn" @click="login">登录</div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'LoginPage',
  data () {
    return { }
  },
  created () { },
  methods: { }
}
</script>

<style lang="less" scoped>
.container {
  padding: 49px 29px;

  .title {
    margin-bottom: 20px;
    h3 {
      font-size: 26px;
      font-weight: normal;
    }
    p {
      line-height: 40px;
      font-size: 14px;
      color: #b8b8b8;
    }
  }

  .form-item {
    border-bottom: 1px solid #f3f1f2;
    padding: 8px;
    margin-bottom: 14px;
    display: flex;
    align-items: center;
    .inp {
      display: block;
      border: none;
      outline: none;
      height: 32px;
      font-size: 14px;
      flex: 1;
    }
    img {
      width: 94px;
      height: 31px;
    }
    button {
      height: 31px;
      border: none;
      font-size: 13px;
      color: #cea26a;
      background-color: transparent;
      padding-right: 9px;
    }
  }

  .login-btn {
    width: 100%;
    height: 42px;
    margin-top: 39px;
    background: linear-gradient(90deg,#ecb53c,#ff9211);
    color: #fff;
    border-radius: 39px;
    box-shadow: 0 10px 20px 0 rgba(0,0,0,.1);
    letter-spacing: 2px;
    display: flex;
    justify-content: center;
    align-items: center;
  }
}
</style>
3.封装axios模块

把axios请求进行封装到request模块, 主要目的是添加一些配置, 如基础地址, 请求和响应拦截器等

如果项目中需要请求不同的地址,可以创建多个axios实例

步骤

  1. 官网: Axios 实例 | Axios中文文档 | Axios中文网
  2. 安装: yarn add axios
  3. 新建src/utils/request.js模块
import axios from 'axios'

// 1,创建axios实例
const instance = axios.create({
  baseURL: 'http://cba.itlike.com/public/index.php?s=/api/',
  timeout: 1500
})

// 2,添加请求拦截器
instance.interceptors.request.use(function (config) {
  // 在发送请求之前做些什么
  return config
}, function (error) {
  // 对请求错误做些什么
  return Promise.reject(error)
})

// 3,添加响应拦截器
instance.interceptors.response.use(function (response) {
  // 2xx 范围内的状态码都会触发该函数。
  // 对响应数据做点什么(默认axios会多包装一层data,需要响应拦截器中处理一下)
  const res = response.data
  
}, function (error) {
  // 超出 2xx 范围的状态码都会触发该函数。
  // 对响应错误做点什么
  return Promise.reject(error)
})

export default instance
  1. 测试使用
... ...
<script>
import request from '@/utils/request'
export default {
  created () {
    request.get('/captcha/image')
  }
}
</script>
4.封装api模块

把请求封装成方法, 与页面分离, 统一管理请求

好处

  1. 请求与页面逻辑分离
  2. 相同的请求可以复用
  3. 请求统一管理便于查找修改

步骤

  1. 新建请求模块 src/api/login.js
  2. 封装请求函数( 方法要按需导出, 请求结果要return )
// 登录相关接口
import request from '@/utils/request'

// 获取图形验证码
export const getPriceCode = () => {
  return request.get('/captcha/image')
}
  1. 页面中导入使用
<script>
import { getPriceCode } from '@/api/login'

export default {
  ... ...
  created () {
    this.getPicCode()
  },
  methods: {
    // 获取图形验证码
    async getPicCode () {
      const { data: { base64, key } } = await getPriceCode()
    },
  }
}
</script>

5.图形验证码

图形验证码本质就是一个请求回来的图片, 作用就是强制人机交互, 可以抵御机器自动化攻击, 比如通过批量请求爆破接口, 恶意刷票,论坛灌水等

需求:

  1. 效果

  1. 把请求回来的base64图片,动态展示出来
  2. 点击验证码盒子, 要刷新验证码

步骤:

  1. 图片就是让用户看的
  2. 用户输入的验证码要和key值一起提交, 服务器才能判断验证是否成功

  1. 代码实现
<template>
  <div class="login">
   ... ...
    <div class="form-item">
      <!-- 3,展示验证码 -->
      <input v-model="picCode" class="inp" maxlength="5" placeholder="请输入图形验证码" type="text">
      <img v-if="picUrl" :src="picUrl" @click="getPicCode" alt="">
    </div>
   ... ...
  </div>
</template>

<script>
import { getPriceCode, getMsgCode, codeLogin } from '@/api/login'

export default {
  name: 'LoginPage',
  data () {
    return {
      picKey: '', // 将来请求传递的图形验证码唯一标识
      picUrl: '', // 存储请求渲染的图片地址
      picCode: '', // 用户输入的图形验证码
    }
  },
  created () {
    this.getPicCode()
  },
  methods: {
    // 1,获取图形验证码
    async getPicCode () {
      const { data: { base64, key } } = await getPriceCode()
      //2, 保存关键数据
      this.picUrl = base64 // 存储图片地址
      this.picKey = key // 存储唯一标识
    },
  }
}
</script>
6.totas轻提示
  1. 注册安装

  1. 导入调用: 组件和非组件都可以使用

  1. this调用: 必须组件内调用

  1. 在注册该组件后, vant自动把该方法, 挂载到了vue原型上 (Vue.prototype.$toast = xxxx)
7,短信验证码

  1. 点击按钮, 实现倒计时效果
  2. 倒计时之前, 校验手机号和验证码
  3. 封装接口, 获取短信验证码
<template>
  <div class="login">
    <div class="container">
      <div class="form">
        <div class="form-item">
          <input v-model="mobile" class="inp" maxlength="11" placeholder="请输入手机号码" type="text">
        </div>
        <div class="form-item">
          <input v-model="picCode" class="inp" maxlength="5" placeholder="请输入图形验证码" type="text">
          <img v-if="picUrl" :src="picUrl" @click="getPicCode" alt="">
        </div>
        <div class="form-item">
          <input v-model="msgCode" class="inp" placeholder="请输入短信验证码" type="text">
          <button @click="getMagCode">
            {{ second === totalSecond ? '获取验证码' : second + '秒后重新发送'}}
          </button>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import { getMsgCode } from '@/api/login'

export default {
  name: 'LoginPage',
  data () {
    return {
     ... ...
      totalSecond: 60, // 总秒数 (用于归位)
      second: 60, // 当前秒数,(用于倒计时)
      timer: null, // 定时器 id
      mobile: '', // 手机号
      picCode: '', // 用户输入的图形验证码
      msgCode: '246810' // 短信验证码
    }
  },
  methods: {
    // 手机号/图形码校验
    validFn () {
      if (!/^1[3-9]\d{9}$/.test(this.mobile)) {
        this.$toast('请输入正确的手机号码')
        return false
      }

      if (!/^\w{4}$/.test(this.picCode)) {
        this.$toast('请输入正确的图形验证码')
        return false
      }

      return true
    },

    //  获取短信验证码
    async getMagCode () {
      // 2,校验手机号/图形码
      if (!this.validFn()) return

      // 定时器为null 且 当前秒数==总秒数, 才可以获取短信验证码
      if (!this.timer && this.second === this.totalSecond) {
        // 3,获取短信验证码
        await getMsgCode(this.picCode, this.picKey, this.mobile)
        this.$toast('验证码已发送')

        // 1,倒计时效果
        this.timer = setInterval(() => {
          this.second--

          if (this.second <= 0) {
            clearInterval(this.timer) // 清除定时器
            this.timer = null // 清除定时器
            this.second = this.totalSecond // 重置秒数
          }
        }, 1000)
      }
    },

    destroyed () {
    // 清除定时器
      clearInterval(this.timer)
    }
  }
}
</script>

8.登录

  1. 封装登录接口
  2. 登录前校验(手机号/图形验证码/短信验证码)
  3. 发起登录请求, 成功后提示, 跳转首页
<template>
  <div class="login">
      ... ...
      <div class="login-btn" @click="login">登录</div>
    </div>
  </div>
</template>

<script>
import { getPriceCode, getMsgCode, codeLogin } from '@/api/login'

export default {
  name: 'LoginPage',
  data () {
    return {
      mobile: '', // 手机号
      msgCode: '246810' // 短信验证码
    }
  },
  methods: {
    ... ...
    // 登录
    async login () {
      // 校验手机号/图形码
      if (!this.validFn()) return

      // 校验短信验证码
      if (!/^\d{6}$/.test(this.msgCode)) {
        this.$toast('请输入正确的短信验证码')
        return
      }

      // 登录
      const res = await codeLogin(this.mobile, this.msgCode)
      this.$toast('登录成功')
      this.$router.replace('/')
    },
  }
}
</script>
9.统一处理错误

所有的请求, 都可能出现错误,, 可以单独处理, 但是更好的建议是, 通过响应拦截器, 统一处理接口错误

优势:

  1. 统一处理请求错误后, 请求相关的代码都只需要考虑正常逻辑即可
  2. 全局处理请求错误依赖后台, 后台需要返回合适的错误信息

import { Toast } from 'vant'
... ...
// 响应拦截器
instance.interceptors.response.use(function (response) {
  // 2xx 范围内的状态码都会触发该函数。
  const res = response.data
  
  // 统一处理请求错误
  if (res.status !== 200) {
    // 预期: 响应的状态码非200, 抛出错误, 给用户提示
    // 原理: await只会等待成功的promise, 抛出错误后程序不再往下执行
    Toast(res.message) // 弹出提示
    return Promise.reject(res) // 返回一个错误的promise
  }

  return res
}, function (error) {
  // 超出 2xx 范围的状态码都会触发该函数。
  // 对响应错误做点什么
  return Promise.reject(error)
})

10.保存权证信息

把权证信息保存到vuex中, 方便其他页面的使用

  1. 构建user模块
export default {
  namespaced: true,
  state () {
    return {}
  },
  mutations: {},
  actions: {},
  getters: {}
}
  1. 挂载到vuex
import user from './modules/user'

export default new Vuex.Store({
  ... ...
  modules: {
    user,
  }
})
  1. 提供mutations
export default {
  namespaced: true,
  state () {
    return {
      // 个人权证相关
      userInfo: {
        token: '',
        userId: '',
      }
    }
  },
  mutations: {
    // 设置用户信息
    SET_USER_INFO (state, userInfo) {
      state.userInfo = userInfo
      setInfo(userInfo) // 存储用户信息到本地
    }
  },
  actions: {},
  getters: {}
}
  1. 页面中调用
<script>
export default {
  ... ...
  methods: {
    ... ...
    // 登录
    async login () {
      ... ...
      const res = await codeLogin(this.mobile, this.msgCode)
      // 保存用户权证
      this.$store.commit('user/SET_USER_INFO', res.data)
      ... ...
    },
  }
}
</script>
11.持久化存储

封装storage存储模块, 利用本地存储, 进行vuex持久化处理

// 约定通用的键名
const INFO_KEY = 'hm_shopping_info'

// 获取个人信息
export const getInfo = () => {
  const defaultObj = { token: '', userId: '' }
  const result = localStorage.getItem(INFO_KEY)
  return result ? JSON.parse(result) : defaultObj
}

// 设置个人信息
export const setInfo = (obj) => {
  localStorage.setItem(INFO_KEY, JSON.stringify(obj))
}

// 清除个人信息
export const clearInfo = () => {
  localStorage.removeItem(INFO_KEY)
}
import { getInfo, setInfo } from '@/utils/storage'

export default {
  namespaced: true,
  state () {
    return {
      // 个人权证相关
      userInfo: getInfo()
    }
  },
  mutations: {
    // 设置用户信息
    SET_USER_INFO (state, userInfo) {
      state.userInfo = userInfo
      setInfo(userInfo) // 存储用户信息到本地
    }
  },
  actions: {},
  getters: {}
}
12.添加loading效果

请求的时候统一添加loading效果

好处

  1. 节流处理: 防止用户在一次请求结束之前, 多次进行点击, 发送无效的请求
  2. 友好提示: 告知用户, 正在加载中, 用户体验更好

步骤

  1. 在请求拦截器中, 打开loading
  2. 在响应拦截器中, 关闭loading

import { Toast } from 'vant'

// 请求拦截器
instance.interceptors.request.use(function (config) {
  // 在发送请求之前做些什么
  // 1,开启loading效果
  Toast.loading({
    message: '加载中...',
    forbidClick: true,
    loadingType: 'spinner', // loading的类型
    duration: 0 // 不会自动消失
  })
 ... ...
}, function (error) {
  ... ...
})

// 响应拦截器
instance.interceptors.response.use(function (response) {
  if (res.status !== 200) {
    ... ...
  } else {
    // 请求正常, 关闭loading效果,代码继续执行后面的业务
    Toast.clear()
  }
}, function (error) {
    ... ...
})

13.页面鉴权

基于全局前置路由守卫, 进行页面访问拦截处理

路由导航守卫

  1. 所有的路由一旦被匹配到, 都会先经过全局前置守卫
  2. 只有全局前置守卫放行, 才会真正解析渲染组件, 才能看到页面的内容
  3. fullPath可以拿到路由参数, path只可以拿到路由

import store from '@/store/index'
... ...
// 所有的权限页面
const authUrls = ['/pay','/myorder'] 
// 路由前置守卫
router.beforeEach((to, from, next) => {
  if (!authUrls.includes(to.path)) {
    // 非权限页面 直接放行
    next()
    // return是必须的, 不然会一直执行next()
    return
  }

  const token = store.getters.token
  if (token) {
    // 权限页面, 判断有token, 放行
    next()
  } else {
    // 权限页面, 无token,强制去登录
    next('/login')
  }
})
... ...
export default new Vuex.Store({
  getters: {
    // 方便获取token, 封装getters
    token (state) {
      return state.user.userInfo.token
    }
  },
 ... ...
})

首页

1.0静态结构

2.0封装接口
import request from '@/utils/request'

// 获取首页数据
export const getHomeData = () => {
  return request.get('/page/detail', {
    params: {
      pageId: 0
    }
  })
}
3.0页面调用
<script>
import { getHomeData } from '@/api/home'
export default {
  data () {
    return {
      bannerList: [], // 轮播
      navList: [], // 导航
      proList: [] // 商品
    }
  },
  async created () {
    const { data: { pageData } } = await getHomeData()
    this.bannerList = pageData.items[1].data
    this.navList = pageData.items[3].data
    this.proList = pageData.items[6].data
  }
}
</script>
4.0动态渲染
<template>
  <div class="home">
    <!-- 导航条 -->
    <van-nav-bar title="智慧商城" fixed />

    <!-- 搜索框 -->
    <van-search
      readonly
      shape="round"
      background="#f1f1f2"
      placeholder="请在此输入搜索关键词"
      @click="$router.push('/search')"
    />

    <!-- 轮播图 -->
    <van-swipe class="my-swipe" :autoplay="3000" indicator-color="white">
      <van-swipe-item v-for="item in bannerList" :key="item.imgUrl">
        <img :src="item.imgUrl" alt="">
      </van-swipe-item>
    </van-swipe>

    <!-- 导航 -->
    <van-grid column-num="5" icon-size="40">
      <van-grid-item
        v-for="item in navList" :key="item.imgUrl"
        :icon="item.imgUrl"
        text="新品首发"
        @click="$router.push('/category')"
      />
    </van-grid>

    <!-- 主会场 -->
    <div class="main">
      <img src="@/assets/main.png" alt="">
    </div>

    <!-- 猜你喜欢 -->
    <div class="guess">
      <p class="guess-title">—— 猜你喜欢 ——</p>

      <div class="goods-list">
        <GoodsItem v-for="item in proList" :key="item.goods_id" :item="item"></GoodsItem>
      </div>
    </div>
  </div>
</template>
<template>
    <div v-if="item.goods_id" class="goods-item" @click="$router.push(`/prodetail/${item.goods_id}`)">
      <div class="left">
        <img :src="item.goods_image" alt="" />
      </div>
      <div class="right">
        <p class="tit text-ellipsis-2">
          {{ item.goods_name }}
        </p>
        <p class="count">已售 {{ item.goods_sales }} 件</p>
        <p class="price">
          <span class="new">¥{{ item.goods_price_min }}</span>
          <span class="old">¥{{ item.goods_price_max }}</span>
        </p>
      </div>
    </div>
  </template>

<script>
export default {
  name: 'GoodsItem',
  props: {
    item: {
      type: Object,
      default: () => {
        return {}
      }
    }
  }
}
</script>
5.0搜索管理页

需求

  1. 搜索历史动态渲染
  2. 点击搜索按钮或者历史记录, 进行搜索
  3. 已存在的搜索关键字, 先移除再添加到最前面
  4. 不存在的搜索关键字, 添加到最前面
  5. 点击清空图标, 清空历史记录
  6. 持久化储存

代码

<template>
  <div class="search">
    <van-nav-bar title="商品搜索" left-arrow @click-left="$router.go(-1)" />

    <van-search v-model="search" show-action placeholder="请输入搜索关键词" clearable>
      <template #action>
        <div @click="goSearch(search)">搜索</div>
      </template>
    </van-search>

    <!-- 搜索历史 -->
    <div class="search-history" v-if="history.length > 0">
      <div class="title">
        <span>最近搜索</span>
        <van-icon @click="clear" name="delete-o" size="16" />
      </div>
      <div class="list">
        <div v-for="item in history" :key="item" class="list-item" @click="goSearch(item)">
          {{ item }}
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import { setHistory, getHistory } from '@/utils/storage'
export default {
  name: 'SearchIndex',
  data () {
    return {
      search: '', // 输入框的内容
      history: getHistory() // 历史记录
    }
  },
  methods: {
    // 添加搜索历史
    goSearch (key) {
      const index = this.history.indexOf(key)
      if (index !== -1) {
        // 存在于原数组, 删除
        this.history.splice(index, 1)
      }
      // 头部追加
      this.history.unshift(key)
      // 持久化存储
      setHistory(this.history)
      // 跳转列表页
      this.$router.push(`/searchlist?search=${key}`)
    },

    // 清空搜索历史
    clear () {
      this.history = []
      setHistory([])
    }
  }
}
</script>
// 约定通用的键名
const HISTORY_KEY = 'hm_history_key'

// 添加搜索历史记录
export const setHistory = (arr) => {
  localStorage.setItem(HISTORY_KEY, JSON.stringify(arr))
}

// 获取搜索历史记录
export const getHistory = () => {
  const result = localStorage.getItem(HISTORY_KEY)
  return result ? JSON.parse(result) : []
}
6.0搜索列表页

需求

  1. 准备静态结构
  2. 封装接口
  3. 请求数据
  4. 动态渲染

代码

import request from '@/utils/request'

// 获取搜索商品列表
export const getProList = (obj) => {
  const { categoryId, goodsName, page } = obj
  return request.get('/goods/list', {
    params: {
      categoryId, // 商品分类id
      goodsName,  // 商品名称
      page        // 当前页
    }
  })
}
<template>
  <div class="search">
    <van-nav-bar fixed title="商品列表" left-arrow @click-left="$router.go(-1)" />

    <van-search
      readonly
      shape="round"
      background="#ffffff"
      :value="querySearch || '搜索商品'"
      show-action
      @click="$router.push('/search')"
    >
      <template #action>
        <van-icon class="tool" name="apps-o" />
      </template>
    </van-search>

    <!-- 排序选项按钮 -->
    <div class="sort-btns">
      <div class="sort-item">综合</div>
      <div class="sort-item">销量</div>
      <div class="sort-item">价格 </div>
    </div>
      
     <!-- 3,渲染商品列表 -->
    <div class="goods-list">
      <GoodsItem v-for="item in proList" :key="item.goods_id" :item="item"></GoodsItem>
    </div>
  </div>
</template>

<script>
import GoodsItem from '@/components/GoodsItem.vue'
import { getProList } from '@/api/product'
export default {
  name: 'SearchIndex',
  components: {
    GoodsItem
  },
  computed: {
    // 1, 获取地址栏的搜索关键字
    querySearch () {
      return this.$route.query.search
    }
  },
  data () {
    return {
      page: 1,
      proList: []
    }
  },
  // 2, 请求列表数据
  async created () {
    const { data: { list } } = await getProList({
      goodsName: this.querySearch,
      page: this.page
      // 商品分类id: 兼容商品分类页, 
      // 如果值是undefiend或null, axios会自动过滤
      categoryId: this.$route.query.categoryId,
    })
    this.proList = list.data
  }
}
</script>
7.0商品分类页

需求

  1. 准备静态结构
  2. 封装接口
  3. 请求数据
  4. 动态渲染
  5. 点击跳转搜索列表页

代码

import request from '@/utils/request'

// 获取分类数据
export const getCategoryData = () => {
  return request.get('/category/list')
}
<template>
  <div class="category">
    <!-- 分类 -->
    <van-nav-bar title="全部分类" fixed />

    <!-- 搜索框 -->
    <van-search
      readonly
      shape="round"
      background="#f1f1f2"
      placeholder="请输入搜索关键词"
      @click="$router.push('/search')"
    />

    <!-- 分类列表 -->
    <!-- 2,渲染数据 -->
    <div class="list-box">
      <div class="left">
        <ul>
          <li v-for="(item, index) in list" :key="item.category_id">
            <a :class="{ active: index === activeIndex }" @click="activeIndex = index" href="javascript:;">{{ item.name }}</a>
          </li>
        </ul>
      </div>
      <div class="right">
        <!-- 3,点击二级分类, 跳转搜索列表页  -->
        <div @click="$router.push(`/searchlist?categoryId=${item.category_id}`)" v-for="item in list[activeIndex]?.children" :key="item.category_id" class="cate-goods">
          <img :src="item.image?.external_url" alt="">
          <p>{{ item.name }}</p>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import { getCategoryData } from '@/api/category'
export default {
  name: 'CategoryPage',
  created () {
    // 1, 请求数据
    this.getCategoryList()
  },
  data () {
    return {
      list: [],
      activeIndex: 0
    }
  },
  methods: {
    async getCategoryList () {
      const { data: { list } } = await getCategoryData()
      this.list = list
    }
  }
}
</script>

商品详情页

步骤

  1. 静态结构
  2. 封装接口
  3. 请求数据
  4. 渲染页面

代码

import request from '@/utils/request'

// 获取商品详情数据
export const getProDetail = (goodsId) => {
  return request.get('/goods/detail', {
    params: {
      goodsId
    }
  })
}

// 获取商品评价
export const getProComments = (goodsId, limit) => {
  return request.get('/comment/listRows', {
    params: {
      goodsId,
      limit
    }
  })
}
<template>
  <div class="prodetail">
    <van-nav-bar fixed title="商品详情页" left-arrow @click-left="$router.go(-1)" />

    <van-swipe :autoplay="4000" @change="onChange">
      <van-swipe-item v-for="(image, index) in images" :key="index">
        <img :src="image.external_url" />
      </van-swipe-item>

      <template #indicator>
        <div class="custom-indicator">{{ current + 1 }} / {{ images.length }}</div>
      </template>
    </van-swipe>

    <!-- 3,商品说明 -->
    <div class="info">
      <div class="title">
        <div class="price">
          <span class="now">¥{{ detail.goods_price_min }}</span>
          <span class="oldprice">¥{{ detail.goods_price_max }}</span>
        </div>
        <div class="sellcount">已售 {{ detail.goods_sales }} 件</div>
      </div>
      <div class="msg text-ellipsis-2">
        {{ detail.goods_name }}
      </div>

      <div class="service">
        <div class="left-words">
          <span><van-icon name="passed" />七天无理由退货</span>
          <span><van-icon name="passed" />48小时发货</span>
        </div>
        <div class="right-icon">
          <van-icon name="arrow" />
        </div>
      </div>
    </div>

    <!-- 5,商品评价 -->
    <div class="comment">
      <div class="comment-title">
        <div class="left">商品评价 ({{ total }}条)</div>
        <div class="right">查看更多 <van-icon name="arrow" /> </div>
      </div>
      <div class="comment-list">
        <div class="comment-item" v-for="item in commentList" :key="item.comment_id">
          <div class="top">
            <!-- 头像可能为null, 所以设置一个默认值 --> 
            <img :src="item.user.avatar_url || defaultImg" alt="">
            <div class="name">{{ item.user.nick_name }}</div>
            <van-rate :size="16" :value="item.score / 2" color="#ffd21e" void-icon="star" void-color="#eee"/>
          </div>
          <div class="content">
            {{ item.content }}
          </div>
          <div class="time">
            {{ item.create_time }}
          </div>
        </div>
      </div>
    </div>

    <!-- 4,商品描述 -->
    <div class="desc" v-html="detail.content">
    </div>
  </div>
</template>

<script>
import { getProComments, getProDetail } from '@/api/product'
import defaultImg from '@/assets/default-avatar.png'

export default {
  name: 'ProDetail',
  data () {
    return {
      images: [],
      current: 0,
      detail: {},
      total: 0, // 评价总数
      commentList: [], // 评价列表
      defaultImg, // 评论的默认头像(头像可能会为null)
    }
  },
  computed: {
    goodsId () {
      return this.$route.params.id
    }
  },
  created () {
    this.getDetail()
    this.getComments()
  },
  methods: {
    onChange (index) {
      this.current = index
    },
    // 1,获取商品详情
    async getDetail () {
      const { data: { detail } } = await getProDetail(this.goodsId)
      this.detail = detail
      this.images = detail.goods_images
    },
    // 2,获取商品评价
    async getComments () {
      const { data: { list, total } } = await getProComments(this.goodsId, 3)
      this.commentList = list
      this.total = total
    },
  }
}
</script>

加入购物车

1.0唤起弹层

需求

  1. 熟悉van-action-sheet组件
  2. 完善弹层结构
  3. 动态渲染

<template>
  <div class="prodetail">
    ... ...
    <!-- 底部 -->
    <div class="footer">
      <div @click="$router.push('/')" class="icon-home">
        <van-icon name="wap-home-o" />
        <span>首页</span>
      </div>
      <div @click="$router.push('/cart')" class="icon-cart">
        <span v-if="cartTotal > 0" class="num">{{ cartTotal }}</span>
        <van-icon name="shopping-cart-o" />
        <span>购物车</span>
      </div>
      <div @click="addFn" class="btn-add">加入购物车</div>
      <div @click="buyNow" class="btn-buy">立刻购买</div>
    </div>

    <!-- 加入购物车/立即购买 公用的弹层 -->
    <van-action-sheet v-model="showPannel" :title="mode === 'cart' ? '加入购物车' : '立刻购买'">
      <div class="product">
        <div class="product-title">
          <div class="left">
            <img :src="detail.goods_image" alt="">
          </div>
          <div class="right">
            <div class="price">
              <span>¥</span>
              <span class="nowprice">{{ detail.goods_price_min }}</span>
            </div>
            <div class="count">
              <span>库存</span>
              <span>{{ detail.stock_total }}</span>
            </div>
          </div>
        </div>
        <div class="num-box">
          <span>数量</span>
          <span>数字框组件占位</span?
        </div>

        <!-- 有库存才显示提交按钮 -->
        <div class="showbtn" v-if="detail.stock_total > 0">
          <div class="btn" v-if="mode === 'cart'">加入购物车</div>
          <div class="btn now" v-else >立刻购买</div>
        </div>
        <!-- 没有库存显示商品暂无 -->
        <div class="btn-none" v-else>该商品已抢完</div>
      </div>
    </van-action-sheet>
  </div>
</template>
2.0数字框组件

步骤

  1. 静态结构, 左中右三部分
  2. 数字框的数字, 应该是外部传递
  3. 点击+-号, 可以修改数据
  4. 使用v-model实现封装
  5. 数字不能小于1

代码

<template>
 <!-- 加入购物车/立即购买 公用的弹层 -->
    <van-action-sheet>
     ... ...
      <div class="num-box">
          <span>数量</span>
          <!-- 1, 使用组件 -->
          <!-- v-model 本质上 :value 和 @input 的简写 -->
          <CountBox v-model="addCount"></CountBox>
        </div>
    </van-action-sheet>
</template>

<script>
import CountBox from '@/components/CountBox.vue'
export default {
  components: {
    CountBox
  },
  data () {
    return {
      addCount: 1, // 数字框绑定的数据
    }
  },
}
</script>
<template>
  <div class="count-box">
    <button @click="handleSub" class="minus">-</button>
    <input :value="value" @change="handleChange" class="inp" type="text">
    <button @click="handleAdd" class="add">+</button>
  </div>
</template>

<script>
export default {
  props: {
    value: {
      type: Number,
      default: 1
    }
  },
  methods: {
    // +号事件
    handleSub () {
      if (this.value <= 1) {
        return
      }
      this.$emit('input', this.value - 1)
    },
    // -号事件
    handleAdd () {
      this.$emit('input', this.value + 1)
    },
    // 输入事件
    handleChange (e) {
      const num = +e.target.value // 转数字处理 (1) 数字 (2) NaN

      // 输入了不合法的文本 或 输入了负值,回退成原来的 value 值
      if (isNaN(num) || num < 1) {
        e.target.value = this.value
        return
      }

      this.$emit('input', num)
    }
  }
}
</script>
3.0添加购物车

步骤

  1. 加入购物车, 是一个登录后的用户才能进行的操作
  2. 不存在token, 引导用户登录, 登录完回跳
  3. 存在token, 继续加入购物车
  4. 封装添加购物车的接口(添加请求头参数)
  5. 加入购物车后, 展示数量的角标

代码

// 添加请求拦截器
instance.interceptors.request.use(function (config) {

  // 只要有token, 就在请求时携带token, 便于请求需要授权的接口
  const token = store.getters.token
  if (token) {
    config.headers['Access-Token'] = token
    config.headers.platform = 'H5'
  }

  return config
}, function (error) {
  ... ...
})
import request from '@/utils/request'

// 加入购物车
// goodsId    => 商品id     iphone8
// goodsSkuId => 商品规格id  红色的iphone8  粉色的iphone8
export const addCart = (goodsId, goodsNum, goodsSkuId) => {
  return request.post('/cart/add', {
    goodsId,
    goodsNum,
    goodsSkuId
  })
}
<template>
  <div class="prodetail">
    ... ...
    <!-- 底部 -->
    <div class="footer">
     ... ...
      <div @click="$router.push('/cart')" class="icon-cart">
        <!-- 6,展示购物车角标 -->
        <span v-if="cartTotal > 0" class="num">{{ cartTotal }}</span>
        <van-icon name="shopping-cart-o" />
        <span>购物车</span>
      </div>
    </div>

    <!-- 加入购物车/立即购买 公用的弹层 -->
    <van-action-sheet>
        ... ...
        <div class="showbtn" v-if="detail.stock_total > 0">
          <!-- 1, 加入购物车 -->
          <div class="btn" v-if="mode === 'cart'" @click="addCart">加入购物车</div>
          <div class="btn now" v-else @click="goBuyNow">立刻购买</div>
        </div>
    </van-action-sheet>
  </div>
</template>

<script>
import { addCart } from '@/api/cart'

export default {
  name: 'ProDetail',
  data () {
    return {
      showPannel: false, // 控制弹层的显示隐藏
      mode: 'cart', // 标记弹层状态
      addCount: 1, // 数字框绑定的数据
      cartTotal: 0 // 购物车角标
    }
  },
  computed: {
    goodsId () {
      return this.$route.params.id
    }
  },
  methods: {
    // 登录判断
    loginConfirm () {
      if (!this.$store.getters.token) {
        // 4,没有登录, 展示弹窗
        this.$dialog.confirm({
          title: '温馨提示',
          message: '登录后才可以操作购物车',
          confirmButtonText: '去登录',
          cancelButtonText: '再逛逛'
        })
          .then(() => {
            this.$router.replace({
              path: '/login',
              query: {
                 // 携带参数,用于回跳
                backPath: this.$route.fullPath
              }
            })
          })
        
        return false
      }

      // 已经登录
      return true
    }
    
    // 2,加入购物车
    async addCart () {
      // 3,判断是否登录
      if (!this.loginConfirm()) {
        return
      }
      // 5,已经登录
      const { data } = await addCart(this.goodsId, this.addCount, this.detail.skuList[0].goods_sku_id)
      this.cartTotal = data.cartTotal // 保存角标
      this.$toast('加入购物车成功')
      this.showPannel = false
    },
  }
}
</script>
<script>
export default {
  methods: {
    async login () {
      ... ...
      this.$toast('登录成功')
      // 判断处理: 
      // 1,地址栏有参数,别的页面拦截过来的->回跳操作
      // 2,地址栏无参数->去首页
      const url = this.$route.query.backPath || '/'
      this.$router.replace(url)
    },
  }
}
</script>

购物车

需求

  1. 静态结构
  2. 创建vuex的cart模块
  3. 请求数据, 动态渲染购物车列表
  4. 封装getters实现动态统计
  5. 全选反选功能
  6. 数字框修改数量功能
  7. 编辑切换状态, 删除功能
  8. 空购物车处理

1.0创建cart模块
import request from '@/utils/request'

// 获取购物车列表
export const getCartList = () => {
  return request.get('/cart/list')
}
import { getCartList } from '@/api/cart'

export default {
  namespaced: true,
  state () {
    return {
      cartList: []
    }
  },
  mutations: {
    // 转存购物车数据
    setCartList (state, cartList) {
      state.cartList = cartList
    },
  },
  actions: {
    // 获取购物车列表
    async GET_CART_LIST ({ commit }) {
      const { data } = await getCartList()

      // 后台返回的数据中缺少复选框状态, 我们自己添加
      data.list.forEach(item => {
        item.isChecked = true
      })

      commit('setCartList', data.list)
    },
  },
  getters: {}
}
import cart from './modules/cart'

export default new Vuex.Store({
  modules: {
    cart
  }
})
<script>
export default {
  computed: {
    // 用户是否登录
    isLogin () {
      return this.$store.getters.token
    }
  },
  created () {
    // 必须是登录过的用户,才能用户购物车列表
    if (this.isLogin) {
      this.$store.dispatch('cart/GET_CART_LIST')
    }
  },
}
</script>
2.0渲染购物车列表
<template>
  <div class="cart">
    ... ...
    <div v-if="isLogin && cartList.length > 0">
      <!-- 购物车列表 -->
      <div class="cart-list">
        <div class="cart-item" v-for="item in cartList" :key="item.goods_id">
          <van-checkbox :value="item.isChecked"></van-checkbox>
          <div class="show">
            <img :src="item.goods.goods_image" alt="">
          </div>
          <div class="info">
            <span class="tit text-ellipsis-2">{{ item.goods.goods_name }}</span>
            <span class="bottom">
              <div class="price">¥ <span>{{ item.goods.goods_price_min }}</span></div>
              <!-- 既希望保留原本的形参,又需要通过调用函数传参 => 箭头函数包装一层 -->
              <!-- @input="changeCount" -->
              <CountBox :value="item.goods_num"></CountBox>
            </span>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import { mapState } from 'vuex'
export default {
  computed: {
    ...mapState('cart', ['cartList']),
  },
}
</script>
3.0动态统计
export default {
  namespaced: true,
  state () {
    return {
      cartList: []
    }
  },
  getters: {
    // 所有商品的总数
    cartTotal (state) {
      return state.cartList.reduce((sum, item) => sum + item.goods_num, 0)
    },
    // 选中的商品
    selCartList (state) {
      return state.cartList.filter(item => item.isChecked)
    },
    // 选中的商品总数
    selCount (stat, getterse) {
      // 注意: 可以在getters中继续使用其他getters的值
      return getterse.selCartList.reduce((sum, item) => sum + item.goods_num, 0)
    },
    // 选中商品的总价
    selPrice (stat, getters) {
      return getters.selCartList.reduce((sum, item) => {
        return sum + item.goods.goods_price_min * item.goods_num
      }, 0).toFixed(2)
    },
  }
}
<template>
  <div class="cart">
    <div v-if="isLogin && cartList.length > 0">
      <!-- 购物车开头 -->
      <div class="cart-title">
        <span class="all">共<i>{{ cartTotal }}</i>件商品</span>
        ... ...
      </div>

      <div class="footer-fixed">
        ... ...
        <div class="all-total">
          <div class="price">
            <span>合计:</span>
            <span>¥ <i class="totalPrice">{{ selPrice }}</i></span>
          </div>
          <div v-if="!isEdit" class="goPay" :class="{ disabled: selCount === 0 }">结算({{ selCount }})</div>
          <div v-else class="delete" :class="{ disabled: selCount === 0 }" >删除</div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import { mapGetters } from 'vuex'
export default {
  computed: {
    ...mapGetters('cart', ['cartTotal', 'selCartList', 'selCount', 'selPrice']),
  },
}
</script>
4.0全选反选功能
export default {
  namespaced: true,
  state () {
    return {
      cartList: []
    }
  },
  mutations: {
    // 改变商品的选中状态
    toggleCheck (state, goodsIs) {
      const goods = state.cartList.find((item) => item.goods_id === goodsIs)
      goods.isChecked = !goods.isChecked
    },
    // 改变所有商品的状态
    toggleAllCheck (state, status) {
      state.cartList.forEach(item => {
        item.isChecked = status
      })
    },
  },
  getters: {
    // 是否全选
    isAllChecked (state) {
      return state.cartList.every(item => item.isChecked)
    }
  }
}
<template>
  <div class="cart">
  ... ...
    <div v-if="isLogin && cartList.length > 0">
      <!-- 购物车列表 -->
      <div class="cart-list">
        <div class="cart-item" v-for="item in cartList" :key="item.goods_id">
          <!-- 1, 切换商品选中状态 -->
          <van-checkbox @click="toggleCheck(item.goods_id)"  :value="item.isChecked"></van-checkbox>
          ... ...
        </div>
      </div>

      <div class="footer-fixed">
        <!-- 3, 全选/全不选商品  -->
        <div @click="toggleAllCheck" class="all-check">
          <!-- 2, 如果所有商品都选中, 自动激活全选按钮 -->
          <van-checkbox :value="isAllChecked"  icon-size="18"></van-checkbox>
          全选
        </div>
        ... ...
      </div>
    </div>
  </div>
</template>

<script>
import { mapGetters } from 'vuex'
export default {
  computed: {
    ...mapGetters('cart', ['isAllChecked']),
  },

  methods: {
    // 商品选择框
    toggleCheck (goodsId) {
      this.$store.commit('cart/toggleCheck', goodsId)
    },
    // 商品全选按钮
    toggleAllCheck () {
      this.$store.commit('cart/toggleAllCheck', !this.isAllChecked)
    },
  },
}
</script>
5.0修改数量
import request from '@/utils/request'
// 更新购物车商品数量
export const changeCount = (goodsId, goodsNum, goodsSkuId) => {
  return request.post('/cart/update', {
    goodsId,
    goodsNum,
    goodsSkuId
  })
}
import { changeCount } from '@/api/cart'

export default {
  namespaced: true,
  state () {
    return {
      cartList: []
    }
  },
  mutations: {
    // 改变商品数量
    changeGoodsCount (state, obj) {
      const { goodsId, goodsNum } = obj
      const goods = state.cartList.find(item => item.goods_id === goodsId)
      goods.goods_num = goodsNum
    }
  },
  actions: {
    // 修改购物车商品的数量
    async CHANGE_GOODS_NUM ({ commit }, obj) {
      const { goodsId, goodsNum, goodsSkuId } = obj
      // 先更新本地数量
      commit('changeGoodsCount', { goodsId, goodsNum })
      // 再更新后台数量
      await changeCount(goodsId, goodsNum, goodsSkuId)
    },
  },
}
<template>
 <div class="cart-list">
    <div class="cart-item" v-for="item in cartList" :key="item.goods_id">
      ... ...  
      <!-- 既希望保留原本的形参,又需要通过调用函数传参 => 箭头函数包装一层 -->
      <!-- @input="changeCount" -->
      <CountBox @input="(value) => changeCount(value, item.goods_id, item.goods_sku_id)" :value="item.goods_num"></CountBox>
      ... ...
    </div>
 </div>
</template>

<script>
import { mapGetters, mapState } from 'vuex'
export default {
  methods: {
    // 修改商品数量
    changeCount (goodsNum, goodsId, goodsSkuId) {
      // console.log(goodsNum, goodsId, goodsSkuId)
      // 调用 vuex 的 action,进行数量的修改
      this.$store.dispatch('cart/CHANGE_GOODS_NUM', {
        goodsNum,
        goodsId,
        goodsSkuId
      })
    },
  },
}
</script>

6.0切换状态

点击编辑, 结算模式切换到删除模式, 点击删除可以删除购物车商品, 点击结算跳转到订单结算页面

import request from '@/utils/request'

// 删除购物车商品
export const delSelect = (cartIds) => {
  return request.post('/cart/clear', {
    cartIds
  })
}
import { delSelect } from '@/api/cart'

export default {
  actions: {
    // 删除商品
    async DEL_SELECT_GOODS ({ getters, dispatch }) {
      // 拿到选中的商品id
      const goodsIds = getters.selCartList.map(item => item.id)
      // 删除
      await delSelect(goodsIds)
      // 刷新数据
      dispatch('GET_CART_LIST')
    }
  },
}
<template>
 <div>
      <!-- 购物车开头 -->
      <div class="cart-title">
        <!-- 1, 切换页面模式 -->
        <span class="edit" @click="isEdit = !isEdit">
          <van-icon name="edit" />
          编辑
        </span>
      </div>

      <div class="footer-fixed">
        <div class="all-total">
         ... ...
          <!-- 2, 不同模式显示不同按钮 -->
          <div v-if="!isEdit" class="goPay" :class="{ disabled: selCount === 0 }">结算({{ selCount }})</div>
          <div v-else @click="handleDel" class="delete" :class="{ disabled: selCount === 0 }" >删除</div>
        </div>
      </div>
    </div>
</template>

<script>
export default {
  data () {
    return {
      // 页面模式(编辑|删除)
      isEdit: false
    }
  },
  methods: {
    // 4,删除商品
    async handleDel () {
      if (this.selCount === 0) return false
      await this.$store.dispatch('cart/DEL_SELECT_GOODS')
      this.isEdit = false
    },
  },
  watch: {
    // 3,根据页面模式改变商品的选中状态
    isEdit (value) {
      if (value) {
        // 删除模式, 商品全不选
        this.$store.commit('cart/toggleAllCheck', false)
      } else {
        // 结算模式, 商品全选
        this.$store.commit('cart/toggleAllCheck', true)
      }
    }
  }
}
</script>
7.0空购物车
<template>
  <div class="cart">
    <!-- 数据列表 -->
    <div v-if="isLogin && cartList.length > 0">
      ... ...
    </div>

    <!-- 空数据列表 -->
    <div class="empty-cart" v-else>
      <img src="@/assets/empty.png" alt="">
      <div class="tips">
        您的购物车是空的, 快去逛逛吧
      </div>
      <div class="btn" @click="$router.push('/')">去逛逛</div>
    </div>
  </div>
</template>

<script>
export default {
  computed: {
    // 用户是否登录
    isLogin () {
      return this.$store.getters.token
    }
  },
}
</script>

订单结算台

需求

  1. 订单收货地址管理
  2. 封装订单确认接口
  3. 结算台渲染
  4. 购物车结算
  5. 立即购买结算
  6. minxis混入
  7. 提交订单并支付

01.收货地址管理
import request from '@/utils/request'

// 添加地址列表
export const postAddress = (data) => {
  return request({
    headers: {
      'Content-Type': 'application/json;charset=UTF-8'
    },
    url: '/address/add',
    method: 'POST',
    data
  })
}
<template>
   <div>
    <van-nav-bar title="地址管理" left-arrow @click-left="$router.go(-1)" />
    <van-button type="primary" @click="add">添加收获地址</van-button>
   </div>
 </template>

<script>
import { postAddress } from '@/api/address'

export default {
  name: 'AddressIndex',
  methods: {
    // 添加收获地址
    async add () {
      await postAddress({
        form: {
          name: '张小二',
          phone: '18999292929',
          region: [
            {
              value: 782,
              label: '上海'
            },
            {
              value: 783,
              label: '上海市'
            },
            {
              value: 785,
              label: '徐汇区'
            }
          ],
          detail: '北京路1号楼8888室'
        }
      })
    }
  }
}
</script>
02.封装订单确认接口

订单确认成功后,拿到返回值, 渲染到订单结算台

import request from '@/utils/request'

// 订单结算确认
// mode: cart    => obj { cartIds }
// mode: buyNow  => obj { goodsId  goodsNum  goodsSkuId }
export const checkOrder = (mode, obj) => {
  return request.get('/checkout/order', {
    params: {
      mode, // cart(购物车) buyNow(立即购买)
      delivery: 10, // 10 快递配送 20 门店自提
      couponId: 0, // 优惠券ID 传0 不使用优惠券
      isUsePoints: 0, // 积分 传0 不使用积分
      ...obj // 将传递过来的参数对象 动态展开
    }
  })
}
03.结算台渲染
import request from '@/utils/request'

// 获取地址列表
export const getAddressList = () => {
  return request.get('/address/list')
}
}
<template>
   <div class="pay">
    <van-nav-bar fixed title="订单结算台" left-arrow @click-left="$router.go(-1)" />

    <!-- 2,地址相关 -->
    <div class="address" @click="$router.push('/address')">
      <div class="left-icon">
        <van-icon name="logistics" />
      </div>

      <div class="info" v-if="selectedAddress.address_id">
        <div class="info-content">
          <span class="name">{{ selectedAddress.name }}</span>
          <span class="mobile">{{ selectedAddress.phone }}</span>
        </div>
        <div class="info-address">
          {{ longAddress }}
        </div>
      </div>

      <div class="info" v-else>
        请选择配送地址
      </div>

      <div class="right-icon">
        <van-icon name="arrow" />
      </div>
    </div>

    <!-- 3,订单明细 -->
    <div class="pay-list" v-if="order.goodsList">
      <div class="list">
        <div class="goods-item" v-for="item in order.goodsList" :key="item.goods_id">
            <div class="left">
              <img :src="item.goods_image" alt="" />
            </div>
            <div class="right">
              <p class="tit text-ellipsis-2">
                {{ item.goods_name }}
              </p>
              <p class="info">
                <span class="count">x{{ item.total_num }}</span>
                <span class="price">¥{{ item.total_pay_price }}</span>
              </p>
            </div>
        </div>
      </div>

      <div class="flow-num-box">
        <span>共 {{ order.orderTotalNum }} 件商品,合计:</span>
        <span class="money">¥{{ order.orderTotalPrice }}</span>
      </div>

      <div class="pay-detail">
        <div class="pay-cell">
          <span>订单总金额:</span>
          <span class="red">¥{{ order.orderTotalPrice }}</span>
        </div>

        <div class="pay-cell">
          <span>优惠券:</span>
          <span>无优惠券可用</span>
        </div>

        <div class="pay-cell">
          <span>配送费用:</span>
          <span v-if="!selectedAddress">请先选择配送地址</span>
          <span v-else class="red">+¥0.00</span>
        </div>
      </div>

      <!-- 支付方式 -->
      <div class="pay-way">
        <span class="tit">支付方式</span>
        <div class="pay-cell">
          <span><van-icon name="balance-o" />余额支付(可用 ¥ {{ personal.balance }} 元)</span>
          <!-- <span>请先选择配送地址</span> -->
          <span class="red"><van-icon name="passed" /></span>
        </div>
      </div>

      <!-- 买家留言 -->
      <div class="buytips">
        <textarea v-model="remark" placeholder="选填:买家留言(50字内)" name="" id="" cols="30" rows="10"></textarea>
      </div>
    </div>

    <!-- 底部提交 -->
    <div class="footer-fixed">
      <div class="left">实付款:<span>¥{{ order.orderTotalPrice }}</span></div>
      <div class="tipsbtn">提交订单</div>
    </div>
  </div>
</template>

<script>
import { getAddressList } from '@/api/address'

export default {
  name: 'PayIndex',
  data () {
    return {
      addressList: [], // 收获地址
      order: {},
      personal: {},
      remark: '' // 备注留言
    }
  },
  computed: {
    // 选中的默认地址
    selectedAddress () {
      // 这里地址管理非主线业务,直接获取第一个项作为选中的地址
      return this.addressList[0] || {}
    },
    // 完整的收获地址
    longAddress () {
      const region = this.selectedAddress.region
      return region.province + region.city + region.region + this.selectedAddress.detail
    },
    // 拿到订单模式
    mode () {
      return this.$route.query.mode
    },
    // 拿到商品的id
    cartIds () {
      return this.$route.query.cartIds
    },
    // 拿到商品的id
    goodsId () {
      return this.$route.query.goodsId
    },
    // 拿到商品的skuid
    goodsSkuId () {
      return this.$route.query.goodsSkuId
    },
    // 拿到商品数量
    goodsNum () {
      return this.$route.query.goodsNum
    }
  },
  created () {
    // 获取收货地址
    this.getAddressList()
    // 获取订单详情
    this.getOrderList()
  },
  methods: {
    // 1,获取收获地址
    async getAddressList () {
      const { data: { list } } = await getAddressList()
      this.addressList = list
    },
    // 2,获取订单详情
    async getOrderList () {
      // 购物车结算
      if (this.mode === 'cart') {
        const { data: { order, personal } } = await checkOrder(this.mode, {
          cartIds: this.cartIds
        })
        this.order = order
        this.personal = personal
      }
      // 立刻购买结算
      if (this.mode === 'buyNow') {
        const { data: { order, personal } } = await checkOrder(this.mode, {
          goodsId: this.goodsId,
          goodsSkuId: this.goodsSkuId,
          goodsNum: this.goodsNum
        })
        this.order = order
        this.personal = personal
      }
    }
  }
}
</script>
04.购物车结算
<template>
   <div class="all-total">
     ... ...
     <div @click="goPay">结算({{ selCount }})</div>
     ... ...
   </div>
</template>

<script>
export default {
  methods: {
    // 去支付
    goPay () {
      // 判断有没有选中商品
      if (this.selCount > 0) {
        // 有选中的 商品 才进行结算跳转
        this.$router.push({
          path: '/pay',
          query: {
            mode: 'cart',
            // 参数示例: 'cartId,cartId,cartId'
            cartIds: this.selCartList.map(item => item.id).join(',') 
          }
        })
      }
    }
  },
}
</script>
05.立即购买结算
<template>
    <!-- 加入购物车/立即购买 公用的弹层 -->
    <van-action-sheet>
      ... ...
      <div class="showbtn" v-if="detail.stock_total > 0">
          ... ...
          <div class="btn now" v-else @click="goBuyNow">立刻购买</div>
        </div>
    </van-action-sheet>
</template>

<script>
export default {
  methods: {
    // 登录提示
    // 判断是否登录, 登录返回true, 未登录返回false(提示去登录)
    loginConfirm () {
      if (!this.$store.getters.token) {
        // 没有登录, 去登录页
        // 携带参数,用于回跳
        this.$dialog.confirm({
          title: '温馨提示',
          message: '登录后才可以操作购物车',
          confirmButtonText: '去登录',
          cancelButtonText: '再逛逛'
        })
          .then(() => {
            this.$router.replace({
              path: '/login',
              query: {
                backPath: this.$route.fullPath
              }
            })
          })
          .catch(() => {})
        return false
      }

      // 已经登录
      return true
    }
    
    // 立即购买
    goBuyNow () {
      // 判断是否登录
      if (!this.loginConfirm()) {
        return
      }
      // 已经登录
      this.$router.push({
        path: '/pay',
        query: {
          mode: 'buyNow',
          goodsId: this.goodsId,
          goodsSkuId: this.detail.skuList[0].goods_sku_id,
          goodsNum: this.addCount
        }
      })
    }
  }
}
</script>
06.minxis混入
export default {
  // 混入文件里写的就是Vue实例的配置项,可以在需要的文件中使用, 提高代码复用性
  // 支持 data methods computed 生命周期函数 ...
  //  注意:
  //   1, 如果混入文件和组件内, 提供了重名的属性, 组件内属性的优先级更高
  //   2, 如果编写了生命周期函数, 混入文件内和组件内的生命周期函数不会冲突,会被数组管理
  //      统一在组件内执行
  data () {},
  methods: {
    // 登录提示
    // 判断是否登录, 登录返回true, 未登录返回false(提示去登录)
    loginConfirm () {
      if (!this.$store.getters.token) {
        // 没有登录, 去登录页
        // 携带参数,用于回跳
        this.$dialog.confirm({
          title: '温馨提示',
          message: '登录后才可以操作购物车',
          confirmButtonText: '去登录',
          cancelButtonText: '再逛逛'
        })
          .then(() => {
            this.$router.replace({
              path: '/login',
              query: {
                backPath: this.$route.fullPath
              }
            })
          })
          .catch(() => {})
        return false
      }

      // 已经登录
      return true
    }
  }
}
<script>
import loginConfirm from '@/mixins/loginConfirm'

export default {
  name: 'ProDetail',
  mixins: [loginConfirm],
  methods: {
    // 立即购买
    goBuyNow () {
      // 判断是否登录
      if (!this.loginConfirm()) {
        return
      }
    ... ...
    }
  }
}
</script>
07.提交订单并支付
import request from '@/utils/request'
// 提交订单
// mode: cart    => obj { cartIds, remark }
// mode: buyNow  => obj { goodsId, goodsNum, goodsSkuId, remark }
export const submitOrder = (mode, obj) => {
  return request.post('/checkout/submit', {
    mode,
    delivery: 10, // 10 快递配送
    couponId: 0,
    isUsePoints: 0,
    payType: 10, // 余额支付
    ...obj
  })
}
<template>
   <div class="pay">
    <!-- 底部提交 -->
    <div class="footer-fixed">
      ... ...
      <div class="tipsbtn" @click="submitOrder">提交订单</div>
    </div>
  </div>
</template>

<script>
import { submitOrder } from '@/api/order'
export default {
  methods: {
    // 订单支付
    async submitOrder () {
      if (this.mode === 'cart') {
        await submitOrder(this.mode, {
          cartIds: this.cartIds,
          remark: this.remark
        })
      }
      if (this.mode === 'buyNow') {
        await submitOrder(this.mode, {
          goodsId: this.goodsId,
          goodsSkuId: this.goodsSkuId,
          goodsNum: this.goodsNum,
          remark: this.remark
        })
      }
      this.$toast.success('支付成功')
      this.$router.replace('/myorder')
    },
  }
}
</script>

订单管理

import request from '@/utils/request'
// 订单列表
export const getMyOrderList = (dataType, page) => {
  return request.get('/order/list', {
    params: {
      dataType,
      page 
    }
  })
}

<template>
  <div class="order-list-item" v-if="item.order_id">
    <div class="tit">
      <div class="time">{{ item.create_time }}</div>
      <div class="status">
        <span>{{ item.state_text }}</span>
      </div>
    </div>
    <div class="list" >
      <div class="list-item" v-for="(goods, index) in item.goods" :key="index">
        <div class="goods-img">
          <img :src="goods.goods_image" alt="">
        </div>
        <div class="goods-content text-ellipsis-2">
          {{ goods.goods_name }}
        </div>
        <div class="goods-trade">
          <p>¥ {{ goods.total_pay_price }}</p>
          <p>x {{ goods.total_num }}</p>
        </div>
      </div>
    </div>
    <div class="total">
      共 {{ item.total_num }} 件商品,总金额 ¥{{ item.total_price }}
    </div>
    <div class="actions">
      <div v-if="item.order_status === 10">
        <span v-if="item.pay_status === 10">立刻付款</span>
        <span v-else-if="item.delivery_status === 10">申请取消</span>
        <span v-else-if="item.delivery_status === 20 || item.delivery_status === 30">确认收货</span>
      </div>
      <div v-if="item.order_status === 30">
        <span>评价</span>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    item: {
      type: Object,
      default: () => {
        return {}
      }
    }
  }
}
</script>
<template>
  <div class="order">
    <van-nav-bar title="我的订单" left-arrow @click-left="$router.go(-1)" />

    <van-tabs v-model="active" sticky>
      <van-tab name="all" title="全部"></van-tab>
      <van-tab name="payment" title="待支付"></van-tab>
      <van-tab name="delivery" title="待发货"></van-tab>
      <van-tab name="received" title="待收货"></van-tab>
      <van-tab name="comment" title="待评价"></van-tab>
    </van-tabs>

    <OrderListItem v-for="item in list" :key="item.order_id" :item="item"></OrderListItem>
  </div>
</template>

<script>
import OrderListItem from '@/components/OrderListItem.vue'
import { getMyOrderList } from '@/api/order'
export default {
  name: 'OrderPage',
  components: {
    OrderListItem
  },
  data () {
    return {
      active: this.$route.query.dataType || 'all',
      page: 1,
      list: []
    }
  },
  methods: {
    async getOrderList () {
      const { data: { list } } = await getMyOrderList(this.active, this.page)
      list.data.forEach((item) => {
        item.total_num = 0
        item.goods.forEach(goods => {
          item.total_num += goods.total_num
        })
      })
      this.list = list.data
    }
  },
  watch: {
    active: {
      immediate: true,
      handler () {
        this.getOrderList()
      }
    }
  }
}
</script>

个人中心

import request from '@/utils/request'

// 获取个人信息
export const getUserInfoDetail = () => {
  return request.get('/user/info')
}

import { getInfo, setInfo } from '@/utils/storage'

export default {
  namespaced: true,
  state () {
    return {
      // 个人权证相关
      userInfo: getInfo()
    }
  },
  mutations: {
    // 设置用户信息
    SET_USER_INFO (state, userInfo) {
      state.userInfo = userInfo
      setInfo(userInfo) // 存储用户信息到本地
    }
  },
  actions: {
    // 退出登录
    logout ({ commit }) {
      // 清空个人信息
      commit('SET_USER_INFO', {})
      // 情况购物车信息(跨模块调用mutations)
      // commit('模块名/方法名', 传值/null, { root: true(开启全局) })
      commit('cart/setCartList', [], { root: true })
    }
  },
  getters: {}
}
<template>
  <div class="user">
    <div class="head-page" v-if="isLogin">
      <div class="head-img">
        <img src="@/assets/default-avatar.png" alt="" />
      </div>
      <div class="info">
        <div class="mobile">{{ detail.mobile }}</div>
        <div class="vip">
          <van-icon name="diamond-o" />
          普通会员
        </div>
      </div>
    </div>

    <div v-else class="head-page" @click="$router.push('/login')">
      <div class="head-img">
        <img src="@/assets/default-avatar.png" alt="" />
      </div>
      <div class="info">
        <div class="mobile">未登录</div>
        <div class="words">点击登录账号</div>
      </div>
    </div>

    <div class="my-asset">
      <div class="asset-left">
        <div class="asset-left-item">
          <span>{{ detail.pay_money || 0 }}</span>
          <span>账户余额</span>
        </div>
        <div class="asset-left-item">
          <span>0</span>
          <span>积分</span>
        </div>
        <div class="asset-left-item">
          <span>0</span>
          <span>优惠券</span>
        </div>
      </div>
      <div class="asset-right">
        <div class="asset-right-item">
          <van-icon name="balance-pay" />
          <span>我的钱包</span>
        </div>
      </div>
    </div>
    <div class="order-navbar">
      <div class="order-navbar-item" @click="$router.push('/myorder?dataType=all')">
        <van-icon name="balance-list-o" />
        <span>全部订单</span>
      </div>
      <div class="order-navbar-item" @click="$router.push('/myorder?dataType=payment')">
        <van-icon name="clock-o" />
        <span>待支付</span>
      </div>
      <div class="order-navbar-item" @click="$router.push('/myorder?dataType=delivery')">
        <van-icon name="logistics" />
        <span>待发货</span>
      </div>
      <div class="order-navbar-item" @click="$router.push('/myorder?dataType=received')">
        <van-icon name="send-gift-o" />
        <span>待收货</span>
      </div>
    </div>

    <div class="service">
      <div class="title">我的服务</div>
      <div class="content">
        <div class="content-item">
          <van-icon name="records" />
          <span>收货地址</span>
        </div>
        <div class="content-item">
          <van-icon name="gift-o" />
          <span>领券中心</span>
        </div>
        <div class="content-item">
          <van-icon name="gift-card-o" />
          <span>优惠券</span>
        </div>
        <div class="content-item">
          <van-icon name="question-o" />
          <span>我的帮助</span>
        </div>
        <div class="content-item">
          <van-icon name="balance-o" />
          <span>我的积分</span>
        </div>
        <div class="content-item">
          <van-icon name="refund-o" />
          <span>退换/售后</span>
        </div>
      </div>
    </div>

    <div class="logout-btn">
     <button @click="logout">退出登录</button>
    </div>
  </div>
</template>

<script>
import { getUserInfoDetail } from '@/api/user.js'
export default {
  name: 'UserPage',
  data () {
    return {
      detail: {}
    }
  },
  created () {
    if (this.isLogin) {
      this.getUserInfoDetail()
    }
  },
  computed: {
    isLogin () {
      return this.$store.getters.token
    }
  },
  methods: {
    async getUserInfoDetail () {
      const { data: { userInfo } } = await getUserInfoDetail()
      this.detail = userInfo
    },
    logout () {
      this.$dialog.confirm({
        title: '温馨提示',
        message: '你确认要退出么'
      }).then(() => {
        // 退出是一个动作 => 包含了两步,分别是将 user 和 cart 进行重置
        this.$store.dispatch('user/logout')
      }).catch(() => {})
    }
  }
}
</script>

打包优化

vue脚手架只是开发过程中, 协助开发的工具, 开发完成后, 脚手架不参与上线

打包的作用

  1. 将多个文件压缩合并成一个文件
  2. 语法降级
  3. less sass ts语法解析
  4. 打包后, 生成浏览器可以直接运行的网页

打包命令

  1. yarn build
  2. 根目录下常见dist目录, 目录中存放打包后的文件
  3. 默认情况下, 需要放到服务器根目录, 如果需要双击运行, 需要配置相对路径

  1. 配置前的打包文件, 以绝对路径的形式查找资源

  1. 配置后的打包文件, 以相对路径的形式查找资源 (此处省略了 ./)

路由懒加载

当打包构建应用时, JS的会被打包在一起,文件就会很大, 影响页面加载, 可以把不同路由对应的组件分割成不同的代码块,然后当路由被访问时, 才加载对应的代码, 这样加载效率大大提高

步骤

  1. 异步组件改造

  1. 使用异步

  1. 打包结果对比

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1821366.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数据中台-知识图谱平台

【数据分析小兵】专注数据中台产品领域,覆盖开发套件,包含数据集成、数据建模、数据开发、数据服务、数据可视化、数据治理相关产品以及相关行业的技术方案的分享。对数据中台产品想要体验、做二次开发、关注方案资料、做技术交流的朋友们&#xff0c;可以关注我。 1. 概述 随着…

LabVIEW开发需求制定与管理

LabVIEW开发中的需求制定是确保项目成功的关键环节。本文从用户和开发者的角度详细分析了需求涉及的方面、需求的意义、好的需求和不好需求的区别及其对开发进度和质量的影响&#xff0c;帮助用户和开发者更好地进行需求管理&#xff0c;提升项目的成功率和软件质量。 一、需求…

【CT】LeetCode手撕—5. 最长回文子串

目录 题目1-思路2- 实现⭐5. 最长回文子串——题解思路 3- ACM实现 题目 原题连接&#xff1a;5. 最长回文子串 1-思路 子串的定义&#xff1a;子串是原始字符串的一个连续部分子序列的定义&#xff1a;子序列是原始字符串的一个子集记录最长回文子串的起始位置以及其长度&am…

H5小程序视频编辑解决方案,广泛适用,灵活部署

如何在微信小程序、网页、HTML5等WEB场景中实现轻量化视频制作&#xff0c;满足多样化的运营需求&#xff0c;一直是企业面临的挑战。美摄科技凭借其在视频编辑领域的深厚积累和创新技术&#xff0c;为企业量身打造了一套H5/小程序视频编辑解决方案&#xff0c;助力企业轻松应对…

批量文件重命名技巧:轻松替换删除文件夹名中的字母,实现高效文件管理新境界

在数字化时代&#xff0c;我们每天都会面对大量的文件和文件夹。无论是工作文档、学习资料还是个人收藏&#xff0c;文件命名的规范性都显得尤为重要。然而&#xff0c;手动一个一个去修改文件名&#xff0c;不仅耗时耗力&#xff0c;还容易出错。那么&#xff0c;有没有一种方…

C++并发之定时互斥(std::timed_mutex)

目录 1 概述2 使用实例3 接口使用3.1 construct3.2 lock3.3 try_lock3.4 try_lock_for3.5 try_lock_until3.6 unlock 1 概述 定时互斥是一种时间可锁定的对象&#xff0c;它设计用于在代码的关键部分需要独占访问时发出信号&#xff0c;就像常规互斥一样&#xff0c;但还支持定…

树莓派4B学习笔记8:开机自启动Python脚本_kill关闭后台脚本

今日继续学习树莓派4B 4G&#xff1a;&#xff08;Raspberry Pi&#xff0c;简称RPi或RasPi&#xff09; 本人所用树莓派4B 装载的系统与版本如下: 版本可用命令 (lsb_release -a) 查询: Opencv 版本是4.5.1&#xff1a; 紧接着上篇文章学习的串口通信,今日学习如何让树莓派开机…

zabbix自定义监控mysql状态和延迟

zabbix自定义监控mysql状态和延迟 文章目录 zabbix自定义监控mysql状态和延迟zabbix自定义监控mysql状态配置主从配置自定义监控添加监控项添加触发器模拟测试异常 zabbix自定义监控mysql延迟配置自定义监控添加监控项添加触发器测试 zabbix自定义监控mysql状态 配置主从 1.安…

H5漂流瓶交友源码|社交漂流瓶H5源码 附安装教程

H5漂流瓶交友源码|社交漂流瓶H5源码 附安装教程 搭建教程 环境&#xff1a;Nginx 1.20.1-MySQL 5.6.50-PHP-7.3 上传源码至网站根目录&#xff0c;创建并导入数据库 数据库信息修改&#xff1a;/config/database.php 网站运行目录/public 配置文件加入&#xff08;从24行…

PHP和Mysql前后端交互效果实现

一、连接数据库基本函数 mysqli_connect(); 作用&#xff1a;创建数据库连接&#xff0c;打开一个新的mysql的连接。传参顺序&#xff1a;数据库地址、数据库账号、数据库密码 <?phpecho mysqli_connect("localhost",root,root) ?> /*结果&#xff1a;F…

基于springboot实现中山社区医疗综合服务平台系统项目【项目源码+论文说明】

基于springboot实现中山社区医疗综合服务平台系统演示 摘要 传统信息的管理大部分依赖于管理人员的手工登记与管理&#xff0c;然而&#xff0c;随着近些年信息技术的迅猛发展&#xff0c;让许多比较老套的信息管理模式进行了更新迭代&#xff0c;居民信息因为其管理内容繁杂&…

数智技术教学解决方案

前言 随着大数据、云计算、人工智能等技术的迅猛发展&#xff0c;教育领域正迎来一场深刻的变革。这场变革不仅仅是教学方式的转变&#xff0c;更是教育理念、教学模式乃至教育生态系统的重塑。唯众作为教育技术领域的领军企业&#xff0c;深刻认识到数智技术在教学中的重要性&…

2024大交通场景空间策展洞察报告

来源&#xff1a;邻汇吧&万一商管 近期历史回顾&#xff1a; 2024国内工商业储能市场研究报告.pdf 2023幸福企业白皮书.pdf 2024年欧亚地区移动经济报告.pdf 内容供应链变革 2023人工智能与首席营销官&#xff08;CMO&#xff09; AI科技对PC产业的影响.pdf 金融业数据应用…

基于YOLO检测算法(单检测器网络+多视频输入)设计与实现

在单摄像头目标检测的基础上&#xff0c;实现单网络多线程的实时目标检测。 1&#xff0c;应用场景 在安防领域&#xff0c;YOLO的多摄像头实时目标检测应用具有以下特点和优势&#xff1a; 实时性能&#xff1a; YOLO算法以非常高的速度运行&#xff0c;能够实现实时目标检测…

ComfyUI 集成混元DIT(comfyui-hydit)

最近腾讯官方推出了ComfyUI插件comfyui-hydit 。是一个专门为腾讯的 Hunyuan-DiT 模型设计的自定义节点和工作流。本文主要介绍如何通过ComfyUI来运行腾讯新出的支持中文提示词的混元文生图大模型Hunyuan-DiT 环境准备 插件 从腾讯混元DIT 源码库获取插件源码&#xff1a; h…

深圳某老牌地产公司曝3小时裁所有员工

大家好&#xff01; 我是老洪&#xff0c;今日&#xff0c;我偶然间瞥见一则新闻&#xff0c;心头一震&#xff0c;惊讶之情难以言表。 据多家权威媒体纷纷报道&#xff0c;近日&#xff0c;深圳一家历史悠久的地产巨头&#xff0c;竟然在短短三小时内&#xff0c;果断地挥别了…

算法课程笔记——线段树维护哈希

算法课程笔记——线段树维护哈希 提前空出来

苹果AI时代:Apple Intelligence能否守护隐私与未来?

最近&#xff0c;苹果展示了其人工智能底牌&#xff0c;推出了Apple Intelligence(重新定义AI)&#xff0c;这是一套基础模型&#xff0c;将极大地改变苹果消费者使用其产品的方式。 虽然仍需在实际中证明自己&#xff0c;但它是一个强有力的演示&#xff0c;至少从普通用户的…

防火墙对于企业究竟起到哪些作用?

在当今数字化时代&#xff0c;企业网络安全已成为关乎企业生存与发展的战略要务。防火墙作为网络安全的基石&#xff0c;对于构建企业网络的安全防护体系至关重要。本文将深入剖析防火墙在企业网络安全中的多重价值&#xff0c;并结合具体案例&#xff0c;探讨如何科学运用防火…

【WEB前端2024】3D智体编程:乔布斯3D纪念馆-第40课-实时订阅后端数据

【WEB前端2024】3D智体编程&#xff1a;乔布斯3D纪念馆-第40课-实时订阅后端数据 使用dtns.network德塔世界&#xff08;开源的智体世界引擎&#xff09;&#xff0c;策划和设计《乔布斯超大型的开源3D纪念馆》的系列教程。dtns.network是一款主要由JavaScript编写的智体世界引…