【vue前端项目实战案例】之Vue仿饿了么App

本文将介绍一款仿“饿了么”商家页面的App。该案例是基于 Vue2.0 + Vue Router + webpack + ES6
等技术栈实现的一款外卖类App,适合初学者进行学习。

项目源码下载链接在文章末尾

1 项目概述

该项目是一款仿“饿了么”商家页面的外卖类App,主要有以下功能。

  • 商品导航。
  • 商品列表使用手势上下滑动。
  • 购物车中商品的添加和删除操作。
  • 点击商品查看详情。
  • 商家评价。
  • 商家信息。

1.1 开发环境

首先需要安装Node.js 12以上的版本,因为Node.js中已经继承了NPM,所以无需在单独安装NPM。然后再安装Vue脚手架(Vue-CLI)以及创建项目。
项目的调试使用Google Chrome浏览器的控制台进行,在浏览器中按下F12键,然后单击“切换设备工具栏”,进入移动端的调试界面,可以选择相应的设备进行调试,效果如图1 所示。
在这里插入图片描述
图 1 项目效果图

1.2 项目结构

项目结构如图2所示,其中src文件夹是项目的源文件目录,src文件夹下的项目结构如图3所示。
在这里插入图片描述
图2 项目结构

在这里插入图片描述
图3 src文件夹

项目结构中主要文件说明如下。

  • dist:项目打包后的静态文件存放目录。
  • node_modules:项目依赖管理目录。
  • public:项目的静态文件存放目录,也是本地服务器的根目录。
  • src:项目源文件存放目录。
  • package.json:项目npm配置文件。

src文件夹目录说明如下。

  • assets:静态资源文件存放目。
  • components:公共组件存放目录。
  • router:路由配置文件存放目录。
  • store:状态管理配置存放目录。
  • views:视图组件存放目录。
  • App.vue:项目的根组件。
  • main.js:项目的入口文件。

2 入口文件

项目的入口文件有 index.html、main.js和App.vue三个文件,这些入口文件的具体内容介绍如下。

2.1 项目入口页面

index.html是项目默认的主渲染页面文件,主要用于Vue实例挂载点的声明与DOM渲染。代码如下:

<!DOCTYPE html>
<html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1.0"><link rel="icon" href="<%= BASE_URL %>favicon.ico"><title><%= htmlWebpackPlugin.options.title %></title></head><body><div id="app"></div></body>
</html>

2.2 程序入口文件

main.js是程序的入口文件,主要用于加载各种公共组件和初始化Vue实例。本项目中的路由设置和引用的Vant UI组件库就是在该文件中定义的。代码如下:

import Vue from 'vue'
import App from './App.vue'
import './cube-ui'
import './register'import 'common/stylus/index.styl'Vue.config.productionTip = falsenew Vue({render: h => h(App)
}).$mount('#app')

本项目案例使用了 Cube UI 组件库,在项目src目录下创建 cube-ui.js 文件,用于引入项目中要用到的组件,代码如下:

import Vue from 'vue'
import {Style,TabBar,Popup,Dialog,Scroll,Slide,ScrollNav,ScrollNavBar
} from 'cube-ui'Vue.use(TabBar)
Vue.use(Popup)
Vue.use(Dialog)
Vue.use(Scroll)
Vue.use(Slide)
Vue.use(ScrollNav)
Vue.use(ScrollNavBar)

2.3 组件入口文件

App.vue是项目的根组件,所有的页面都是在App.vue下面切换的,所有的页面组件都是App.vue的子组件。在App.vue组件内只需要使用 组件作为占位符,就可以实现各个页面的引入。代码如下:

<template><div id="app" @touchmove.prevent><v-header :seller="seller"></v-header><div class="tab-wrapper"><tab :tabs="tabs"></tab></div></div>
</template><script>import qs from 'query-string'import { getSeller } from 'api'import VHeader from 'components/v-header/v-header'import Goods from 'components/goods/goods'import Ratings from 'components/ratings/ratings'import Seller from 'components/seller/seller'import Tab from 'components/tab/tab'export default {data() {return {seller: {id: qs.parse(location.search).id}}},computed: {tabs() {return [{label: '商品',component: Goods,data: {seller: this.seller}},{label: '评论',component: Ratings,data: {seller: this.seller}},{label: '商家',component: Seller,data: {seller: this.seller}}]}},created() {this._getSeller()},methods: {_getSeller() {getSeller({id: this.seller.id}).then((seller) => {this.seller = Object.assign({}, this.seller, seller)})}},components: {Tab,VHeader}}
</script><style lang="stylus" scoped>#app.tab-wrapperposition: fixedtop: 136pxleft: 0right: 0bottom: 0
</style>

3 项目组件

项目中所有页面组件都在views文件夹中定义,具体组件内容介绍如下。

3.1 头部组件

头部组件主要展示商家的基本信息,如图4所示。
在这里插入图片描述
图 4 头部组件效果

代码如下:

<template><div class="header" @click="showDetail"><div class="content-wrapper"><div class="avatar"><img width="64" height="64" :src="seller.avatar"></div><div class="content"><div class="title"><span class="brand"></span><span class="name">{{seller.name}}</span></div><div class="description">{{seller.description}}/{{seller.deliveryTime}}分钟送达</div><div v-if="seller.supports" class="support"><support-ico :size=1 :type="seller.supports[0].type"></support-ico><span class="text">{{seller.supports[0].description}}</span></div></div><div v-if="seller.supports" class="support-count"><span class="count">{{seller.supports.length}}个</span><i class="icon-keyboard_arrow_right"></i></div></div><div class="bulletin-wrapper"><span class="bulletin-title"></span><span class="bulletin-text">{{seller.bulletin}}</span><i class="icon-keyboard_arrow_right"></i></div><div class="background"><img :src="seller.avatar" width="100%" height="100%"></div></div>
</template><script type="text/ecmascript-6">import SupportIco from 'components/support-ico/support-ico'export default {name: 'v-header',props: {seller: {type: Object,default() {return {}}}},methods: {showDetail() {this.headerDetailComp = this.headerDetailComp || this.$createHeaderDetail({$props: {seller: 'seller'}})this.headerDetailComp.show()}},components: {SupportIco}}
</script><style lang="stylus" rel="stylesheet/stylus">@import "~common/stylus/mixin"@import "~common/stylus/variable".headerposition: relativeoverflow: hiddencolor: $color-whitebackground: $color-background-ss.content-wrapperposition: relativedisplay: flexalign-items: centerpadding: 24px 12px 18px 24px.avatarflex: 0 0 64pxwidth: 64pxmargin-right: 16pximgborder-radius: 2px.contentflex: 1.titledisplay: flexalign-items: centermargin-bottom: 8px.brandwidth: 30pxheight: 18pxbg-image('brand')background-size: 30px 18pxbackground-repeat: no-repeat.namemargin-left: 6pxfont-size: $fontsize-largefont-weight: bold.descriptionmargin-bottom: 8pxline-height: 12pxfont-size: $fontsize-small.supportdisplay: flexalign-items: center.support-icomargin-right: 4px.textline-height: 12pxfont-size: $fontsize-small-s.support-countposition: absoluteright: 12pxbottom: 14pxdisplay: flexalign-items: centerpadding: 0 8pxheight: 24pxline-height: 24pxtext-align: centerborder-radius: 14pxbackground: $color-background-sss.countfont-size: $fontsize-small-s.icon-keyboard_arrow_rightmargin-left: 2pxline-height: 24pxfont-size: $fontsize-small-s.bulletin-wrapperposition: relativedisplay: flexalign-items: centerheight: 28pxline-height: 28pxpadding: 0 8pxbackground: $color-background-sss.bulletin-titleflex: 0 0 22pxwidth: 22pxheight: 12pxmargin-right: 4pxbg-image('bulletin')background-size: 22px 12pxbackground-repeat: no-repeat.bulletin-textflex: 1white-space: nowrapoverflow: hiddentext-overflow: ellipsisfont-size: $fontsize-small-s.icon-keyboard_arrow_rightflex: 0 0 10pxwidth: 10pxfont-size: $fontsize-small-s.backgroundposition: absolutetop: 0left: 0width: 100%height: 100%z-index: -1filter: blur(10px)
</style>

3.2 商品标签栏与侧边导航组件

在商家信息下方,通过商品标签栏实现商品、评价和商家信息的切换,在商品标签中,通过侧边导航实现对商品列表的滚动和分类展示等功能。效果如图5所示。

在这里插入图片描述
图 5 商品标签栏效果

代码如下:

<template><div class="tab"><cube-tab-bar:useTransition=false:showSlider=truev-model="selectedLabel":data="tabs"ref="tabBar"class="border-bottom-1px"></cube-tab-bar><div class="slide-wrapper"><cube-slide:loop=false:auto-play=false:show-dots=false:initial-index="index"ref="slide":options="slideOptions"@scroll="onScroll"@change="onChange"><cube-slide-item v-for="(tab,index) in tabs" :key="index"><component ref="component" :is="tab.component" :data="tab.data"></component></cube-slide-item></cube-slide></div></div>
</template><script>export default {name: 'tab',props: {tabs: {type: Array,default() {return []}},initialIndex: {type: Number,default: 0}},data() {return {index: this.initialIndex,slideOptions: {listenScroll: true,probeType: 3,directionLockThreshold: 0}}},computed: {selectedLabel: {get() {return this.tabs[this.index].label},set(newVal) {this.index = this.tabs.findIndex((value) => {return value.label === newVal})}}},mounted() {this.onChange(this.index)},methods: {onScroll(pos) {const tabBarWidth = this.$refs.tabBar.$el.clientWidthconst slideWidth = this.$refs.slide.slide.scrollerWidthconst transform = -pos.x / slideWidth * tabBarWidththis.$refs.tabBar.setSliderTransform(transform)},onChange(current) {this.index = currentconst instance = this.$refs.component[current]if (instance && instance.fetch) {instance.fetch()}}}}
</script><style lang="stylus" scoped>@import "~common/stylus/variable".tabdisplay: flexflex-direction: columnheight: 100%>>> .cube-tabpadding: 10px 0.slide-wrapperflex: 1overflow: hidden
</style>

3.3 购物车组件

在购物车组件中,当没有任何商品的情况下,无法直接选择,效果如图6所示。当选择商品后,购物车将被激活,效果如图7所示。
在这里插入图片描述
图 6 购物车默认状态

在这里插入图片描述
图 7 选择商品后的状态

当点击购物车图标后,将显示用户选中的商品,效果如图8所示,在购物车商品列表页面中可以对商品进行加减操作,也可以直接清空购物车。
在这里插入图片描述
图8 购物车商品列表

当点击“去结算”按钮时,将弹出购买商品花费的金额提示对话框,效果如图9所示。
在这里插入图片描述
图9 提示对话框

具体实现的代码如下。
商品购物车组件 shop-cart.vue 文件代码如下:

<template><div><div class="shopcart"><div class="content" @click="toggleList"><div class="content-left"><div class="logo-wrapper"><div class="logo" :class="{'highlight':totalCount>0}"><i class="icon-shopping_cart" :class="{'highlight':totalCount>0}"></i></div><div class="num" v-show="totalCount>0"><bubble :num="totalCount"></bubble></div></div><div class="price" :class="{'highlight':totalPrice>0}">¥{{totalPrice}}</div><div class="desc">另需配送费¥{{deliveryPrice}}元</div></div><div class="content-right" @click="pay"><div class="pay" :class="payClass">{{payDesc}}</div></div></div><div class="ball-container"><div v-for="(ball,index) in balls" :key="index"><transition@before-enter="beforeDrop"@enter="dropping"@after-enter="afterDrop"><div class="ball" v-show="ball.show"><div class="inner inner-hook"></div></div></transition></div></div></div></div>
</template><script>import Bubble from 'components/bubble/bubble'const BALL_LEN = 10const innerClsHook = 'inner-hook'function createBalls() {let balls = []for (let i = 0; i < BALL_LEN; i++) {balls.push({show: false})}return balls}export default {name: 'shop-cart',props: {selectFoods: {type: Array,default() {return []}},deliveryPrice: {type: Number,default: 0},minPrice: {type: Number,default: 0},sticky: {type: Boolean,default: false},fold: {type: Boolean,default: true}},data() {return {balls: createBalls(),listFold: this.fold}},created() {this.dropBalls = []},computed: {totalPrice() {let total = 0this.selectFoods.forEach((food) => {total += food.price * food.count})return total},totalCount() {let count = 0this.selectFoods.forEach((food) => {count += food.count})return count},payDesc() {if (this.totalPrice === 0) {return `${this.minPrice}元起送`} else if (this.totalPrice < this.minPrice) {let diff = this.minPrice - this.totalPricereturn `还差¥${diff}元起送`} else {return '去结算'}},payClass() {if (!this.totalCount || this.totalPrice < this.minPrice) {return 'not-enough'} else {return 'enough'}}},methods: {toggleList() {if (this.listFold) {if (!this.totalCount) {return}this.listFold = falsethis._showShopCartList()this._showShopCartSticky()} else {this.listFold = truethis._hideShopCartList()}},pay(e) {if (this.totalPrice < this.minPrice) {return}this.$createDialog({title: '支付',content: `您需要支付${this.totalPrice}`}).show()e.stopPropagation()},drop(el) {for (let i = 0; i < this.balls.length; i++) {const ball = this.balls[i]if (!ball.show) {ball.show = trueball.el = elthis.dropBalls.push(ball)return}}},beforeDrop(el) {const ball = this.dropBalls[this.dropBalls.length - 1]const rect = ball.el.getBoundingClientRect()const x = rect.left - 32const y = -(window.innerHeight - rect.top - 22)el.style.display = ''el.style.transform = el.style.webkitTransform = `translate3d(0,${y}px,0)`const inner = el.getElementsByClassName(innerClsHook)[0]inner.style.transform = inner.style.webkitTransform = `translate3d(${x}px,0,0)`},dropping(el, done) {this._reflow = document.body.offsetHeightel.style.transform = el.style.webkitTransform = `translate3d(0,0,0)`const inner = el.getElementsByClassName(innerClsHook)[0]inner.style.transform = inner.style.webkitTransform = `translate3d(0,0,0)`el.addEventListener('transitionend', done)},afterDrop(el) {const ball = this.dropBalls.shift()if (ball) {ball.show = falseel.style.display = 'none'}},_showShopCartList() {this.shopCartListComp = this.shopCartListComp || this.$createShopCartList({$props: {selectFoods: 'selectFoods'},$events: {leave: () => {this._hideShopCartSticky()},hide: () => {this.listFold = true},add: (el) => {this.shopCartStickyComp.drop(el)}}})this.shopCartListComp.show()},_showShopCartSticky() {this.shopCartStickyComp = this.shopCartStickyComp || this.$createShopCartSticky({$props: {selectFoods: 'selectFoods',deliveryPrice: 'deliveryPrice',minPrice: 'minPrice',fold: 'listFold',list: this.shopCartListComp}})this.shopCartStickyComp.show()},_hideShopCartList() {const list = this.sticky ? this.$parent.list : this.shopCartListComplist.hide && list.hide()},_hideShopCartSticky() {this.shopCartStickyComp.hide()}},watch: {fold(newVal) {this.listFold = newVal},totalCount(count) {if (!this.fold && count === 0) {this._hideShopCartList()}}},components: {Bubble}}
</script><style lang="stylus" scoped>@import "~common/stylus/mixin"@import "~common/stylus/variable".shopcartheight: 100%.contentdisplay: flexbackground: $color-backgroundfont-size: 0color: $color-light-grey.content-leftflex: 1.logo-wrapperdisplay: inline-blockvertical-align: topposition: relativetop: -10pxmargin: 0 12pxpadding: 6pxwidth: 56pxheight: 56pxbox-sizing: border-boxborder-radius: 50%background: $color-background.logowidth: 100%height: 100%border-radius: 50%text-align: centerbackground: $color-dark-grey&.highlightbackground: $color-blue.icon-shopping_cartline-height: 44pxfont-size: $fontsize-large-xxxcolor: $color-light-grey&.highlightcolor: $color-white.numposition: absolutetop: 0right: 0.pricedisplay: inline-blockvertical-align: topmargin-top: 12pxline-height: 24pxpadding-right: 12pxbox-sizing: border-boxborder-right: 1px solid rgba(255, 255, 255, 0.1)font-weight: 700font-size: $fontsize-large&.highlightcolor: $color-white.descdisplay: inline-blockvertical-align: topmargin: 12px 0 0 12pxline-height: 24pxfont-size: $fontsize-small-s.content-rightflex: 0 0 105pxwidth: 105px.payheight: 48pxline-height: 48pxtext-align: centerfont-weight: 700font-size: $fontsize-small&.not-enoughbackground: $color-dark-grey&.enoughbackground: $color-greencolor: $color-white.ball-container.ballposition: fixedleft: 32pxbottom: 22pxz-index: 200transition: all 0.4s cubic-bezier(0.49, -0.29, 0.75, 0.41).innerwidth: 16pxheight: 16pxborder-radius: 50%background: $color-bluetransition: all 0.4s linear
</style>

商品购物车列表组件 shop-cart-list.vue 文件代码如下:

<template><transition name="fade"><cube-popup:mask-closable=truev-show="visible"@mask-click="maskClick"position="bottom"type="shop-cart-list":z-index=90><transitionname="move"@after-leave="afterLeave"><div v-show="visible"><div class="list-header"><h1 class="title">购物车</h1><span class="empty" @click="empty">清空</span></div><cube-scroll class="list-content" ref="listContent"><ul><liclass="food"v-for="(food,index) in selectFoods":key="index"><span class="name">{{food.name}}</span><div class="price"><span>¥{{food.price*food.count}}</span></div><div class="cart-control-wrapper"><cart-control @add="onAdd" :food="food"></cart-control></div></li></ul></cube-scroll></div></transition></cube-popup></transition>
</template><script>import CartControl from 'components/cart-control/cart-control'import popupMixin from 'common/mixins/popup'const EVENT_SHOW = 'show'const EVENT_ADD = 'add'const EVENT_LEAVE = 'leave'export default {name: 'shop-cart-list',mixins: [popupMixin],props: {selectFoods: {type: Array,default() {return []}}},created() {this.$on(EVENT_SHOW, () => {this.$nextTick(() => {this.$refs.listContent.refresh()})})},methods: {onAdd(target) {this.$emit(EVENT_ADD, target)},afterLeave() {this.$emit(EVENT_LEAVE)},maskClick() {this.hide()},empty() {this.dialogComp = this.$createDialog({type: 'confirm',content: '清空购物车?',$events: {confirm: () => {this.selectFoods.forEach((food) => {food.count = 0})this.hide()}}})this.dialogComp.show()}},components: {CartControl}}
</script><style lang="stylus" scoped>@import "~common/stylus/variable".cube-shop-cart-listbottom: 48px&.fade-enter, &.fade-leave-activeopacity: 0&.fade-enter-active, &.fade-leave-activetransition: all .3s ease-in-out.move-enter, .move-leave-activetransform: translate3d(0, 100%, 0).move-enter-active, .move-leave-activetransition: all .3s ease-in-out.list-headerheight: 40pxline-height: 40pxpadding: 0 18pxbackground: $color-background-ssss.titlefloat: leftfont-size: $fontsize-mediumcolor: $color-dark-grey.emptyfloat: rightfont-size: $fontsize-smallcolor: $color-blue.list-contentpadding: 0 18pxmax-height: 217pxoverflow: hiddenbackground: $color-white.foodposition: relativepadding: 12px 0box-sizing: border-box.nameline-height: 24pxfont-size: $fontsize-mediumcolor: $color-dark-grey.priceposition: absoluteright: 90pxbottom: 12pxline-height: 24pxfont-weight: 700font-size: $fontsize-mediumcolor: $color-red.cart-control-wrapperposition: absoluteright: 0bottom: 6px</style>

3.4 商品列表组件

在商品标签页面中,商品列表主要展示所有商品的信息,可以点击商品卡片右侧的加号添加购物车。效果如图10所示。
在这里插入图片描述
图 10 商品列表效果

代码如下:

<template><div class="goods"><div class="scroll-nav-wrapper"><cube-scroll-nav:side=true:data="goods":options="scrollOptions"v-if="goods.length"><template slot="bar" slot-scope="props"><cube-scroll-nav-bardirection="vertical":labels="props.labels":txts="barTxts":current="props.current"><template slot-scope="props"><div class="text"><support-icov-if="props.txt.type>=1":size=3:type="props.txt.type"></support-ico><span>{{props.txt.name}}</span><span class="num" v-if="props.txt.count"><bubble :num="props.txt.count"></bubble></span></div></template></cube-scroll-nav-bar></template><cube-scroll-nav-panelv-for="good in goods":key="good.name":label="good.name":title="good.name"><ul><li@click="selectFood(food)"v-for="food in good.foods":key="food.name"class="food-item"><div class="icon"><img width="57" height="57" :src="food.icon"></div><div class="content"><h2 class="name">{{food.name}}</h2><p class="desc">{{food.description}}</p><div class="extra"><span class="count">月售{{food.sellCount}}份</span><span>好评率{{food.rating}}%</span></div><div class="price"><span class="now">¥{{food.price}}</span><span class="old" v-show="food.oldPrice">¥{{food.oldPrice}}</span></div><div class="cart-control-wrapper"><cart-control @add="onAdd" :food="food"></cart-control></div></div></li></ul></cube-scroll-nav-panel></cube-scroll-nav></div><div class="shop-cart-wrapper"><shop-cartref="shopCart":select-foods="selectFoods":delivery-price="seller.deliveryPrice":min-price="seller.minPrice"></shop-cart></div></div>
</template><script>import { getGoods } from 'api'import CartControl from 'components/cart-control/cart-control'import ShopCart from 'components/shop-cart/shop-cart'import Food from 'components/food/food'import SupportIco from 'components/support-ico/support-ico'import Bubble from 'components/bubble/bubble'export default {name: 'goods',props: {data: {type: Object,default() {return {}}}},data() {return {goods: [],selectedFood: {},scrollOptions: {click: false,directionLockThreshold: 0}}},computed: {seller() {return this.data.seller},selectFoods() {let foods = []this.goods.forEach((good) => {good.foods.forEach((food) => {if (food.count) {foods.push(food)}})})return foods},barTxts() {let ret = []this.goods.forEach((good) => {const {type, name, foods} = goodlet count = 0foods.forEach((food) => {count += food.count || 0})ret.push({type,name,count})})return ret}},methods: {fetch() {if (!this.fetched) {this.fetched = truegetGoods({id: this.seller.id}).then((goods) => {this.goods = goods})}},selectFood(food) {this.selectedFood = foodthis._showFood()this._showShopCartSticky()},onAdd(target) {this.$refs.shopCart.drop(target)},_showFood() {this.foodComp = this.foodComp || this.$createFood({$props: {food: 'selectedFood'},$events: {add: (target) => {this.shopCartStickyComp.drop(target)},leave: () => {this._hideShopCartSticky()}}})this.foodComp.show()},_showShopCartSticky() {this.shopCartStickyComp = this.shopCartStickyComp || this.$createShopCartSticky({$props: {selectFoods: 'selectFoods',deliveryPrice: this.seller.deliveryPrice,minPrice: this.seller.minPrice,fold: true}})this.shopCartStickyComp.show()},_hideShopCartSticky() {this.shopCartStickyComp.hide()}},components: {Bubble,SupportIco,CartControl,ShopCart,Food}}
</script><style lang="stylus" scoped>@import "~common/stylus/mixin"@import "~common/stylus/variable".goodsposition: relativetext-align: leftheight: 100%.scroll-nav-wrapperposition: absolutewidth: 100%top: 0left: 0bottom: 48px>>> .cube-scroll-nav-barwidth: 80pxwhite-space: normaloverflow: hidden>>> .cube-scroll-nav-bar-itempadding: 0 10pxdisplay: flexalign-items: centerheight: 56pxline-height: 14pxfont-size: $fontsize-smallbackground: $color-background-ssss.textflex: 1position: relative.numposition: absoluteright: -8pxtop: -10px.support-icodisplay: inline-blockvertical-align: topmargin-right: 4px>>> .cube-scroll-nav-bar-item_activebackground: $color-whitecolor: $color-dark-grey>>> .cube-scroll-nav-panel-titlepadding-left: 14pxheight: 26pxline-height: 26pxborder-left: 2px solid $color-col-linefont-size: $fontsize-smallcolor: $color-greybackground: $color-background-ssss.food-itemdisplay: flexmargin: 18pxpadding-bottom: 18pxposition: relative&:last-childborder-none()margin-bottom: 0.iconflex: 0 0 57pxmargin-right: 10pximgheight: auto.contentflex: 1.namemargin: 2px 0 8px 0height: 14pxline-height: 14pxfont-size: $fontsize-mediumcolor: $color-dark-grey.desc, .extraline-height: 10pxfont-size: $fontsize-small-scolor: $color-light-grey.descline-height: 12pxmargin-bottom: 8px.extra.countmargin-right: 12px.pricefont-weight: 700line-height: 24px.nowmargin-right: 8pxfont-size: $fontsize-mediumcolor: $color-red.oldtext-decoration: line-throughfont-size: $fontsize-small-scolor: $color-light-grey.cart-control-wrapperposition: absoluteright: 0bottom: 12px.shop-cart-wrapperposition: absoluteleft: 0bottom: 0z-index: 50width: 100%height: 48px
</style>

3.5 商家公告组件

点击头部区域,会弹出商家公告的详细内容,效果如图11所示。
在这里插入图片描述
图11 商家公告内容

代码如下:

<template><transition name="fade"><div v-show="visible" class="header-detail" @touchmove.stop.prevent><div class="detail-wrapper clear-fix"><div class="detail-main"><h1 class="name">{{seller.name}}</h1><div class="star-wrapper"><star :size="48" :score="seller.score"></star></div><div class="title"><div class="line"></div><div class="text">优惠信息</div><div class="line"></div></div><ul v-if="seller.supports" class="supports"><li class="support-item" v-for="(item,index) in seller.supports" :key="item.id"><support-ico :size=2 :type="seller.supports[index].type"></support-ico><span class="text">{{seller.supports[index].description}}</span></li></ul><div class="title"><div class="line"></div><div class="text">商家公告</div><div class="line"></div></div><div class="bulletin"><p class="content">{{seller.bulletin}}</p></div></div></div><div class="detail-close" @click="hide"><i class="icon-close"></i></div></div></transition>
</template><script>import popupMixin from 'common/mixins/popup'import Star from 'components/star/star'import SupportIco from 'components/support-ico/support-ico'export default {name: 'header-detail',mixins: [popupMixin],props: {seller: {type: Object,default() {return {}}}},components: {SupportIco,Star}}
</script><style lang="stylus" scoped>@import "~common/stylus/mixin"@import "~common/stylus/variable".header-detailposition: fixedz-index: 100top: 0left: 0width: 100%height: 100%overflow: autobackdrop-filter: blur(10px)opacity: 1color: $color-whitebackground: $color-background-s&.fade-enter-active, &.fade-leave-activetransition: all 0.5s&.fade-enter, &.fade-leave-activeopacity: 0background: $color-background.detail-wrapperdisplay: inline-blockwidth: 100%min-height: 100%.detail-mainmargin-top: 64pxpadding-bottom: 64px.nameline-height: 16pxtext-align: centerfont-size: $fontsize-largefont-weight: 700.star-wrappermargin-top: 18pxpadding: 2px 0text-align: center.titledisplay: flexwidth: 80%margin: 28px auto 24px auto.lineflex: 1position: relativetop: -6pxborder-bottom: 1px solid rgba(255, 255, 255, 0.2).textpadding: 0 12pxfont-weight: 700font-size: $fontsize-medium.supportswidth: 80%margin: 0 auto.support-itemdisplay: flexalign-items: centerpadding: 0 12pxmargin-bottom: 12px&:last-childmargin-bottom: 0.support-icomargin-right: 6px.textline-height: 16pxfont-size: $fontsize-small.bulletinwidth: 80%margin: 0 auto.contentpadding: 0 12pxline-height: 24pxfont-size: $fontsize-small.detail-closeposition: relativewidth: 30pxheight: 30pxmargin: -64px auto 0 autoclear: bothfont-size: $fontsize-large-xxxx
</style>

3.6 评价内容组件

在商家评价内容的组件中,共有两个组成部分,一个是商家的评分组件,效果如图12所示;另一个是评价列表内容,效果如图13所示。
在这里插入图片描述
图 12 评分组件效果

在这里插入图片描述
图 13 评价列表效果

商家评分组件 ratings.vue 文件代码如下:

<template><cube-scroll ref="scroll" class="ratings" :options="scrollOptions"><div class="ratings-content"><div class="overview"><div class="overview-left"><h1 class="score">{{seller.score}}</h1><div class="title">综合评分</div><div class="rank">高于周边商家{{seller.rankRate}}%</div></div><div class="overview-right"><div class="score-wrapper"><span class="title">服务态度</span><star :size="36" :score="seller.serviceScore"></star><span class="score">{{seller.serviceScore}}</span></div><div class="score-wrapper"><span class="title">商品评分</span><star :size="36" :score="seller.foodScore"></star><span class="score">{{seller.foodScore}}</span></div><div class="delivery-wrapper"><span class="title">送达时间</span><span class="delivery">{{seller.deliveryTime}}分钟</span></div></div></div><split></split><rating-select@select="onSelect"@toggle="onToggle":selectType="selectType":onlyContent="onlyContent":ratings="ratings"></rating-select><div class="rating-wrapper"><ul><liv-for="(rating,index) in computedRatings":key="index"class="rating-item border-bottom-1px"><div class="avatar"><img width="28" height="28" :src="rating.avatar"></div><div class="content"><h1 class="name">{{rating.username}}</h1><div class="star-wrapper"><star :size="24" :score="rating.score"></star><span class="delivery" v-show="rating.deliveryTime">{{rating.deliveryTime}}</span></div><p class="text">{{rating.text}}</p><div class="recommend" v-show="rating.recommend && rating.recommend.length"><span class="icon-thumb_up"></span><spanclass="item"v-for="(item,index) in rating.recommend":key="index">{{item}}</span></div><div class="time">{{format(rating.rateTime)}}</div></div></li></ul></div></div></cube-scroll>
</template><script>import Star from 'components/star/star'import RatingSelect from 'components/rating-select/rating-select'import Split from 'components/split/split'import ratingMixin from 'common/mixins/rating'import { getRatings } from 'api'import moment from 'moment'export default {name: 'ratings',mixins: [ratingMixin],props: {data: {type: Object}},data () {return {ratings: [],scrollOptions: {click: false,directionLockThreshold: 0}}},computed: {seller () {return this.data.seller || {}}},methods: {fetch () {if (!this.fetched) {this.fetched = truegetRatings({id: this.seller.id}).then((ratings) => {this.ratings = ratings})}},format (time) {return moment(time).format('YYYY-MM-DD hh:mm')}},components: {Star,Split,RatingSelect},watch: {selectType () {this.$nextTick(() => {this.$refs.scroll.refresh()})}}}
</script><style lang="stylus" scoped>@import "~common/stylus/variable"@import "~common/stylus/mixin".ratingsposition: relativetext-align: leftwhite-space: normalheight: 100%.overviewdisplay: flexpadding: 18px 0.overview-leftflex: 0 0 137pxpadding: 6px 0width: 137pxborder-right: 1px solid $color-col-linetext-align: center@media only screen and (max-width: 320px)flex: 0 0 120pxwidth: 120px.scoremargin-bottom: 6pxline-height: 28pxfont-size: $fontsize-large-xxxcolor: $color-orange.titlemargin-bottom: 8pxline-height: 12pxfont-size: $fontsize-smallcolor: $color-dark-grey.rankline-height: 10pxfont-size: $fontsize-small-scolor: $color-light-grey.overview-rightflex: 1padding: 6px 0 6px 24px@media only screen and (max-width: 320px)padding-left: 6px.score-wrapperdisplay: flexalign-items: centermargin-bottom: 8px.titleline-height: 18pxfont-size: $fontsize-smallcolor: $color-dark-grey.starmargin: 0 12px.scoreline-height: 18pxfont-size: $fontsize-smallcolor: $color-orange.delivery-wrapperdisplay: flexalign-items: center.titleline-height: 18pxfont-size: $fontsize-smallcolor: $color-dark-grey.deliverymargin-left: 12pxfont-size: $fontsize-smallcolor: $color-light-grey.rating-wrapperpadding: 0 18px.rating-itemdisplay: flexpadding: 18px 0&:last-childborder-none().avatarflex: 0 0 28pxwidth: 28pxmargin-right: 12pximgheight: autoborder-radius: 50%.contentposition: relativeflex: 1.namemargin-bottom: 4pxline-height: 12pxfont-size: $fontsize-small-scolor: $color-dark-grey.star-wrappermargin-bottom: 6pxdisplay: flexalign-items: center.starmargin-right: 6px.deliveryfont-size: $fontsize-small-scolor: $color-light-grey.textmargin-bottom: 8pxline-height: 18pxcolor: $color-dark-greyfont-size: $fontsize-small.recommenddisplay: flexalign-items: centerflex-wrap: wrapline-height: 16px.icon-thumb_up, .itemmargin: 0 8px 4px 0font-size: $fontsize-small-s.icon-thumb_upcolor: $color-blue.itempadding: 0 6pxborder: 1px solid $color-row-lineborder-radius: 1pxcolor: $color-light-greybackground: $color-white.timeposition: absolutetop: 0right: 0line-height: 12pxfont-size: $fontsize-smallcolor: $color-light-grey
</style>

评价内容列表组件 rating-select.vue 文件代码如下:

<template><div class="rating-select"><div class="rating-type border-bottom-1px"><span @click="select(2)" class="block positive" :class="{'active':selectType===2}">{{desc.all}}<spanclass="count">{{ratings.length}}</span></span><span @click="select(0)" class="block positive" :class="{'active':selectType===0}">{{desc.positive}}<spanclass="count">{{positives.length}}</span></span><span @click="select(1)" class="block negative" :class="{'active':selectType===1}">{{desc.negative}}<spanclass="count">{{negatives.length}}</span></span></div><div @click="toggleContent" class="switch" :class="{'on':onlyContent}"><span class="icon-check_circle"></span><span class="text">只看有内容的评价</span></div></div>
</template>
<script>const POSITIVE = 0const NEGATIVE = 1const ALL = 2const EVENT_TOGGLE = 'toggle'const EVENT_SELECT = 'select'export default {props: {ratings: {type: Array,default() {return []}},selectType: {type: Number,default: ALL},onlyContent: {type: Boolean,default: false},desc: {type: Object,default() {return {all: '全部',positive: '满意',negative: '不满意'}}}},computed: {positives() {return this.ratings.filter((rating) => {return rating.rateType === POSITIVE})},negatives() {return this.ratings.filter((rating) => {return rating.rateType === NEGATIVE})}},methods: {select(type) {this.$emit(EVENT_SELECT, type)},toggleContent() {this.$emit(EVENT_TOGGLE)}}}
</script>
<style lang="stylus" rel="stylesheet/stylus">@import "~common/stylus/variable".rating-select.rating-typepadding: 18px 0margin: 0 18px.blockdisplay: inline-blockpadding: 8px 12pxmargin-right: 8pxline-height: 16pxborder-radius: 1pxfont-size: $fontsize-smallcolor: $color-grey&.activecolor: $color-white.countmargin-left: 2px&.positivebackground: $color-light-blue&.activebackground: $color-blue&.negativebackground: $color-light-grey-s&.activebackground: $color-grey.switchdisplay: flexalign-items: centerpadding: 12px 18pxline-height: 24pxborder-bottom: 1px solid $color-row-linecolor: $color-light-grey&.on.icon-check_circlecolor: $color-green.icon-check_circlemargin-right: 4pxfont-size: $fontsize-large-xxx.textfont-size: $fontsize-small
</style>

3.7 商家信息组件

商家信息组件中设计了商家的星级和服务内容,效果如图14所示。

在这里插入图片描述
图 14 商家服务信息效果

以及商家的优惠活动和公告内容。效果如图15所示。

在这里插入图片描述
图15 商家活动公告内容

代码如下:

<template><cube-scroll class="seller" :options="sellerScrollOptions"><div class="seller-content"><div class="overview"><h1 class="title">{{seller.name}}</h1><div class="desc border-bottom-1px"><star :size="36" :score="seller.score"></star><span class="text">({{seller.ratingCount}})</span><span class="text">月售{{seller.sellCount}}单</span></div><ul class="remark"><li class="block"><h2>起送价</h2><div class="content"><span class="stress">{{seller.minPrice}}</span></div></li><li class="block"><h2>商家配送</h2><div class="content"><span class="stress">{{seller.deliveryPrice}}</span></div></li><li class="block"><h2>平均配送时间</h2><div class="content"><span class="stress">{{seller.deliveryTime}}</span>分钟</div></li></ul><div class="favorite" @click="toggleFavorite"><span class="icon-favorite" :class="{'active':favorite}"></span><span class="text">{{favoriteText}}</span></div></div><split></split><div class="bulletin"><h1 class="title">公告与活动</h1><div class="content-wrapper border-bottom-1px"><p class="content">{{seller.bulletin}}</p></div><ul v-if="seller.supports" class="supports"><liclass="support-item border-bottom-1px"v-for="(item,index) in seller.supports":key="index"><support-ico :size=4 :type="seller.supports[index].type"></support-ico><span class="text">{{seller.supports[index].description}}</span></li></ul></div><split></split><div class="pics"><h1 class="title">商家实景</h1><cube-scroll class="pic-wrapper" :options="picScrollOptions"><ul class="pic-list"><li class="pic-item"v-for="(pic,index) in seller.pics":key="index"><img :src="pic" width="120" height="90"></li></ul></cube-scroll></div><split></split><div class="info"><h1 class="title border-bottom-1px">商家信息</h1><ul><liclass="info-item border-bottom-1px"v-for="(info,index) in seller.infos":key="index">{{info}}</li></ul></div></div></cube-scroll>
</template><script>import { saveToLocal, loadFromLocal } from 'common/js/storage'import Star from 'components/star/star'import Split from 'components/split/split'import SupportIco from 'components/support-ico/support-ico'export default {props: {data: {type: Object,default() {return {}}}},data() {return {favorite: false,sellerScrollOptions: {directionLockThreshold: 0,click: false},picScrollOptions: {scrollX: true,stopPropagation: true,directionLockThreshold: 0}}},computed: {seller() {return this.data.seller || {}},favoriteText() {return this.favorite ? '已收藏' : '收藏'}},created() {this.favorite = loadFromLocal(this.seller.id, 'favorite', false)},methods: {toggleFavorite() {this.favorite = !this.favoritesaveToLocal(this.seller.id, 'favorite', this.favorite)}},components: {SupportIco,Star,Split}}
</script><style lang="stylus" scoped>@import "~common/stylus/variable"@import "~common/stylus/mixin".sellerheight: 100%text-align: left.overviewposition: relativepadding: 18px.titlemargin-bottom: 8pxline-height: 14pxfont-size: $fontsize-mediumcolor: $color-dark-grey.descdisplay: flexalign-items: centerpadding-bottom: 18px.starmargin-right: 8px.textmargin-right: 12pxline-height: 18pxfont-size: $fontsize-small-scolor: $color-grey.remarkdisplay: flexpadding-top: 18px.blockflex: 1text-align: centerborder-right: 1px solid $color-col-line&:last-childborder: noneh2margin-bottom: 4pxline-height: 10pxfont-size: $fontsize-small-scolor: $color-light-grey.contentline-height: 24pxfont-size: $fontsize-small-scolor: $color-dark-grey.stressfont-size: $fontsize-large-xxx.favoriteposition: absolutewidth: 50pxright: 11pxtop: 18pxtext-align: center.icon-favoritedisplay: blockmargin-bottom: 4pxline-height: 24pxfont-size: $fontsize-large-xxxcolor: $color-light-grey-s&.activecolor: $color-red.textline-height: 10pxfont-size: $fontsize-small-scolor: $color-grey.bulletinpadding: 18px 18px 0 18pxwhite-space: normal.titlemargin-bottom: 8pxline-height: 14pxcolor: $color-dark-greyfont-size: $fontsize-medium.content-wrapperpadding: 0 12px 16px 12px.contentline-height: 24pxfont-size: $fontsize-smallcolor: $color-red.supports.support-itemdisplay: flexalign-items: centerpadding: 16px 12px&:last-childborder-none().support-icomargin-right: 6px.textline-height: 16pxfont-size: $fontsize-smallcolor: $color-dark-grey.picspadding: 18px.titlemargin-bottom: 12pxline-height: 14pxcolor: $color-dark-greyfont-size: $fontsize-medium.pic-wrapperdisplay: flexalign-items: center.pic-list.pic-itemdisplay: inline-blockmargin-right: 6pxwidth: 120pxheight: 90px&:last-childmargin: 0.infopadding: 18px 18px 0 18pxcolor: $color-dark-grey.titlepadding-bottom: 12pxline-height: 14pxfont-size: $fontsize-medium.info-itempadding: 16px 12pxline-height: 16pxfont-size: $fontsize-small&:last-childborder-none()
</style>

项目源码下载:
https://download.csdn.net/download/p445098355/89570496

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

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

相关文章

【深度学习】“复杂场景下基于深度学习的卷积神经网络在鸟类多类别识别中的模型设计与性能优化研究“(上)

【深度学习】“复杂场景下基于深度学习的卷积神经网络在鸟类多类别识别中的模型设计与性能优化研究”(上) 大家好 我是寸铁&#x1f44a; 【深度学习】“复杂场景下基于深度学习的卷积神经网络在鸟类多类别识别中的模型设计与性能优化研究”(上)✨ 喜欢的小伙伴可以点点关注 &a…

LangChain-v0.2 构建 PDF 采集和问答系统

PDF 文件通常包含其他来源无法获取的重要非结构化数据。它们可能非常长&#xff0c;而且与纯文本文件不同&#xff0c;通常无法直接输入到语言模型的提示中。 在本中&#xff0c;我们将创建一个可以回答有关 PDF 文件的问题的系统。更具体地说&#xff0c;就是使用文档加载器加…

【前端 14】Vue常见指令

Vue常见指令 Vue.js 是一个构建用户界面的渐进式框架&#xff0c;它通过一系列简洁的指令&#xff08;Directives&#xff09;来增强HTML的功能&#xff0c;使得开发者能够更加方便地构建出响应式的Web应用。本文将详细讲解Vue中的几个核心指令&#xff1a;v-bind、v-model、v…

AndroidAOSP定制之关闭某些app的通知

AndroidAOSP定制之关闭某些app的通知 前言&#xff1a; ​ 最近在做AOSP系统定制时发现gms定制好了后&#xff0c;Google应用商店用不了&#xff0c;提示此设备未获得Play保护机制认证&#xff0c;Google应用和服务无法在此设备上运行,查看官方文档和资料&#xff0c;说是由于…

【学习笔记】| 03 - 使用STM32CubeMX新建工程

使用STM32CubeMX新建工程是一个相对直观的过程&#xff0c;主要涉及到选择芯片型号、配置外设、时钟系统、GPIO引脚等&#xff0c;并最终生成工程代码。以下是一个详细的步骤指南&#xff1a; 一、打开STM32CubeMX并新建工程 启动STM32CubeMX&#xff1a; 打开STM32CubeMX软件…

FPGA实验6: 有时钟使能两位十进制计数器的设计

一、实验目的与要求 1.. 熟练掌握使用原理图设计较复杂电路&#xff1b; 2. 学习原理图设计中总线的表示以及使用方法。 二、实验原理 运用Quartus II 集成环境下的图形设计方法设计有时钟使能的两位十进制计数器。进行波形仿真和分析、引脚分配并下载到实验设备上进行功能…

[ECharts] There is a chart instance already initialized on the dom. 已存在图表,渲染重复

报错&#xff1a;已存在图表&#xff0c;渲染重复 解决: 在合适的时机执行 dispose 方法即可 // echarts 全局存入 实例 let myChart: any;// 在你的 initChart 初始化 Echarts 方法中 先执行清理方法 const initChart () > {// 执行清理方法然后初始化if(myChart){cons…

AndroidStudio 开发环境搭建

文章目录 AndroidStudio 开发环境搭建JDK 下载与安装&#xff0c;配置环境变量JDK1.8 下载安装配置环境变量新建JAVA_HOME编辑Path 下载AndroidStudio最新版本历史版本先安装JDK&#xff0c;后启动AS以管理员身份运行打开解决双击打不开的问题Error:你的主机中的软件中止了一个…

Coggle数据科学 | 大模型技术内参:39 种提示工程 在 29 种 NLP 任务精度对比

本文来源公众号“Coggle数据科学”&#xff0c;仅用于学术分享&#xff0c;侵权删&#xff0c;干货满满。 原文链接&#xff1a;大模型技术内参&#xff1a;39 种提示工程 在 29 种 NLP 任务精度对比 大语言模型&#xff08;LLMs&#xff09;在许多不同的自然语言处理&#x…

Linux网络-netstat命令

作者介绍&#xff1a;简历上没有一个精通的运维工程师。希望大家多多关注我&#xff0c;我尽量把自己会的都分享给大家&#xff0c;下面的思维导图也是预计更新的内容和当前进度(不定时更新)。 Linux服务器作为一个常用的网络服务器&#xff0c;主要的作用就是向客户端提供网络…

1143. 最长公共子序列(详细版)

目录 dp解法&#xff1a; 1.状态代表什么&#xff1a; 2. 状态转移方程 3.初始化 3. so为什么要这样&#xff1f; 代码实现&#xff1a; 给定两个字符串 text1 和 text2&#xff0c;返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 &#xff0c;返回 0…

从小白到架构师:万字长文 | 社交媒体应用系统设计

移动互联网时代&#xff0c;社交媒体应用彻底改变了我们联系和共享信息的方式。这些平台在幕后处理庞大的用户群、数据存储和实时交互。 在本文中&#xff0c;我们将深入探讨如何设计一个可扩展且高性能的社交媒体应用系统。我们将探讨关键组件、流程图、功能需求以及容量规划…

科普文:分布式数据一致性协议Paxos

1 什么是Paxos Paxos协议其实说的就是Paxos算法, Paxos算法是基于消息传递且具有高度容错特性的一致性算 法&#xff0c;是目前公认的解决分布式一致性问题最有效的算法之一。 Paxos由 莱斯利兰伯特(Leslie Lamport)于1998年在《The Part-Time Parliament》论文中首次公 开&…

Gitops-Argo-Cli安装与使用

一、安装Argo-Cli工具 Release v2.9.21 argoproj/argo-cd GitHub **选择合适的符合你操作系统以及CPU架构的二进制文件 #依v2.9.21-X86-64-Linux操作系统为例 wget https://github.com/argoproj/argo-cd/releases/download/v2.9.21/argocd-linux-amd64 #添加执行权限并且移…

论文中的流程图参考图片

写论文的时候&#xff0c;在绘制流程图时&#xff0c;一直纠结n是大写还是小写&#xff0c;用不用斜体&#xff0c;号两边要不要空格。今天找到了一张标准的流程图来参考。图片来自 Zhi-Chang Ba et al, Combination of DCE-MRI and NME-DWI via Deep Neural Network for Predi…

虚拟机复制后网络不可用,报错“network.service - LSB: Bring up/down networking”

查询IP地址&#xff0c;eth33 没有显示IP地址 尝试重启&#xff0c;有报错&#xff0c;并且有提示&#xff0c;按照提示执行下看看 解决办法 chkconfig NetworkManager offsystemctl disable NetworkManager.serviceservice NetworkManager stopservice network restart 之后检…

边缘计算网关项目(含上报进程、32Modbus采集进程、设备搜索响应进程源码)

目录 边缘层 架构说明 包含知识点 数据上报进程 功能描述 功能开发 上报线程 数据存储线程 指令处理线程 项目源码 上报模块.c代码&#xff1a; 上报模块Makefile代码&#xff1a; STM32采集模块.c代码 设备搜索响应模块Linux部分.c代码 设备搜索响应模块Qt端代码.h …

C语言画蜡烛图

GPT-4o (OpenAI) 在C语言中&#xff0c;绘制蜡烛图&#xff08;Candlestick Chart&#xff09;不是直接的任务&#xff0c;因为C语言本身不包含高级图形绘制库。然而&#xff0c;可以通过某些图形库来完成这项任务&#xff0c;例如使用GTK、SDL、OpenGL等。 以下是通过GTK库绘…

Hive3:Hive初体验

1、创建表 CREATE TABLE test(id INT, name STRING, gender STRING);2、新增数据 INSERT INTO test VALUES(1, 王力红, 男); INSERT INTO test VALUES(2, 钉钉盯, 女); INSERT INTO test VALUES(3, 咔咔咔, 女);3、查询数据 简单查询 select * from test;带聚合函数的查询 …

论文写作之latex配置(VSCODE+TEXT LIVE)

1.overleaf 初学者学习latex可以用这个练习&#xff0c;可以在线编辑十分方便&#xff0c;但是编译时间受限制 网站&#xff1a;https://www.overleaf.com/project 2.Tex live 选择一个.iso文件下载 网站&#xff1a;Index of /CTAN/systems/texlive/Images/ 下载成功&am…