初始化项目

This commit is contained in:
2026-09-01 22:15:50 +08:00
commit b5a26a8848
11217 changed files with 1710987 additions and 0 deletions
+187
View File
@@ -0,0 +1,187 @@
<template>
<view class="container">
<!-- 标题 -->
<view class="page-title">收货地址</view>
<!-- 表单组件 -->
<view class="form-wrapper">
<u-form :model="form" ref="uForm" label-width="140rpx">
<u-form-item label="姓名" prop="name">
<u-input v-model="form.name" placeholder="请输入收货人姓名" />
</u-form-item>
<u-form-item label="电话" prop="phone">
<u-input v-model="form.phone" placeholder="请输入收货人手机号" />
</u-form-item>
<u-form-item label="地区" prop="region">
<select-region ref="sRegion" v-model="form.region" />
</u-form-item>
<u-form-item label="详细地址" prop="detail" :border-bottom="false">
<u-input v-model="form.detail" placeholder="街道门牌、楼层等信息" />
</u-form-item>
</u-form>
</view>
<!-- 操作按钮 -->
<view class="footer">
<view class="btn-wrapper">
<view class="btn-item btn-item-main" :class="{ disabled }" @click="handleSubmit()">保存</view>
</view>
</view>
</view>
</template>
<script>
import SelectRegion from '@/components/select-region/select-region'
import { isMobile } from '@/utils/verify'
import * as AddressApi from '@/api/address'
// 表单字段元素
const form = {
name: '',
phone: '',
region: [],
detail: ''
}
// 表单验证规则
const rules = {
name: [{
required: true,
message: '请输入姓名',
trigger: ['blur', 'change']
}],
phone: [{
required: true,
message: '请输入手机号',
trigger: ['blur', 'change']
}, {
// 自定义验证函数
validator: (rule, value, callback) => {
// 返回true表示校验通过,返回false表示不通过
return isMobile(value)
},
message: '手机号码不正确',
// 触发器可以同时用blur和change
trigger: ['blur'],
}],
region: [{
required: true,
message: '请选择省市区',
trigger: ['blur', 'change'],
type: 'array'
}],
detail: [{
required: true,
message: '请输入详细地址',
trigger: ['blur', 'change']
}],
}
export default {
components: {
SelectRegion
},
data() {
return {
form,
rules,
// 按钮禁用
disabled: false
}
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {},
// 必须要在onReady生命周期,因为onLoad生命周期组件可能尚未创建完毕
onReady() {
this.$refs.uForm.setRules(this.rules)
},
methods: {
// 表单提交
handleSubmit() {
const app = this
if (app.disabled) {
return false
}
app.$refs.uForm.validate(valid => {
if (valid) {
app.disabled = true
AddressApi.add(app.form)
.then(result => {
app.$toast(result.message)
uni.navigateBack()
})
.finally(() => app.disabled = false)
}
})
}
}
}
</script>
<style>
page {
background: #f7f8fa;
}
</style>
<style lang="scss" scoped>
.page-title {
width: 94%;
margin: 0 auto;
padding-top: 40rpx;
font-size: 28rpx;
color: rgba(69, 90, 100, 0.6);
}
.form-wrapper {
margin: 20rpx auto 20rpx auto;
padding: 0 40rpx;
width: 94%;
box-shadow: 0 1rpx 5rpx 0px rgba(0, 0, 0, 0.05);
border-radius: 16rpx;
background: #fff;
}
// 底部操作栏
.footer {
margin-top: 80rpx;
.btn-wrapper {
height: 100%;
// display: flex;
// align-items: center;
padding: 0 20rpx;
}
.btn-item {
flex: 1;
font-size: 28rpx;
height: 86rpx;
color: #fff;
border-radius: 50rpx;
display: flex;
justify-content: center;
align-items: center;
}
.btn-item-wechat {
background: #0ba90b;
margin-bottom: 26rpx;
}
.btn-item-main {
background: linear-gradient(to right, #f9211c, #ff6335);
color: #fff;
// 禁用按钮
&.disabled {
opacity: 0.6;
}
}
}
</style>
+309
View File
@@ -0,0 +1,309 @@
<template>
<view class="container">
<view class="addres-list">
<view class="address-item" v-for="(item, index) in list" :key="index">
<view class="contacts">
<text class="name">{{ item.name }}</text>
<text class="phone">{{ item.phone }}</text>
</view>
<view class="address">
<text class="region" v-for="(region, idx) in item.region" :key="idx">{{ region }}</text>
<text class="detail">{{ item.detail }}</text>
</view>
<view class="line"></view>
<view class="item-option">
<view class="_left">
<label class="item-radio" @click.stop="handleSetDefault(item.address_id)">
<radio class="radio" color="#fa2209" :checked="item.address_id == defaultId"></radio>
<text class="text">{{ item.address_id == defaultId ? '默认' : '选择' }}</text>
</label>
</view>
<view class="_right">
<view class="events">
<view class="event-item" @click="handleUpdate(item.address_id)">
<text class="iconfont icon-edit"></text>
<text class="title">编辑</text>
</view>
<view class="event-item" @click="handleRemove(item.address_id)">
<text class="iconfont icon-delete"></text>
<text class="title">删除</text>
</view>
</view>
</view>
</view>
</view>
</view>
<empty v-if="!list.length" :isLoading="isLoading" tips="亲,暂无收货地址" />
<!-- 底部操作按钮 -->
<view class="footer-fixed">
<view class="btn-wrapper">
<view class="btn-item btn-item-main" @click="handleCreate()">添加新地址</view>
</view>
</view>
</view>
</template>
<script>
import * as AddressApi from '@/api/address'
import Empty from '@/components/empty'
export default {
components: {
Empty
},
data() {
return {
//当前页面参数
options: {},
// 正在加载
isLoading: true,
// 收货地址列表
list: [],
// 默认收货地址
defaultId: null
}
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
// 当前页面参数
this.options = options
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
// 获取页面数据
this.getPageData()
},
methods: {
// 获取页面数据
getPageData() {
const app = this
app.isLoading = true
Promise.all([app.getDefaultId(), app.getAddressList()])
.then(() => {
// 列表排序把默认收货地址放到最前
app.onReorder()
})
.finally(() => app.isLoading = false)
},
// 获取收货地址列表
getAddressList() {
const app = this
return new Promise((resolve, reject) => {
AddressApi.list()
.then(result => {
app.list = result.data.list
resolve(result)
})
.catch(reject)
})
},
// 获取默认的收货地址
getDefaultId() {
return new Promise((resolve, reject) => {
const app = this
AddressApi.defaultId()
.then(result => {
app.defaultId = result.data.defaultId
resolve(result)
})
.catch(reject)
})
},
// 列表排序把默认收货地址放到最前
onReorder() {
const app = this
app.list.sort(item => {
return item.address_id == app.defaultId ? -1 : 1
})
},
/**
* 添加新地址
*/
handleCreate() {
this.$navTo('pages/address/create')
},
/**
* 编辑地址
* @param {int} addressId 收货地址ID
*/
handleUpdate(addressId) {
this.$navTo('pages/address/update', { addressId })
},
/**
* 删除收货地址
* @param {int} addressId 收货地址ID
*/
handleRemove(addressId) {
const app = this
uni.showModal({
title: "提示",
content: "您确定要删除当前收货地址吗?",
success({ confirm }) {
confirm && app.onRemove(addressId)
}
});
},
/**
* 确认删除收货地址
* @param {int} addressId 收货地址ID
*/
onRemove(addressId) {
const app = this
AddressApi.remove(addressId)
.then(result => {
app.getPageData()
})
},
/**
* 设置为默认地址
* @param {Object} addressId
*/
handleSetDefault(addressId) {
const app = this
AddressApi.setDefault(addressId)
.then(result => {
app.defaultId = addressId
app.options.from === 'checkout' && uni.navigateBack()
})
}
}
}
</script>
<style lang="scss" scoped>
.addres-list {
padding-top: 20rpx;
// 设置ios刘海屏底部横线安全区域
padding-bottom: calc(constant(safe-area-inset-bottom) + 140rpx);
padding-bottom: calc(env(safe-area-inset-bottom) + 140rpx);
}
// 项目内容
.address-item {
margin: 20rpx auto 20rpx auto;
padding: 30rpx 40rpx;
width: 94%;
box-shadow: 0 1rpx 5rpx 0px rgba(0, 0, 0, 0.05);
border-radius: 16rpx;
background: #fff;
}
.contacts {
font-size: 30rpx;
margin-bottom: 16rpx;
.name {
margin-right: 16rpx;
}
}
.address {
font-size: 28rpx;
.region {
margin-right: 10rpx;
}
}
.line {
margin: 20rpx 0;
border-bottom: 1rpx solid #f3f3f3;
}
.item-option {
display: flex;
justify-content: space-between;
height: 48rpx;
// 单选框
.item-radio {
font-size: 28rpx;
.radio {
vertical-align: middle;
transform: scale(0.76)
}
.text {
vertical-align: middle;
}
}
// 操作
.events {
display: flex;
align-items: center;
line-height: 48rpx;
.event-item {
font-size: 28rpx;
margin-right: 26rpx;
color: #4c4c4c;
&:last-child {
margin-right: 0;
}
.title {
margin-left: 8rpx;
}
}
}
}
// 底部操作栏
.footer-fixed {
position: fixed;
bottom: var(--window-bottom);
left: 0;
right: 0;
min-height: 120rpx;
z-index: 11;
box-shadow: 0 -4rpx 40rpx 0 rgba(151, 151, 151, 0.24);
background: #fff;
// 设置ios刘海屏底部横线安全区域
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
.btn-wrapper {
height: 120rpx;
display: flex;
align-items: center;
padding: 0 40rpx;
}
.btn-item {
flex: 1;
font-size: 28rpx;
height: 80rpx;
line-height: 80rpx;
text-align: center;
color: #fff;
border-radius: 50rpx;
}
.btn-item-main {
background: linear-gradient(to right, #f9211c, #ff6335);
}
}
</style>
+225
View File
@@ -0,0 +1,225 @@
<template>
<view class="container">
<!-- 标题 -->
<view class="page-title">收货地址</view>
<!-- 表单组件 -->
<view class="form-wrapper">
<u-form :model="form" ref="uForm" label-width="140rpx">
<u-form-item label="姓名" prop="name">
<u-input v-model="form.name" placeholder="请输入收货人姓名" />
</u-form-item>
<u-form-item label="电话" prop="phone">
<u-input v-model="form.phone" placeholder="请输入收货人手机号" />
</u-form-item>
<u-form-item label="地区" prop="region">
<select-region ref="sRegion" v-model="form.region" />
</u-form-item>
<u-form-item label="详细地址" prop="detail" :border-bottom="false">
<u-input v-model="form.detail" placeholder="街道门牌、楼层等信息" />
</u-form-item>
</u-form>
</view>
<!-- 操作按钮 -->
<view class="footer">
<view class="btn-wrapper">
<view class="btn-item btn-item-main" :class="{ disabled }" @click="handleSubmit()">保存</view>
</view>
</view>
</view>
</template>
<script>
import SelectRegion from '@/components/select-region/select-region'
import { isMobile } from '@/utils/verify'
import * as AddressApi from '@/api/address'
// 表单验证规则
const rules = {
name: [{
required: true,
message: '请输入姓名',
trigger: ['blur', 'change']
}],
phone: [{
required: true,
message: '请输入手机号',
trigger: ['blur', 'change']
}, {
// 自定义验证函数
validator: (rule, value, callback) => {
// 返回true表示校验通过,返回false表示不通过
return isMobile(value)
},
message: '手机号码不正确',
// 触发器可以同时用blur和change
trigger: ['blur'],
}],
region: [{
required: true,
message: '请选择省市区',
trigger: ['blur', 'change'],
type: 'array'
}],
detail: [{
required: true,
message: '请输入详细地址',
trigger: ['blur', 'change']
}],
}
export default {
components: {
SelectRegion
},
data() {
return {
form: {
name: '',
phone: '',
region: [],
detail: ''
},
rules,
// 加载中
isLoading: true,
// 按钮禁用
disabled: false,
// 当前收货地址ID
addressId: null
}
},
/**
* 生命周期函数--监听页面加载
*/
onLoad({ addressId }) {
// 当前收货地址ID
this.addressId = addressId
// 获取当前记录详情
this.getDetail()
},
// 必须要在onReady生命周期,因为onLoad生命周期组件可能尚未创建完毕
onReady() {
this.$refs.uForm.setRules(this.rules)
},
methods: {
// 获取当前记录详情
getDetail() {
const app = this
AddressApi.detail(app.addressId)
.then(result => {
const detail = result.data.detail
app.createFormData(detail)
})
},
// 生成默认的表单数据
createFormData(detail) {
const { form } = this
form.name = detail.name
form.phone = detail.phone
form.detail = detail.detail
form.region = this.createRegion(detail)
},
createRegion(detail) {
return [{
label: detail.region.province,
value: detail.province_id
}, {
label: detail.region.city,
value: detail.city_id
}, {
label: detail.region.region,
value: detail.region_id
}]
},
// 表单提交
handleSubmit() {
const app = this
if (app.disabled) {
return false
}
app.$refs.uForm.validate(valid => {
if (valid) {
app.disabled = true
AddressApi.edit(app.addressId, app.form)
.then(result => {
app.$toast(result.message)
uni.navigateBack()
})
.finally(() => app.disabled = false)
}
})
}
}
}
</script>
<style>
page {
background: #f7f8fa;
}
</style>
<style lang="scss" scoped>
.page-title {
width: 94%;
margin: 0 auto;
padding-top: 40rpx;
font-size: 28rpx;
color: rgba(69, 90, 100, 0.6);
}
.form-wrapper {
margin: 20rpx auto 20rpx auto;
padding: 0 40rpx;
width: 94%;
box-shadow: 0 1rpx 5rpx 0px rgba(0, 0, 0, 0.05);
border-radius: 16rpx;
background: #fff;
}
// 底部操作栏
.footer {
margin-top: 80rpx;
.btn-wrapper {
height: 100%;
// display: flex;
// align-items: center;
padding: 0 20rpx;
}
.btn-item {
flex: 1;
font-size: 28rpx;
height: 86rpx;
color: #fff;
border-radius: 50rpx;
display: flex;
justify-content: center;
align-items: center;
}
.btn-item-wechat {
background: #0ba90b;
margin-bottom: 26rpx;
}
.btn-item-main {
background: linear-gradient(to right, #f9211c, #ff6335);
color: #fff;
// 禁用按钮
&.disabled {
opacity: 0.6;
}
}
}
</style>
+107
View File
@@ -0,0 +1,107 @@
<template>
<view v-if="!isLoading" class="container b-f p-b">
<view class="article-title">
<text class="f-32">{{ detail.title }}</text>
</view>
<view class="article-little dis-flex flex-x-between m-top10">
<view class="article-little__left">
<text class="article-views f-24 col-8">{{ detail.show_views }}次浏览</text>
</view>
<view class="article-little__right">
<text class="article-views f-24 col-8">{{ detail.view_time }}</text>
</view>
</view>
<view class="article-content m-top20">
<mp-html :content="detail.content" />
</view>
<!-- 快捷导航 -->
<shortcut />
</view>
</template>
<script>
import Shortcut from '@/components/shortcut'
import * as ArticleApi from '@/api/article'
export default {
components: {
Shortcut
},
data() {
return {
// 当前文章ID
articleId: null,
// 加载中
isLoading: true,
// 当前文章详情
detail: null
}
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
// 记录文章ID
this.articleId = options.articleId
// 获取文章详情
this.getArticleDetail()
},
methods: {
// 获取文章详情
getArticleDetail() {
const app = this
app.isLoading = true
ArticleApi.detail(app.articleId)
.then(result => {
app.detail = result.data.detail
})
.finally(() => app.isLoading = false)
}
},
/**
* 分享当前页面
*/
onShareAppMessage() {
const app = this
// 构建页面参数
const params = app.$getShareUrlParams({ articleId: app.articleId });
return {
title: app.detail.title,
path: "/pages/article/detail?" + params
}
},
/**
* 分享到朋友圈
* 本接口为 Beta 版本,暂只在 Android 平台支持,详见分享到朋友圈 (Beta)
* https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/share-timeline.html
*/
onShareTimeline() {
const app = this
// 构建页面参数
const params = app.$getShareUrlParams({ articleId: app.articleId });
return {
title: app.detail.title,
path: "/pages/article/detail?" + params
}
}
}
</script>
<style lang="scss" scoped>
.container {
min-height: 100vh;
padding: 20rpx;
background: #fff;
}
.article-content {
font-size: 28rpx;
}
</style>
+251
View File
@@ -0,0 +1,251 @@
<template>
<mescroll-body ref="mescrollRef" :sticky="true" @init="mescrollInit" :down="{ use: false }" :up="upOption"
@up="upCallback">
<!-- tab栏 -->
<u-tabs :list="tabList" :is-scroll="true" :current="curTab" active-color="#fd4a5f" :duration="0.2"
@change="onChangeTab" />
<!-- 文章列表 -->
<view class="article-list">
<view class="article-item" :class="[`show-type__${item.show_type}`]" v-for="(item, index) in articleList.data"
:key="index" @click="onTargetDetail(item.article_id)">
<!-- 小图模式 -->
<block v-if="item.show_type == 10">
<view class="article-item__left flex-box">
<view class="article-item__title">
<text class="twoline-hide">{{ item.title }}</text>
</view>
<view class="article-item__footer m-top10">
<text class="article-views f-24 col-8">{{ item.show_views }}次浏览</text>
</view>
</view>
<view class="article-item__image">
<image class="image" mode="widthFix" :src="item.image_url"></image>
</view>
</block>
<!-- 大图模式 -->
<block v-if="item.show_type == 20">
<view class="article-item__title">
<text class="twoline-hide">{{ item.title }}</text>
</view>
<view class="article-item__image m-top20">
<image class="image" mode="widthFix" :src="item.image_url"></image>
</view>
<view class="article-item__footer m-top10">
<text class="article-views f-24 col-8">{{ item.show_views }}次浏览</text>
</view>
</block>
</view>
</view>
</mescroll-body>
</template>
<script>
import MescrollBody from '@/components/mescroll-uni/mescroll-body.vue'
import MescrollMixin from '@/components/mescroll-uni/mescroll-mixins'
import * as ArticleApi from '@/api/article'
import * as CategoryApi from '@/api/article/category'
import { getEmptyPaginateObj, getMoreListData } from '@/core/app'
const pageSize = 15
export default {
components: {
MescrollBody
},
mixins: [MescrollMixin],
data() {
return {
// 选项卡列表
tabList: [],
// 当前选项
curTab: 0,
// 文章列表
articleList: getEmptyPaginateObj(),
// 上拉加载配置
upOption: {
// 首次自动执行
auto: true,
// 每页数据的数量; 默认10
page: { size: pageSize },
// 数量要大于3条才显示无更多数据
noMoreSize: 3,
}
}
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
const app = this
// 获取文章分类数据
app.getCategoryList(options.categoryId)
},
methods: {
/**
* 上拉加载的回调 (页面初始化时也会执行一次)
* 其中page.num:当前页 从1开始, page.size:每页数据条数,默认10
* @param {Object} page
*/
upCallback(page) {
const app = this
// 设置列表数据
app.getArticleList(page.num)
.then(list => {
const curPageLen = list.data.length
const totalSize = list.data.total
app.mescroll.endBySize(curPageLen, totalSize)
})
.catch(() => app.mescroll.endErr())
},
// 获取文章分类数据
getCategoryList(categoryId) {
CategoryApi.list().then(result => {
this.setTabList(result.data.list, categoryId)
})
},
// 设置选项卡数据
setTabList(categoryList, categoryId) {
const app = this
app.tabList = [{ value: 0, name: '全部' }]
categoryList.forEach(item => {
app.tabList.push({ value: item.category_id, name: item.name })
})
if (categoryId > 0) {
const findIndex = app.tabList.findIndex(item => item.value == categoryId)
app.curTab = findIndex > -1 ? findIndex : 0
}
},
/**
* 获取文章列表
* @param {Number} pageNo 页码
*/
getArticleList(pageNo = 1) {
const app = this
return new Promise((resolve, reject) => {
ArticleApi.list({ categoryId: app.getTabValue(), page: pageNo }, { load: false })
.then(result => {
// 合并新数据
const newList = result.data.list
app.articleList.data = getMoreListData(newList, app.articleList, pageNo)
resolve(newList)
})
.catch(reject)
})
},
// 切换标签项
onChangeTab(index) {
// 设置当前选中的标签
this.curTab = index
// 刷新订单列表
this.onRefreshList()
},
// 获取当前标签项的值
getTabValue() {
const app = this
return app.tabList.length ? app.tabList[app.curTab].value : 0
},
// 刷新列表数据
onRefreshList() {
this.articleList = getEmptyPaginateObj()
setTimeout(() => this.mescroll.resetUpScroll(), 120)
},
// 跳转文章详情页
onTargetDetail(articleId) {
this.$navTo('pages/article/detail', { articleId })
}
},
/**
* 分享当前页面
*/
onShareAppMessage() {
return {
title: '文章首页',
path: "/pages/article/index?" + this.$getShareUrlParams()
}
},
/**
* 分享到朋友圈
* 本接口为 Beta 版本,暂只在 Android 平台支持,详见分享到朋友圈 (Beta)
* https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/share-timeline.html
*/
onShareTimeline() {
return {
title: '文章首页',
path: "/pages/article/index?" + this.$getShareUrlParams()
}
}
}
</script>
<style lang="scss" scoped>
.container {
min-height: 100vh;
}
// 文章列表
.article-list {
padding-top: 20rpx;
line-height: 1;
background: #f7f7f7;
}
.article-item {
margin-bottom: 20rpx;
padding: 30rpx;
background: #fff;
&:last-child {
margin-bottom: 0;
}
.article-item__title {
max-height: 74rpx;
font-size: 28rpx;
line-height: 38rpx;
color: #333;
}
.article-item__image .image {
display: block;
}
}
// 小图模式
.show-type__10 {
display: flex;
.article-item__left {
padding-right: 20rpx;
}
.article-item__title {
// min-height: 72rpx;
}
.article-item__image .image {
width: 240rpx;
}
}
// 大图模式
.show-type__20 .article-item__image .image {
width: 100%;
}
</style>
+500
View File
@@ -0,0 +1,500 @@
<template>
<view class="container">
<!-- 页面顶部 -->
<view v-if="list.length" class="head-info">
<view class="cart-total">
<text>共</text>
<text class="active">{{ total }}</text>
<text>件商品</text>
</view>
<view class="cart-edit" @click="handleToggleMode">
<view v-if="mode == 'normal'" class="normal">
<text class="icon iconfont icon-bianji"></text>
<text>编辑</text>
</view>
<view v-if="mode == 'edit'" class="edit">
<text>完成</text>
</view>
</view>
</view>
<!-- 购物车商品列表 -->
<view v-if="list.length" class="cart-list">
<view class="cart-item" v-for="(item, index) in list" :key="index">
<label class="item-radio" @click.stop="handleCheckItem(item.id)">
<radio class="radio" color="#fa2209" :checked="inArray(item.id, checkedIds)" />
</label>
<view class="goods-image" @click="onTargetGoods(item.goods_id)">
<image class="image" :src="item.goods.goods_image" mode="scaleToFill"></image>
</view>
<view class="item-content">
<view class="goods-title" @click="onTargetGoods(item.goods_id)">
<text class="twoline-hide">{{ item.goods.goods_name }}</text>
</view>
<view class="goods-props clearfix">
<view class="goods-props-item" v-for="(props, idx) in item.goods.skuInfo.goods_props" :key="idx">
<text>{{ props.value.name }}</text>
</view>
</view>
<view class="item-foot">
<view class="goods-price">
<text class="unit">¥</text>
<text class="value">{{ item.goods.skuInfo.goods_price }}</text>
</view>
<view class="stepper">
<u-number-box :min="1" :max="1" :value="item.goods_num" :step="1" @change="onChangeStepper($event, item)" />
</view>
</view>
</view>
</view>
</view>
<!-- 购物车数据为空 -->
<empty v-if="!list.length" :isLoading="isLoading" :custom-style="{ padding: '180rpx 50rpx' }" tips="您的购物车是空的, 快去逛逛吧">
<view slot="slot" class="empty-ipt" @click="onTargetIndex">
<text>去逛逛</text>
</view>
</empty>
<!-- 底部操作栏 -->
<view v-if="list.length" class="footer-fixed">
<label class="all-radio" @click="handleCheckAll">
<radio class="radio" color="#fa2209" :checked="checkedIds.length > 0 && checkedIds.length === list.length" />
<text>全选</text>
</label>
<view class="total-info">
<text>合计:</text>
<view class="goods-price">
<text class="unit">¥</text>
<text class="value">{{ totalPrice }}</text>
</view>
</view>
<view class="cart-action">
<view class="btn-wrapper">
<!-- dev:下面的disabled条件使用checkedIds.join方式判断 -->
<!-- dev:通常情况下vue项目使用checkedIds.length更合理, 但是length属性在微信小程序中不起作用 -->
<view v-if="mode == 'normal'" class="btn-item btn-main" :class="{ disabled: checkedIds.join() == '' }" @click="handleOrder()">
<text>去结算 {{ checkedIds.length > 0 ? `(${checkedIds.length})` : '' }}</text>
</view>
<view v-if="mode == 'edit'" class="btn-item btn-main" :class="{ disabled: !checkedIds.length }" @click="handleDelete()">
<text>删除</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import { inArray, arrayIntersect, debounce } from '@/utils/util'
import { checkLogin, setCartTotalNum, setCartTabBadge } from '@/core/app'
import * as CartApi from '@/api/cart'
import Empty from '@/components/empty'
const CartIdsIndex = 'CartIds'
export default {
components: {
Empty
},
data() {
return {
inArray,
// 正在加载
isLoading: true,
// 当前模式: normal正常 edit编辑
mode: 'normal',
// 购物车商品列表
list: [],
// 购物车商品总数量
total: null,
// 选中的商品ID记录
checkedIds: [],
// 选中的商品总金额
totalPrice: '0.00'
}
},
watch: {
// 监听选中的商品
checkedIds: {
handler(val) {
// 计算合计金额
this.onCalcTotalPrice()
// 记录到缓存中
uni.setStorageSync(CartIdsIndex, val)
},
immediate: false
},
// 监听购物车商品总数量
total(val) {
// 缓存并设置角标
setCartTotalNum(val)
setCartTabBadge()
}
},
/**
* 生命周期函数--监听页面显示
*/
onShow(options) {
// 获取购物车商品列表
checkLogin() ? this.getCartList() : this.isLoading = false
// 获取缓存中的选中记录
this.checkedIds = uni.getStorageSync(CartIdsIndex)
},
methods: {
// 计算合计金额 (根据选中的商品)
onCalcTotalPrice() {
const app = this
// 选中的商品记录
const checkedList = app.list.filter(item => inArray(item.id, app.checkedIds))
// 计算总金额
let tempPrice = 0;
checkedList.forEach(item => {
// 商品单价, 为了方便计算先转换单位为分 (整数)
const unitPrice = item.goods.skuInfo.goods_price * 100
tempPrice += unitPrice * item.goods_num
})
app.totalPrice = (tempPrice / 100).toFixed(2)
},
// 获取购物车商品列表
getCartList() {
const app = this
app.isLoading = true
CartApi.list()
.then(result => {
app.list = result.data.list
app.total = result.data.cartTotal
// 清除checkedIds中无效的ID
app.onClearInvalidId()
})
.finally(() => app.isLoading = false)
},
// 清除checkedIds中无效的ID
onClearInvalidId() {
const app = this
const listIds = app.list.map(item => item.id)
app.checkedIds = arrayIntersect(listIds, app.checkedIds)
},
// 切换当前模式
handleToggleMode() {
this.mode = this.mode == 'normal' ? 'edit' : 'normal'
},
// 监听步进器更改事件
onChangeStepper({ value }, item) {
// 这里是组织首次启动时的执行
if (item.goods_num == value) return
// 记录一个节流函数句柄
if (!item.debounceHandle) {
item.oldValue = item.goods_num
item.debounceHandle = debounce(this.onUpdateCartNum, 500)
}
// 更新商品数量
item.goods_num = value
// 提交更新购物车数量 (节流)
item.debounceHandle(item, item.oldValue, value)
},
// 提交更新购物车数量
onUpdateCartNum(item, oldValue, newValue) {
const app = this
CartApi.update(item.goods_id, item.goods_sku_id, newValue)
.then(result => {
// 更新商品数量
app.total = result.data.cartTotal
// 重新计算合计金额
app.onCalcTotalPrice()
// 清除节流函数句柄
item.debounceHandle = null
})
.catch(err => {
// 还原商品数量
item.goods_num = oldValue
setTimeout(() => app.$toast(err.errMsg), 10)
})
},
// 跳转到商品详情页
onTargetGoods(goodsId) {
this.$navTo('pages/goods/detail', { goodsId })
},
// 点击去逛逛按钮, 跳转到首页
onTargetIndex() {
this.$navTo('pages/index/index')
},
// 选中商品
handleCheckItem(cartId) {
const { checkedIds } = this
const index = checkedIds.findIndex(id => id === cartId)
index < 0 ? checkedIds.push(cartId) : checkedIds.splice(index, 1)
},
// 全选事件
handleCheckAll() {
const { checkedIds, list } = this
this.checkedIds = checkedIds.length === list.length ? [] : list.map(item => item.id)
},
// 结算选中的商品
handleOrder() {
const app = this
if (app.checkedIds.length) {
const cartIds = app.checkedIds.join()
app.$navTo('pages/checkout/index', { mode: 'cart', cartIds })
}
},
// 删除选中的商品弹窗事件
handleDelete() {
const app = this
if (!app.checkedIds.length) {
return false
}
uni.showModal({
title: '友情提示',
content: '您确定要删除该商品吗?',
showCancel: true,
success({ confirm }) {
// 确认删除
confirm && app.onClearCart()
}
})
},
// 确认删除商品
onClearCart() {
const app = this
CartApi.clear(app.checkedIds)
.then(result => {
app.getCartList()
app.handleToggleMode()
})
}
}
}
</script>
<style>
page {
background: #f5f5f5;
}
</style>
<style lang="scss" scoped>
// 页面顶部
.head-info {
display: flex;
justify-content: space-between;
align-items: center;
padding: 4rpx 30rpx;
// background-color: #fff;
height: 80rpx;
.cart-total {
font-size: 28rpx;
color: #333;
.active {
color: #FA2209;
margin: 0 2rpx;
}
}
.cart-edit {
padding-left: 20rpx;
.icon {
margin-right: 12rpx;
}
.edit {
color: #fa2209;
}
}
}
// 购物车列表
.cart-list {
padding: 0 16rpx 110rpx 16rpx;
}
.cart-item {
background: #fff;
border-radius: 12rpx;
display: flex;
align-items: center;
padding: 30rpx 16rpx;
margin-bottom: 24rpx;
.item-radio {
width: 56rpx;
height: 80rpx;
line-height: 80rpx;
margin-right: 10rpx;
text-align: center;
.radio {
transform: scale(0.76)
}
}
.goods-image {
width: 200rpx;
height: 200rpx;
.image {
display: block;
width: 100%;
height: 100%;
border-radius: 8rpx;
}
}
.item-content {
flex: 1;
padding-left: 24rpx;
.goods-title {
font-size: 28rpx;
max-height: 76rpx;
}
.goods-props {
margin-top: 14rpx;
height: 40rpx;
color: #ababab;
font-size: 24rpx;
overflow: hidden;
.goods-props-item {
display: inline-block;
margin-right: 14rpx;
padding: 4rpx 16rpx;
border-radius: 12rpx;
background-color: #F5F5F5;
width: auto;
}
}
.item-foot {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 20rpx;
.goods-price {
vertical-align: bottom;
color: $uni-text-color-active;
.unit {
font-size: 24rpx;
}
.value {
font-size: 32rpx;
}
}
}
}
}
// 空数据按钮
.empty-ipt {
width: 220rpx;
margin: 0 auto;
font-size: 32rpx;
height: 64rpx;
line-height: 64rpx;
text-align: center;
color: #fff;
border-radius: 50rpx;
background: linear-gradient(to right, #f9211c, #ff6335);
}
// 底部操作栏
.footer-fixed {
display: flex;
align-items: center;
height: 96rpx;
background: #fff;
padding: 0 30rpx;
position: fixed;
bottom: var(--window-bottom);
left: 0;
right: 0;
z-index: 11;
.all-radio {
width: 140rpx;
display: flex;
align-items: center;
.radio {
margin-bottom: -4rpx;
transform: scale(0.76)
}
}
.total-info {
flex: 1;
display: flex;
align-items: center;
justify-content: flex-end;
padding-right: 30rpx;
.goods-price {
vertical-align: bottom;
color: #fa2209;
.unit {
font-size: 24rpx;
}
.value {
font-size: 32rpx;
}
}
}
.cart-action {
width: 200rpx;
.btn-wrapper {
height: 100%;
display: flex;
align-items: center;
}
.btn-item {
flex: 1;
font-size: 28rpx;
height: 72rpx;
line-height: 72rpx;
text-align: center;
color: #fff;
border-radius: 50rpx;
}
// 立即购买按钮
.btn-main {
background: linear-gradient(to right, #f9211c, #ff6335);
// 禁用按钮
&.disabled {
background: #ff9779;
}
}
}
}
</style>
@@ -0,0 +1,414 @@
<template>
<view class="container">
<!-- 一级分类 -->
<scroll-view class="cate-left" :scroll-y="true" :style="{ height: `${scrollHeight}px` }" @touchmove.stop.prevent>
<!-- <text class="type-nav" :class="{ selected: curIndex == -1 }" @click="handleSelectNav(-1)">全部</text> -->
<text class="type-nav" :class="{ selected: curIndex == index }" v-for="(item, index) in list" :key="index" @click="handleSelectNav(index)">{{ item.name }}</text>
</scroll-view>
<mescroll-body ref="mescrollRef" :sticky="true" @init="mescrollInit" :down="{ use: false }" :up="upOption" :bottombar="false" @up="upCallback">
<view class="cate-content">
<!-- 子分类 -->
<view v-if="subCateList.length" class="sub-cate-list clearfix" :class="{ 'display-fold': !showSubCate }" @touchmove.stop.prevent>
<view class="nav-icon" @click="handleShowSubCate">
<text class="iconfont" :class="[ showSubCate ? 'icon-arrow-up' : 'icon-arrow-down' ]"></text>
</view>
<view class="sub-cate-item" :class="{ selected: curIndex2 == -1 }" @click="handleSelectSubCate(-1)">
<text>全部</text>
</view>
<view class="sub-cate-item" v-for="(item, index) in subCateList" :key="index" :class="{ selected: curIndex2 == index }" @click="handleSelectSubCate(index)">
<text>{{ item.name }}</text>
</view>
</view>
<!-- 商品列表 -->
<view class="goods-list">
<view class="goods-item--container" v-for="(item, index) in goodsList.data" :key="index">
<view class="goods-item" @click="onTargetGoods(item.goods_id)">
<!-- 商品图片 -->
<view class="goods-item_left">
<image class="image" :src="item.goods_image"></image>
</view>
<view class="goods-item_right">
<!-- 商品标题 -->
<view class="goods-name">
<text class="twoline-hide">{{ item.goods_name }}</text>
</view>
<!-- 商品信息 -->
<view class="goods-item_desc">
<view class="desc_footer">
<view class="item-prices oneline-hide">
<text class="price_x">¥{{ item.goods_price_min }}</text>
<text v-if="item.line_price_min > 0" class="price_y">¥{{ item.line_price_min }}</text>
</view>
<!-- <add-cart-btn v-if="setting.showAddCart" :btnStyle="setting.cartStyle" @click="handleAddCart(item)" /> -->
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 遮罩层 -->
<view class="mask" v-show="showSubCate" @touchmove.stop.prevent @click="handleShowSubCate"></view>
<!-- 加入购物车组件 -->
<!-- <AddCartPopup ref="AddCartPopup" @addCart="onUpdateCartTabBadge" /> -->
</view>
</mescroll-body>
</view>
</template>
<script>
import MescrollBody from '@/components/mescroll-uni/mescroll-body.vue'
import MescrollMixin from '@/components/mescroll-uni/mescroll-mixins'
import { getEmptyPaginateObj, getMoreListData, setCartTabBadge } from '@/core/app'
import { PageCategoryStyleEnum } from '@/common/enum/store/page/category'
import Empty from '@/components/empty'
import AddCartBtn from '@/components/add-cart-btn'
import AddCartPopup from '@/components/add-cart-popup'
import { rpx2px } from '@/utils/util'
import * as GoodsApi from '@/api/goods'
const pageSize = 15
export default {
components: {
MescrollBody,
Empty,
AddCartBtn,
AddCartPopup
},
mixins: [MescrollMixin],
props: {
// 分类列表
list: {
type: Array,
default: []
},
// 分类设置
setting: {
type: Object,
default: () => {}
},
},
watch: {
list: {
immediate: true,
handler(newList) {
// && this.curIndex === -1
if (newList.length > 0) {
this.curIndex = 0
this.onRefreshList()
}
}
}
},
data() {
return {
// 枚举类
PageCategoryStyleEnum,
// 列表高度
scrollHeight: 0,
// 一级分类:指针
curIndex: -1,
// 是否显示子分类
showSubCate: false,
// 二级分类:指针
curIndex2: -1,
// 商品列表
goodsList: getEmptyPaginateObj(),
// 上拉加载配置
upOption: {
// 首次自动执行
auto: true,
// 每页数据的数量; 默认10
page: { size: pageSize },
// 数量要大于3条才显示无更多数据
noMoreSize: 3,
// 返回顶部
toTop: { right: 30, bottom: 48, zIndex: 9 }
}
}
},
created() {
// 设置分类列表高度
this.setListHeight()
},
computed: {
// 二级分类列表
subCateList() {
if (this.list[this.curIndex] && this.list[this.curIndex].children) {
return this.list[this.curIndex].children
}
return []
}
},
methods: {
/**
* 上拉加载的回调 (页面初始化时也会执行一次)
* 其中page.num:当前页 从1开始, page.size:每页数据条数,默认10
* @param {Object} page
*/
upCallback(page) {
const app = this
// 设置列表数据
app.getGoodsList(page.num)
.then(list => {
const curPageLen = list.data.length
const totalSize = list.data.total
app.mescroll.endBySize(curPageLen, totalSize)
})
.catch(() => app.mescroll.endErr())
},
/**
* 获取商品列表
* @param {Number} pageNo 页码
*/
getGoodsList(pageNo = 1) {
const app = this
const categoryId = app.getCategoryId()
return new Promise((resolve, reject) => {
GoodsApi.list({ categoryId, page: pageNo }, { load: false })
.then(result => {
const newList = result.data.list
app.goodsList.data = getMoreListData(newList, app.goodsList, pageNo)
app.goodsList.last_page = newList.last_page
resolve(newList)
})
.catch(reject)
})
},
// 获取当前选择的分类ID
getCategoryId() {
const app = this
if (app.curIndex2 > -1) {
return app.subCateList[app.curIndex2].category_id
}
return app.curIndex > -1 ? app.list[app.curIndex].category_id : 0
},
// 设置列表内容的高度
setListHeight() {
const { windowHeight } = uni.getSystemInfoSync()
this.scrollHeight = windowHeight - rpx2px(88)
},
// 一级分类:选中分类
handleSelectNav(index) {
this.curIndex = index
this.onRefreshList()
this.showSubCate = false
this.curIndex2 = -1
},
// 二级分类:选中分类
handleSelectSubCate(index) {
this.curIndex2 = index
this.showSubCate = false
this.onRefreshList()
},
// 刷新列表数据
onRefreshList() {
this.goodsList = getEmptyPaginateObj()
setTimeout(() => this.mescroll.resetUpScroll(), 120)
},
// 跳转至商品列表页
onTargetGoods(goodsId) {
this.$navTo('pages/goods/detail', { goodsId })
},
// 点击加入购物车
handleAddCart(item) {
this.$refs.AddCartPopup.handle(item)
},
// 更新购物车角标
onUpdateCartTabBadge() {
console.log('onUpdateCartTabBadge')
setCartTabBadge()
},
// 切换子分类显示状态
handleShowSubCate() {
this.showSubCate = !this.showSubCate
}
}
}
</script>
<style lang="scss" scoped>
.container {
padding-left: 173rpx;
}
// 分类内容
.cate-content {
z-index: 1;
background: #fff;
padding-top: 88rpx;
min-height: 300rpx;
}
// 一级分类+二级分类 20
.cate-left {
position: fixed;
top: calc(88rpx + var(--window-top));
left: var(--window-left);
bottom: var(--window-bottom);
width: 173rpx;
height: 100%;
background: #f8f8f8;
color: #444;
}
// 左侧一级分类
.type-nav {
position: relative;
height: 90rpx;
z-index: 10;
display: block;
font-size: 26rpx;
display: flex;
justify-content: center;
align-items: center;
&.selected {
background: #fff;
border-right: none;
font-size: 28rpx;
color: #fa2209;
}
}
// 商品列表
.goods-list {
background: #fff;
position: relative;
}
.goods-item {
padding: 28rpx 22rpx;
display: flex;
}
.goods-item_left {
position: relative;
background: #fff;
margin-right: 20rpx;
.image {
display: block;
width: 180rpx;
height: 180rpx;
}
}
.goods-item_right {
position: relative;
flex: 1;
.goods-name {
display: block;
width: 100%;
min-height: 68rpx;
font-size: 28rpx;
line-height: 1.3;
color: #333;
}
}
.goods-item_desc {
margin-top: 20rpx;
.people {
margin-right: 14rpx;
}
.desc_footer {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
position: absolute;
right: 0rpx;
bottom: 0rpx;
min-height: 44rpx;
.item-prices {
padding-right: 6rpx;
.price_x {
margin-right: 14rpx;
color: rgb(240, 60, 60);
font-size: 28rpx;
}
.price_y {
color: #999;
text-decoration: line-through;
font-size: 24rpx;
}
}
}
}
// 子分类
.sub-cate-list {
background-color: #fff;
width: 100%;
z-index: 9;
padding: 8rpx 40rpx 0 14rpx;
overflow: hidden;
position: sticky;
top: calc(88rpx + var(--window-top));
&.display-fold {
height: 86rpx;
}
.nav-icon {
position: absolute;
right: 16rpx;
top: 12rpx;
font-size: 32rpx;
}
.sub-cate-item {
float: left;
background: #f8f8f8;
padding: 10rpx 30rpx;
margin-right: 22rpx;
margin-bottom: 24rpx;
font-size: 26rpx;
border-radius: 14rpx;
border: 1rpx solid #f8f8f8;
&.selected {
color: #fa2209;
border: 1rpx solid #fa2209;
}
}
}
// 子分类遮罩层
.mask {
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
z-index: 8;
background-color: rgba(0, 0, 0, 0.4);
transition: all 0.3s ease-in-out 0s;
}
</style>
@@ -0,0 +1,110 @@
<template>
<view class="primary">
<!-- 一级分类(大图) 10 -->
<view v-if="list.length > 0 && display == PageCategoryStyleEnum.ONE_LEVEL_BIG.value" class="cate-content">
<view class="cate-wrapper cate_style__10 clearfix">
<view class="cate-item" v-for="(item, index) in list" :key="index" @click="onTargetGoodsList(item.category_id)">
<image v-if="item.image" class="image" mode="widthFix" :src="item.image.preview_url"></image>
</view>
</view>
</view>
<!-- 一级分类(小图) 11 -->
<view v-if="list.length > 0 && display == PageCategoryStyleEnum.ONE_LEVEL_SMALL.value" class="cate-content">
<view class="cate-wrapper cate_style__11 clearfix">
<view class="cate-item" v-for="(item, index) in list" :key="index" @click="onTargetGoodsList(item.category_id)">
<image v-if="item.image" class="image" mode="widthFix" :src="item.image.preview_url"></image>
<view class="cate-name">{{ item.name }}</view>
</view>
</view>
</view>
<empty v-if="!list.length" :tips="'亲,暂无商品分类' + display" />
</view>
</template>
<script>
import { PageCategoryStyleEnum } from '@/common/enum/store/page/category'
import Empty from '@/components/empty'
export default {
components: {
Empty
},
props: {
// 分类页样式
display: {
type: Number,
default: 10
},
// 分类列表
list: {
type: Array,
default: []
}
},
data() {
return {
// 枚举类
PageCategoryStyleEnum,
}
},
methods: {
// 跳转至商品列表页
onTargetGoodsList(categoryId) {
this.$navTo('pages/goods/list', { categoryId })
}
}
}
</script>
<style lang="scss" scoped>
// 分类内容
.cate-content {
z-index: 1;
background: #fff;
padding-top: 96rpx;
.cate-wrapper {
padding: 0 20rpx 20rpx 20rpx;
box-sizing: border-box;
}
}
// 一级分类(大图) 10
.cate_style__10 .cate-item {
margin-bottom: 20rpx;
&:last-child {
margin-bottom: 0;
}
.image {
display: block;
width: 100%;
height: auto;
}
}
// 一级分类(小图) 11
.cate_style__11 .cate-item {
float: left;
padding: 25rpx;
width: 33.3333%;
text-align: center;
box-sizing: border-box;
.image {
display: block;
width: 100%;
height: 33vw;
margin-bottom: 12rpx;
}
.cate-name {
font-size: 28rpx;
color: #555;
}
}
</style>
@@ -0,0 +1,180 @@
<template>
<view v-if="list.length > 0" class="secondary">
<!-- 二级分类 20 -->
<view class="cate-content">
<!-- 左侧 一级分类 -->
<scroll-view class="cate-left" :scroll-y="true" :style="{ height: `${scrollHeight}px` }">
<text class="type-nav" :class="{ selected: curIndex == index }" v-for="(item, index) in list" :key="index"
@click="handleSelectNav(index)">{{ item.name }}</text>
</scroll-view>
<!-- 右侧 二级分类 -->
<scroll-view class="cate-right" :scroll-top="scrollTop" :scroll-y="true" :style="{ height: `${scrollHeight}px` }">
<view v-if="list[curIndex]" class="cate-right-cont">
<view class="cate-two-box">
<view class="cate-cont-box">
<view class="flex-three" v-for="(item, idx) in list[curIndex].children" :key="idx" @click="onTargetGoodsList(item.category_id)">
<view class="cate-img-padding">
<view v-if="item.image" class="cate-img">
<image class="image" mode="scaleToFill" :src="item.image.preview_url"></image>
</view>
</view>
<text class="name oneline-hide">{{ item.name }}</text>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
<empty v-if="!list.length" />
</view>
</template>
<script>
import { PageCategoryStyleEnum } from '@/common/enum/store/page/category'
import Empty from '@/components/empty'
import { rpx2px } from '@/utils/util'
export default {
components: {
Empty
},
props: {
// 分类列表
list: {
type: Array,
default: []
},
},
data() {
return {
// 枚举类
PageCategoryStyleEnum,
// 列表高度
scrollHeight: 0,
// 一级分类:指针
curIndex: 0,
// 内容区竖向滚动条位置
scrollTop: 0,
}
},
created() {
// 设置分类列表高度
this.setListHeight()
},
methods: {
// 设置列表内容的高度
setListHeight() {
const { windowHeight } = uni.getSystemInfoSync()
this.scrollHeight = windowHeight - rpx2px(96)
},
// 一级分类:选中分类
handleSelectNav(index) {
this.curIndex = index
this.scrollTop = 0
},
// 跳转至商品列表页
onTargetGoodsList(categoryId) {
this.$navTo('pages/goods/list', { categoryId })
}
}
}
</script>
<style lang="scss" scoped>
// 分类内容
.cate-content {
display: flex;
z-index: 1;
background: #fff;
padding-top: 96rpx;
}
// 一级分类+二级分类 20
.cate-left {
height: 100%;
display: flex;
flex-direction: column;
flex: 0 0 23%;
background: #f8f8f8;
color: #444;
}
.cate-right {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
.cate-right-cont {
width: 100%;
display: flex;
flex-flow: row wrap;
align-content: flex-start;
padding-top: 15rpx;
.cate-two-box {
width: 100%;
padding: 0 10px;
}
}
}
// 左侧一级分类
.type-nav {
position: relative;
height: 90rpx;
z-index: 10;
display: block;
font-size: 26rpx;
display: flex;
justify-content: center;
align-items: center;
&.selected {
background: #fff;
color: #fa2209;
border-right: none;
font-size: 28rpx;
}
}
// 右侧二级分类
.cate-cont-box {
margin-bottom: 30rpx;
padding-bottom: 10rpx;
background: #fff;
overflow: hidden;
.name {
display: block;
padding-bottom: 30rpx;
text-align: center;
font-size: 26rpx;
color: #444444;
}
.cate-img-padding {
padding: 16rpx 16rpx 4rpx 16rpx;
}
.cate-img {
position: relative;
width: 100%;
padding-top: 100%;
.image {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
border-radius: 10rpx;
}
}
}
</style>
+211
View File
@@ -0,0 +1,211 @@
<template>
<view class="container">
<!-- 搜索框 -->
<search class="search" tips="搜索商品" @event="$navTo('pages/search/index')" />
<select-province class='province' ref="choiceProvince" :provinces="provinceList" :value="province" @change="onProvinceChange" v-model="province" />
<!-- 一级分类 -->
<primary v-if="setting.style == PageCategoryStyleEnum.ONE_LEVEL_BIG.value || setting.style == PageCategoryStyleEnum.ONE_LEVEL_SMALL.value"
:display="setting.style" :list="list" />
<!-- 二级分类 -->
<secondary v-if="setting.style == PageCategoryStyleEnum.TWO_LEVEL.value" :list="list" />
<!-- 分类+商品 -->
<commodity v-if="setting.style == PageCategoryStyleEnum.COMMODITY.value" ref="mescrollItem" :list="list" :setting="setting" />
</view>
</template>
<script>
import SelectProvince from '@/components/select-province/select-province'
import MescrollCompMixin from '@/components/mescroll-uni/mixins/mescroll-comp'
import { setCartTabBadge } from '@/core/app'
import SettingKeyEnum from '@/common/enum/setting/Key'
import { PageCategoryStyleEnum } from '@/common/enum/store/page/category'
import SettingModel from '@/common/model/Setting'
import * as CategoryApi from '@/api/category'
import Empty from '@/components/empty'
import Search from '@/components/search'
import Primary from './components/primary'
import Secondary from './components/secondary'
import Commodity from './components/commodity'
// 最后一次刷新时间
let lastRefreshTime;
export default {
components: {
Search,
Empty,
Primary,
Secondary,
Commodity,
SelectProvince
},
mixins: [MescrollCompMixin],
data() {
return {
// 枚举类
PageCategoryStyleEnum,
// 分类列表
list: [],
// 分类模板设置
setting: {},
trade: {},
// 正在加载中
isLoading: true,
// 地区
provinceList: [],
province: '',
}
},
/**
* 生命周期函数--监听页面加载
*/
onLoad() {
this.initPage()
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
// 每间隔5分钟自动刷新一次页面数据
const curTime = new Date().getTime()
if ((curTime - lastRefreshTime) > 5 * 60 * 1000) {
this.initPage()
}
},
methods: {
async initPage(){
try {
// 第一步:获取商城设置(等待返回)
const setting = await SettingModel.data(false) // 如果你不传参数,默认根据配置决定是否用缓存
console.log('商城设置:', setting)
const trade = setting[SettingKeyEnum.TRADE.value]
this.provinceList = trade.province_list.split(',')
// 第二步:获取本地已选择的省份
const savedProvince = uni.getStorageSync('province')
if (savedProvince) {
this.province = savedProvince
this.onRefreshPage()
} else {
this.$refs.choiceProvince.open()
}
} catch (e) {
console.error('获取商城设置失败:', e)
}
},
// 刷新页面
onRefreshPage() {
// 记录刷新时间
lastRefreshTime = new Date().getTime()
// 获取页面数据
this.getPageData()
// 更新购物车角标
setCartTabBadge()
},
// 获取页面数据
getPageData() {
const app = this
app.isLoading = true
Promise.all([
// 获取分类模板设置
// 优化建议: 可以将此处的false改为true 启用缓存
SettingModel.data(false),
// 获取分类列表
CategoryApi.list()
])
.then(result => {
// 初始化分类模板设置
app.initSetting(result[0])
// 初始化分类列表数据
app.initCategory(result[1])
})
.finally(() => app.isLoading = false)
},
/**
* 初始化分类模板设置
* @param {Object} result
*/
initSetting(setting) {
this.setting = setting[SettingKeyEnum.PAGE_CATEGORY_TEMPLATE.value]
this.trade = setting[SettingKeyEnum.TRADE.value]
this.provinceList = this.trade.province_list.split(',')
},
/**
* 初始化分类列表数据
* @param {Object} result
*/
initCategory(result) {
this.list = result.data.list
},
onProvinceChange(val) {
this.province = val
// 存储省份
uni.setStorageSync('province', val)
// 加载页面数据
this.onRefreshPage()
}
},
/**
* 设置分享内容
*/
onShareAppMessage() {
const app = this
return {
title: _this.templet.shareTitle,
path: '/pages/category/index?' + app.$getShareUrlParams()
}
},
/**
* 分享到朋友圈
* 本接口为 Beta 版本,暂只在 Android 平台支持,详见分享到朋友圈 (Beta)
* https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/share-timeline.html
*/
onShareTimeline() {
const app = this
return {
title: _this.templet.shareTitle,
path: '/pages/category/index?' + app.$getShareUrlParams()
}
}
}
</script>
<style>
page {
background: #fff;
}
</style>
<style lang="scss" scoped>
// 搜索框
.search {
position: fixed;
top: var(--window-top);
left: var(--window-left);
right: var(--window-right);
z-index: 9;
width: 78%;
}
.province {
position: fixed;
top: var(--window-top);
right: var(--window-right);
width: 20%;
height: 40px;
z-index: 9;
line-height: 40px;
}
</style>
+545
View File
@@ -0,0 +1,545 @@
<template>
<view class="container p-bottom" v-if="order.goodsList.length">
<!-- 快递配送:配送地址 -->
<view v-if="curDelivery == DeliveryTypeEnum.EXPRESS.value" @click="onSelectAddress" class="flow-delivery">
<view class="flow-delivery__detail dis-flex flex-y-center">
<view class="detail-location dis-flex">
<text class="iconfont icon-dingwei"></text>
</view>
<view class="detail-content flex-box">
<block v-if="order.address">
<view class="detail-content__title dis-flex">
<text class="f-30">{{ order.address.name }}</text>
<text class="detail-content__title-phone f-28">{{ order.address.phone }}</text>
</view>
<view class="address detail-content__describe">
<text class="region" v-for="(region, idx) in order.address.region" :key="idx">{{ region }}</text>
<text class="detail">{{ order.address.detail }}</text>
</view>
</block>
<block v-else>
<view class="detail-content__describe dis-flex">
<text class="col-6">请选择配送地址</text>
</view>
</block>
</view>
<view class="detail-arrow dis-flex">
<text class="iconfont icon-arrow-right"></text>
</view>
</view>
</view>
<!-- 商品列表 -->
<view class="m-top20">
<view class="checkout_list" v-for="(item, index) in order.goodsList" :key="index">
<view class="flow-shopList dis-flex" data-index="index" @click="onTargetGoods(item.goods_id)">
<!-- 商品图片 -->
<view class="flow-list-left">
<image mode="scaleToFill" :src="item.goods_image"></image>
</view>
<view class="flow-list-right flex-box">
<!-- 商品名称 -->
<text class="goods-name twoline-hide">{{ item.goods_name }}</text>
<!-- 商品规格 -->
<view class="goods-props clearfix">
<view class="goods-props-item" v-for="(props, idx) in item.skuInfo.goods_props" :key="idx">
<text class="group-name">{{ props.group.name }}: </text>
<text>{{ props.value.name }};</text>
</view>
</view>
<!-- 商品数量和单价 -->
<view class="flow-list-cont dis-flex flex-x-between flex-y-center">
<text class="small">×{{ item.total_num }}</text>
<text class="flow-cont" :class="[item.is_user_grade ? 'price-delete' : '']">¥{{ item.goods_price }}</text>
</view>
<!-- 会员折扣价 -->
<view v-if="item.is_user_grade" class="grade-price">
<text>会员折扣价:¥{{ item.grade_goods_price }}</text>
</view>
</view>
</view>
</view>
<view class="flow-num-box b-f">
<text>共{{ order.orderTotalNum }}件商品,合计:</text>
<text class="flow-money col-m">¥{{ order.orderTotalPrice }}</text>
</view>
</view>
<!-- 商品金额 -->
<view class="flow-all-money b-f m-top20">
<view class="flow-all-list dis-flex">
<text class="flex-five">订单总金额:</text>
<view class="flex-five t-r">
<text class="col-m">¥{{ order.orderTotalPrice }}</text>
</view>
</view>
<!-- 优惠券 -->
<view class="flow-all-list dis-flex">
<text class="flex-five">优惠券:</text>
<view class="flex-five t-r">
<view v-if="order.couponList.length > 0" @click="handleShowPopup()">
<text class="col-m" v-if="order.couponId > 0">-¥{{ order.couponMoney }}</text>
<text class="col-m" v-else>有{{ order.couponList.length }}张优惠券</text>
<text class="right-arrow iconfont icon-arrow-right"></text>
</view>
<text v-else class="">无优惠券可用</text>
</view>
</view>
<!-- 积分抵扣 -->
<view v-if="order.isAllowPoints" class="points flow-all-list dis-flex flex-y-center">
<view class="block-left flex-five" @click="handleShowPoints()">
<text class="title">可用{{ setting.points_name }}抵扣:</text>
<text class="iconfont icon-help"></text>
</view>
<view class="flex-five dis-flex flex-x-end flex-y-center">
<text class="points-money col-m">-¥{{ order.pointsMoney }}</text>
<u-switch v-model="isUsePoints" size="48" active-color="#07c160" @change="getOrderData()"></u-switch>
</view>
</view>
<!-- 配送费用 -->
<view v-if="curDelivery == DeliveryTypeEnum.EXPRESS.value" class="dis-flex flow-all-list">
<text class="flex-five">配送费用:</text>
<view class="flex-five t-r">
<view v-if="order.address">
<text class="col-m" v-if="order.isIntraRegion">+¥{{ order.expressPrice }}</text>
<text v-else>不在配送范围</text>
</view>
<view v-else>
<text class="col-7">请先选择配送地址</text>
</view>
</view>
</view>
</view>
<!-- 支付方式 -->
<view class="pay-method flow-all-money b-f m-top20">
<view class="flow-all-list dis-flex">
<text class="flex-five">支付方式</text>
</view>
<!-- 微信支付 -->
<!-- #ifdef MP-WEIXIN -->
<view class="pay-item dis-flex flex-x-between" @click="handleSelectPayType(PayTypeEnum.WECHAT.value)">
<view class="item-left dis-flex flex-y-center">
<view class="item-left_icon wechat">
<text class="iconfont icon-wechat-pay"></text>
</view>
<view class="item-left_text">
<text>{{ PayTypeEnum.WECHAT.name }}</text>
</view>
</view>
<view class="item-right col-m" v-if="curPayType == PayTypeEnum.WECHAT.value">
<text class="iconfont icon-check"></text>
</view>
</view>
<!-- #endif -->
<!-- 余额支付 -->
<view class="pay-item dis-flex flex-x-between" @click="handleSelectPayType(PayTypeEnum.BALANCE.value)">
<view class="item-left dis-flex flex-y-center">
<view class="item-left_icon balance">
<text class="iconfont icon-balance-pay"></text>
</view>
<view class="item-left_text">
<text>{{ PayTypeEnum.BALANCE.name }}</text>
</view>
<view class="user-balance">
<text>(可用¥{{ personal.balance }}元)</text>
</view>
</view>
<view class="item-right col-m" v-if="curPayType == PayTypeEnum.BALANCE.value">
<text class="iconfont icon-check"></text>
</view>
</view>
</view>
<!-- 买家留言 -->
<view class="flow-all-money b-f m-top20">
<view class="ipt-wrapper dis-flex flow-all-list">
<input v-model="remark" placeholder="选填:买家留言(50字以内)"></input>
</view>
</view>
<!-- 提交订单 -->
<view class="flow-fixed-footer b-f m-top10">
<view class="dis-flex chackout-box">
<view class="chackout-left pl-12">实付款:
<text class="col-m">¥{{ order.orderPayPrice }}</text>
</view>
<view class="chackout-right" @click="onSubmitOrder()">
<view class="flow-btn f-32" :class="{ disabled }">提交订单</view>
</view>
</view>
</view>
<!-- 积分说明弹窗 -->
<u-modal v-model="showPoints" :title="`${setting.points_name}说明`">
<scroll-view class="points-content" :scroll-y="true">
<text>{{ setting.points_describe }}</text>
</scroll-view>
</u-modal>
<!-- 优惠券弹出框 -->
<u-popup v-model="showPopup" mode="bottom">
<view class="popup__coupon">
<view class="coupon__title f-30">选择优惠券</view>
<!-- 优惠券列表 -->
<view class="coupon-list">
<scroll-view :scroll-y="true" style="height: 565rpx;">
<view class="coupon-item" v-for="(item, index) in order.couponList" :key="index">
<view class="item-wrapper"
:class="[item.is_apply ? 'color-' + CouponColors[index % CouponColors.length] : 'color-gray']"
@click="handleSelectCoupon(index)">
<view class="coupon-type">{{ CouponTypeEnum[item.coupon_type].name }}</view>
<view class="tip dis-flex flex-dir-column flex-x-center">
<view v-if="item.coupon_type == CouponTypeEnum.FULL_DISCOUNT.value">
<text class="f-30">¥</text>
<text class="money">{{ item.reduce_price }}</text>
</view>
<text class="money"
v-if="item.coupon_type == CouponTypeEnum.DISCOUNT.value">{{ item.discount }}折</text>
<text class="pay-line">满{{ item.min_price }}元可用</text>
</view>
<view class="split-line"></view>
<view class="content dis-flex flex-dir-column flex-x-between">
<view class="title">{{ item.name }}</view>
<view class="bottom dis-flex flex-y-center">
<view class="time flex-box">
<block v-if="item.start_time === item.end_time">{{ item.start_time }}</block>
<block v-else>{{ item.start_time }}~{{ item.end_time }}</block>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
<!-- 不使用优惠券 -->
<view class="coupon__do_not dis-flex flex-y-center flex-x-center">
<view class="control dis-flex flex-y-center flex-x-center" @click="handleNotUseCoupon()">
<text class="f-26">不使用优惠券</text>
</view>
</view>
</view>
</u-popup>
</view>
</template>
<script>
import * as Verify from '@/utils/verify'
import * as CheckoutApi from '@/api/checkout'
import { CouponTypeEnum } from '@/common/enum/coupon'
import DeliveryTypeEnum from '@/common/enum/order/DeliveryType'
import PayTypeEnum from '@/common/enum/order/PayType'
import { wxPayment } from '@/core/app'
const CouponColors = ['red', 'blue', 'violet', 'yellow']
export default {
data() {
return {
// 枚举类
DeliveryTypeEnum,
PayTypeEnum,
CouponTypeEnum,
// 当前页面参数
options: {},
// 优惠券颜色组
CouponColors,
// 当前选中的配送方式
curDelivery: null,
// 当前选中的支付方式
curPayType: PayTypeEnum.BALANCE.value,
// 选择的优惠券
selectCouponId: 0,
// 是否使用积分抵扣
isUsePoints: false,
// 买家留言
remark: '',
// 禁用submit按钮
disabled: false,
// 是否显示积分说明
showPoints: false,
// 是否显示优惠券弹窗
showPopup: false,
// 按钮禁用
disabled: false,
// 订单信息 (从后端api中获取)
order: {
// 商品列表
goodsList: [],
// 优惠券列表
couponList: [],
// 是否存在收货地址
existAddress: false,
// 默认收货地址
address: null,
// 是否存在收货地址
existAddress: false,
// 当前用户收货城市是否存在配送规则中
isIntraRegion: true,
// 是否存在错误
hasError: false,
// 错误信息
errorMsg: ''
},
// 个人信息
personal: {},
// 商城设置
setting: {}
}
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
this.options = options
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
// 获取当前订单信息
this.getOrderData()
},
methods: {
// 获取订单数据
getOrderData() {
const app = this
// 请求的参数
const params = {
delivery: app.curDelivery || 0,
couponId: app.selectCouponId || 0,
isUsePoints: app.isUsePoints ? 1 : 0,
}
// 请求api
CheckoutApi.order(app.options.mode, app.getRequestParam())
.then(result => {
app.initData(result.data)
})
.catch(err => {
// 绑定手机号
if (err.result) {
const errData = err.result.data
if(errData.isBindMobile){
app.navToBindMobile()
}
}
})
},
// 初始化数据
initData({ order, setting, personal }) {
const app = this
app.order = order
app.selectCouponId = order.couponId
app.personal = personal
app.setting = setting
// 显示错误信息
if (order.hasError) {
app.$toast(order.errorMsg)
}
// 当前选择的配送方式
app.curDelivery = order.delivery
// 如果只有一种配送方式则不显示选项卡
app.isShowTab = setting.deliveryType.length > 1
// 当前选择支付方式 (如果是微信小程序默认使用微信支付)
console.log(app.platform)
if (app.platform === 'MP-WEIXIN') {
app.curPayType = PayTypeEnum.WECHAT.value
}
console.log(app.curPayType)
},
// 获取api请求的参数
getRequestParam() {
const app = this
const { options } = app
// 结算模式的固定参数
const modeParam = {}
// 结算模式: 立即购买
if (options.mode === 'buyNow') {
modeParam.goodsId = options.goodsId
modeParam.goodsNum = options.goodsNum
modeParam.goodsSkuId = options.goodsSkuId
}
// 结算模式: 购物车
if (options.mode === 'cart') {
modeParam.cartIds = options.cartIds
}
// 订单结算参数(用户选择)
const orderParam = {
delivery: app.curDelivery || 0,
couponId: app.selectCouponId || 0,
isUsePoints: app.isUsePoints ? 1 : 0,
}
return { ...orderParam, ...modeParam }
},
// 显示积分说明
handleShowPoints() {
this.showPoints = true
},
// 显示优惠券弹窗
handleShowPopup() {
this.showPopup = true
},
// 选择优惠券
handleSelectCoupon(index) {
const app = this
const { couponList } = app.order
// 当前选择的优惠券
const couponItem = couponList[index]
// 判断是否在适用范围
if (!couponItem.is_apply) {
app.$toast(couponItem.not_apply_info)
return
}
// 记录选中的优惠券id
app.selectCouponId = couponItem.user_coupon_id
// 重新获取订单信息
app.getOrderData()
// 隐藏优惠券弹层
app.showPopup = false
},
// 不使用优惠券
handleNotUseCoupon() {
const app = this
app.selectCouponId = -1
// 重新获取订单信息
app.getOrderData()
// 隐藏优惠券弹层
app.showPopup = false
},
// 选择支付方式
handleSelectPayType(value) {
this.curPayType = value
},
// 快递配送:选择收货地址
onSelectAddress() {
this.$navTo('pages/address/index', { from: 'checkout' })
},
// 跳转到商品详情页
onTargetGoods(goodsId) {
this.$navTo('pages/goods/detail', { goodsId })
},
// 订单提交
onSubmitOrder() {
const app = this
if (app.disabled) {
return false
}
// 表单验证
if (!app.onVerifyFrom()) {
return false
}
// 按钮禁用
app.disabled = true
// 请求api
CheckoutApi.submit(app.options.mode, app.getFormData())
.then(result => app.onSubmitCallback(result))
.catch(err => {
console.log(err);
if (err.result) {
const errData = err.result.data
if (errData.is_created) {
app.navToMyOrder()
return false
}else if(errData.isBindMobile){
app.navToBindMobile()
return false
}
}
app.disabled = false
})
},
// 订单提交成功后回调
onSubmitCallback(result) {
const app = this
// 发起微信支付
if (result.data.payType == PayTypeEnum.WECHAT.value) {
wxPayment(result.data.payment)
.then(() => app.$success('支付成功'))
.catch(err => app.$error('订单未支付'))
.finally(() => {
app.disabled = false
app.navToMyOrder()
})
}
// 余额支付
if (result.data.payType == PayTypeEnum.BALANCE.value) {
app.$success('支付成功')
app.disabled = false
app.navToMyOrder()
}
},
// 跳转到我的订单(等待1秒)
navToMyOrder() {
setTimeout(() => {
this.$navTo('pages/order/index')
}, 1000)
},
// 跳转到绑定手机号页面(等待1秒)
navToBindMobile() {
setTimeout(() => {
this.$navTo('pages/user/bind/index')
}, 1000)
},
// 表单提交的数据
getFormData() {
const app = this
const { options } = app
// 表单数据
const form = {
delivery: app.curDelivery,
payType: app.curPayType,
couponId: app.selectCouponId || 0,
isUsePoints: app.isUsePoints ? 1 : 0,
remark: app.remark || '',
}
// 创建订单-立即购买
if (options.mode === 'buyNow') {
form.goodsId = options.goodsId
form.goodsNum = options.goodsNum
form.goodsSkuId = options.goodsSkuId
}
// 创建订单-购物车结算
if (options.mode === 'cart') {
form.cartIds = options.cartIds || null
}
return form
},
// 表单验证
onVerifyFrom() {
const app = this
if (app.hasError) {
app.$toast(app.errorMsg)
return false
}
return true
},
}
}
</script>
<style lang="scss" scoped>
@import "./style.scss";
</style>
+445
View File
@@ -0,0 +1,445 @@
// 配送信息
.flow-delivery {
padding: 34rpx 30rpx;
background: #fff url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAANYAAAANCAYAAADVGpDCAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA4ZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNS1jMDIxIDc5LjE1NTc3MiwgMjAxNC8wMS8xMy0xOTo0NDowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDo3Yjk4M2ExYy1jMDhkLTQ1OTktYTI0Ny1kZjNjYzdiYTQ5ZTgiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NDQwNkY3RkU5N0NGMTFFNUI3N0M4NTU4MzM2RjlFODIiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NDQwNkY3RkQ5N0NGMTFFNUI3N0M4NTU4MzM2RjlFODIiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTQgKE1hY2ludG9zaCkiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDowNzgwZWI1NS03OGFhLTQzOTUtODQ4OC1lOWI5YmVlYTY1ZDciIHN0UmVmOmRvY3VtZW50SUQ9ImFkb2JlOmRvY2lkOnBob3Rvc2hvcDo1OTRiYzUyMy1jMzc3LTExNzgtYTdkZS04NGY3YmM1ZGIxMDMiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz556PLxAAACBElEQVR42tyaSyhEYRTHP48imlKibDQeSSlkSlEWLCRFsZNH5FE2FqQ8ErIRC9lIkTwXSpMkWWChhEJCSnlkoUZGSsr78f98n43CMFPu/Z/6NZuZ2zn33/+cb869XkmLx8IDEQaGQJbgiytQDSY3MyL+LYnL/HxPXSoHDIJQQq2WQQk4Dbbb/yUB29LJ+6e3B66VB3ZITbUIEqSpCGoJBP1ghtBUD6ARpEtTGSEhXzd+awE9oJzQUPegWdf3QlBPMhgDMYRa7YNisGWkpP5qrBQtVBShUHugUE9hs4fUtwG0utlEjRivoA/Ug1sj3vjffr8FNJEK1auPFHcE9UTq5pdK2PwcoAzMG7mjuRrRYEIfK9jiDJSCBZJ6ynSTsBBqNQ0qgdPISbq6vJCFbJOaagrEk5gqWNczRGiqG1Ah1LLMafRkf5pYIUKtZnMJDXUNasAIST2ZYFioRx9ssQaKwJFZEv5uYmWDXVJTrYBEElP562PfPKGpnkAbSDOTqb6aWAGgW6iHol5kQj2CdtAJngnqkc1hHMQRNr9DPaXWzZj8Z2PZtFCxhEIdaKE2CGqRJ4060AH8CLUaALX6f5VpBZLhI9SaeZXQVHKNLt84SCIxVbhQi5YuQlNd6OVElZlN9TGxrGBUn2PZ4lyoTdIsST0FQj0UDSLUak6ot3gcBLVY3wQYAJoVXxmNERajAAAAAElFTkSuQmCC') bottom left repeat-x;
background-size: 120rpx auto;
.detail-location {
font-size: 36rpx;
}
.detail-content {
padding: 0 20rpx;
.detail-content__title-phone {
margin-left: 10rpx;
}
.detail-content__describe {
font-size: 28rpx;
color: #777;
}
}
.detail-content__title {
margin-bottom: 6rpx;
}
}
// 买家留言
.flow-all-money {
.ipt-wrapper {
input {
font-size: 28rpx;
width: 100%;
height: 75rpx;
}
}
}
// 商品列表
.checkout_list {
padding: 20rpx 30rpx 4rpx 30rpx;
background: #fff;
border-bottom: 1rpx solid rgb(248, 248, 248);
.flow-shopList {
padding: 5rpx 0 10rpx;
border-bottom: 1rpx solid rgb(248, 248, 248);
&:last-child {
border-bottom: 0;
}
}
}
.flow-header-left {
padding-left: 90rpx;
}
/* 会员价 */
.flow-shopList {
.flow-list-right {
.flow-cont {
&.price-delete {
font-size: 26rpx;
color: #777;
text-decoration: line-through;
}
}
}
.grade-price {
padding-top: 8rpx;
font-size: 28rpx;
color: #ff495e;
text-align: right;
}
.goods-name{
font-size: 28rpx;
color: #333;
}
}
/* 优惠券选择 */
.popup__coupon {
width: 750rpx;
background: #fff;
box-sizing: border-box;
padding: 30rpx;
.coupon__do_not {
.control {
width: 90%;
height: 72rpx;
margin-bottom: 24rpx;
color: #888;
border: 1rpx solid #e3e3e3;
border-radius: 10rpx;
/* #ifdef H5 */
max-width: 1120rpx;
/* #endif */
}
}
.coupon__title {
text-align: center;
margin-bottom: 30rpx;
}
.coupon-list {
/* #ifdef H5 */
max-width: 1120rpx;
margin: 0 auto;
/* #endif */
}
.coupon-item {
overflow: hidden;
margin-bottom: 22rpx;
}
.item-wrapper {
width: 100%;
display: flex;
background: #fff;
border-radius: 8rpx;
color: #fff;
height: 180rpx;
.coupon-type {
position: absolute;
top: 0;
right: 0;
z-index: 10;
width: 128rpx;
padding: 6rpx 0;
background: #a771ff;
font-size: 20rpx;
text-align: center;
color: #ffffff;
transform: rotate(45deg);
transform-origin: 64rpx 64rpx;
}
&.color-blue {
background: linear-gradient(-125deg, #57bdbf, #2f9de2);
}
&.color-red {
background: linear-gradient(-128deg, #ff6d6d, #ff3636);
}
&.color-violet {
background: linear-gradient(-113deg, #ef86ff, #b66ff5);
.coupon-type {
background: #55b5ff;
}
}
&.color-yellow {
background: linear-gradient(-141deg, #f7d059, #fdb054);
}
&.color-gray {
background: linear-gradient(-113deg, #bdbdbd, #a2a1a2);
.coupon-type {
background: #9e9e9e;
}
}
.content {
flex: 1;
padding: 30rpx 20rpx;
border-radius: 16rpx 0 0 16rpx;
.title {
font-size: 32rpx;
}
.bottom {
.time {
font-size: 24rpx;
}
.receive {
height: 46rpx;
width: 122rpx;
line-height: 46rpx;
text-align: center;
border: 1rpx solid #fff;
border-radius: 30rpx;
color: #fff;
font-size: 24rpx;
&.state {
border: none;
}
}
}
}
.tip {
position: relative;
flex: 0 0 32%;
text-align: center;
border-radius: 0 16rpx 16rpx 0;
.money {
font-weight: bold;
font-size: 52rpx;
}
.pay-line {
font-size: 22rpx;
}
}
.split-line {
position: relative;
flex: 0 0 0;
border-left: 4rpx solid #fff;
margin: 0 10rpx 0 6rpx;
background: #fff;
&:before,
{
border-radius: 0 0 16rpx 16rpx;
top: 0;
}
&:after {
border-radius: 16rpx 16rpx 0 0;
bottom: 0;
}
&:before,
&:after {
content: '';
position: absolute;
width: 24rpx;
height: 12rpx;
background: #f7f7f7;
left: -14rpx;
z-index: 1;
}
}
}
}
/* 积分抵扣 */
.points {
.title {
margin-right: 5rpx;
}
.icon-help {
font-size: 28rpx;
}
.points-money {
margin-right: 20rpx;
}
}
/* 支付方式 */
.pay-method {
.pay-item {
padding: 24rpx 0;
font-size: 28rpx;
border-bottom: 1rpx solid rgb(248, 248, 248);
.item-left_icon {
margin-right: 20rpx;
font-size: 32rpx;
&.wechat {
color: #00c800;
}
&.balance {
color: #ff9700;
}
}
}
.user-balance {
margin-left: 20rpx;
font-size: 26rpx;
// color: #464646;
}
}
// 商品规格
.goods-props {
padding-top: 10rpx;
font-size: 24rpx;
color: #999;
.goods-props-item {
float: left;
.group-name {
margin-right: 6rpx;
}
}
}
// 右侧箭头
.right-arrow {
margin-left: 16rpx;
// color: #777;
font-size: 26rpx;
}
// 底部操作栏
.flow-fixed-footer {
position: fixed;
bottom: var(--window-bottom);
left: var(--window-left);
right: var(--window-right);
// width: 100%;
background: #fff;
border-top: 1px solid #eee;
z-index: 11;
// 设置ios刘海屏底部横线安全区域
padding-bottom: calc(constant(safe-area-inset-bottom) + var(--window-bottom));
padding-bottom: calc(env(safe-area-inset-bottom) + var(--window-bottom));
.chackout-left {
font-size: 28rpx;
line-height: 92rpx;
color: #777;
flex: 4;
padding-left: 12px;
}
.chackout-right {
font-size: 34rpx;
flex: 2;
}
// 提交按钮
.flow-btn {
background: linear-gradient(to right, #f9211c, #ff6335);
color: #fff;
text-align: center;
line-height: 92rpx;
display: block;
font-size: 28rpx;
// 禁用按钮
&.disabled {
background: #ff9779;
}
}
}
// 积分说明
.points-content {
padding: 30rpx 48rpx;
font-size: 28rpx;
line-height: 50rpx;
text-align: left;
color: #606266;
height: 620rpx;
box-sizing: border-box;
}
/* 共几件商品 */
.flow-num-box {
font-size: 28rpx;
color: #777;
padding: 16rpx 24rpx;
text-align: right;
}
/* app.scss */
.flow-shopList {
padding: 18rpx 0;
.flow-list-left {
margin-right: 20rpx;
image {
width: 180rpx;
height: 180rpx;
border: 1rpx solid #eee;
background: #fff;
}
}
.flow-list-right {
.flow-cont {
font-size: 28rpx;
color: #fa2209;
}
.small {
font-size: 26rpx;
color: #777;
}
.flow-list-cont {
padding-top: 10rpx;
}
}
}
.flow-all-money {
padding: 0 24rpx;
color: #444;
.flow-all-list {
font-size: 28rpx;
padding: 20rpx 0;
border-bottom: 1rpx solid rgb(248, 248, 248);
}
.flow-all-list:last-child {
border-bottom: none;
}
.flow-all-list-cont {
font-size: 28rpx;
padding: 10rpx 0;
}
.flow-arrow {
justify-content: flex-end;
align-items: center;
}
}
+274
View File
@@ -0,0 +1,274 @@
<template>
<mescroll-body ref="mescrollRef" :sticky="true" @init="mescrollInit" :down="{ use: false }" :up="upOption" @up="upCallback">
<!-- tab栏 -->
<u-tabs :list="tabs" :is-scroll="false" :current="curTab" active-color="#FA2209" :duration="0.2" @change="onChangeTab" />
<!-- 商品评价列表 -->
<view class="comment-list">
<view class="comment-item" v-for="(item, index) in list.data" :key="index">
<view class="item-head">
<!-- 用户信息 -->
<view class="user-info">
<avatar-image class="user-avatar" :url="item.user.avatar_url" :width="50" />
<text class="user-name f-26">{{ item.user.nick_name }}</text>
</view>
<!-- 评星 -->
<u-rate active-color="#f4a213" :current="rates[item.score]" :disabled="true" />
<!-- 评价日期-->
<view class="flex-box f-22 col-9 t-r">{{ item.create_time }}</view>
</view>
<!-- 评价内容 -->
<view class="item-content m-top20">
<text class="f-26">{{ item.content }}</text>
</view>
<!-- 评价图片 -->
<view class="images-list clearfix" v-if="item.images.length">
<view class="image-preview" v-for="(image, imgIdx) in item.images" :key="imgIdx">
<image class="image" mode="aspectFill" :src="image.image_url" @click="onPreviewImages(index, imgIdx)"></image>
</view>
</view>
<!-- 商品规格 -->
<view class="goods-props clearfix">
<view class="goods-props-item" v-for="(props, idx) in item.orderGoods.goods_props" :key="idx">
<text class="group-name">{{ props.group.name }}: </text>
<text>{{ props.value.name }};</text>
</view>
</view>
</view>
</view>
</mescroll-body>
</template>
<script>
import MescrollBody from '@/components/mescroll-uni/mescroll-body.vue'
import MescrollMixin from '@/components/mescroll-uni/mescroll-mixins'
import AvatarImage from '@/components/avatar-image'
import { getEmptyPaginateObj, getMoreListData } from '@/core/app'
import * as CommentApi from '@/api/comment'
const pageSize = 15
const tabs = [{
name: `全部`,
scoreType: -1
}, {
name: `好评`,
scoreType: 10
}, {
name: `中评`,
scoreType: 20
}, {
name: `差评`,
scoreType: 30
}]
export default {
components: {
MescrollBody,
AvatarImage
},
mixins: [MescrollMixin],
data() {
return {
// 当前商品ID
goodsId: null,
// 当前标签索引
curTab: 0,
// 评价列表数据
list: getEmptyPaginateObj(),
// 评价总数量
total: { all: 0, negative: 0, praise: 0, review: 0 },
// 评星数据转换
rates: { 10: 5, 20: 3, 30: 1 },
// 标签栏数据
tabs,
// 上拉加载配置
upOption: {
// 首次自动执行
auto: true,
// 每页数据的数量; 默认10
page: { size: pageSize },
// 数量要大于4条才显示无更多数据
noMoreSize: 4,
// 空布局
empty: {
tip: '亲,暂无相关商品评价'
}
},
}
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
// 记录属性值
this.goodsId = options.goodsId
// 获取指定评分总数
this.getTotal()
},
methods: {
/**
* 上拉加载的回调 (页面初始化时也会执行一次)
* 其中page.num:当前页 从1开始, page.size:每页数据条数,默认10
* @param {Object} page
*/
upCallback(page) {
const app = this
// 设置列表数据
app.getCommentList(page.num)
.then(list => {
const curPageLen = list.data.length
const totalSize = list.data.total
app.mescroll.endBySize(curPageLen, totalSize)
})
.catch(() => app.mescroll.endErr())
},
// 加载评价列表数据
getCommentList(pageNo = 1) {
const app = this
return new Promise((resolve, reject) => {
CommentApi.list(app.goodsId, { scoreType: app.getScoreType(), page: pageNo }, { load: false })
.then(result => {
// 合并新数据
const newList = result.data.list
app.list.data = getMoreListData(newList, app.list, pageNo)
resolve(newList)
})
})
},
// 评分类型
getScoreType() {
return this.tabs[this.curTab].scoreType
},
// 获取指定评分总数
getTotal() {
const app = this
CommentApi.total(app.goodsId)
.then(result => {
// tab标签内容
const total = result.data.total
app.getTabs(total)
})
},
// 获取tab标签内容
getTabs(total) {
const tabs = this.tabs
tabs[0].name = `全部(${total.all})`
tabs[1].name = `好评(${total.praise})`
tabs[2].name = `中评(${total.review})`
tabs[3].name = `差评(${total.negative})`
},
// 切换标签项
onChangeTab(index) {
const app = this
// 设置当前选中的标签
app.curTab = index
// 刷新评价列表
app.onRefreshList()
},
// 刷新评价列表
onRefreshList() {
this.list = getEmptyPaginateObj()
setTimeout(() => {
this.mescroll.resetUpScroll()
}, 120)
},
// 预览评价图片
onPreviewImages(dataIdx, imgIndex) {
const app = this
const images = app.list.data[dataIdx].images
const imageUrls = images.map(item => item.image_url)
uni.previewImage({
current: imageUrls[imgIndex],
urls: imageUrls
})
}
}
}
</script>
<style lang="scss" scoped>
.comment-item {
padding: 30rpx;
box-sizing: border-box;
border-bottom: 1rpx solid #f7f7f7;
background: #fff;
}
.item-head {
display: flex;
align-items: center;
// 用户信息
.user-info {
margin-right: 15rpx;
display: flex;
align-items: center;
.user-avatar {
margin-right: 15rpx;
}
.user-name {
color: #999;
}
}
}
// 评价内容
.item-content {
font-size: 30rpx;
color: #333;
margin: 16rpx 0;
}
// 评价图片
.images-list {
&::after {
clear: both;
content: " ";
display: table;
}
.image-preview {
float: left;
margin-bottom: 15rpx;
margin-right: 15rpx;
&:nth-child(3n+0) {
margin-right: 0;
}
.image {
display: block;
width: 220rpx;
height: 220rpx;
}
}
}
// 商品规格
.goods-props {
font-size: 24rpx;
color: #999;
.goods-props-item {
float: left;
.group-name {
margin-right: 6rpx;
}
}
}
</style>
+250
View File
@@ -0,0 +1,250 @@
<template>
<view class="container">
<view v-if="list.length" class="coupon-list">
<view class="coupon-item" v-for="(item, index) in list" :key="index">
<view class="item-wrapper"
:class="[ item.state.value ? 'color-' + color[index % color.length] : 'color-gray' ]">
<view class="coupon-type">{{ CouponTypeEnum[item.coupon_type].name }}</view>
<view class="tip dis-flex flex-dir-column flex-x-center">
<view v-if="item.coupon_type == CouponTypeEnum.FULL_DISCOUNT.value">
<text class="f-30">¥</text>
<text class="money">{{ item.reduce_price }}</text>
</view>
<text class="money" v-if="item.coupon_type == CouponTypeEnum.DISCOUNT.value">{{ item.discount }}折</text>
<text class="pay-line">满{{ item.min_price }}元可用</text>
</view>
<view class="split-line"></view>
<view class="content dis-flex flex-dir-column flex-x-between">
<view class="title oneline-hide">{{ item.name }}</view>
<view class="bottom dis-flex flex-y-center">
<view class="time flex-box">
<text v-if="item.expire_type == 10">领取{{ item.expire_day }}天内有效</text>
<text v-if="item.expire_type == 20">
<block v-if="item.start_time === item.end_time">{{ item.start_time }} 当天有效</block>
<block v-else>{{ item.start_time }}~{{ item.end_time }}</block>
</text>
</view>
<view class="receive" v-if="item.state.value" @click="receive(item.coupon_id)">
<text>立即领取</text>
</view>
<view v-else class="receive state">
<text>{{ item.state.text }}</text>
</view>
</view>
</view>
</view>
</view>
</view>
<empty v-if="!list.length" :isLoading="isLoading" />
</view>
</template>
<script>
import * as CouponApi from '@/api/coupon'
import * as MyCouponApi from '@/api/myCoupon'
import { CouponTypeEnum } from '@/common/enum/coupon'
import Empty from '@/components/empty'
const color = ['red', 'blue', 'violet', 'yellow']
export default {
components: {
Empty
},
data() {
return {
// 枚举类
CouponTypeEnum,
// 颜色组
color,
// 优惠券列表
list: [],
// 正在加载中
isLoading: true
}
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
// 获取优惠券列表
this.getCouponList()
},
methods: {
/**
* 获取优惠券列表
* @param {bool} load 是否显示loading弹窗
*/
getCouponList(load = true) {
const app = this
app.isLoading = true
CouponApi.list({}, { load })
.then(result => {
app.list = result.data.list
})
.finally(() => app.isLoading = false)
},
// 立即领取
receive(couponId) {
const app = this
MyCouponApi.receive(couponId)
.then(result => {
// 显示领取成功提示
app.$success(result.message)
// 刷新优惠券列表
app.getCouponList(false)
})
}
}
}
</script>
<style lang="scss" scoped>
.coupon-list {
padding: 20rpx;
}
.coupon-item {
position: relative;
overflow: hidden;
margin-bottom: 22rpx;
}
.item-wrapper {
width: 100%;
display: flex;
background: #fff;
border-radius: 8rpx;
color: #fff;
height: 180rpx;
.coupon-type {
position: absolute;
top: 0;
right: 0;
z-index: 10;
width: 128rpx;
padding: 3px 0;
background: #a771ff;
font-size: 20rpx;
text-align: center;
color: #ffffff;
transform: rotate(45deg);
transform-origin: 64rpx 64rpx;
}
&.color-blue {
background: linear-gradient(-125deg, #57bdbf, #2f9de2);
}
&.color-red {
background: linear-gradient(-128deg, #ff6d6d, #ff3636);
}
&.color-violet {
background: linear-gradient(-113deg, #ef86ff, #b66ff5);
.coupon-type {
background: #55b5ff;
}
}
&.color-yellow {
background: linear-gradient(-141deg, #f7d059, #fdb054);
}
&.color-gray {
background: linear-gradient(-113deg, #bdbdbd, #a2a1a2);
.coupon-type {
background: #9e9e9e;
}
}
.content {
flex: 1;
padding: 30rpx 20rpx;
border-radius: 8px 0 0 8px;
.title {
width: 400rpx;
font-size: 32rpx;
}
.bottom {
.time {
font-size: 24rpx;
}
.receive {
height: 46rpx;
width: 122rpx;
line-height: 46rpx;
text-align: center;
border: 1px solid #fff;
border-radius: 30rpx;
color: #fff;
font-size: 24rpx;
&.state {
border: none;
}
}
}
}
.tip {
position: relative;
flex: 0 0 32%;
text-align: center;
border-radius: 0 8px 8px 0;
.money {
font-weight: bold;
font-size: 52rpx;
}
.pay-line {
font-size: 22rpx;
}
}
.split-line {
position: relative;
flex: 0 0 0;
border-left: 4rpx solid #fff;
margin: 0 5px 0 3px;
background: #fff;
&:before,
{
border-radius: 0 0 16rpx 16rpx;
top: 0;
}
&:after {
border-radius: 16rpx 16rpx 0 0;
bottom: 0;
}
&:before,
&:after {
content: '';
position: absolute;
width: 24rpx;
height: 12rpx;
background: #f7f7f7;
left: -14rpx;
z-index: 1;
}
}
}
</style>
+120
View File
@@ -0,0 +1,120 @@
<template>
<view class="container">
<!-- 店铺页面组件 -->
<Page :items="items" />
</view>
</template>
<script>
import * as Api from '@/api/page'
import Page from '@/components/page'
const App = getApp()
export default {
components: {
Page
},
data() {
return {
// 页面参数
options: {},
// 页面属性
page: {},
// 页面元素
items: []
}
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
// 当前页面参数
this.options = options
// 加载页面数据
this.getPageData()
},
methods: {
/**
* 加载页面数据
* @param {Object} callback
*/
getPageData(callback) {
const app = this
const pageId = app.options.pageId || 0
Api.detail(pageId)
.then(result => {
// 设置页面数据
const { data: { pageData } } = result
app.page = pageData.page
app.items = pageData.items
// 设置顶部导航栏栏
app.setPageBar();
})
.finally(() => callback && callback())
},
/**
* 设置顶部导航栏
*/
setPageBar() {
const { page } = this
// 设置页面标题
uni.setNavigationBarTitle({
title: page.params.title
})
// 设置navbar标题、颜色
uni.setNavigationBarColor({
frontColor: page.style.titleTextColor === 'white' ? '#ffffff' : '#000000',
backgroundColor: page.style.titleBackgroundColor
})
}
},
/**
* 下拉刷新
*/
onPullDownRefresh() {
// 获取首页数据
this.getPageData(() => {
uni.stopPullDownRefresh()
})
},
/**
* 分享当前页面
*/
onShareAppMessage() {
const app = this
const { page } = app
return {
title: page.params.share_title,
path: "/pages/index/index?" + app.$getShareUrlParams()
}
},
/**
* 分享到朋友圈
* 本接口为 Beta 版本,暂只在 Android 平台支持,详见分享到朋友圈 (Beta)
* https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/share-timeline.html
*/
onShareTimeline() {
const app = this
const { page } = app
return {
title: page.params.share_title,
path: "/pages/index/index?" + app.$getShareUrlParams()
}
}
}
</script>
<style lang="scss" scoped>
.container {
background: #fff;
}
</style>
+29
View File
@@ -0,0 +1,29 @@
<template>
</template>
<script>
export default {
data() {
return {
}
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
},
methods: {
}
}
</script>
<style lang="scss" scoped>
</style>
@@ -0,0 +1,162 @@
<template>
<!-- 商品评价 -->
<view v-if="!isLoading && list.length" class="goods-comment m-top20">
<view class="item-title dis-flex">
<view class="block-left flex-box">
商品评价 (<text class="total">{{ total }}条</text>)
</view>
<view class="block-right">
<text @click="onTargetToComment" class="show-more col-9">查看更多</text>
<text class="iconfont icon-arrow-right"></text>
</view>
</view>
<!-- 评论列表 -->
<view class="comment-list">
<view class="comment-item" v-for="(item, index) in list" :key="index">
<view class="comment-item_row dis-flex flex-y-center">
<view class="user-info dis-flex flex-y-center">
<avatar-image class="user-avatar" :url="item.user.avatar_url" :width="50" />
<text class="user-name">{{ item.user.nick_name }}</text>
</view>
<!-- 评星 -->
<view class="star-rating">
<u-rate active-color="#f4a213" :current="rates[item.score]" :disabled="true" />
</view>
</view>
<view class="item-content m-top20">
<text class="f-26 twoline-hide">{{ item.content }}</text>
</view>
<view class="comment-time">{{ item.create_time }}</view>
</view>
</view>
</view>
</template>
<script>
import AvatarImage from '@/components/avatar-image'
import * as CommentApi from '@/api/comment'
export default {
components: {
AvatarImage
},
props: {
// 商品ID
goodsId: {
type: Number,
default: null
},
// 加载多少条记录 默认2条
limit: {
type: Number,
default: 2
}
},
data() {
return {
// 正在加载
isLoading: true,
// 评星数据转换
rates: { 10: 5, 20: 3, 30: 1 },
// 评价列表数据
list: [],
// 评价总数量
total: 0
}
},
created() {
// 加载评价列表数据
this.getCommentList()
},
methods: {
// 加载评价列表数据
getCommentList() {
const app = this
app.isLoading = true
CommentApi.listRows(app.goodsId, app.limit)
.then(result => {
app.list = result.data.list
app.total = result.data.total
})
.catch(err => err)
.finally(() => app.isLoading = false)
},
// 跳转到评论列表页
onTargetToComment() {
const app = this
app.$navTo('pages/comment/index', { goodsId: app.goodsId })
}
}
}
</script>
<style lang="scss" scoped>
.goods-comment {
padding: 20rpx 30rpx;
background-color: #fff;
}
.item-title {
font-size: 28rpx;
margin-bottom: 25rpx;
.total {
margin: 0 4rpx;
}
.show-more {
margin-right: 8rpx;
font-size: 24rpx;
}
}
.comment-item {
padding: 15rpx 5rpx;
margin-bottom: 10rpx;
border-bottom: 1rpx solid #f5f5f5;
&:last-child {
margin-bottom: 0;
border-bottom: none;
}
.comment-item_row {
margin-bottom: 10rpx;
}
}
.user-info {
margin-right: 15rpx;
.user-avatar {
width: 50rpx;
height: 50rpx;
border-radius: 50%;
margin-right: 10rpx;
}
.user-name {
font-size: 24rpx;
}
}
.item-content {
color: #333;
margin: 16rpx 0;
max-height: 76rpx;
line-height: 38rpx;
}
.comment-time {
font-size: 24rpx;
color: #999;
margin-top: 10rpx;
}
</style>
@@ -0,0 +1,158 @@
<template>
<view v-if="list.length" class="service-wrapper">
<!-- 服务简述 -->
<view class="service-simple" @click="handlePopup">
<view class="s-list">
<view class="s-item" v-for="(item, index) in list" :key="index">
<text class="item-icon iconfont icon-fuwu"></text>
<text class="item-val">{{ item.name }}</text>
</view>
</view>
<!-- 扩展箭头 -->
<view class="s-arrow f-26 col-9 t-r">
<text class="iconfont icon-arrow-right"></text>
</view>
</view>
<!-- 详情内容弹窗 -->
<u-popup v-model="showPopup" mode="bottom" :closeable="true" :border-radius="26">
<view class="service-content">
<view class="title">服务</view>
<scroll-view class="content-scroll" :scroll-y="true">
<view class="s-list clearfix">
<view class="s-item" v-for="(item, index) in list" :key="index">
<text class="item-icon iconfont icon-fuwu"></text>
<view class="item-val">{{ item.name }}</view>
<view class="item-summary">{{ item.summary }}</view>
</view>
</view>
</scroll-view>
</view>
</u-popup>
</view>
</template>
<script>
import * as ServiceApi from '@/api/goods/service'
export default {
props: {
// 商品ID
goodsId: {
type: Number,
default: null
}
},
data() {
return {
// 正在加载
isLoading: true,
// 显示详情内容弹窗
showPopup: false,
// 服务列表数据
list: []
}
},
created() {
// 获取商品服务列表
this.getServiceList()
},
methods: {
// 获取商品服务列表
getServiceList() {
const app = this
app.isLoading = true
ServiceApi.list(app.goodsId)
.then(result => app.list = result.data.list)
.finally(() => app.isLoading = false)
},
// 显示弹窗
handlePopup() {
this.showPopup = !this.showPopup
}
}
}
</script>
<style lang="scss" scoped>
.service-wrapper {
min-height: 24rpx;
margin-bottom: -24rpx;
}
// 服务简述
.service-simple {
padding: 24rpx 30rpx;
display: flex;
align-items: center;
.s-list {
flex: 1;
margin-left: -15rpx;
}
.s-item {
float: left;
font-size: 26rpx;
margin: 8rpx 15rpx;
.item-icon {
color: #FA2209;
}
.item-val {
margin-left: 12rpx;
}
}
}
// 服务详细内容
.service-content {
padding: 24rpx;
.title {
font-size: 30rpx;
margin-bottom: 50rpx;
font-weight: bold;
text-align: center;
}
.content-scroll {
min-height: 400rpx;
max-height: 760rpx;
}
.s-list {
padding: 0 30rpx 0 80rpx;
}
.s-item {
position: relative;
margin-bottom: 60rpx;
.item-icon {
position: absolute;
top: 6rpx;
left: -50rpx;
color: #FA2209;
}
.item-val {
font-size: 28rpx;
}
.item-summary {
font-size: 26rpx;
margin-top: 20rpx;
color: #6d6d6d;
}
}
}
</style>
@@ -0,0 +1,173 @@
<template>
<goods-sku-popup :value="value" @input="onChangeValue" border-radius="20" :localdata="goodsInfo" :mode="skuMode"
:maskCloseAble="true" @open="openSkuPopup" @close="closeSkuPopup" @add-cart="addCart" @buy-now="buyNow"
buyNowText="立即购买" />
</template>
<script>
import { setCartTotalNum } from '@/core/app'
import * as CartApi from '@/api/cart'
import GoodsSkuPopup from '@/components/goods-sku-popup'
export default {
components: {
GoodsSkuPopup
},
model: {
prop: 'value',
event: 'input'
},
props: {
// true 组件显示 false 组件隐藏
value: {
Type: Boolean,
default: false
},
// 模式 1:都显示 2:只显示购物车 3:只显示立即购买
skuMode: {
type: Number,
default: 1
},
// 商品详情信息
goods: {
type: Object,
default: {}
}
},
data() {
return {
goodsInfo: {}
}
},
created() {
const app = this
const { goods } = app
app.goodsInfo = {
_id: goods.goods_id,
name: goods.goods_name,
goods_thumb: goods.goods_image,
sku_list: app.getSkuList(),
spec_list: app.getSpecList()
}
},
methods: {
// 监听组件显示隐藏
onChangeValue(val) {
this.$emit('input', val)
},
// 整理商品SKU列表
getSkuList() {
const app = this
const { goods: { goods_name, goods_image, skuList } } = app
const skuData = []
skuList.forEach(item => {
skuData.push({
_id: item.id,
goods_sku_id: item.goods_sku_id,
goods_id: item.goods_id,
goods_name: goods_name,
image: item.image_url ? item.image_url : goods_image,
price: item.goods_price * 100,
stock: item.stock_num,
spec_value_ids: item.spec_value_ids,
sku_name_arr: app.getSkuNameArr(item.spec_value_ids)
})
})
return skuData
},
// 获取sku记录的规格值列表
getSkuNameArr(specValueIds) {
const app = this
const defaultData = ['默认']
const skuNameArr = []
if (specValueIds) {
specValueIds.forEach((valueId, groupIndex) => {
const specValueName = app.getSpecValueName(valueId, groupIndex)
skuNameArr.push(specValueName)
})
}
return skuNameArr.length ? skuNameArr : defaultData
},
// 获取指定的规格值名称
getSpecValueName(valueId, groupIndex) {
const app = this
const { goods: { specList } } = app
const res = specList[groupIndex].valueList.find(specValue => {
return specValue.spec_value_id == valueId
})
return res.spec_value
},
// 整理规格数据
getSpecList() {
const { goods: { specList } } = this
const defaultData = [{ name: '默认', list: [{ name: '默认' }] }]
const specData = []
specList.forEach(group => {
const children = []
group.valueList.forEach(specValue => {
children.push({ name: specValue.spec_value })
})
specData.push({
name: group.spec_name,
list: children
})
})
return specData.length ? specData : defaultData
},
// sku组件 开始-----------------------------------------------------------
openSkuPopup() {
// console.log("监听 - 打开sku组件")
},
closeSkuPopup() {
// console.log("监听 - 关闭sku组件")
},
// 加入购物车按钮
addCart(selectShop) {
const app = this
const { goods_id, goods_sku_id, buy_num } = selectShop
CartApi.add(goods_id, goods_sku_id, buy_num)
.then(result => {
// 显示成功
app.$toast(result.message)
// 隐藏当前弹窗
app.onChangeValue(false)
// 购物车商品总数量
const cartTotal = result.data.cartTotal
// 缓存购物车数量
setCartTotalNum(cartTotal)
// 传递给父级
app.$emit('addCart', cartTotal)
})
},
// 立即购买
buyNow(selectShop) {
// 跳转到订单结算页
this.$navTo('pages/checkout/index', {
mode: 'buyNow',
goodsId: selectShop.goods_id,
goodsSkuId: selectShop.goods_sku_id,
goodsNum: selectShop.buy_num
})
// 隐藏当前弹窗
this.onChangeValue(false)
}
}
}
</script>
<style lang="scss" scoped>
</style>
@@ -0,0 +1,146 @@
<template>
<!-- 商品图片 -->
<view class="images-swiper">
<swiper class="swiper-box" :autoplay="autoplay" :duration="duration" :indicator-dots="indicatorDots"
:interval="interval" :circular="true" @change="setCurrent">
<!-- 主图视频 -->
<swiper-item v-if="video">
<view class="slide-video">
<video id="myVideo" class="video" :poster="videoCover.preview_url" :src="video.external_url" controls
x5-playsinline playsinline webkit-playsinline webkit-playsinline x5-video-player-type="h5"
x5-video-player-fullscreen x5-video-orientation="portrait" :enable-progress-gesture="false"
@play="onVideoPlay"></video>
</view>
</swiper-item>
<!-- 轮播图片 -->
<swiper-item v-for="(item, index) in images" :key="index" @click="onPreviewImages(index)">
<view class="slide-image">
<image class="image" :draggable="false" :src="item.preview_url"></image>
</view>
</swiper-item>
</swiper>
<view class="swiper-count">
<text>{{ currentIndex }}</text>
<text>/</text>
<text>{{ images.length + (video ? 1 : 0) }}</text>
</view>
</view>
</template>
<script>
export default {
props: {
// 主图视频
video: {
type: Object,
default () {
return null
}
},
// 主图视频封面
videoCover: {
type: Object,
default () {
return null
}
},
// 图片轮播
images: {
type: Array,
default: []
}
},
data() {
return {
indicatorDots: true, // 是否显示面板指示点
autoplay: true, // 是否自动切换
interval: 4000, // 自动切换时间间隔
duration: 800, // 滑动动画时长
currentIndex: 1, // 轮播图指针
}
},
methods: {
// 事件:视频开始播放
onVideoPlay(e) {
this.autoplay = false
},
// 设置轮播图当前指针 数字
setCurrent({ detail }) {
const app = this
app.currentIndex = detail.current + 1
},
// 浏览商品图片
onPreviewImages(index) {
const app = this
const imageUrls = []
app.images.forEach(item => {
imageUrls.push(item.preview_url);
});
uni.previewImage({
current: imageUrls[index],
urls: imageUrls
})
}
}
}
</script>
<style lang="scss" scoped>
// swiper组件
.images-swiper {
position: relative;
}
.swiper-box {
width: 100%;
height: 100vw;
/* #ifdef H5 */
max-width: 480px;
max-height: 480px;
margin: 0 auto;
/* #endif */
// 主图视频
.slide-video {
width: 100%;
height: 100%;
.video {
display: block;
width: 100%;
height: 100%;
}
}
// 图片轮播
.slide-image {
position: relative;
width: 100%;
height: 100%;
.image {
display: block;
width: 100%;
height: 100%;
}
}
}
// swiper计数
.swiper-count {
position: absolute;
right: 36rpx;
bottom: 72rpx;
padding: 2rpx 18rpx;
background: rgba(0, 0, 0, 0.363);
border-radius: 50rpx;
color: #fff;
font-size: 26rpx;
}
</style>
+227
View File
@@ -0,0 +1,227 @@
.container {
// 设置ios刘海屏底部横线安全区域
// 110 - 18 + 4
padding-bottom: calc(constant(safe-area-inset-bottom) + 106rpx + 6rpx);
padding-bottom: calc(env(safe-area-inset-bottom) + 106rpx + 6rpx);
}
// 商品信息
.goods-info {
background: #fff;
padding: 25rpx 30rpx;
}
.info-item__top {
min-height: 40rpx;
margin-bottom: 20rpx;
line-height: 1;
}
.floor-price__samll {
font-size: 26rpx;
line-height: 1;
color: #FA2209;
margin-bottom: -10rpx;
}
// 商品价
.floor-price {
color: #FA2209;
margin-right: 15rpx;
font-size: 42rpx;
}
.original-price {
font-size: 26rpx;
text-decoration: line-through;
color: #959595;
margin-right: 15rpx;
margin-bottom: -6rpx;
}
// 会员价标签
.user-grade {
background: #3c3c3c;
border-radius: 6rpx;
padding: 8rpx 14rpx;
margin-right: 15rpx;
font-size: 24rpx;
color: #EEE0C3;
}
.goods-sales {
font-size: 24rpx;
color: #959595;
}
.info-item__name .goods-name {
font-size: 30rpx;
}
/* 商品分享 */
.goods-share__line {
border-left: 1rpx solid #f4f4f4;
height: 60rpx;
margin: 0 30rpx;
}
.goods-share .share-btn {
line-height: normal;
padding: 0;
background: none;
border-radius: 0;
box-shadow: none;
font-size: 8pt;
border: none;
color: #191919;
}
.goods-share .share-btn::after {
border: none;
}
.goods-share .share__icon {
font-size: 40rpx;
margin-bottom: 5rpx;
}
// 商品卖点
.info-item_selling-point {
margin-top: 8rpx;
font-size: 24rpx;
color: #808080;
}
// 选择商品规格
.goods-choice {
padding: 26rpx 30rpx;
font-size: 28rpx;
.spec-list {
display: flex;
align-items: center;
.spec-name {
margin-right: 10rpx;
}
}
}
// 商品详情
.goods-content .item-title {
padding: 26rpx 30rpx;
font-size: 28rpx;
}
// 底部操作栏
.footer-fixed {
position: fixed;
bottom: var(--window-bottom);
left: 0;
right: 0;
display: flex;
z-index: 11;
box-shadow: 0 -4rpx 40rpx 0 rgba(151, 151, 151, 0.24);
background: #fff;
// 设置ios刘海屏底部横线安全区域
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
.footer-container {
width: 100%;
display: flex;
height: 106rpx;
}
// 快捷菜单
.foo-item-fast {
box-sizing: border-box;
min-width: 214rpx;
line-height: 1;
display: flex;
align-items: center;
justify-content: space-evenly;
margin-right: 12rpx;
.fast-item {
position: relative;
padding: 4rpx 0;
line-height: 1;
text-align: center;
width: 84rpx;
&--cart {
margin-left: 6rpx;
.fast-icon { margin-left: -12rpx; }
}
// 角标
.fast-badge {
display: inline-block;
box-sizing: border-box;
min-width: 16px;
padding: 0 3px;
color: #fff;
font-weight: 500;
font-size: 12px;
font-family: -apple-system-font, Helvetica Neue, Arial, sans-serif;
line-height: 1.2;
text-align: center;
background-color: #ee0a24;
border: 1px solid #fff;
border-radius: 999px;
}
.fast-badge--fixed {
position: absolute;
top: 0;
right: 0;
transform-origin: 100%
}
.fast-icon {
font-size: 44rpx;
margin-bottom: 8rpx;
}
.fast-text {
font-size: 22rpx;
}
}
}
// 操作按钮
.foo-item-btn {
flex: 1;
.btn-wrapper {
height: 100%;
display: flex;
align-items: center;
}
.btn-item {
flex: 1;
font-size: 28rpx;
height: 72rpx;
margin-right: 16rpx;
color: #fff;
border-radius: 50rpx;
display: flex;
justify-content: center;
align-items: center;
}
// 立即购买按钮
.btn-item-main {
background: linear-gradient(to right, #f9211c, #ff6335);
}
// 购物车按钮
.btn-item-deputy {
background: linear-gradient(to right, #ffa600, #ffbb00);
}
}
+290
View File
@@ -0,0 +1,290 @@
<template>
<view v-show="!isLoading" class="container">
<!-- 商品图片轮播 -->
<SlideImage v-if="!isLoading" :video="goods.video" :videoCover="goods.videoCover" :images="goods.goods_images" />
<!-- 商品信息 -->
<view v-if="!isLoading" class="goods-info m-top20">
<!-- 价格、销量 -->
<view class="info-item info-item__top dis-flex flex-x-between flex-y-end">
<view class="block-left dis-flex flex-y-center">
<!-- 商品售价 -->
<text class="floor-price__samll">¥</text>
<text class="floor-price">{{ goods.goods_price_min }}</text>
<!-- 会员价标签 -->
<view v-if="goods.is_user_grade" class="user-grade">
<text>会员价</text>
</view>
<!-- 划线价 -->
<text v-if="goods.line_price_min > 0" class="original-price">¥{{ goods.line_price_min }}</text>
</view>
<view class="block-right dis-flex">
<!-- 销量 -->
<view class="goods-sales">
<text>已售{{ goods.goods_sales }}件</text>
</view>
</view>
</view>
<!-- 标题、分享 -->
<view class="info-item info-item__name dis-flex flex-y-center">
<view class="goods-name flex-box">
<text class="twoline-hide">{{ goods.goods_name }}</text>
</view>
<!-- #ifdef MP-WEIXIN -->
<view class="goods-share__line"></view>
<view class="goods-share">
<button class="share-btn dis-flex flex-dir-column" open-type="share">
<text class="share__icon iconfont icon-fenxiang"></text>
<text class="f-24">分享</text>
</button>
</view>
<!-- #endif -->
</view>
<!-- 商品卖点 -->
<view v-if="goods.selling_point" class="info-item info-item_selling-point">
<text>{{ goods.selling_point }}</text>
</view>
</view>
<!-- 选择商品规格 -->
<view v-if="goods.spec_type == 20" class="goods-choice m-top20 b-f" @click="onShowSkuPopup(1)">
<view class="spec-list">
<view class="flex-box">
<text class="col-8">选择:</text>
<text class="spec-name" v-for="(item, index) in goods.specList" :key="index">{{ item.spec_name }}</text>
</view>
<view class="f-26 col-9 t-r">
<text class="iconfont icon-arrow-right"></text>
</view>
</view>
</view>
<!-- 商品服务 -->
<Service v-if="!isLoading" :goods-id="goodsId" />
<!-- 商品SKU弹窗 -->
<SkuPopup v-if="!isLoading" v-model="showSkuPopup" :skuMode="skuMode" :goods="goods" @addCart="onAddCart" />
<!-- 商品评价 -->
<Comment v-if="!isLoading" :goods-id="goodsId" :limit="2" />
<!-- 商品描述 -->
<view v-if="!isLoading" class="goods-content m-top20">
<view class="item-title b-f">
<text>商品描述</text>
</view>
<block v-if="goods.content != ''">
<view class="goods-content__detail b-f">
<mp-html :content="goods.content" />
</view>
</block>
<empty v-else tips="亲,暂无商品描述" />
</view>
<!-- 底部选项卡 -->
<view class="footer-fixed">
<view class="footer-container">
<!-- 导航图标 -->
<view class="foo-item-fast">
<!-- 首页 -->
<view class="fast-item fast-item--home" @click="onTargetHome">
<view class="fast-icon">
<text class="iconfont icon-shouye"></text>
</view>
<view class="fast-text">
<text>首页</text>
</view>
</view>
<!-- 客服 (仅微信小程序端显示) -->
<!-- #ifdef MP-WEIXIN -->
<button class="btn-normal" open-type="contact">
<view class="fast-item">
<view class="fast-icon">
<text class="iconfont icon-kefu1"></text>
</view>
<view class="fast-text">
<text>客服</text>
</view>
</view>
</button>
<!-- #endif -->
<!-- 购物车 -->
<!-- <view class="fast-item fast-item--cart" @click="onTargetCart">
<view v-if="cartTotal > 0" class="fast-badge fast-badge--fixed">{{ cartTotal > 99 ? '99+' : cartTotal }}
</view>
<view class="fast-icon">
<text class="iconfont icon-gouwuche"></text>
</view>
<view class="fast-text">
<text>购物车</text>
</view>
</view> -->
</view>
<!-- 操作按钮 -->
<view class="foo-item-btn">
<view class="btn-wrapper">
<!-- <view class="btn-item btn-item-deputy" @click="onShowSkuPopup(2)">
<text>加入购物车</text>
</view> -->
<view class="btn-item btn-item-main" @click="onShowSkuPopup(3)">
<text>立即购买</text>
</view>
</view>
</view>
</view>
</view>
<!-- 快捷导航 -->
<!-- <shortcut bottom="120rpx" /> -->
</view>
</template>
<script>
import * as GoodsApi from '@/api/goods'
import * as CartApi from '@/api/cart'
// import Shortcut from '@/components/shortcut'
import SlideImage from './components/SlideImage'
import SkuPopup from './components/SkuPopup'
import Comment from './components/Comment'
import Service from './components/Service'
export default {
components: {
// Shortcut,
SlideImage,
SkuPopup,
Comment,
Service
},
data() {
return {
// 正在加载
isLoading: true,
// 当前商品ID
goodsId: null,
// 商品详情
goods: {},
// 购物车总数量
cartTotal: 0,
// 显示/隐藏SKU弹窗
showSkuPopup: false,
// 模式 1:都显示 2:只显示购物车 3:只显示立即购买
skuMode: 1
}
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
// 记录商品ID
this.goodsId = parseInt(options.goodsId)
// 加载页面数据
this.onRefreshPage()
},
methods: {
// 刷新页面数据
onRefreshPage() {
const app = this
app.isLoading = true
Promise.all([app.getGoodsDetail(), app.getCartTotal()])
.finally(() => app.isLoading = false)
},
// 获取商品信息
getGoodsDetail() {
const app = this
return new Promise((resolve, reject) => {
GoodsApi.detail(app.goodsId)
.then(result => {
app.goods = result.data.detail
resolve(result)
})
.catch(reject)
})
},
// 获取购物车总数量
getCartTotal() {
const app = this
return new Promise((resolve, reject) => {
CartApi.total()
.then(result => {
app.cartTotal = result.data.cartTotal
resolve(result)
})
.catch(reject)
})
},
// 更新购物车数量
onAddCart(total) {
this.cartTotal = total
},
/**
* 显示/隐藏SKU弹窗
* @param {skuMode} 模式 1:都显示 2:只显示购物车 3:只显示立即购买
*/
onShowSkuPopup(skuMode = 1) {
this.skuMode = skuMode
this.showSkuPopup = !this.showSkuPopup
},
// 跳转到首页
onTargetHome(e) {
this.$navTo('pages/index/index')
},
// 跳转到购物车页
onTargetCart() {
this.$navTo('pages/cart/index')
},
},
/**
* 分享当前页面
*/
onShareAppMessage() {
const app = this
// 构建页面参数
const params = app.$getShareUrlParams({
goodsId: app.goodsId,
})
return {
title: app.goods.goods_name,
path: `/pages/goods/detail?${params}`
}
},
/**
* 分享到朋友圈
* 本接口为 Beta 版本,暂只在 Android 平台支持,详见分享到朋友圈 (Beta)
* https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/share-timeline.html
*/
onShareTimeline() {
const app = this
// 构建页面参数
const params = app.$getShareUrlParams({
goodsId: app.goodsId,
})
return {
title: app.goods.goods_name,
path: `/pages/goods/detail?${params}`
}
}
}
</script>
<style>
page {
background: #fafafa;
}
</style>
<style lang="scss" scoped>
@import "./detail.scss";
</style>
+473
View File
@@ -0,0 +1,473 @@
<template>
<mescroll-body ref="mescrollRef" :sticky="true" @init="mescrollInit" :down="{ native: true }" @down="downCallback" :up="upOption" @up="upCallback">
<!-- 页面头部 -->
<view class="header">
<search class="search" :tips="options.search ? options.search : '搜索商品'" @event="handleSearch" />
<!-- 切换列表显示方式 -->
<view class="show-view" @click="handleShowView">
<text class="iconfont icon-view-tile" v-if="showView"></text>
<text class="iconfont icon-view-list" v-else></text>
</view>
</view>
<!-- 排序标签 -->
<view class="store-sort">
<view class="sort-item" :class="{ active: sortType === 'all' }" @click="handleSortType('all')">
<text>综合</text>
</view>
<view class="sort-item" :class="{ active: sortType === 'sales' }" @click="handleSortType('sales')">
<text>销量</text>
</view>
<view class="sort-item sort-item-price" :class="{ active: sortType === 'price' }" @click="handleSortType('price')">
<text>价格</text>
<view class="price-arrow">
<view class="icon up" :class="{ active: sortType === 'price' && !sortPrice }">
<text class="iconfont icon-arrow-up"></text>
</view>
<view class="icon down" :class="{ active: sortType === 'price' && sortPrice }">
<text class="iconfont icon-arrow-down"></text>
</view>
</view>
</view>
</view>
<!-- 商品列表 -->
<view class="goods-list clearfix" :class="['column-' + (showView ? '1' : '2')]">
<view class="goods-item" v-for="(item, index) in list.data" :key="index" @click="onTargetDetail(item.goods_id)">
<!-- 单列显示 -->
<view v-if="showView" class="dis-flex">
<!-- 商品图片 -->
<view class="goods-item_left">
<image class="image" :src="item.goods_image"></image>
</view>
<view class="goods-item_right">
<!-- 商品名称 -->
<view class="goods-name">
<text class="twoline-hide">{{ item.goods_name }}</text>
</view>
<view class="goods-item_desc">
<!-- 商品卖点 -->
<view class="desc-selling_point dis-flex">
<text class="oneline-hide">{{ item.selling_point }}</text>
</view>
<!-- 商品销量 -->
<view class="desc-goods_sales dis-flex">
<text>已售{{ item.goods_sales }}件</text>
</view>
<!-- 商品价格 -->
<view class="desc_footer">
<text class="price_x">¥{{ item.goods_price_min }}</text>
<text class="price_y col-9" v-if="item.line_price_min > 0">¥{{ item.line_price_min }}</text>
</view>
</view>
</view>
</view>
<!-- 多列显示 -->
<view v-else class="">
<!-- 商品图片 -->
<view class="goods-image">
<image class="image" mode="aspectFill" :src="item.goods_image"></image>
</view>
<view class="detail">
<!-- 商品标题 -->
<view class="goods-name">
<text class="twoline-hide">{{ item.goods_name }}</text>
</view>
<!-- 商品价格 -->
<view class="detail-price oneline-hide">
<text class="goods-price f-30 col-m">¥{{ item.goods_price_min }}</text>
<text v-if="item.line_price_min > 0" class="line-price col-9 f-24">¥{{ item.line_price_min }}</text>
</view>
</view>
</view>
</view>
</view>
</mescroll-body>
</template>
<script>
import MescrollBody from '@/components/mescroll-uni/mescroll-body.vue'
import MescrollMixin from '@/components/mescroll-uni/mescroll-mixins'
import * as GoodsApi from '@/api/goods'
import { getEmptyPaginateObj, getMoreListData } from '@/core/app'
import Search from '@/components/search'
const pageSize = 15
const showViewKey = 'GoodsList-ShowView';
export default {
components: {
MescrollBody,
Search
},
mixins: [MescrollMixin],
data() {
return {
showView: false, // 列表显示方式 (true列表、false平铺)
sortType: 'all', // 排序类型
sortPrice: false, // 价格排序 (true高到低 false低到高)
options: {}, // 当前页面参数
list: getEmptyPaginateObj(), // 商品列表数据
// 上拉加载配置
upOption: {
// 首次自动执行
auto: true,
// 每页数据的数量; 默认10
page: { size: pageSize },
// 数量要大于4条才显示无更多数据
noMoreSize: 4,
}
}
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
// 记录options
this.options = options
let location = options.location ? options.location : ''
if(location == 1){
uni.setNavigationBarTitle({
title: '当前城市可用油券'
});
}
// 设置默认列表显示方式
this.setShowView()
},
methods: {
/**
* 上拉加载的回调 (页面初始化时也会执行一次)
* 其中page.num:当前页 从1开始, page.size:每页数据条数,默认10
* @param {Object} page
*/
upCallback(page) {
const app = this
// 设置列表数据
app.getGoodsList(page.num)
.then(list => {
const curPageLen = list.data.length
const totalSize = list.data.total
app.mescroll.endBySize(curPageLen, totalSize)
if(curPageLen === 0){
app.$toast('您所在省份暂无油卡')
}
})
.catch(() => app.mescroll.endErr())
},
// 设置默认列表显示方式
setShowView() {
this.showView = uni.getStorageSync(showViewKey) || false
},
/**
* 获取商品列表
* @param {number} pageNo 页码
*/
getGoodsList(pageNo = 1) {
const app = this
console.log(app.options)
const param = {
sortType: app.sortType,
sortPrice: Number(app.sortPrice),
categoryId: app.options.categoryId || 0,
goodsName: app.options.search || '',
location: app.options.location || '',
page: pageNo
}
return new Promise((resolve, reject) => {
GoodsApi.list(param)
.then(result => {
// 合并新数据
const newList = result.data.list
app.list.data = getMoreListData(newList, app.list, pageNo)
resolve(newList)
})
.catch(reject)
})
},
// 切换排序方式
handleSortType(newSortType) {
const app = this
const newSortPrice = newSortType === 'price' ? !app.sortPrice : true
app.sortType = newSortType
app.sortPrice = newSortPrice
// 刷新列表数据
app.list = getEmptyPaginateObj()
app.mescroll.resetUpScroll()
},
// 切换列表显示方式
handleShowView() {
const app = this
app.showView = !app.showView
uni.setStorageSync(showViewKey, app.showView)
},
// 跳转商品详情页
onTargetDetail(goodsId) {
this.$navTo('pages/goods/detail', { goodsId })
},
/**
* 商品搜索
*/
handleSearch() {
const searchPageUrl = 'pages/search/index'
// 判断来源页面
let pages = getCurrentPages()
if (pages.length > 1 &&
pages[pages.length - 2].route === searchPageUrl) {
uni.navigateBack()
return
}
// 跳转到商品搜索页
this.$navTo(searchPageUrl)
}
},
/**
* 设置分享内容
*/
onShareAppMessage() {
// 构建分享参数
return {
title: "全部分类",
path: "/pages/category/index?" + this.$getShareUrlParams()
}
},
/**
* 分享到朋友圈
* 本接口为 Beta 版本,暂只在 Android 平台支持,详见分享到朋友圈 (Beta)
* https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/share-timeline.html
*/
onShareTimeline() {
// 构建分享参数
return {
title: "全部分类",
path: "/pages/category/index?" + this.$getShareUrlParams()
}
}
}
</script>
<style lang="scss" scoped>
// 页面头部
.header {
display: flex;
align-items: center;
background-color: #fff;
// 搜索框
.search {
flex: 1;
}
// 切换显示方式
.show-view {
width: 60rpx;
height: 60rpx;
line-height: 60rpx;
font-size: 36rpx;
color: #505050;
}
}
// 排序组件
.store-sort {
position: sticky;
top: var(--window-top);
display: flex;
padding: 20rpx 0;
font-size: 28rpx;
background: #fff;
color: #000;
z-index: 99;
.sort-item {
flex-basis: 33.3333%;
display: flex;
justify-content: center;
align-items: center;
height: 50rpx;
&.active {
color: #e49a3d;
}
}
.sort-item-price .price-arrow {
margin-left: 20rpx;
font-size: 24rpx;
color: #000;
.icon {
&.active {
color: #e49a3d;
}
&.up {
margin-bottom: -16rpx;
}
&.down {
margin-top: -16rpx;
}
}
}
}
// 商品列表
.goods-list {
padding: 4rpx;
box-sizing: border-box;
}
// 单列显示
.goods-list.column-1 {
.goods-item {
width: 100%;
height: 280rpx;
margin-bottom: 12rpx;
padding: 20rpx;
box-sizing: border-box;
background: #fff;
line-height: 1.6;
&:last-child {
margin-bottom: 0;
}
}
.goods-item_left {
display: flex;
width: 300rpx;
background: #fff;
align-items: center;
.image {
display: block;
width: 240rpx;
height: 240rpx;
}
}
.goods-item_right {
position: relative;
// width: 450rpx;
flex: 1;
.goods-name {
margin-top: 10rpx;
min-height: 68rpx;
line-height: 1.3;
white-space: normal;
color: #484848;
font-size: 26rpx;
}
}
.goods-item_desc {
margin-top: 8rpx;
}
.desc-selling_point {
width: 400rpx;
font-size: 24rpx;
color: #e49a3d;
}
.desc-goods_sales {
color: #999;
font-size: 24rpx;
}
.desc_footer {
font-size: 24rpx;
.price_x {
margin-right: 16rpx;
color: #f03c3c;
font-size: 30rpx;
}
.price_y {
text-decoration: line-through;
}
}
}
// 平铺显示
.goods-list.column-2 {
.goods-item {
width: 50%;
}
}
.goods-item {
float: left;
box-sizing: border-box;
padding: 6rpx;
.goods-image {
position: relative;
width: 100%;
height: 0;
padding-bottom: 100%;
overflow: hidden;
background: #fff;
&:after {
content: '';
display: block;
margin-top: 100%;
}
.image {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
-o-object-fit: cover;
object-fit: cover;
}
}
.detail {
padding: 8rpx;
background: #fff;
.goods-name {
min-height: 68rpx;
line-height: 32rpx;
white-space: normal;
color: #484848;
font-size: 26rpx;
}
.detail-price {
.goods-price {
margin-right: 8rpx;
}
.line-price {
text-decoration: line-through;
}
}
}
}
</style>
+106
View File
@@ -0,0 +1,106 @@
<template>
<view class="container">
<mescroll-body ref="mescrollRef" :sticky="true" @init="mescrollInit" :down="{ use: false }" :up="upOption"
@up="upCallback">
<view class="help cont-box b-f" v-for="(item, index) in list.data" :key="index">
<view class="title">
<text>{{ item.title }}</text>
</view>
<view class="content">
<text>{{ item.content }}</text>
</view>
</view>
</mescroll-body>
</view>
</template>
<script>
import MescrollBody from '@/components/mescroll-uni/mescroll-body.vue'
import MescrollMixin from '@/components/mescroll-uni/mescroll-mixins'
import * as HelpApi from '@/api/help'
import { getEmptyPaginateObj, getMoreListData } from '@/core/app'
const pageSize = 15
export default {
components: {
MescrollBody
},
mixins: [MescrollMixin],
data() {
return {
list: getEmptyPaginateObj(),
// 上拉加载配置
upOption: {
// 首次自动执行
auto: true,
// 每页数据的数量; 默认10
page: { size: pageSize },
// 数量要大于12条才显示无更多数据
noMoreSize: 12,
// 空布局
empty: {
tip: '亲,暂无相关数据'
}
}
}
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {},
methods: {
/**
* 上拉加载的回调 (页面初始化时也会执行一次)
* 其中page.num:当前页 从1开始, page.size:每页数据条数,默认10
* @param {Object} page
*/
upCallback(page) {
const app = this
// 设置列表数据
app.getHelpList(page.num)
.then(list => {
const curPageLen = list.data.length
const totalSize = list.data.total
app.mescroll.endBySize(curPageLen, totalSize)
})
.catch(() => app.mescroll.endErr())
},
// 获取帮助列表
getHelpList(pageNo = 1) {
const app = this
return new Promise((resolve, reject) => {
HelpApi.list({ page: pageNo })
.then(result => {
// 合并新数据
const newList = result.data.list
app.list.data = getMoreListData(newList, app.list, pageNo)
resolve(newList)
})
})
}
}
}
</script>
<style lang="scss" scoped>
.help {
border-bottom: 1rpx solid #f6f6f9;
.title {
font-size: 32rpx;
color: #333;
margin-bottom: 10rpx;
}
.content {
font-size: 26rpx;
color: #666;
}
}
</style>
+132
View File
@@ -0,0 +1,132 @@
<template>
<view class="container">
<!-- 店铺页面组件 -->
<Page :items="items" />
</view>
</template>
<script>
import { setCartTabBadge } from '@/core/app'
import * as Api from '@/api/page'
import Page from '@/components/page'
const App = getApp()
export default {
components: {
Page
},
data() {
return {
// 页面参数
options: {},
// 页面属性
page: {},
// 页面元素
items: []
}
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
// 当前页面参数
this.options = options
// 加载页面数据
this.getPageData();
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
// 更新购物车角标
setCartTabBadge()
},
methods: {
/**
* 加载页面数据
* @param {Object} callback
*/
getPageData(callback) {
const app = this
const pageId = app.options.pageId || 0
Api.detail(pageId)
.then(result => {
// 设置页面数据
const { data: { pageData } } = result
app.page = pageData.page
app.items = pageData.items
// 设置顶部导航栏栏
app.setPageBar();
})
.finally(() => callback && callback())
},
/**
* 设置顶部导航栏
*/
setPageBar() {
const {
page
} = this
// 设置页面标题
uni.setNavigationBarTitle({
title: page.params.title
});
// 设置navbar标题、颜色
uni.setNavigationBarColor({
frontColor: page.style.titleTextColor === 'white' ? '#ffffff' : '#000000',
backgroundColor: page.style.titleBackgroundColor
})
},
},
/**
* 下拉刷新
*/
onPullDownRefresh() {
// 获取首页数据
this.getPageData(() => {
uni.stopPullDownRefresh()
})
},
/**
* 分享当前页面
*/
onShareAppMessage() {
const app = this
const { page } = app
return {
title: page.params.share_title,
path: "/pages/index/index?" + app.$getShareUrlParams()
}
},
/**
* 分享到朋友圈
* 本接口为 Beta 版本,暂只在 Android 平台支持,详见分享到朋友圈 (Beta)
* https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/share-timeline.html
*/
onShareTimeline() {
const app = this
const { page } = app
return {
title: page.params.share_title,
path: "/pages/index/index?" + app.$getShareUrlParams()
}
}
}
</script>
<style lang="scss" scoped>
.container {
background: #fff;
}
</style>
@@ -0,0 +1,350 @@
<template>
<view class="container">
<!-- 页面头部 -->
<view class="header">
<view class="title">
<text>手机号登录</text>
</view>
<view class="sub-title">
<text>未注册的手机号登录后将自动注册</text>
</view>
</view>
<!-- 表单 -->
<view class="login-form">
<!-- 手机号 -->
<view class="form-item">
<input class="form-item--input" type="number" v-model="mobile" maxlength="11" placeholder="请输入手机号码" />
</view>
<!-- 图形验证码 -->
<view class="form-item">
<input class="form-item--input" type="text" v-model="captchaCode" maxlength="5" placeholder="请输入图形验证码" />
<view class="form-item--parts">
<view class="captcha" @click="getCaptcha()">
<image class="image" :src="captcha.base64"></image>
</view>
</view>
</view>
<!-- 短信验证码 -->
<view class="form-item">
<input class="form-item--input" type="number" v-model="smsCode" maxlength="6" placeholder="请输入短信验证码" />
<view class="form-item--parts">
<view class="captcha-sms" @click="handelSmsCaptcha()">
<text v-if="!smsState" class="activate">获取验证码</text>
<text v-else class="un-activate">重新发送({{ times }})秒</text>
</view>
</view>
</view>
<!-- 登录按钮 -->
<view class="login-button" @click="handleLogin">
<text>登录</text>
</view>
</view>
<!-- 微信授权手机号一键登录 -->
<!-- #ifdef MP-WEIXIN -->
<MpWeixinMobile :isParty="isParty" :partyData="partyData" />
<!-- #endif -->
</view>
</template>
<script>
import store from '@/store'
import * as LoginApi from '@/api/login'
import * as CaptchaApi from '@/api/captcha'
import * as Verify from '@/utils/verify'
import MpWeixinMobile from './mp-weixin-mobile'
// 倒计时时长(秒)
const times = 60
// 表单验证场景
const GET_CAPTCHA = 10
const SUBMIT_LOGIN = 20
export default {
components: {
MpWeixinMobile
},
props: {
// 是否存在第三方用户信息
isParty: {
type: Boolean,
default: () => false
},
// 第三方用户信息数据
partyData: {
type: Object
}
},
data() {
return {
// 正在加载
isLoading: false,
// 图形验证码信息
captcha: {},
// 短信验证码发送状态
smsState: false,
// 倒计时
times,
// 手机号
mobile: '',
// 图形验证码
captchaCode: '',
// 短信验证码
smsCode: ''
}
},
/**
* 生命周期函数--监听页面加载
*/
created() {
console.log('main created')
// 获取图形验证码
this.getCaptcha()
},
methods: {
// 获取图形验证码
getCaptcha() {
const app = this
CaptchaApi.image()
.then(result => app.captcha = result.data)
},
// 点击发送短信验证码
handelSmsCaptcha() {
const app = this
if (!app.isLoading && !app.smsState && app.formValidation(GET_CAPTCHA)) {
app.sendSmsCaptcha()
}
},
// 表单验证
formValidation(scene = GET_CAPTCHA) {
const app = this
// 验证获取短信验证码
if (scene === GET_CAPTCHA) {
if (!app.validteMobile(app.mobile) || !app.validteCaptchaCode(app.captchaCode)) {
return false
}
}
// 验证提交登录
if (scene === SUBMIT_LOGIN) {
if (!app.validteMobile(app.mobile) || !app.validteSmsCode(app.smsCode)) {
return false
}
}
return true
},
// 验证手机号
validteMobile(str) {
if (Verify.isEmpty(str)) {
this.$toast('请先输入手机号')
return false
}
if (!Verify.isMobile(str)) {
this.$toast('请输入正确格式的手机号')
return false
}
return true
},
// 验证图形验证码
validteCaptchaCode(str) {
if (Verify.isEmpty(str)) {
this.$toast('请先输入图形验证码')
return false
}
return true
},
// 验证短信验证码
validteSmsCode(str) {
if (Verify.isEmpty(str)) {
this.$toast('请先输入短信验证码')
return false
}
return true
},
// 请求发送短信验证码接口
sendSmsCaptcha() {
const app = this
app.isLoading = true
CaptchaApi.sendSmsCaptcha({
form: {
captchaKey: app.captcha.key,
captchaCode: app.captchaCode,
mobile: app.mobile
}
})
.then(result => {
// 显示发送成功
app.$toast(result.message)
// 执行定时器
app.timer()
})
.catch(() => app.getCaptcha())
.finally(() => app.isLoading = false)
},
// 执行定时器
timer() {
const app = this
app.smsState = true
const inter = setInterval(() => {
app.times = app.times - 1
if (app.times <= 0) {
app.smsState = false
app.times = times
clearInterval(inter)
}
}, 1000)
},
// 点击登录
handleLogin() {
const app = this
if (!app.isLoading && app.formValidation(SUBMIT_LOGIN)) {
app.submitLogin()
}
},
// 确认登录
submitLogin() {
const app = this
app.isLoading = true
store.dispatch('Login', {
smsCode: app.smsCode,
mobile: app.mobile,
isParty: app.isParty,
partyData: app.partyData
})
.then(result => {
// 显示登录成功
app.$toast(result.message)
// 相应全局事件订阅: 刷新上级页面数据
uni.$emit('syncRefresh', true)
// 跳转回原页面
setTimeout(() => app.onNavigateBack(1), 2000)
})
.catch(err => {
// 跳转回原页面
if (err.result.data.isBack) {
setTimeout(() => app.onNavigateBack(1), 2000)
}
})
.finally(() => app.isLoading = false)
},
/**
* 登录成功-跳转回原页面
*/
onNavigateBack(delta = 1) {
const pages = getCurrentPages()
if (pages.length > 1) {
uni.navigateBack({
delta: Number(delta || 1)
})
} else {
this.$navTo('pages/index/index')
}
}
}
}
</script>
<style lang="scss" scoped>
.container {
padding: 100rpx 60rpx;
min-height: 100vh;
background-color: #fff;
}
// 页面头部
.header {
margin-bottom: 60rpx;
.title {
color: #191919;
font-size: 54rpx;
}
.sub-title {
margin-top: 20rpx;
color: #b3b3b3;
font-size: 28rpx;
}
}
// 输入框元素
.form-item {
display: flex;
padding: 18rpx;
border-bottom: 1rpx solid #f3f1f2;
margin-bottom: 30rpx;
height: 96rpx;
&--input {
font-size: 28rpx;
letter-spacing: 1rpx;
flex: 1;
height: 100%;
}
&--parts {
min-width: 100rpx;
height: 100%;
}
// 图形验证码
.captcha {
height: 100%;
.image {
display: block;
width: 192rpx;
height: 100%;
}
}
// 短信验证码
.captcha-sms {
font-size: 28rpx;
line-height: 50rpx;
padding-right: 20rpx;
.activate {
color: #cea26a;
}
.un-activate {
color: #9e9e9e;
}
}
}
// 登录按钮
.login-button {
width: 100%;
height: 86rpx;
margin-top: 80rpx;
background: linear-gradient(to right, #ecb53c, #ff9211);
color: #fff;
border-radius: 80rpx;
box-shadow: 0px 10px 20px 0px rgba(0, 0, 0, 0.1);
letter-spacing: 5rpx;
display: flex;
justify-content: center;
align-items: center;
}
</style>
@@ -0,0 +1,142 @@
<template>
<!-- 微信授权手机号一键登录 -->
<view class="wechat-auth">
<button class="btn-normal" open-type="getPhoneNumber" @getphonenumber="handelMpWeixinMobileLogin($event)" @click="clickPhoneNumber">
<view class="wechat-auth-container">
<image class="icon" src="../../../static/channel/wechat.png"></image>
<text class="title">微信手机号一键登录</text>
</view>
</button>
</view>
</template>
<script>
import store from '@/store'
import { isEmpty } from '@/utils/util'
export default {
props: {
// 是否存在第三方用户信息
isParty: {
type: Boolean,
default: () => false
},
// 第三方用户信息数据
partyData: {
type: Object
}
},
data() {
return {
// 微信小程序登录凭证 (code)
// 提交到后端,用于换取openid
code: ''
}
},
methods: {
// 按钮点击事件: 获取微信手机号按钮
// 实现目的: 在getphonenumber事件触发之前获取微信登录code
// 因为如果在getphonenumber事件中获取code的话,提交到后端的encryptedData会存在解密不了的情况
async clickPhoneNumber() {
this.code = await this.getCode()
},
// 微信授权获取手机号一键登录
// getphonenumber事件的回调方法
async handelMpWeixinMobileLogin({ detail }) {
const app = this
if (detail.errMsg != 'getPhoneNumber:ok') {
console.log('微信授权获取手机号失败', detail.errMsg)
// app.$error(detail.errMsg)
return
}
app.isLoading = true
store.dispatch('LoginMpWxMobile', {
code: app.code,
encryptedData: detail.encryptedData,
iv: detail.iv,
isParty: app.isParty,
partyData: app.partyData,
refereeId: store.getters.refereeId
})
.then(result => {
// 显示登录成功
app.$toast(result.message)
// 相应全局事件订阅: 刷新上级页面数据
uni.$emit('syncRefresh', true)
// 跳转回原页面
setTimeout(() => app.onNavigateBack(1), 2000)
})
.catch(err => {
const resultData = err.result.data
// 显示错误信息
if (isEmpty(resultData)) {
app.$toast(err.result.message)
}
// 跳转回原页面
if (resultData.isBack) {
setTimeout(() => app.onNavigateBack(1), 2000)
}
})
.finally(() => app.isLoading = false)
},
// 获取微信登录的code
// https://developers.weixin.qq.com/miniprogram/dev/api/open-api/login/wx.login.html
getCode() {
return new Promise((resolve, reject) => {
uni.login({
provider: 'weixin',
success: res => {
console.log('code', res.code)
resolve(res.code)
},
fail: reject
})
})
},
/**
* 登录成功-跳转回原页面
*/
onNavigateBack(delta = 1) {
const pages = getCurrentPages()
if (pages.length > 1) {
uni.navigateBack({
delta: Number(delta || 1)
})
} else {
this.$navTo('pages/index/index')
}
}
}
}
</script>
<style lang="scss" scoped>
// 微信授权登录
.wechat-auth {
width: 320rpx;
margin: 50rpx auto 0 auto;
.wechat-auth-container {
display: flex;
justify-content: center;
}
.icon {
width: 38rpx;
height: 38rpx;
margin-right: 15rpx;
}
.title {
font-size: 28rpx;
color: #666666;
}
}
</style>
@@ -0,0 +1,237 @@
<template>
<view class="container">
<view class="wechatapp">
<view class="header">
<!-- <open-data class="avatar" type="userAvatarUrl"></open-data> -->
<image class="image" :src="storeInfo && storeInfo.image_url ? storeInfo.image_url : '/static/default-avatar.png'"></image>
</view>
</view>
<view class="auth-title">申请获取以下权限</view>
<view class="auth-subtitle">获得你的公开信息(昵称、头像等)</view>
<view class="login-btn">
<!-- 获取微信用户信息 -->
<button class="button btn-normal" @click.stop="getUserProfile">授权登录</button>
</view>
<view class="no-login-btn">
<button class="button btn-normal" @click="handleCancel">暂不登录</button>
</view>
</view>
</template>
<script>
import store from '@/store'
import { isEmpty } from '@/utils/util'
import SettingModel from '@/common/model/Setting'
export default {
data() {
return {
// 商城基本信息
storeInfo: undefined,
// 微信小程序登录凭证 (code)
// 提交到后端,用于换取openid
code: ''
}
},
created() {
// 获取商城基本信息
this.getStoreInfo()
},
methods: {
// 获取商城基本信息
getStoreInfo() {
SettingModel.item('store').then(store => this.storeInfo = store)
// SettingModel.h5Url(true)
},
// 获取code
// https://developers.weixin.qq.com/miniprogram/dev/api/open-api/login/wx.login.html
getCode() {
return new Promise((resolve, reject) => {
uni.login({
provider: 'weixin',
success: res => {
console.log('code', res.code)
resolve(res.code)
},
fail: reject
})
})
},
// 获取微信用户信息(新版)
getUserProfile() {
const app = this
wx.canIUse('getUserProfile') && wx.getUserProfile({
lang: 'zh_CN',
desc: '获取用户相关信息',
success({ userInfo }) {
console.log('用户同意了授权')
console.log('userInfo:', userInfo)
// 授权成功事件
app.onAuthSuccess(userInfo)
},
fail() {
console.log('用户拒绝了授权')
}
})
},
// 授权成功事件
// 这里分为两个逻辑:
// 1.将code和userInfo提交到后端,如果存在该用户 则实现自动登录,无需再填写手机号
// 2.如果不存在该用户, 则显示注册页面, 需填写手机号
// 3.如果后端报错了, 则显示错误信息
async onAuthSuccess(userInfo) {
const app = this
// 提交到后端
store.dispatch('LoginMpWx', {
partyData: {
code: await app.getCode(),
oauth: 'MP-WEIXIN',
userInfo
},
refereeId: store.getters.refereeId
})
.then(result => {
// 一键登录成功
app.$toast(result.message)
// 相应全局事件订阅: 刷新上级页面数据
uni.$emit('syncRefresh', true)
// 跳转回原页面
setTimeout(() => app.onNavigateBack(), 2000)
})
.catch(err => {
const resultData = err.result.data
// 显示错误信息
if (isEmpty(resultData)) {
app.$toast(err.result.message)
}
// 跳转回原页面
if (resultData.isBack) {
setTimeout(() => app.onNavigateBack(1), 2000)
}
// 判断还需绑定手机号
if (resultData.isBindMobile) {
app.onEmitSuccess(userInfo)
}
})
},
// 将oauth提交给父级
// 这里要重新获取code, 因为上一次获取的code不能复用(会报错)
async onEmitSuccess(userInfo) {
const app = this
app.$emit('success', {
oauth: 'MP-WEIXIN', // 第三方登录类型: MP-WEIXIN
code: await app.getCode(), // 微信登录的code, 用于换取openid
userInfo // 微信用户信息
})
},
/**
* 暂不登录
*/
handleCancel() {
// 跳转回原页面
this.onNavigateBack()
},
/**
* 登录成功-跳转回原页面
*/
onNavigateBack(delta = 1) {
const pages = getCurrentPages()
if (pages.length > 1) {
uni.navigateBack({
delta: Number(delta || 1)
})
} else {
this.$navTo('pages/index/index')
}
}
}
}
</script>
<style lang="scss" scoped>
.container {
padding: 0 60rpx;
font-size: 32rpx;
background: #fff;
min-height: 100vh;
}
.wechatapp {
padding: 80rpx 0 48rpx;
border-bottom: 1rpx solid #e3e3e3;
margin-bottom: 72rpx;
text-align: center;
.header {
width: 190rpx;
height: 190rpx;
border: 4rpx solid #fff;
margin: 0 auto 0;
border-radius: 50%;
overflow: hidden;
box-shadow: 2rpx 0 10rpx rgba(50, 50, 50, 0.3);
.image {
display: block;
width: 100%;
height: 100%;
}
}
}
.auth-title {
color: #585858;
font-size: 34rpx;
margin-bottom: 40rpx;
}
.auth-subtitle {
color: #888;
margin-bottom: 88rpx;
font-size: 28rpx;
}
.login-btn {
padding: 0 20rpx;
.button {
height: 88rpx;
background: #04be01;
color: #fff;
font-size: 30rpx;
border-radius: 999rpx;
display: flex;
justify-content: center;
align-items: center;
}
}
.no-login-btn {
margin-top: 20rpx;
padding: 0 20rpx;
.button {
height: 88rpx;
background: #dfdfdf;
color: #fff;
font-size: 30rpx;
border-radius: 999rpx;
display: flex;
justify-content: center;
align-items: center;
}
}
</style>
+87
View File
@@ -0,0 +1,87 @@
<template>
<view v-if="isLoad" class="login">
<MpWeixin v-if="isMpWeixinAuth" @success="onGetUserInfoSuccess" />
<Main v-else :isParty="isParty" :partyData="partyData" />
</view>
</template>
<script>
import Main from './components/main'
import MpWeixin from './components/mp-weixin'
import SettingKeyEnum from '@/common/enum/setting/Key'
import SettingModel from '@/common/model/Setting'
export default {
components: {
Main,
MpWeixin
},
data() {
return {
// 数据加载完成 [防止在微信小程序端onLoad和view渲染同步进行]
isLoad: false,
// 是否显示微信小程序授权登录
isMpWeixinAuth: false,
// 是否存在第三方用户信息
isParty: false,
// 第三方用户信息数据
partyData: {}
}
},
/**
* 生命周期函数--监听页面加载
*/
async onLoad(options) {
console.log('login onLoad')
// 设置当前是否显示微信小程序授权登录
await this.setShowUserInfo()
// 数据加载完成
this.isLoad = true
console.log('isLoad', true)
},
methods: {
/**
* 设置当前是否显示微信小程序授权登录
* - 条件1: 只有微信小程序才显示获取用户信息按钮
* - 条件2: 后台设置是否已开启该选项 [后台-客户端-注册设置]
*/
async setShowUserInfo() {
console.log('setShowUserInfo start')
const app = this
// 判断当前客户端是微信小程序, 并且支持getUserProfile接口
const isMpWeixin = app.platform === 'MP-WEIXIN' && wx.canIUse('getUserProfile')
// 获取后台设置
await SettingModel.item(SettingKeyEnum.REGISTER.value, false)
.then(setting => {
app.isMpWeixinAuth = isMpWeixin && setting.isOauthMpweixin
console.log('setShowUserInfo complete')
})
},
// 获取到用户信息的回调函数
onGetUserInfoSuccess({ oauth, code, userInfo }) {
// 记录第三方用户信息数据
this.partyData = { oauth, code, userInfo }
// 显示注册页面
this.onShowRegister()
},
// 显示注册页面
onShowRegister() {
// 是否显示微信小程序授权登录
this.isMpWeixinAuth = false
// 已获取到了第三方用户信息
this.isParty = true
}
}
}
</script>
<style lang="scss" scoped>
</style>
+306
View File
@@ -0,0 +1,306 @@
<template>
<view class="container">
<mescroll-body ref="mescrollRef" :sticky="true" @init="mescrollInit" :down="{ use: false }" :up="upOption"
@up="upCallback">
<!-- tab栏 -->
<u-tabs :list="tabs" :is-scroll="false" :current="curTab" active-color="#FA2209" :duration="0.2"
@change="onChangeTab" />
<!-- 优惠券列表 -->
<view class="coupon-list">
<view class="coupon-item" v-for="(item, index) in list.data" :key="index">
<view class="item-wrapper" :class="['color-' + (item.state.value ? color[index % color.length] : 'gray')]">
<view class="coupon-type">{{ CouponTypeEnum[item.coupon_type].name }}</view>
<view class="tip dis-flex flex-dir-column flex-x-center">
<view v-if="item.coupon_type == CouponTypeEnum.FULL_DISCOUNT.value">
<text class="f-30">¥</text>
<text class="money">{{ item.reduce_price }}</text>
</view>
<text class="money" v-if="item.coupon_type == CouponTypeEnum.DISCOUNT.value">{{ item.discount }}折</text>
<text class="pay-line">满{{ item.min_price }}元可用</text>
</view>
<view class="split-line"></view>
<view class="content dis-flex flex-dir-column flex-x-between">
<view class="title">{{ item.name }}</view>
<view class="bottom dis-flex flex-y-center">
<view class="time flex-box">
<block v-if="item.start_time === item.end_time">{{ item.start_time }} 当天有效</block>
<block v-else>{{ item.start_time }}~{{ item.end_time }}</block>
</view>
<view class="receive state">
<text>{{ item.state.text }}</text>
</view>
</view>
</view>
</view>
</view>
</view>
</mescroll-body>
</view>
</template>
<script>
import MescrollBody from '@/components/mescroll-uni/mescroll-body.vue'
import MescrollMixin from '@/components/mescroll-uni/mescroll-mixins'
import { getEmptyPaginateObj, getMoreListData } from '@/core/app'
import * as MyCouponApi from '@/api/myCoupon'
import { CouponTypeEnum } from '@/common/enum/coupon'
const color = ['red', 'blue', 'violet', 'yellow']
const pageSize = 15
const tabs = [{
name: `未使用`,
value: 'isUnused'
}, {
name: `已使用`,
value: 'isUse'
}, {
name: `已过期`,
value: 'isExpire'
}]
export default {
components: {
MescrollBody
},
mixins: [MescrollMixin],
data() {
return {
// 枚举类
CouponTypeEnum,
// 颜色组
color,
// 标签栏数据
tabs,
// 当前标签索引
curTab: 0,
// 优惠券列表数据
list: getEmptyPaginateObj(),
// 上拉加载配置
upOption: {
// 首次自动执行
auto: true,
// 每页数据的数量; 默认10
page: { size: pageSize },
// 数量要大于4条才显示无更多数据
noMoreSize: 4,
// 空布局
empty: {
tip: '亲,暂无相关优惠券'
}
}
}
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
},
methods: {
/**
* 上拉加载的回调 (页面初始化时也会执行一次)
* 其中page.num:当前页 从1开始, page.size:每页数据条数,默认10
* @param {Object} page
*/
upCallback(page) {
const app = this
// 设置列表数据
app.getCouponList(page.num)
.then(list => {
const curPageLen = list.data.length
const totalSize = list.data.total
app.mescroll.endBySize(curPageLen, totalSize)
})
.catch(() => app.mescroll.endErr())
},
/**
* 获取优惠券列表
*/
getCouponList(pageNo = 1) {
const app = this
return new Promise((resolve, reject) => {
MyCouponApi.list({ dataType: app.getTabValue(), page: pageNo }, { load: false })
.then(result => {
// 合并新数据
const newList = result.data.list
app.list.data = getMoreListData(newList, app.list, pageNo)
resolve(newList)
})
})
},
// 评分类型
getTabValue() {
return this.tabs[this.curTab].value
},
// 切换标签项
onChangeTab(index) {
const app = this
// 设置当前选中的标签
app.curTab = index
// 刷新优惠券列表
app.onRefreshList()
},
// 刷新优惠券列表
onRefreshList() {
this.list = getEmptyPaginateObj()
setTimeout(() => {
this.mescroll.resetUpScroll()
}, 120)
},
}
}
</script>
<style lang="scss" scoped>
.coupon-list {
padding: 20rpx;
}
.coupon-item {
position: relative;
overflow: hidden;
margin-bottom: 22rpx;
}
.item-wrapper {
width: 100%;
display: flex;
background: #fff;
border-radius: 8rpx;
color: #fff;
height: 180rpx;
.coupon-type {
position: absolute;
top: 0;
right: 0;
z-index: 10;
width: 128rpx;
padding: 6rpx 0;
background: #a771ff;
font-size: 20rpx;
text-align: center;
color: #ffffff;
transform: rotate(45deg);
transform-origin: 64rpx 64rpx;
}
&.color-blue {
background: linear-gradient(-125deg, #57bdbf, #2f9de2);
}
&.color-red {
background: linear-gradient(-128deg, #ff6d6d, #ff3636);
}
&.color-violet {
background: linear-gradient(-113deg, #ef86ff, #b66ff5);
.coupon-type {
background: #55b5ff;
}
}
&.color-yellow {
background: linear-gradient(-141deg, #f7d059, #fdb054);
}
&.color-gray {
background: linear-gradient(-113deg, #bdbdbd, #a2a1a2);
.coupon-type {
background: #9e9e9e;
}
}
.content {
flex: 1;
padding: 30rpx 20rpx;
border-radius: 16rpx 0 0 16rpx;
.title {
font-size: 32rpx;
}
.bottom {
.time {
font-size: 24rpx;
}
.receive {
height: 46rpx;
width: 122rpx;
line-height: 46rpx;
text-align: center;
border: 1rpx solid #fff;
border-radius: 30rpx;
color: #fff;
font-size: 24rpx;
&.state {
border: none;
}
}
}
}
.tip {
position: relative;
flex: 0 0 32%;
text-align: center;
border-radius: 0 16rpx 16rpx 0;
.money {
font-weight: bold;
font-size: 52rpx;
}
.pay-line {
font-size: 22rpx;
}
}
.split-line {
position: relative;
flex: 0 0 0;
border-left: 4rpx solid #fff;
margin: 0 10rpx 0 6rpx;
background: #fff;
&:before,
{
border-radius: 0 0 16rpx 16rpx;
top: 0;
}
&:after {
border-radius: 16rpx 16rpx 0 0;
bottom: 0;
}
&:before,
&:after {
content: '';
position: absolute;
width: 24rpx;
height: 12rpx;
background: #f7f7f7;
left: -14rpx;
z-index: 1;
}
}
}
</style>
+136
View File
@@ -0,0 +1,136 @@
<template>
<view class="container">
<!-- 订单页列表 -->
<view class="order-list">
<view class="order-item" v-for="(item, index) in orderList" :key="index" @click="$navTo(item.path)">
<view class="order-item-icon" :style="{ color: item.color }">
<text class="iconfont" :class="item.icon"></text>
</view>
<view class="order-item-name">{{ item.name }}</view>
<view class="icon-arrow">
<text class="iconfont icon-arrow-right"></text>
</view>
</view>
</view>
<!-- 操作按钮 -->
<view class="footer">
<view class="btn-wrapper">
<view class="btn-item btn-item-main" @click="$navTo('pages/index/index')">
<view class="btn-item-icon">
<text class="iconfont icon-shouye2"></text>
</view>
<view class="btn-item-name">
<text>返回首页</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import * as UserApi from '@/api/user'
// 订单页数据
const orderList = [{
name: '商城订单',
icon: 'icon-qpdingdan',
color: 'rgb(253 65 0)',
path: 'pages/order/index'
},
{
name: '充值订单',
icon: 'icon-jifen',
color: 'rgb(191, 150, 7)',
path: 'pages/wallet/recharge/order'
}
]
export default {
data() {
return {
orderList
}
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
// 获取当前用户信息
this.getUserInfo()
},
methods: {
// 获取当前用户信息(验证是否登录)
getUserInfo() {
const app = this
UserApi.info()
},
}
}
</script>
<style lang="scss" scoped>
.order-list {
padding: 0 25rpx;
background-color: #fff;
.order-item {
position: relative;
padding: 26rpx 16rpx;
border-bottom: 1rpx solid #eee;
display: flex;
justify-content: flex-start;
align-items: center;
font-size: 30rpx;
&:last-child {
border-bottom: none;
}
}
.order-item-icon {
font-size: 34rpx;
margin-right: 18rpx;
}
.icon-arrow {
position: absolute;
top: 26rpx;
right: 6rpx;
}
}
// 底部操作栏
.footer {
position: fixed;
bottom: var(--window-bottom);
left: 0;
right: 0;
z-index: 11;
box-shadow: 0 -4rpx 40rpx 0 rgba(151, 151, 151, 0.24);
background: #fff;
// 设置ios刘海屏底部横线安全区域
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
.btn-item {
font-size: 30rpx;
height: 90rpx;
border-radius: 50rpx;
display: flex;
justify-content: center;
align-items: center;
}
.btn-item-icon {
font-size: 34rpx;
margin-right: 18rpx;
}
}
</style>
+571
View File
@@ -0,0 +1,571 @@
<template>
<view v-if="!isLoading" class="container">
<view class="goods-list">
<view class="goods-item" v-for="(item, index) in goodsList" :key="index">
<!-- 商品详情 -->
<view class="goods-main">
<!-- 商品图片 -->
<view class="goods-image">
<image class="image" :src="item.goods_image" mode="scaleToFill"></image>
</view>
<!-- 商品信息 -->
<view class="goods-content">
<view class="goods-title"><text class="twoline-hide">{{ item.goods_name }}</text></view>
<view class="goods-props clearfix">
<view class="goods-props-item" v-for="(props, idx) in item.goods_props" :key="idx">
<text>{{ props.value.name }}</text>
</view>
</view>
</view>
<!-- 交易信息 -->
<view class="goods-trade">
<view class="goods-price">
<text class="unit">¥</text>
<text class="value">{{ item.goods_price }}</text>
</view>
<view class="goods-num">
<text>×{{ item.total_num }}</text>
</view>
</view>
</view>
<!-- 选择评价 -->
<view class="score-row">
<view class="score-item score-praise" :class="{ active: formData[index].score == 10 }"
@click="setScore(index, 10)">
<view class="score">
<text class="score-icon iconfont icon-haoping"></text>
<text class="score-text">好评</text>
</view>
</view>
<view class="score-item score-review" :class="{ active: formData[index].score == 20 }"
@click="setScore(index, 20)">
<view class="score">
<text class="score-icon iconfont icon-zhongping"></text>
<text class="score-text">中评</text>
</view>
</view>
<view class="score-item score-negative" :class="{ active: formData[index].score == 30 }"
@click="setScore(index, 30)">
<view class="score">
<text class="score-icon iconfont icon-chaping"></text>
<text class="score-text">差评</text>
</view>
</view>
</view>
<!-- 评价内容 -->
<view class="form-content">
<textarea class="textarea" v-model="formData[index].content" maxlength="500"
placeholder="请输入评价内容 (留空则不评价)"></textarea>
</view>
<!-- 图片列表 -->
<view class="image-list">
<view class="image-preview" v-for="(image, imageIndex) in formData[index].imageList" :key="imageIndex">
<text class="image-delete iconfont icon-shanchu" @click="deleteImage(index, imageIndex)"></text>
<image class="image" mode="aspectFill" :src="image.path"></image>
</view>
<view v-if="!formData[index].imageList || formData[index].imageList.length < maxImageLength"
class="image-picker" @click="chooseImage(index)">
<text class="choose-icon iconfont icon-camera"></text>
<text class="choose-text">上传图片</text>
</view>
</view>
</view>
</view>
<!-- 底部操作按钮 -->
<view class="footer-fixed">
<view class="btn-wrapper">
<view class="btn-item btn-item-main" :class="{ disabled }" @click="handleSubmit()">确认提交</view>
</view>
</view>
</view>
</template>
<script>
import * as UploadApi from '@/api/upload'
import * as OrderCommentApi from '@/api/order/comment'
const maxImageLength = 6
export default {
data() {
return {
// 正在加载
isLoading: true,
// 当前订单ID
orderId: null,
// 待评价商品列表
goodsList: [],
// 表单数据
formData: [],
// 最大图片数量
maxImageLength,
// 按钮禁用
disabled: false
}
},
/**
* 生命周期函数--监听页面加载
*/
onLoad({ orderId }) {
this.orderId = orderId
// 获取待评价商品列表
this.getGoodsList()
},
methods: {
// 获取待评价商品列表
getGoodsList() {
const app = this
app.isLoading = true
OrderCommentApi.list(app.orderId)
.then(result => {
app.goodsList = result.data.goodsList
app.initFormData()
app.isLoading = false
})
},
// 初始化form数据
initFormData() {
const { goodsList } = this
const formData = goodsList.map(item => {
return {
goods_id: item.goods_id,
order_goods_id: item.order_goods_id,
score: 10,
content: '',
imageList: [],
uploaded: []
}
})
this.formData = formData
},
// 设置评分
setScore(index, score) {
this.formData[index].score = score
},
// 选择图片
chooseImage(index) {
const app = this
const oldImageList = app.formData[index].imageList
// 选择图片
uni.chooseImage({
count: maxImageLength - oldImageList.length,
sizeType: ['original', 'compressed'], // 可以指定是原图还是压缩图,默认二者都有
sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有
success({ tempFiles }) {
// tempFiles = [{path:'xxx', size:100}]
app.formData[index].imageList = oldImageList.concat(tempFiles)
}
});
},
// 删除图片
deleteImage(index, imageIndex) {
this.formData[index].imageList.splice(imageIndex, 1)
},
// 表单提交
handleSubmit() {
const app = this
// 判断是否重复提交
if (app.disabled === true) return false
// 按钮禁用
app.disabled = true
// 判断是否需要上传图片
const imagesLength = app.getImagesLength()
if (imagesLength > 0) {
app.uploadFile()
.then(() => {
console.log('then')
app.onSubmit()
})
.catch(err => {
console.log('catch')
app.disabled = false
if (err.statusCode !== 0) {
app.$toast(err.errMsg)
}
console.log('err', err)
})
} else {
app.onSubmit()
}
},
// 统计图片数量
getImagesLength() {
const { formData } = this
let imagesLength = 0
formData.forEach(item => {
if (item.content.trim()) {
imagesLength += item.imageList.length
}
})
return imagesLength
},
// 提交到后端
onSubmit() {
const app = this
OrderCommentApi.submit(app.orderId, app.formData)
.then(result => {
app.$toast(result.message)
setTimeout(() => {
app.disabled = false
uni.navigateBack()
}, 1500)
})
.catch(err => app.disabled = false)
},
// 上传图片
uploadFile() {
const app = this
const { formData } = app
// 整理上传文件路径
const files = []
formData.forEach((item, index) => {
if (item.content.trim() && item.imageList.length) {
const images = item.imageList.map(image => image)
files.push({ formDataIndex: index, images })
}
})
// 批量上传
return new Promise((resolve, reject) => {
Promise.all(files.map((file, index) => {
return new Promise((resolve, reject) => {
UploadApi.image(file.images)
.then(fileIds => {
app.formData[index].uploaded = fileIds
resolve(fileIds)
})
.catch(reject)
})
}))
.then(resolve, reject)
})
}
}
}
</script>
<style lang="scss" scoped>
.container {
// 设置ios刘海屏底部横线安全区域
padding-bottom: constant(env(safe-area-inset-bottom) + 140rpx);
padding-bottom: calc(env(safe-area-inset-bottom) + 140rpx);
}
.goods-list {
font-size: 28rpx;
padding-top: 30rpx;
}
.goods-item {
width: 94%;
background: #fff;
padding: 24rpx 24rpx;
box-shadow: 0 1rpx 5rpx 0px rgba(0, 0, 0, 0.05);
margin: 0 auto 30rpx auto;
border-radius: 20rpx;
.goods-detail {
padding: 24rpx 20rpx;
.left {
.goods-image {
display: block;
width: 150rpx;
height: 150rpx;
}
}
.right {
padding-left: 20rpx;
}
}
.score-row {
display: flex;
justify-content: space-around;
padding: 24rpx 20rpx;
.score-item {
display: flex;
justify-content: center;
align-items: center;
&.score-praise {
color: #ff4544;
.score-icon {
background: #ff4544;
}
}
&.score-review {
color: #fcb500;
.score-icon {
background: #fcb500;
}
}
&.score-negative {
color: #9b9b9b;
.score-icon {
background: #9b9b9b;
}
}
.score {
padding: 10rpx 20rpx 10rpx 10rpx;
border-radius: 30rpx;
.score-icon {
margin-right: 10rpx;
padding: 10rpx;
border-radius: 50%;
font-size: 30rpx;
color: #fff;
}
}
&.active {
.score {
color: #fff;
}
&.score-praise {
.score {
background: #ff4544;
}
}
&.score-review {
.score {
background: #fcb500;
}
}
&.score-negative {
.score {
background: #9b9b9b;
}
}
}
}
}
// 评价内容
.form-content {
padding: 14rpx 10rpx;
.textarea {
width: 100%;
height: 220rpx;
padding: 12rpx;
border: 1rpx solid #e8e8e8;
border-radius: 5rpx;
box-sizing: border-box;
font-size: 26rpx;
}
}
.image-list {
padding: 0 20rpx;
margin-top: 20rpx;
margin-bottom: -20rpx;
&:after {
clear: both;
content: " ";
display: table;
}
.image {
display: block;
width: 100%;
height: 100%;
}
.image-picker,
.image-preview {
width: 184rpx;
height: 184rpx;
margin-right: 30rpx;
margin-bottom: 30rpx;
float: left;
&:nth-child(3n+0) {
margin-right: 0;
}
}
.image-picker {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border: 1rpx dashed #ccc;
color: #ccc;
.choose-icon {
font-size: 48rpx;
margin-bottom: 6rpx;
}
.choose-text {
font-size: 24rpx;
}
}
.image-preview {
position: relative;
.image-delete {
position: absolute;
top: -15rpx;
right: -15rpx;
height: 42rpx;
width: 42rpx;
line-height: 42rpx;
background: rgba(0, 0, 0, 0.64);
border-radius: 50%;
color: #fff;
font-weight: bolder;
font-size: 22rpx;
z-index: 10;
text-align: center;
}
}
}
}
// 商品项
.goods-main {
display: flex;
margin-bottom: 20rpx;
// 商品图片
.goods-image {
width: 180rpx;
height: 180rpx;
.image {
display: block;
width: 100%;
height: 100%;
border-radius: 8rpx;
}
}
// 商品内容
.goods-content {
flex: 1;
padding-left: 16rpx;
padding-top: 16rpx;
.goods-title {
font-size: 26rpx;
max-height: 76rpx;
}
.goods-props {
margin-top: 14rpx;
height: 40rpx;
color: #ababab;
font-size: 24rpx;
overflow: hidden;
.goods-props-item {
display: inline-block;
margin-right: 14rpx;
padding: 4rpx 16rpx;
border-radius: 12rpx;
background-color: #F5F5F5;
width: auto;
}
}
}
// 交易信息
.goods-trade {
padding-top: 16rpx;
width: 150rpx;
text-align: right;
color: $uni-text-color-grey;
font-size: 26rpx;
.goods-price {
vertical-align: bottom;
margin-bottom: 16rpx;
.unit {
margin-right: -2rpx;
font-size: 24rpx;
}
}
}
}
// 底部操作栏
.footer-fixed {
position: fixed;
bottom: var(--window-bottom);
left: 0;
right: 0;
height: 96rpx;
z-index: 11;
box-shadow: 0 -4rpx 40rpx 0 rgba(151, 151, 151, 0.24);
background: #fff;
// 设置ios刘海屏底部横线安全区域
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
.btn-wrapper {
height: 100%;
display: flex;
align-items: center;
padding: 0 20rpx;
}
.btn-item {
flex: 1;
font-size: 28rpx;
height: 80rpx;
line-height: 80rpx;
text-align: center;
color: #fff;
border-radius: 50rpx;
}
.btn-item-main {
background: linear-gradient(to right, #f9211c, #ff6335);
// 禁用按钮
&.disabled {
background: #ff9779;
}
}
}
</style>
+915
View File
@@ -0,0 +1,915 @@
<template>
<view v-if="!isLoading" class="container">
<view class="header">
<!-- 订单状态 -->
<view class="order-status">
<view class="status-icon">
<!-- 进行中的订单 -->
<block v-if="order.order_status == OrderStatusEnum.NORMAL.value">
<!-- 待支付 -->
<block v-if="order.pay_status == PayStatusEnum.PENDING.value">
<image class="image" src="/static/order/status/wait_pay.png" mode="aspectFit"></image>
</block>
<!-- 待发货 -->
<block v-else-if="order.delivery_status == DeliveryStatusEnum.NOT_DELIVERED.value">
<image class="image" src="/static/order/status/wait_deliver.png" mode="aspectFit"></image>
</block>
<!-- 待收货 -->
<block v-else-if="order.receipt_status == ReceiptStatusEnum.NOT_RECEIVED.value">
<image class="image" src="/static/order/status/wait_receipt.png" mode="aspectFit"></image>
</block>
</block>
<!-- 已完成 -->
<block v-if="order.order_status == OrderStatusEnum.COMPLETED.value">
<image class="image" src="/static/order/status/received.png" mode="aspectFit"></image>
</block>
<!-- 已取消/待取消 -->
<block v-if="order.order_status == OrderStatusEnum.CANCELLED.value || order.order_status == OrderStatusEnum.APPLY_CANCEL.value">
<image class="image" src="/static/order/status/close.png" mode="aspectFit"></image>
</block>
</view>
<view class="status-text">
<text>{{ order.state_text }}</text>
</view>
</view>
<!-- 下一步操作 -->
<view class="next-action" v-if="order.order_status == OrderStatusEnum.NORMAL.value">
<view v-if="order.pay_status == PayStatusEnum.PENDING.value" class="action-btn" @click="onPay()">去支付</view>
<view v-if="order.delivery_status == DeliveryStatusEnum.DELIVERED.value && order.receipt_status == ReceiptStatusEnum.NOT_RECEIVED.value" class="action-btn" @click="onReceipt(order.order_id)">确认收货</view>
</view>
</view>
<!-- 快递配送:配送地址 -->
<view class="delivery-address i-card" v-if="order.address">
<view class="link-man">
<text class="name">{{ order.address.name }}</text>
<text class="phone">{{ order.address.phone }}</text>
</view>
<view class="address">
<text class="region" v-for="(region, idx) in order.address.region" :key="idx">{{ region }}</text>
<text class="detail">{{ order.address.detail }}</text>
</view>
</view>
<!-- 物流信息 -->
<view v-if="order.delivery_type == DeliveryTypeEnum.EXPRESS.value && order.delivery_status == DeliveryStatusEnum.DELIVERED.value && order.express" class="express i-card" @click="handleTargetExpress()">
<view class="main">
<view class="info-item">
<view class="item-lable">物流公司</view>
<view class="item-content">
<text>{{ order.express.express_name }}</text>
</view>
</view>
<view class="info-item">
<view class="item-lable">物流单号</view>
<view class="item-content">
<text>{{ order.express_no }}</text>
<view class="act-copy" @click.stop="handleCopy(order.express_no)">
<text>复制</text>
</view>
</view>
</view>
</view>
<view class="right-arrow">
<text class="iconfont icon-arrow-right"></text>
</view>
</view>
<!-- 商品列表 -->
<view class="goods-list i-card">
<view class="goods-item" v-for="(goods, idx) in order.goods" :key="idx">
<view class="goods-main" @click="handleTargetGoods(goods.goods_id)">
<!-- 商品图片 -->
<view class="goods-image">
<image class="image" :src="goods.goods_image" mode="scaleToFill"></image>
</view>
<!-- 商品信息 -->
<view class="goods-content">
<view class="goods-title"><text class="twoline-hide">{{ goods.goods_name }}</text></view>
<view class="goods-props clearfix">
<view class="goods-props-item" v-for="(props, idx) in goods.goods_props" :key="idx">
<text>{{ props.value.name }}</text>
</view>
</view>
</view>
<!-- 交易信息 -->
<view class="goods-trade">
<view class="goods-price">
<text class="unit">¥</text>
<text class="value">{{ goods.is_user_grade ? goods.grade_goods_price : goods.goods_price }}</text>
</view>
<view class="goods-num">
<text>×{{ goods.total_num }}</text>
</view>
</view>
</view>
<!-- 申请支付码 -->
<view class="goods-refund">
<text v-if="goods.use_status" class="stata-text">已加油</text>
<!-- #ifdef MP-WEIXIN -->
<view v-else-if="order.order_status === 10 && goods.oil_status" class="action-btn" @click.stop="layerPayQrcode(goods.order_goods_id)">核销码</view>
<!-- #endif -->
<!-- #ifdef !MP-WEIXIN -->
<view v-else-if="order.order_status === 10 && goods.oil_status" class="action-btn" @click.stop="handlePayQrcode(goods.order_goods_id)">核销码</view>
<!-- #endif -->
</view>
<!-- 商品售后 -->
<!-- <view class="goods-refund">
<text v-if="goods.refund" class="stata-text">已申请售后</text>
<view v-else-if="order.isAllowRefund" class="action-btn" @click.stop="handleApplyRefund(goods.order_goods_id)">申请售后</view>
</view> -->
<!-- 过期剩余时间 -->
<view v-if="order.order_status === 10 && goods.oil_status && !goods.use_status" class="order-countdown">
<text class="m-r-6">剩余时间</text>
<count-down :date="goods.expiration_time" separator="zh" theme="text" />
</view>
</view>
</view>
<!-- 订单信息 -->
<view class="order-info i-card">
<view class="info-item">
<view class="item-lable">订单编号</view>
<view class="item-content">
<text>{{ order.order_no }}</text>
<view class="act-copy" @click="handleCopy(order.order_no)">
<text>复制</text>
</view>
</view>
</view>
<view class="info-item">
<view class="item-lable">下单时间</view>
<view class="item-content">
<text>{{ order.create_time }}</text>
</view>
</view>
<view class="info-item">
<view class="item-lable">买家留言</view>
<view class="item-content">
<text>{{ order.buyer_remark ? order.buyer_remark : '--' }}</text>
</view>
</view>
</view>
<!-- 结算信息 -->
<view class="trade-info i-card">
<view class="info-item">
<view class="item-lable">订单金额</view>
<view class="item-content">
<text>¥{{ order.total_price }}</text>
</view>
</view>
<view v-if="order.coupon_money > 0" class="info-item">
<view class="item-lable">优惠券抵扣</view>
<view class="item-content">
<text>-¥{{ order.coupon_money }}</text>
</view>
</view>
<view v-if="order.points_money > 0" class="info-item">
<view class="item-lable">{{ setting.points_name }}抵扣</view>
<view class="item-content">
<text>-¥{{ order.points_money }}</text>
</view>
</view>
<view class="info-item">
<view class="item-lable">运费</view>
<view class="item-content">
<text>+¥{{ order.express_price }}</text>
</view>
</view>
<view v-if="order.update_price.value != '0.00'" class="info-item">
<view class="item-lable">后台改价</view>
<view class="item-content">
<text>{{ order.update_price.symbol }}</text>
<text>¥{{ order.update_price.value }}</text>
</view>
</view>
<view class="divider"></view>
<view class="trade-total">
<text class="lable">实付款</text>
<view class="goods-price">
<text class="unit">¥</text>
<text class="value">{{ order.pay_price }}</text>
</view>
</view>
</view>
<!-- 底部操作按钮 -->
<view v-if="order.order_status != OrderStatusEnum.CANCELLED.value" class="footer-fixed">
<view class="btn-wrapper">
<!-- 未支付取消订单 -->
<!-- <block v-if="order.pay_status == PayStatusEnum.PENDING.value">
<view class="btn-item" @click="onCancel(order.order_id)">取消</view>
</block> -->
<!-- 已支付进行中的订单 -->
<!-- <block v-if="order.order_status != OrderStatusEnum.APPLY_CANCEL.value">
<block v-if="order.pay_status == PayStatusEnum.SUCCESS.value && order.delivery_status == DeliveryStatusEnum.NOT_DELIVERED.value">
<view class="btn-item" @click="onCancel(order.order_id)">申请取消</view>
</block>
</block> -->
<!-- 已申请取消 -->
<!-- <view v-else class="f-28 col-8">取消申请中</view> -->
<!-- 未支付的订单 -->
<!-- <block v-if="order.pay_status == PayStatusEnum.PENDING.value">
<view class="btn-item active" @click="onPay()">去支付</view>
</block> -->
<!-- 确认收货 -->
<!-- <block v-if="order.delivery_status == DeliveryStatusEnum.DELIVERED.value && order.receipt_status == ReceiptStatusEnum.NOT_RECEIVED.value">
<view class="btn-item active" @click="onReceipt(order.order_id)">确认收货</view>
</block> -->
<!-- 订单评价 -->
<!-- <block v-if="order.order_status == OrderStatusEnum.COMPLETED.value && order.is_comment == 0">
<view class="btn-item" @click="handleTargetComment(order.order_id)">评价</view>
</block> -->
</view>
</view>
<!-- 支付方式弹窗 -->
<u-popup v-model="showPayPopup" mode="bottom" border-radius="26" :closeable="true">
<view class="pay-popup">
<view class="title">请选择支付方式</view>
<view class="pop-content">
<!-- 微信支付 -->
<!-- #ifdef MP-WEIXIN -->
<view class="pay-item dis-flex flex-x-between" @click="onSelectPayType(PayTypeEnum.WECHAT.value)">
<view class="item-left dis-flex flex-y-center">
<view class="item-left_icon wechat">
<text class="iconfont icon-wechat-pay"></text>
</view>
<view class="item-left_text">
<text>{{ PayTypeEnum.WECHAT.name }}</text>
</view>
</view>
</view>
<!-- #endif -->
<!-- 余额支付 -->
<view class="pay-item dis-flex flex-x-between" @click="onSelectPayType(PayTypeEnum.BALANCE.value)">
<view class="item-left dis-flex flex-y-center">
<view class="item-left_icon balance">
<text class="iconfont icon-balance-pay"></text>
</view>
<view class="item-left_text">
<text>{{ PayTypeEnum.BALANCE.name }}</text>
</view>
</view>
</view>
</view>
</view>
</u-popup>
</view>
</template>
<script>
import {
DeliveryStatusEnum,
DeliveryTypeEnum,
OrderStatusEnum,
PayStatusEnum,
PayTypeEnum,
ReceiptStatusEnum
} from '@/common/enum/order'
import * as OrderApi from '@/api/order'
import { wxPayment } from '@/core/app'
import CountDown from '@/components/countdown'
export default {
components: {
CountDown
},
data() {
return {
// 枚举类
DeliveryStatusEnum,
DeliveryTypeEnum,
OrderStatusEnum,
PayStatusEnum,
PayTypeEnum,
ReceiptStatusEnum,
// 当前订单ID
orderId: null,
// 加载中
isLoading: true,
// 当前订单详情
order: {},
// 当前设置
setting: {},
// 支付方式弹窗
showPayPopup: false,
// 控制onShow事件是否刷新订单信息
canReset: false,
}
},
/**
* 生命周期函数--监听页面加载
*/
onLoad({ orderId }) {
// 当前订单ID
this.orderId = orderId
// 获取当前订单信息
this.getOrderDetail()
// 注册全局事件订阅: 是否刷新当前订单数据
uni.$on('syncRefresh', (val, isCur) => {
if (!isCur) {
this.canReset = val
}
})
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
this.canReset && this.getOrderDetail()
this.canReset = false
},
methods: {
// 获取当前订单信息
getOrderDetail(canReset = false) {
const app = this
app.isLoading = true
OrderApi.detail(app.orderId)
.then(result => {
app.order = result.data.order
app.setting = result.data.setting
app.isLoading = false
})
// 相应全局事件订阅: 刷新上级页面数据
canReset && uni.$emit('syncRefresh', true, true)
},
// 获取核销码,弹窗展示二维码
layerPayQrcode(orderGoodsId) {
uni.navigateTo({
url: `/pages/qrcode/index?id=`+orderGoodsId
});
},
// 获取核销码,跳转第三方连接
handlePayQrcode(orderGoodsId) {
const app = this
app.isLoading = true
OrderApi.qrCode(app.orderId, {orderGoodsId: orderGoodsId})
.then(result => {
uni.navigateTo({
url: `/pages/webview/webview?url=`+encodeURIComponent(result.data.qrCode)
});
app.isLoading = false
})
},
// 复制指定内容
handleCopy(value) {
const app = this
uni.setClipboardData({
data: value,
success() {
app.$toast('复制成功')
}
})
},
// 跳转到物流跟踪页面
handleTargetExpress() {
this.$navTo('pages/order/express/index', { orderId: this.orderId })
},
// 跳转到商品详情页面
handleTargetGoods(goodsId) {
this.$navTo('pages/goods/detail', { goodsId })
},
// 跳转到申请售后页面
handleApplyRefund(orderGoodsId) {
this.$navTo('pages/refund/apply', { orderGoodsId })
},
// 取消订单
onCancel(orderId) {
const app = this
uni.showModal({
title: '友情提示',
content: '确认要取消该订单吗?',
success(o) {
if (o.confirm) {
OrderApi.cancel(orderId)
.then(result => {
// 显示成功信息
app.$toast(result.message)
// 刷新当前订单数据
app.getOrderDetail(true)
})
}
}
});
},
// 确认收货
onReceipt(orderId) {
const app = this
uni.showModal({
title: '友情提示',
content: '确认收到商品了吗?',
success(o) {
if (o.confirm) {
OrderApi.receipt(orderId)
.then(result => {
// 显示成功信息
app.$success(result.message)
// 刷新当前订单数据
app.getOrderDetail(true)
})
}
}
});
},
// 点击去支付
onPay() {
// 显示支付方式弹窗
this.showPayPopup = true
},
// 选择支付方式
onSelectPayType(payType) {
const app = this
// 隐藏支付方式弹窗
this.showPayPopup = false
// 发起支付请求
OrderApi.pay(app.orderId, payType)
.then(result => app.onSubmitCallback(result))
.catch(err => err)
},
// 订单提交成功后回调
onSubmitCallback(result) {
const app = this
// 发起微信支付
if (result.data.pay_type == PayTypeEnum.WECHAT.value) {
wxPayment(result.data.payment)
.then(() => {
app.$success('支付成功')
setTimeout(() => app.getOrderDetail(true), 1500)
})
.catch(err => app.$error('订单未支付'))
.finally(() => app.disabled = false)
}
// 余额支付
if (result.data.pay_type == PayTypeEnum.BALANCE.value) {
app.$success('支付成功')
app.disabled = false
// 刷新当前订单数据
setTimeout(() => app.getOrderDetail(true), 1500)
}
},
// 跳转到订单评价页
handleTargetComment(orderId) {
this.$navTo('pages/order/comment/index', { orderId })
},
},
}
</script>
<style>
page {
background: #f4f4f4;
}
</style>
<style lang="scss" scoped>
.container {
// 设置ios刘海屏底部横线安全区域
padding-bottom: constant(env(safe-area-inset-bottom) + 106rpx + 6rpx);
padding-bottom: calc(env(safe-area-inset-bottom) + 106rpx + 6rpx);
}
// 页面顶部
.header {
display: flex;
justify-content: space-between;
background-color: #e8c269;
height: 280rpx;
padding: 56rpx 30rpx 0 30rpx;
.order-status {
display: flex;
align-items: center;
height: 128rpx;
.status-icon {
width: 128rpx;
height: 128rpx;
.image {
display: block;
width: 100%;
height: 100%;
}
}
.status-text {
padding-left: 20rpx;
color: #fff;
font-size: 38rpx;
font-weight: bold;
}
}
.next-action {
display: flex;
align-items: center;
height: 128rpx;
.action-btn {
min-width: 152rpx;
height: 56rpx;
padding: 0 30rpx;
line-height: 56rpx;
background-color: #fff;
text-align: center;
border-radius: 28rpx;
border-color: rgb(102, 102, 102);
cursor: pointer;
user-select: none;
color: #c7a157;
}
}
}
// 通栏卡片
.i-card {
background: #fff;
padding: 24rpx 24rpx;
width: 94%;
box-shadow: 0 1rpx 5rpx 0px rgba(0, 0, 0, 0.05);
margin: 0 auto 20rpx auto;
border-radius: 20rpx;
}
// 收货地址
.delivery-address {
margin-top: -50rpx;
.link-man {
line-height: 46rpx;
color: #333;
.name {
margin-right: 10rpx;
}
}
.address {
margin-top: 12rpx;
color: #999;
font-size: 24rpx;
.detail {
margin-left: 6rpx;
}
}
}
// 物流公司
.express {
display: flex;
align-items: center;
.main {
flex: 1;
}
.info-item {
display: flex;
margin-bottom: 24rpx;
&:last-child {
margin-bottom: 0;
}
.item-lable {
display: flex;
align-items: center;
font-size: 24rpx;
color: #999;
margin-right: 30rpx;
}
.item-content {
flex: 1;
display: flex;
align-items: center;
font-size: 26rpx;
color: #333;
.act-copy {
margin-left: 20rpx;
padding: 2rpx 20rpx;
font-size: 22rpx;
color: #666;
border: 1rpx solid #c1c1c1;
border-radius: 16rpx;
}
}
}
// 右侧箭头
.right-arrow {
margin-left: 16rpx;
// color: #777;
font-size: 26rpx;
}
}
// 商品列表
.goods-list {
// 商品项
.goods-item {
margin-bottom: 40rpx;
&:last-child {
margin-bottom: 0;
}
// 商品信息
.goods-main {
display: flex;
}
// 商品图片
.goods-image {
width: 180rpx;
height: 180rpx;
.image {
display: block;
width: 100%;
height: 100%;
border-radius: 8rpx;
}
}
// 商品内容
.goods-content {
flex: 1;
padding-left: 16rpx;
padding-top: 16rpx;
.goods-title {
font-size: 26rpx;
max-height: 76rpx;
}
.goods-props {
margin-top: 14rpx;
height: 40rpx;
color: #ababab;
font-size: 24rpx;
overflow: hidden;
.goods-props-item {
display: inline-block;
margin-right: 14rpx;
padding: 4rpx 16rpx;
border-radius: 12rpx;
background-color: #F5F5F5;
width: auto;
}
}
}
// 交易信息
.goods-trade {
padding-top: 16rpx;
width: 150rpx;
text-align: right;
color: $uni-text-color-grey;
font-size: 26rpx;
.goods-price {
vertical-align: bottom;
margin-bottom: 16rpx;
.unit {
margin-right: -2rpx;
font-size: 24rpx;
}
}
}
// 商品售后
.goods-refund {
display: flex;
justify-content: flex-end;
.stata-text {
font-size: 24rpx;
color: #999;
}
.action-btn {
border-radius: 28rpx;
padding: 8rpx 26rpx;
font-size: 24rpx;
color: #383838;
border: 1rpx solid #a8a8a8;
}
}
.order-countdown {
display: flex;
justify-content: flex-end;
font-size: 14px;
color: #fa2209;
align-items: center;
gap: 10px;
margin-top: 20px;
}
}
}
// 订单信息
.order-info {
.info-item {
display: flex;
margin-bottom: 24rpx;
&:last-child {
margin-bottom: 0;
}
.item-lable {
display: flex;
align-items: center;
font-size: 24rpx;
color: #999;
margin-right: 30rpx;
}
.item-content {
flex: 1;
display: flex;
align-items: center;
font-size: 26rpx;
color: #333;
.act-copy {
margin-left: 20rpx;
padding: 2rpx 20rpx;
font-size: 22rpx;
color: #666;
border: 1rpx solid #c1c1c1;
border-radius: 16rpx;
}
}
}
}
// 交易信息
.trade-info {
.info-item {
display: flex;
margin-bottom: 24rpx;
.item-lable {
font-size: 24rpx;
color: #999;
margin-right: 24rpx;
}
.item-content {
flex: 1;
font-size: 26rpx;
color: #333;
text-align: right;
}
}
.divider {
height: 1rpx;
background: #f1f1f1;
margin-bottom: 24rpx;
}
.trade-total {
display: flex;
justify-content: flex-end;
.goods-price {
margin-left: 12rpx;
vertical-align: bottom;
color: $uni-text-color-active;
.unit {
margin-right: -2rpx;
font-size: 24rpx;
}
}
}
}
/* 底部操作栏 */
.footer-fixed {
position: fixed;
bottom: var(--window-bottom);
left: 0;
right: 0;
z-index: 11;
box-shadow: 0 -4rpx 40rpx 0 rgba(151, 151, 151, 0.24);
background: #fff;
// 设置ios刘海屏底部横线安全区域
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
.btn-wrapper {
height: 106rpx;
display: flex;
align-items: center;
justify-content: flex-end;
padding: 0 30rpx;
}
.btn-item {
min-width: 164rpx;
border-radius: 28rpx;
padding: 10rpx 24rpx;
font-size: 28rpx;
color: #383838;
text-align: center;
border: 1rpx solid #a8a8a8;
margin-left: 24rpx;
&.active {
color: #fff;
border: none;
background: linear-gradient(to right, #f9211c, #ff6335);
}
}
}
// 弹出层-支付方式
.pay-popup {
padding: 24rpx;
.title {
font-size: 30rpx;
margin-bottom: 50rpx;
font-weight: bold;
text-align: center;
}
.pop-content {
min-height: 260rpx;
padding: 0 10rpx;
.pay-item {
padding: 20rpx 35rpx;
font-size: 28rpx;
border-bottom: 1rpx solid #f1f1f1;
&:last-child {
border-bottom: none;
}
.item-left_icon {
margin-right: 20rpx;
font-size: 32rpx;
&.wechat {
color: #00c800;
}
&.balance {
color: #ff9700;
}
}
}
}
}
//
</style>
+202
View File
@@ -0,0 +1,202 @@
<template>
<view v-if="!isLoading" class="container">
<!-- 物流信息 -->
<view class="express i-card">
<view class="info-item">
<view class="item-lable">物流公司</view>
<view class="item-content">
<text>{{ express.express_name }}</text>
</view>
</view>
<view class="info-item">
<view class="item-lable">物流单号</view>
<view class="item-content">
<text>{{ express.express_no }}</text>
<view class="act-copy" @click.stop="handleCopy(express.express_no)">
<text>复制</text>
</view>
</view>
</view>
</view>
<!-- 物流动态 -->
<view class="logis-detail">
<view class="logis-item" :class="{ first: index === 0 }" v-for="(item, index) in express.list" :key="index">
<view class="logis-item-content">
<view class="logis-item-content__describe">
<text class="f-26">{{ item.context }}</text>
</view>
<view class="logis-item-content__time">
<text class="f-22">{{ item.ftime }}</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import * as OrderApi from '@/api/order'
export default {
data() {
return {
// 正在加载
isLoading: true,
// 当前订单ID
orderId: null,
// 物流信息
express: {}
}
},
/**
* 生命周期函数--监听页面加载
*/
onLoad({ orderId }) {
this.orderId = orderId
// 获取当前订单的物流信息
this.getExpress()
},
methods: {
// 获取当前订单的物流信息
getExpress() {
const app = this
app.isLoading = true
OrderApi.express(app.orderId)
.then(result => {
app.express = result.data.express
app.isLoading = false
})
},
// 复制指定内容
handleCopy(value) {
const app = this
uni.setClipboardData({
data: value,
success() {
app.$toast('复制成功')
}
})
}
}
}
</script>
<style lang="scss" scoped>
// 通栏卡片
.i-card {
background: #fff;
padding: 24rpx 24rpx;
// width: 94%;
box-shadow: 0 1rpx 5rpx 0px rgba(0, 0, 0, 0.05);
// margin: 0 auto 20rpx auto;
// border-radius: 20rpx;
}
// 物流公司
.express {
// margin-top: 24rpx;
.info-item {
display: flex;
margin-bottom: 24rpx;
&:last-child {
margin-bottom: 0;
}
.item-lable {
display: flex;
align-items: center;
font-size: 24rpx;
color: #999;
margin-right: 30rpx;
}
.item-content {
flex: 1;
display: flex;
align-items: center;
font-size: 26rpx;
color: #333;
.act-copy {
margin-left: 20rpx;
padding: 2rpx 20rpx;
font-size: 22rpx;
color: #666;
border: 1rpx solid #c1c1c1;
border-radius: 16rpx;
}
}
}
}
.logis-detail {
padding: 30rpx;
background-color: #fff;
.logis-item {
position: relative;
padding: 10px 0 10px 25px;
box-sizing: border-box;
border-left: 2px solid #ccc;
&.first {
border-left: 2px solid #f40;
&:after {
background: #f40;
}
.logis-item-content {
background: #ff6e39;
color: #fff;
&:after {
border-bottom-color: #ff6e39;
}
}
}
&:after {
content: ' ';
display: inline-block;
position: absolute;
left: -6px;
top: 30px;
width: 6px;
height: 6px;
border-radius: 10px;
background: #bdbdbd;
border: 2px solid #fff;
}
.logis-item-content {
position: relative;
background: #f9f9f9;
padding: 10rpx 20rpx;
box-sizing: border-box;
color: #666;
&:after {
content: '';
display: inline-block;
position: absolute;
left: -10px;
top: 18px;
border-left: 10px solid #fff;
border-bottom: 10px solid #f3f3f3;
}
}
}
}
</style>
+676
View File
@@ -0,0 +1,676 @@
<template>
<view class="container">
<mescroll-body ref="mescrollRef" :sticky="true" @init="mescrollInit" :down="{ native: true }" @down="downCallback" :up="upOption" @up="upCallback">
<!-- tab栏 -->
<u-tabs :list="tabs" :is-scroll="false" :current="curTab" active-color="#FA2209" :duration="0.2" @change="onChangeTab" />
<!-- 订单列表 -->
<view class="order-list">
<view class="order-item" v-for="(item, index) in list.data" :key="index">
<view class="item-top">
<view class="item-top-left">
<text class="order-time">{{ item.create_time }}</text>
</view>
<view class="item-top-right">
<text class="state-text">{{ item.state_text }}</text>
</view>
</view>
<!-- 商品列表 -->
<view class="goods-list" @click="handleTargetDetail(item.order_id)">
<view class="goods-child" v-for="(goods, idx) in item.goods" :key="idx">
<view class="goods-item">
<!-- 商品图片 -->
<view class="goods-image">
<image class="image" :src="goods.goods_image" mode="scaleToFill"></image>
</view>
<!-- 商品信息 -->
<view class="goods-content">
<view class="goods-title"><text class="twoline-hide">{{ goods.goods_name }}</text></view>
<view class="goods-props clearfix">
<view class="goods-props-item" v-for="(props, idx) in goods.goods_props" :key="idx">
<text>{{ props.value.name }}</text>
</view>
</view>
</view>
<!-- 交易信息 -->
<view class="goods-trade">
<view class="goods-price">
<text class="unit">¥</text>
<text class="value">{{ goods.is_user_grade ? goods.grade_goods_price : goods.goods_price }}</text>
</view>
<view class="goods-num">
<text>×{{ goods.total_num }}</text>
</view>
<!-- 申请支付码 -->
<view class="goods-refund">
<text v-if="goods.use_status" class="stata-text">已加油</text>
<!-- #ifdef MP-WEIXIN -->
<view v-else-if="item.order_status === 10 && goods.oil_status" class="action-btn" @click.stop="layerPayQrcode(goods.order_goods_id)">核销码</view>
<!-- #endif -->
<!-- #ifdef !MP-WEIXIN -->
<view v-else-if="item.order_status === 10 && goods.oil_status" class="action-btn" @click.stop="handlePayQrcode(goods.order_goods_id)">核销码</view>
<!-- #endif -->
</view>
</view>
</view>
<!-- 过期剩余时间 -->
<view v-if="item.order_status === 10 && goods.oil_status && !goods.use_status" class="order-countdown">
<text class="m-r-6">剩余时间</text>
<count-down :date="goods.expiration_time" separator="zh" theme="text" />
</view>
</view>
</view>
<!-- 订单合计 -->
<view class="order-total">
<text>共{{ item.total_num }}件商品,总金额</text>
<text class="unit">¥</text>
<text class="money">{{ item.pay_price }}</text>
</view>
<!-- 订单操作 -->
<view v-if="item.order_status != OrderStatusEnum.CANCELLED.value" class="order-handle">
<view class="btn-group clearfix">
<!-- 未支付取消订单 -->
<!--<block v-if="item.pay_status == PayStatusEnum.PENDING.value">
<view class="btn-item" @click="onCancel(item.order_id)">取消</view>
</block>-->
<!-- 已支付进行中的订单 -->
<!-- <block v-if="item.order_status != OrderStatusEnum.APPLY_CANCEL.value">
<block v-if="item.pay_status == PayStatusEnum.SUCCESS.value && item.delivery_status == DeliveryStatusEnum.NOT_DELIVERED.value">
<view class="btn-item" @click="onCancel(item.order_id)">申请取消</view>
</block>
</block> -->
<!-- 已申请取消 -->
<!-- <view v-else class="f-28 col-8">取消申请中</view> -->
<!-- 未支付的订单 -->
<!-- <block v-if="item.pay_status == PayStatusEnum.PENDING.value">
<view class="btn-item active" @click="onPay(item.order_id)">去支付</view>
</block> -->
<!-- 确认收货 -->
<!-- <block v-if="item.delivery_status == DeliveryStatusEnum.DELIVERED.value && item.receipt_status == ReceiptStatusEnum.NOT_RECEIVED.value">
<view class="btn-item active" @click="onReceipt(item.order_id)">确认收货</view>
</block> -->
<!-- 订单评价 -->
<!-- <block v-if="item.order_status == OrderStatusEnum.COMPLETED.value && item.is_comment == 0">
<view class="btn-item" @click="handleTargetComment(item.order_id)">评价</view>
</block> -->
</view>
</view>
</view>
</view>
</mescroll-body>
<!-- 支付方式弹窗 -->
<u-popup v-model="showPayPopup" mode="bottom" border-radius="26" :closeable="true">
<view class="pay-popup">
<view class="title">请选择支付方式</view>
<view class="pop-content">
<!-- 微信支付 -->
<!-- #ifdef MP-WEIXIN -->
<view class="pay-item dis-flex flex-x-between" @click="onSelectPayType(PayTypeEnum.WECHAT.value)">
<view class="item-left dis-flex flex-y-center">
<view class="item-left_icon wechat">
<text class="iconfont icon-wechat-pay"></text>
</view>
<view class="item-left_text">
<text>{{ PayTypeEnum.WECHAT.name }}</text>
</view>
</view>
</view>
<!-- #endif -->
<!-- 余额支付 -->
<view class="pay-item dis-flex flex-x-between" @click="onSelectPayType(PayTypeEnum.BALANCE.value)">
<view class="item-left dis-flex flex-y-center">
<view class="item-left_icon balance">
<text class="iconfont icon-balance-pay"></text>
</view>
<view class="item-left_text">
<text>{{ PayTypeEnum.BALANCE.name }}</text>
</view>
</view>
</view>
</view>
</view>
</u-popup>
</view>
</template>
<script>
import {
DeliveryStatusEnum,
DeliveryTypeEnum,
OrderStatusEnum,
PayStatusEnum,
PayTypeEnum,
ReceiptStatusEnum
} from '@/common/enum/order'
import MescrollBody from '@/components/mescroll-uni/mescroll-body.vue'
import MescrollMixin from '@/components/mescroll-uni/mescroll-mixins'
import { getEmptyPaginateObj, getMoreListData } from '@/core/app'
import * as OrderApi from '@/api/order'
import { wxPayment } from '@/core/app'
import CountDown from '@/components/countdown'
// 每页记录数量
const pageSize = 15
// tab栏数据
const tabs = [{
name: `全部`,
value: 'all'
}, {
name: `待支付`,
value: 'payment'
},
{
name: `进行中`,
value: 'progress'
},
{
name: `已完成`,
value: 'complete'
}
/*{
name: `待发货`,
value: 'delivery'
}, {
name: `待收货`,
value: 'received'
}, {
name: `待评价`,
value: 'comment'
}*/]
export default {
components: {
MescrollBody,
CountDown
},
mixins: [MescrollMixin],
data() {
return {
// 枚举类
DeliveryStatusEnum,
DeliveryTypeEnum,
OrderStatusEnum,
PayStatusEnum,
PayTypeEnum,
ReceiptStatusEnum,
// 当前页面参数
options: { dataType: 'all' },
// tab栏数据
tabs,
// 当前标签索引
curTab: 0,
// 订单列表数据
list: getEmptyPaginateObj(),
// 上拉加载配置
upOption: {
// 首次自动执行
auto: true,
// 每页数据的数量; 默认10
page: { size: pageSize },
// 数量要大于4条才显示无更多数据
noMoreSize: 4,
// 空布局
empty: {
tip: '亲,暂无订单记录'
}
},
// 控制onShow事件是否刷新订单列表
canReset: false,
// 支付方式弹窗
showPayPopup: false
}
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
// 初始化当前选中的标签
this.initCurTab(options)
// 注册全局事件订阅: 是否刷新订单列表
uni.$on('syncRefresh', canReset => {
this.canReset = canReset
})
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
this.canReset && this.onRefreshList()
this.canReset = false
},
/**
* 生命周期函数--监听页面的卸载
*/
onUnload() {
// 卸载全局事件订阅
uni.$off('syncRefresh')
},
methods: {
// 初始化当前选中的标签
initCurTab(options) {
const app = this
if (options.dataType) {
const index = app.tabs.findIndex(item => item.value == options.dataType)
app.curTab = index > -1 ? index : 0
}
},
/**
* 上拉加载的回调 (页面初始化时也会执行一次)
* 其中page.num:当前页 从1开始, page.size:每页数据条数,默认10
* @param {Object} page
*/
upCallback(page) {
const app = this
// 设置列表数据
app.getOrderList(page.num)
.then(list => {
const curPageLen = list.data.length
const totalSize = list.data.total
app.mescroll.endBySize(curPageLen, totalSize)
})
.catch(() => app.mescroll.endErr())
},
// 获取订单列表
getOrderList(pageNo = 1) {
const app = this
return new Promise((resolve, reject) => {
OrderApi.list({ dataType: app.getTabValue(), page: pageNo }, { load: false })
.then(result => {
// 合并新数据
const newList = app.initList(result.data.list)
app.list.data = getMoreListData(newList, app.list, pageNo)
resolve(newList)
})
})
},
// 初始化订单列表数据
initList(newList) {
newList.data.forEach(item => {
item.total_num = 0
item.goods.forEach(goods => {
item.total_num += goods.total_num
})
})
return newList
},
// 获取当前标签项的值
getTabValue() {
return this.tabs[this.curTab].value
},
// 切换标签项
onChangeTab(index) {
const app = this
// 设置当前选中的标签
app.curTab = index
// 刷新订单列表
app.onRefreshList()
},
// 刷新订单列表
onRefreshList() {
this.list = getEmptyPaginateObj()
setTimeout(() => {
this.mescroll.resetUpScroll()
}, 120)
},
// 取消订单
onCancel(orderId) {
const app = this
uni.showModal({
title: '友情提示',
content: '确认要取消该订单吗?',
success(o) {
if (o.confirm) {
OrderApi.cancel(orderId)
.then(result => {
// 显示成功信息
app.$toast(result.message)
// 刷新订单列表
app.onRefreshList()
})
}
}
});
},
// 获取核销码,弹窗展示二维码
layerPayQrcode(orderGoodsId) {
uni.navigateTo({
url: `/pages/qrcode/index?id=`+orderGoodsId
});
},
// 获取核销码,跳转第三方连接
handlePayQrcode(orderGoodsId) {
const app = this
app.isLoading = true
OrderApi.qrCode(app.orderId, {orderGoodsId: orderGoodsId})
.then(result => {
uni.navigateTo({
url: `/pages/webview/webview?url=`+encodeURIComponent(result.data.qrCode)
});
app.isLoading = false
})
},
// 确认收货
onReceipt(orderId) {
const app = this
uni.showModal({
title: '友情提示',
content: '确认收到商品了吗?',
success(o) {
if (o.confirm) {
OrderApi.receipt(orderId)
.then(result => {
// 显示成功信息
app.$success(result.message)
// 刷新订单列表
app.onRefreshList()
})
}
}
});
},
// 点击去支付
onPay(orderId) {
// 记录订单id
this.payOrderId = orderId
// 显示支付方式弹窗
this.showPayPopup = true
},
// 选择支付方式
onSelectPayType(payType) {
const app = this
// 隐藏支付方式弹窗
this.showPayPopup = false
// 发起支付请求
OrderApi.pay(app.payOrderId, payType)
.then(result => app.onSubmitCallback(result))
},
// 订单提交成功后回调
onSubmitCallback(result) {
const app = this
// 发起微信支付
if (result.data.pay_type == PayTypeEnum.WECHAT.value) {
wxPayment(result.data.payment)
.then(() => {
app.$success('支付成功')
setTimeout(() => {
app.onRefreshList()
}, 1500)
})
.catch(err => {
app.$error('订单未支付')
})
.finally(() => {
app.disabled = false
})
}
// 余额支付
if (result.data.pay_type == PayTypeEnum.BALANCE.value) {
app.$success('支付成功')
app.disabled = false
setTimeout(() => {
app.onRefreshList()
}, 1500)
}
},
// 跳转到订单详情页
handleTargetDetail(orderId) {
this.$navTo('pages/order/detail', { orderId })
},
// 跳转到订单评价页
handleTargetComment(orderId) {
this.$navTo('pages/order/comment/index', { orderId })
}
},
}
</script>
<style lang="scss" scoped>
// 项目内容
.order-item {
margin: 20rpx auto 20rpx auto;
padding: 30rpx 30rpx;
width: 94%;
box-shadow: 0 1rpx 5rpx 0px rgba(0, 0, 0, 0.05);
border-radius: 16rpx;
background: #fff;
}
// 项目顶部
.item-top {
display: flex;
justify-content: space-between;
font-size: 26rpx;
margin-bottom: 40rpx;
.order-time {
color: #777;
}
.state-text {
color: $uni-text-color-active;
}
}
// 商品列表
.goods-list {
.goods-child{
margin-bottom: 40rpx;
.order-countdown {
display: flex;
justify-content: flex-end;
font-size: 14px;
color: #fa2209;
align-items: center;
gap: 10px;
margin-top: 10px;
}
}
// 商品项
.goods-item {
display: flex;
// 商品图片
.goods-image {
width: 180rpx;
height: 180rpx;
.image {
display: block;
width: 100%;
height: 100%;
border-radius: 8rpx;
}
}
// 商品内容
.goods-content {
flex: 1;
padding-left: 16rpx;
padding-top: 16rpx;
.goods-title {
font-size: 26rpx;
max-height: 76rpx;
}
.goods-props {
margin-top: 14rpx;
height: 40rpx;
color: #ababab;
font-size: 24rpx;
overflow: hidden;
.goods-props-item {
display: inline-block;
margin-right: 14rpx;
padding: 4rpx 16rpx;
border-radius: 12rpx;
background-color: #F5F5F5;
width: auto;
}
}
}
// 交易信息
.goods-trade {
padding-top: 16rpx;
width: 150rpx;
text-align: right;
color: $uni-text-color-grey;
font-size: 26rpx;
.goods-price {
vertical-align: bottom;
margin-bottom: 16rpx;
.unit {
margin-right: -2rpx;
font-size: 24rpx;
}
}
}
}
}
// 订单合计
.order-total {
font-size: 26rpx;
vertical-align: bottom;
text-align: right;
height: 40rpx;
margin-bottom: 30rpx;
.unit {
margin-left: 8rpx;
margin-right: -2rpx;
font-size: 26rpx;
}
.money {
font-size: 28rpx;
}
}
// 订单操作
.order-handle {
.btn-group {
.btn-item {
border-radius: 10rpx;
padding: 6rpx 20rpx;
margin-left: 15rpx;
font-size: 28rpx;
float: right;
color: #383838;
border: 1rpx solid #a8a8a8;
&:last-child {
margin-left: 0;
}
&.active {
color: $uni-text-color-active;
border: 1rpx solid $uni-text-color-active;
}
}
}
}
// 弹出层-支付方式
.pay-popup {
padding: 24rpx;
.title {
font-size: 30rpx;
margin-bottom: 40rpx;
font-weight: bold;
text-align: center;
}
.pop-content {
min-height: 260rpx;
padding: 0 10rpx;
.pay-item {
padding: 24rpx 35rpx;
font-size: 28rpx;
border-bottom: 1rpx solid #f1f1f1;
&:last-child {
border-bottom: none;
}
.item-left_icon {
margin-right: 20rpx;
font-size: 32rpx;
&.wechat {
color: #00c800;
}
&.balance {
color: #ff9700;
}
}
}
}
}
// 商品售后
.goods-refund {
display: flex;
justify-content: flex-end;
margin-top: 10px;
.stata-text {
font-size: 24rpx;
color: #999;
}
.action-btn {
border-radius: 28rpx;
padding: 8rpx 26rpx;
font-size: 24rpx;
color: #383838;
border: 1rpx solid #a8a8a8;
}
}
</style>
+127
View File
@@ -0,0 +1,127 @@
<template>
<view class="container">
<mescroll-body ref="mescrollRef" :sticky="true" @init="mescrollInit" :down="{ use: false }" :up="upOption"
@up="upCallback">
<view class="log-list">
<view v-for="(item, index) in list.data" :key="index" class="log-item">
<view class="item-left flex-box">
<view class="rec-status">
<text>{{ item.describe }}</text>
</view>
<view class="rec-time">
<text>{{ item.create_time }}</text>
</view>
</view>
<view class="item-right" :class="[item.value > 0 ? 'col-green' : 'col-6']">
<text>{{ item.value > 0 ? '+' : '' }}{{ item.value }}</text>
</view>
</view>
</view>
</mescroll-body>
</view>
</template>
<script>
import MescrollBody from '@/components/mescroll-uni/mescroll-body.vue'
import MescrollMixin from '@/components/mescroll-uni/mescroll-mixins'
import * as LogApi from '@/api/points/log'
import { getEmptyPaginateObj, getMoreListData } from '@/core/app'
const pageSize = 15
export default {
components: {
MescrollBody
},
mixins: [MescrollMixin],
data() {
return {
// 充值记录
list: getEmptyPaginateObj(),
// 上拉加载配置
upOption: {
// 首次自动执行
auto: true,
// 每页数据的数量; 默认10
page: { size: pageSize },
// 数量要大于12条才显示无更多数据
noMoreSize: 12,
// 空布局
empty: {
tip: '亲,暂无相关数据'
}
}
}
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {},
methods: {
/**
* 上拉加载的回调 (页面初始化时也会执行一次)
* 其中page.num:当前页 从1开始, page.size:每页数据条数,默认10
* @param {Object} page
*/
upCallback(page) {
const app = this
// 设置列表数据
app.getLogList(page.num)
.then(list => {
const curPageLen = list.data.length
const totalSize = list.data.total
app.mescroll.endBySize(curPageLen, totalSize)
})
.catch(() => app.mescroll.endErr())
},
// 获取积分明细列表
getLogList(pageNo = 1) {
const app = this
return new Promise((resolve, reject) => {
LogApi.list({ page: pageNo })
.then(result => {
// 合并新数据
const newList = result.data.list
app.list.data = getMoreListData(newList, app.list, pageNo)
resolve(newList)
})
})
}
}
}
</script>
<style lang="scss" scoped>
page,
.container {
background: #fff;
}
.log-list {
padding: 0 30rpx;
}
.log-item {
font-size: 28rpx;
padding: 20rpx 20rpx;
line-height: 1.8;
border-bottom: 1rpx solid rgb(238, 238, 238);
display: flex;
justify-content: center;
align-items: center;
}
.rec-status {
color: #333;
.rec-time {
color: rgb(160, 160, 160);
font-size: 26rpx;
}
}
</style>
+75
View File
@@ -0,0 +1,75 @@
<template>
<view class="container">
<view v-if="base64" class="cate-img">
<image class="image" mode="scaleToFill" :src="base64"></image>
</view>
<empty v-else :tips="tip" :isLoading="isLoading" />
</view>
</template>
<script>
import * as OrderApi from '@/api/order'
import Empty from '@/components/empty'
export default {
components: {
Empty
},
data() {
return {
// 二维码base64编码
base64: '',
// 正在加载中
isLoading: true,
// 空布局
tip: '未获取到核销码'
}
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
let id = options.id ? options.id : ''
if(id){
// 获取支付二维码
this.getQrCode(id)
}
},
methods: {
/**
* 获取二维码的base64编码
* @param {string} id
*/
getQrCode(id) {
const app = this
app.isLoading = true
OrderApi.qrCodeImg({orderGoodsId: id})
.then(result => {
app.base64 = result.data.base64
})
.finally(() => app.isLoading = false)
},
}
}
</script>
<style lang="scss" scoped>
.cate-img {
position: relative;
width: 100%;
padding-top: 100%;
.image {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
border-radius: 10rpx;
}
}
</style>
+429
View File
@@ -0,0 +1,429 @@
<template>
<view v-if="!isLoading" class="container">
<!-- 商品详情 -->
<view class="goods-detail b-f dis-flex flex-dir-row">
<view class="left">
<image class="goods-image" :src="goods.goods_image"></image>
</view>
<view class="right dis-flex flex-box flex-dir-column flex-x-around">
<view class="goods-name">
<text class="twoline-hide">{{ goods.goods_name }}</text>
</view>
<view class="dis-flex col-9 f-24">
<view class="flex-box">
<view class="goods-props clearfix">
<view class="goods-props-item" v-for="(props, idx) in goods.goods_props" :key="idx">
<text>{{ props.value.name }}</text>
</view>
</view>
</view>
<text class="t-r">×{{ goods.total_num }}</text>
</view>
</view>
</view>
<!-- 服务类型 -->
<view class="row-service b-f m-top20">
<view class="row-title">服务类型</view>
<view class="service-switch dis-flex">
<view class="switch-item" v-for="(item, index) in RefundTypeEnum.data" :key="index" :class="{ active: formData.type == item.value }"
@click="onSwitchService(item.value)">{{ item.name }}</view>
</view>
</view>
<!-- 申请原因 -->
<view class="row-textarea b-f m-top20">
<view class="row-title">申请原因</view>
<view class="content">
<textarea class="textarea" v-model="formData.content" maxlength="2000" placeholder="请详细填写申请原因,注意保持商品的完好,建议您先与卖家沟通"
placeholderStyle="color:#ccc"></textarea>
</view>
</view>
<!-- 退款金额 -->
<view v-if="formData.type == RefundTypeEnum.RETURN.value" class="row-money b-f m-top20 dis-flex">
<view class="row-title">退款金额</view>
<view class="money col-m">¥{{ goods.total_pay_price }}</view>
</view>
<!-- 上传凭证 -->
<view class="row-voucher b-f m-top20">
<view class="row-title">上传凭证 (最多6张)</view>
<view class="image-list">
<!-- 图片列表 -->
<view class="image-preview" v-for="(image, imageIndex) in imageList" :key="imageIndex">
<text class="image-delete iconfont icon-shanchu" @click="deleteImage(imageIndex)"></text>
<image class="image" mode="aspectFill" :src="image.path"></image>
</view>
<!-- 上传图片 -->
<view v-if="imageList.length < maxImageLength" class="image-picker" @click="chooseImage()">
<text class="choose-icon iconfont icon-camera"></text>
<text class="choose-text">上传图片</text>
</view>
</view>
</view>
<!-- 底部操作按钮 -->
<view class="footer-fixed">
<view class="btn-wrapper">
<view class="btn-item btn-item-main" :class="{ disabled }" @click="handleSubmit()">确认提交</view>
</view>
</view>
</view>
</template>
<script>
import { RefundTypeEnum } from '@/common/enum/order/refund'
import * as UploadApi from '@/api/upload'
import * as RefundApi from '@/api/refund'
const maxImageLength = 6
export default {
data() {
return {
// 枚举类
RefundTypeEnum,
// 正在加载
isLoading: true,
// 订单商品id
orderGoodsId: null,
// 订单商品详情
goods: {},
// 表单数据
formData: {
// 图片上传成功的文件ID集
images: [],
// 服务类型
type: 10,
// 申请原因
content: ''
},
// 用户选择的图片列表
imageList: [],
// 最大图片数量
maxImageLength,
// 按钮禁用
disabled: false
}
},
/**
* 生命周期函数--监听页面加载
*/
onLoad({ orderGoodsId }) {
this.orderGoodsId = orderGoodsId
// 获取订单商品详情
this.getGoodsDetail()
},
methods: {
// 获取订单商品详情
getGoodsDetail() {
const app = this
app.isLoading = true
RefundApi.goods(app.orderGoodsId)
.then(result => {
app.goods = result.data.goods
app.isLoading = false
})
},
// 切换类型
onSwitchService(value) {
this.formData.type = value
},
// 选择图片
chooseImage() {
const app = this
const oldImageList = app.imageList
// 选择图片
uni.chooseImage({
count: maxImageLength - oldImageList.length,
sizeType: ['original', 'compressed'], // 可以指定是原图还是压缩图,默认二者都有
sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有
success({ tempFiles }) {
// tempFiles = [{path:'xxx', size:100}]
app.imageList = oldImageList.concat(tempFiles)
}
});
},
// 删除图片
deleteImage(imageIndex) {
this.imageList.splice(imageIndex, 1)
},
// 表单提交
handleSubmit() {
const app = this
const { imageList } = app
// 判断是否重复提交
if (app.disabled === true) return false
// 按钮禁用
app.disabled = true
// 判断是否需要上传图片
if (imageList.length > 0) {
app.uploadFile()
.then(() => app.onSubmit())
.catch(err => {
app.disabled = false
if (err.statusCode !== 0) {
app.$toast(err.errMsg)
}
console.log('err', err)
})
} else {
app.onSubmit()
}
},
// 提交到后端
onSubmit() {
const app = this
RefundApi.apply(app.orderGoodsId, app.formData)
.then(result => {
app.$toast(result.message)
setTimeout(() => {
app.disabled = false
uni.navigateBack()
}, 1500)
})
.catch(err => app.disabled = false)
},
// 上传图片
uploadFile() {
const app = this
const { imageList } = app
// 批量上传
return new Promise((resolve, reject) => {
if (imageList.length > 0) {
UploadApi.image(imageList)
.then(fileIds => {
app.formData.images = fileIds
resolve(fileIds)
})
.catch(reject)
} else {
resolve()
}
})
}
}
}
</script>
<style lang="scss" scoped>
.container {
// 设置ios刘海屏底部横线安全区域
padding-bottom: calc(constant(safe-area-inset-bottom) + 140rpx);
padding-bottom: calc(env(safe-area-inset-bottom) + 140rpx);
}
.row-title {
color: #888;
margin-bottom: 20rpx;
}
// 商品信息
.goods-detail {
padding: 24rpx 20rpx;
.left {
.goods-image {
display: block;
width: 150rpx;
height: 150rpx;
}
}
.right {
padding-left: 20rpx;
}
.goods-props {
margin-top: 14rpx;
height: 40rpx;
color: #ababab;
font-size: 24rpx;
overflow: hidden;
.goods-props-item {
display: inline-block;
margin-right: 14rpx;
padding: 4rpx 16rpx;
border-radius: 12rpx;
background-color: #F5F5F5;
width: auto;
}
}
}
/* 服务类型 */
.row-service {
padding: 24rpx 20rpx;
}
.service-switch {
.switch-item {
padding: 6rpx 30rpx;
margin-right: 25rpx;
border-radius: 10rpx;
border: 1px solid rgb(177, 177, 177);
color: #888;
&.active {
color: #fc1e56;
border: 1px solid #fc1e56;
}
}
}
/* 申请原因 */
.row-textarea {
padding: 24rpx 20rpx;
.textarea {
width: 100%;
height: 220rpx;
padding: 12rpx;
border: 1rpx solid #e8e8e8;
border-radius: 5rpx;
box-sizing: border-box;
font-size: 26rpx;
}
}
/* 退款金额 */
.row-money {
padding: 24rpx 20rpx;
.row-title {
margin-bottom: 0;
margin-right: 30rpx;
}
}
// 上传凭证
.row-voucher {
padding: 24rpx 20rpx;
.image-list {
padding: 0 20rpx;
margin-top: 20rpx;
margin-bottom: -20rpx;
&:after {
clear: both;
content: " ";
display: table;
}
.image {
display: block;
width: 100%;
height: 100%;
}
.image-picker,
.image-preview {
width: 184rpx;
height: 184rpx;
margin-right: 30rpx;
margin-bottom: 30rpx;
float: left;
&:nth-child(3n+0) {
margin-right: 0;
}
}
.image-picker {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border: 1rpx dashed #ccc;
color: #ccc;
.choose-icon {
font-size: 48rpx;
margin-bottom: 6rpx;
}
.choose-text {
font-size: 24rpx;
}
}
.image-preview {
position: relative;
.image-delete {
position: absolute;
top: -15rpx;
right: -15rpx;
height: 42rpx;
width: 42rpx;
line-height: 42rpx;
background: rgba(0, 0, 0, 0.64);
border-radius: 50%;
color: #fff;
font-weight: bolder;
font-size: 22rpx;
z-index: 10;
text-align: center;
}
}
}
}
// 底部操作栏
.footer-fixed {
position: fixed;
bottom: var(--window-bottom);
left: 0;
right: 0;
z-index: 11;
box-shadow: 0 -4rpx 40rpx 0 rgba(151, 151, 151, 0.24);
background: #fff;
// 设置ios刘海屏底部横线安全区域
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
.btn-wrapper {
height: 120rpx;
display: flex;
align-items: center;
padding: 0 20rpx;
}
.btn-item {
flex: 1;
font-size: 28rpx;
height: 80rpx;
line-height: 80rpx;
text-align: center;
color: #fff;
border-radius: 50rpx;
}
.btn-item-main {
background: linear-gradient(to right, #f9211c, #ff6335);
// 禁用按钮
&.disabled {
background: #ff9779;
}
}
}
</style>
+483
View File
@@ -0,0 +1,483 @@
<template>
<view v-if="!isLoading" class="container p-bottom">
<!-- 顶部状态栏 -->
<view class="detail-header dis-flex flex-y-center">
<view class="header-backdrop">
<image class="image" src="/static/order/refund-bg.png"></image>
</view>
<view class="header-state">
<text class="f-32 col-f">{{ detail.state_text }}</text>
</view>
</view>
<!-- 商品详情 -->
<view class="detail-goods b-f m-top20 dis-flex flex-dir-row" @click="onGoodsDetail(detail.orderGoods.goods_id)">
<view class="left">
<image class="goods-image" :src="detail.orderGoods.goods_image"></image>
</view>
<view class="right dis-flex flex-box flex-dir-column flex-x-around">
<view class="goods-name">
<text class="twoline-hide">{{ detail.orderGoods.goods_name }}</text>
</view>
<view class="dis-flex col-9 f-24">
<view class="flex-box">
<view class="goods-props clearfix">
<view class="goods-props-item" v-for="(props, idx) in detail.orderGoods.goods_props" :key="idx">
<text>{{ props.value.name }}</text>
</view>
</view>
</view>
<text class="t-r">×{{ detail.orderGoods.total_num }}</text>
</view>
</view>
</view>
<!-- 商品金额 -->
<view class="detail-order b-f row-block">
<view class="item dis-flex flex-x-end flex-y-center">
<text class="">商品金额:</text>
<text class="col-m">¥{{ detail.orderGoods.total_pay_price }}</text>
</view>
</view>
<!-- 已退款金额 -->
<view v-if="detail.status == RefundStatusEnum.COMPLETED.value && detail.type == 10"
class="detail-order b-f row-block dis-flex flex-x-end flex-y-center">
<text class="">已退款金额:</text>
<text class="col-m">¥{{ detail.refund_money }}</text>
</view>
<!-- 售后信息 -->
<view v-if="detail.status == RefundStatusEnum.REJECTED.value" class="detail-refund b-f m-top20">
<view class="detail-refund__row dis-flex">
<view class="text">
<text>售后类型:</text>
</view>
<view class="flex-box">
<text>{{ RefundTypeEnum[detail.type].name }}</text>
</view>
</view>
<view class="detail-refund__row dis-flex">
<view class="text">
<text>申请原因:</text>
</view>
<view class="flex-box">
<text>{{ detail.apply_desc }}</text>
</view>
</view>
<view v-if="detail.images.length > 0" class="detail-refund__row dis-flex">
<view class="text">
<text>申请凭证:</text>
</view>
<view class="image-list flex-box">
<view class="image-preview" v-for="(item, index) in detail.images" :key="index">
<image class="image" mode="aspectFill" :src="item.image_url" @click="handlePreviewImages(index)"></image>
</view>
</view>
</view>
</view>
<!-- 售后信息 -->
<view v-if="detail.status.value == RefundStatusEnum.REJECTED.value" class="detail-refund b-f m-top20">
<view class="detail-refund__row dis-flex">
<view class="text">
<text class="col-m">拒绝原因:</text>
</view>
<view class="flex-box">
<text>{{ detail.refuse_desc }}</text>
</view>
</view>
</view>
<!-- 退货物流信息 -->
<view v-if="detail.audit_status == AuditStatusEnum.REVIEWED.value && detail.is_user_send"
class="detail-address b-f m-top20">
<view class="detail-address__row address-title">
<text class="col-m">退货物流信息</text>
</view>
<view class="detail-address__row address-details">
<view class="address-details__row">
<text>物流公司:{{ detail.express.express_name }}</text>
</view>
<view class="address-details__row">
<text>物流单号:{{ detail.express_no }}</text>
</view>
<!-- <view class="address-details__row">
<text>发货状态:{{ detail.is_user_send ? '已发货' : '未发货' }}</text>
</view> -->
<view class="address-details__row">
<text>发货时间:{{ detail.send_time }}</text>
</view>
</view>
</view>
<!-- 商家收货地址 -->
<view v-if="detail.audit_status == AuditStatusEnum.REVIEWED.value" class="detail-address b-f m-top20">
<view class="detail-address__row address-title">
<text class="col-m">商家退货地址</text>
</view>
<view class="detail-address__row address-details">
<view class="address-details__row">
<text>收货人:{{ detail.address.name }}</text>
</view>
<view class="address-details__row">
<text>联系电话:{{ detail.address.phone }}</text>
</view>
<view class="address-details__row dis-flex">
<view class="text">
<text>详细地址:</text>
</view>
<view class="address flex-box">
<text class="region" v-for="(region, idx) in detail.address.region" :key="idx">{{ region }}</text>
<text class="detail">{{ detail.address.detail }}</text>
</view>
</view>
</view>
<view class="detail-address__row address-tips">
<view class="f-26 col-9">
<text>· 未与卖家协商一致情况下,请勿寄到付或平邮</text>
</view>
<view class="f-26 col-9">
<text>· 请填写真实有效物流信息</text>
</view>
</view>
</view>
<!-- 填写物流信息 -->
<form
v-if="detail.type == RefundTypeEnum.RETURN.value && detail.audit_status == AuditStatusEnum.REVIEWED.value && !detail.is_user_send"
@submit="onSubmit()">
<view class="detail-express b-f m-top20">
<view class="form-group dis-flex flex-y-center">
<view class="field">物流公司:</view>
<view class="flex-box">
<picker mode="selector" :range="expressList" range-key="express_name" :value="expressIndex"
@change="onChangeExpress">
<text v-if="expressIndex > -1">{{ expressList[expressIndex].express_name }}</text>
<text v-else class="col-80">请选择物流公司</text>
</picker>
</view>
</view>
<view class="form-group dis-flex flex-y-center">
<view class="field">物流单号:</view>
<view class="flex-box">
<input class="input" v-model="formData.expressNo" placeholder="请填写物流单号"></input>
</view>
</view>
</view>
<!-- 操作按钮 -->
<view class="footer">
<view class="btn-wrapper">
<button class="btn-item btn-item-main btn-normal" :class="{ disabled }" formType="submit">确认发货</button>
</view>
</view>
</form>
</view>
</template>
<script>
import { AuditStatusEnum, RefundStatusEnum, RefundTypeEnum } from '@/common/enum/order/refund'
import * as RefundApi from '@/api/refund'
import * as ExpressApi from '@/api/express'
export default {
data() {
return {
// 枚举类
AuditStatusEnum,
RefundStatusEnum,
RefundTypeEnum,
// 正在加载
isLoading: true,
// 售后单ID
orderRefundId: null,
// 售后单详情
detail: {},
// 物流公司列表
expressList: [],
// 表单数据
formData: {
// 物流公司ID
expressId: null,
// 物流单号
expressNo: ''
},
// 选择的物流公司索引
expressIndex: -1,
// 按钮禁用
disabled: false
}
},
/**
* 生命周期函数--监听页面加载
*/
onLoad({ orderRefundId }) {
// 售后单ID
this.orderRefundId = orderRefundId
// 获取页面数据
this.getPageData()
},
methods: {
// 获取页面数据
getPageData() {
const app = this
app.isLoading = true
Promise.all([app.getRefundDetail(), app.getExpressList()])
.then(result => {
app.isLoading = false
})
},
// 获取售后单详情
getRefundDetail() {
const app = this
return new Promise((resolve, reject) => {
RefundApi.detail(app.orderRefundId)
.then(result => {
app.detail = result.data.detail
resolve()
})
.catch(reject)
})
},
// 获取物流公司列表
getExpressList() {
const app = this
return new Promise((resolve, reject) => {
ExpressApi.list()
.then(result => {
app.expressList = result.data.list
resolve()
})
.catch(reject)
})
},
// 跳转商品详情页
onGoodsDetail(goodsId) {
this.$navTo('pages/goods/detail', { goodsId })
},
// 凭证图片预览
handlePreviewImages(index) {
const { detail: { images } } = this
const imageUrls = images.map(item => item.image_url)
uni.previewImage({
current: imageUrls[index],
urls: imageUrls
})
},
// 选择物流公司
onChangeExpress(e) {
const expressIndex = e.detail.value
const { expressList } = this
this.expressIndex = expressIndex
this.formData.expressId = expressList[expressIndex].express_id
},
// 表单提交
onSubmit() {
const app = this
// 判断是否重复提交
if (app.disabled === true) return false
// 按钮禁用
app.disabled = true
// 提交到后端
RefundApi.delivery(app.orderRefundId, app.formData)
.then(result => {
app.$toast(result.message)
setTimeout(() => {
app.disabled = false
uni.navigateBack()
}, 1500)
})
.catch(err => app.disabled = false)
}
}
}
</script>
<style lang="scss" scoped>
// 顶部状态栏
.detail-header {
position: relative;
width: 100%;
height: 140rpx;
.header-backdrop {
width: 100%;
position: absolute;
top: 0;
left: 0;
z-index: 0;
.image {
display: block;
width: 100%;
height: 140rpx;
}
}
}
.header-state {
z-index: 1;
padding: 0 50rpx;
}
/* 商品详情 */
.detail-goods {
padding: 24rpx 20rpx;
.left {
.goods-image {
display: block;
width: 150rpx;
height: 150rpx;
}
}
.right {
padding-left: 20rpx;
}
.goods-props {
margin-top: 14rpx;
height: 40rpx;
color: #ababab;
font-size: 24rpx;
overflow: hidden;
.goods-props-item {
display: inline-block;
margin-right: 14rpx;
padding: 4rpx 16rpx;
border-radius: 12rpx;
background-color: #F5F5F5;
width: auto;
}
}
}
.detail-order {
padding: 15rpx 20rpx;
font-size: 26rpx;
.item {
margin-bottom: 10rpx;
&:last-child {
margin-bottom: 0;
}
}
}
/* 售后详情 */
.detail-refund {
padding: 15rpx 20rpx;
}
.detail-refund__row {
margin: 20rpx 0;
}
/* 申请凭证 */
.image-list {
margin-bottom: -15rpx;
.image-preview {
margin: 0 15rpx 15rpx 0;
float: left;
.image {
display: block;
width: 180rpx;
height: 180rpx;
}
&:nth-child(3n+0) {
margin-right: 0;
}
}
}
/* 商家收货地址 */
.detail-address {
padding: 20rpx 34rpx;
}
.address-details {
padding: 8rpx 0;
border-bottom: 1px solid #eee;
.address-details__row {
margin: 14rpx 0;
}
}
.address-tips {
margin-top: 16rpx;
line-height: 46rpx;
}
.detail-address__row {
// margin: 18rpx 0;
}
/* 填写物流信息 */
.detail-express {
padding: 10rpx 30rpx;
}
.form-group {
height: 60rpx;
margin: 14rpx 0;
.input {
height: 100%;
font-size: 28rpx;
}
}
/* 底部操作栏 */
.footer {
margin-top: 60rpx;
.btn-wrapper {
height: 100%;
display: flex;
align-items: center;
padding: 0 20rpx;
}
.btn-item {
flex: 1;
font-size: 28rpx;
height: 80rpx;
color: #fff;
border-radius: 50rpx;
display: flex;
justify-content: center;
align-items: center;
}
.btn-item-main {
background: linear-gradient(to right, #f9211c, #ff6335);
// 禁用按钮
&.disabled {
background: #ff9779;
}
}
}
</style>
+263
View File
@@ -0,0 +1,263 @@
<template>
<view class="container">
<mescroll-body ref="mescrollRef" :sticky="true" @init="mescrollInit" :down="{ native: true }" @down="downCallback"
:up="upOption" @up="upCallback">
<!-- tab栏 -->
<u-tabs :list="tabs" :is-scroll="false" :current="curTab" active-color="#FA2209" :duration="0.2"
@change="onChangeTab" />
<!-- 退款/售后单 -->
<view class="widget-list">
<view class="widget-detail" v-for="(item, index) in list.data" :key="index">
<view class="row-block dis-flex flex-y-center">
<view class="flex-box">{{ item.create_time }}</view>
<view class="flex-box t-r">
<text class="col-m">{{ item.state_text }}</text>
</view>
</view>
<view class="detail-goods row-block dis-flex" @click.stop="handleTargetDetail(item.order_refund_id)">
<view class="goods-image">
<image class="image" :src="item.orderGoods.goods_image" mode="aspectFit"></image>
</view>
<view class="goods-right flex-box">
<view class="goods-name">
<text class="twoline-hide">{{ item.orderGoods.goods_name }}</text>
</view>
<view class="goods-props clearfix">
<view class="goods-props-item" v-for="(props, idx) in item.orderGoods.goods_props" :key="idx">
<text>{{ props.value.name }}</text>
</view>
</view>
<view class="goods-num t-r">
<text class="f-26 col-8">×{{ item.orderGoods.total_num }}</text>
</view>
</view>
</view>
<view class="detail-order row-block">
<view class="item dis-flex flex-x-end flex-y-center">
<text class="">付款金额:</text>
<text class="col-m">¥{{ item.orderGoods.total_pay_price }}</text>
</view>
</view>
<view class="detail-operate row-block dis-flex flex-x-end flex-y-center">
<view class="detail-btn btn-detail" @click.stop="handleTargetDetail(item.order_refund_id)">查看详情</view>
</view>
</view>
</view>
</mescroll-body>
</view>
</template>
<script>
import MescrollBody from '@/components/mescroll-uni/mescroll-body.vue'
import MescrollMixin from '@/components/mescroll-uni/mescroll-mixins'
import { getEmptyPaginateObj, getMoreListData } from '@/core/app'
import * as RefundApi from '@/api/refund'
// 每页记录数量
const pageSize = 15
// tab栏数据
const tabs = [{
name: '全部',
value: -1
}, {
name: '待处理',
value: 0
}]
export default {
components: {
MescrollBody
},
mixins: [MescrollMixin],
data() {
return {
// 订单列表数据
list: getEmptyPaginateObj(),
// tabs栏数据
tabs,
// 当前标签索引
curTab: 0,
// 上拉加载配置
upOption: {
// 首次自动执行
auto: true,
// 每页数据的数量; 默认10
page: { size: pageSize },
// 数量要大于2条才显示无更多数据
noMoreSize: 2,
// 空布局
empty: {
tip: '亲,暂无售后单记录'
}
},
// 控制首次触发onShow事件时不刷新列表
canReset: false,
}
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
this.canReset && this.onRefreshList()
// this.canReset = true
},
methods: {
/**
* 上拉加载的回调 (页面初始化时也会执行一次)
* 其中page.num:当前页 从1开始, page.size:每页数据条数,默认10
* @param {Object} page
*/
upCallback(page) {
const app = this
// 设置列表数据
app.getRefundList(page.num)
.then(list => {
const curPageLen = list.data.length
const totalSize = list.data.total
app.mescroll.endBySize(curPageLen, totalSize)
})
.catch(() => app.mescroll.endErr())
},
// 获取退款/售后单列表
getRefundList(pageNo = 1) {
const app = this
return new Promise((resolve, reject) => {
RefundApi.list({ state: app.getTabValue(), page: pageNo }, { load: false })
.then(result => {
// 合并新数据
const newList = result.data.list
app.list.data = getMoreListData(newList, app.list, pageNo)
resolve(newList)
})
})
},
// 切换标签项
onChangeTab(index) {
const app = this
// 设置当前选中的标签
app.curTab = index
// 刷新售后单列表
app.onRefreshList()
},
// 刷新订单列表
onRefreshList() {
this.list = getEmptyPaginateObj()
setTimeout(() => {
this.mescroll.resetUpScroll()
}, 120)
},
// 获取当前标签项的值
getTabValue() {
return this.tabs[this.curTab].value
},
// 跳转到售后单详情页
handleTargetDetail(orderRefundId) {
this.$navTo('pages/refund/detail', { orderRefundId })
},
}
}
</script>
<style lang="scss" scoped>
.widget-detail {
box-sizing: border-box;
background: #fff;
margin-bottom: 20rpx;
.row-block {
padding: 0 20rpx;
min-height: 70rpx;
}
.detail-goods {
padding: 20rpx;
background: #f9f9f9;
.goods-image {
margin-right: 20rpx;
.image {
display: block;
width: 200rpx;
height: 200rpx;
}
}
.goods-right {
padding: 15rpx 0;
}
.goods-name {
margin-bottom: 10rpx;
}
.goods-props {
margin-top: 14rpx;
height: 40rpx;
color: #ababab;
font-size: 24rpx;
overflow: hidden;
.goods-props-item {
display: inline-block;
margin-right: 14rpx;
padding: 4rpx 16rpx;
border-radius: 12rpx;
background-color: #F5F5F5;
width: auto;
}
}
}
.detail-operate {
padding-bottom: 20rpx;
.detail-btn {
border-radius: 4px;
border: 1rpx solid #ccc;
padding: 8rpx 20rpx;
font-size: 28rpx;
color: #555;
margin-left: 10rpx;
}
}
.detail-order {
padding: 10rpx 20rpx;
font-size: 26rpx;
line-height: 50rpx;
height: 50rpx;
.item {
margin-bottom: 10rpx;
&:last-child {
margin-bottom: 0;
}
}
}
}
</style>
+227
View File
@@ -0,0 +1,227 @@
<template>
<view class="container">
<view class="search-wrapper">
<view class="search-input">
<view class="search-input-wrapper">
<view class="left">
<text class="search-icon iconfont icon-search"></text>
</view>
<view class="right">
<input v-model="searchValue" class="input" focus="true" placeholder="请输入您搜索的商品" type="text"></input>
</view>
</view>
</view>
<view class="search-button">
<view class="button" @click="onSearch">搜索</view>
</view>
</view>
<view class="history" v-if="historySearch.length">
<view class="his-head">
<text class="title">最近搜索</text>
<text class="icon iconfont icon-delete" @click="clearSearch"></text>
</view>
<view class="his-list">
<view class="his-item" v-for="(val, index) in historySearch" :key="index">
<view class="history-button" @click="handleQuick(val)">{{ val }}</view>
</view>
</view>
</view>
<!-- </view> -->
</view>
</template>
<script>
const HISTORY_SEARCH = 'historySearch'
export default {
data() {
return {
historySearch: [],
searchValue: ''
}
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
// 获取历史搜索
this.historySearch = this.getHistorySearch()
},
methods: {
/**
* 获取历史搜索
*/
getHistorySearch() {
return uni.getStorageSync(HISTORY_SEARCH) || []
},
/**
* 搜索提交
*/
onSearch() {
const { searchValue } = this
if (searchValue) {
// 记录历史搜索
this.setHistory(searchValue)
// 跳转到商品列表页
this.$navTo('pages/goods/list', { search: searchValue })
}
},
/**
* 记录历史搜索
*/
setHistory(searchValue) {
const data = this.getHistorySearch()
const index = data.indexOf(searchValue)
index > -1 && data.splice(index, 1)
data.unshift(searchValue)
this.historySearch = data
this.onUpdateStorage()
},
/**
* 清空最近搜索记录
*/
clearSearch() {
this.historySearch = []
this.onUpdateStorage()
},
/**
* 更新历史搜索缓存
* @param {Object} data
*/
onUpdateStorage(data) {
uni.setStorageSync(HISTORY_SEARCH, this.historySearch)
},
/**
* 跳转到最近搜索
*/
handleQuick(search) {
this.$navTo('pages/goods/list', { search })
}
}
}
</script>
<style lang="scss" scoped>
.container {
padding: 20rpx;
min-height: 100vh;
background: #f7f7f7;
}
.search-wrapper {
display: flex;
height: 64rpx;
}
// 搜索输入框
.search-input {
width: 80%;
background: #fff;
border-radius: 10rpx 0 0 10rpx;
box-sizing: border-box;
overflow: hidden;
.search-input-wrapper {
display: flex;
.left {
display: flex;
width: 60rpx;
justify-content: center;
align-items: center;
.search-icon {
display: block;
color: #b4b4b4;
font-size: 28rpx;
}
}
.right {
flex: 1;
input {
font-size: 28rpx;
height: 64rpx;
display: flex;
align-items: center;
.input-placeholder {
color: #aba9a9;
}
}
}
}
}
// 搜索按钮
.search-button {
width: 20%;
box-sizing: border-box;
.button {
height: 64rpx;
font-size: 28rpx;
border-radius: 0 10rpx 10rpx 0;
background: #fa2209;
color: #fff;
display: flex;
justify-content: center;
align-items: center;
}
}
// 最近搜索
.history {
.his-head {
font-size: 28rpx;
padding: 50rpx 0 0 0;
color: #777;
.icon {
float: right;
}
}
.his-list {
padding: 20rpx 0;
overflow: hidden;
.his-item {
width: 33.3%;
float: left;
padding: 10rpx;
box-sizing: border-box;
.history-button {
text-align: center;
padding: 14rpx;
line-height: 30rpx;
border-radius: 100rpx;
background: #fff;
font-size: 26rpx;
border: 1rpx solid #efefef;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}
}
</style>
+383
View File
@@ -0,0 +1,383 @@
<template>
<view class="container">
<!-- 地图显示 -->
<map
id="map"
:latitude="userLocation.latitude"
:longitude="userLocation.longitude"
:markers="markers"
show-location
:scale="scale"
style="width: 100%; height: 100vh;"
@markertap="onMarkerTap">
<cover-image class="img-map1" src="/static/center.png"></cover-image>
<cover-image class="img-map2" src="/static/locate.png" @click="clickcontrol"></cover-image>
</map>
<!-- 加油站列表 -->
<view class="station-list">
<view class="station-item" v-if="now.id != undefined">
<view class="station-info">
<text class="station-name">{{ now.name }}</text>
<view>
<text class="station-distance">距您 {{ now.distance }}km</text>
<text class="station-addres">{{ now.address }}</text>
</view>
<view class="station-oil">
<text>0#</text>
<text>92#</text>
<text>95#</text>
</view>
<view class="station-time-info">
<view class="time-info-time">00:00-23:59 营业</view>
<view class="time-info-plan">
<text>便利点</text>
<text>卫生间</text>
</view>
</view>
</view>
<view class="station-action">
<button class="navigate-button" @click="navigate()">导航加油站</button>
<button class="navigate-button" @click="oil()">去购券</button>
</view>
</view>
<view class="station-item" v-else>
<view class="station-info">
<text class="station-name">您所在的城市,暂无可用加油站</text>
</view>
</view>
</view>
</view>
</template>
<script>
import * as StationApi from '@/api/station'
export default {
data() {
return {
userLocation: {
latitude: 0,
longitude: 0
},
markers: [
],
scale: 13,
now: {}
};
},
mounted() {
this.getUserLocation();
},
methods: {
// 获取用户当前的位置
getUserLocation() {
// let latitude = '25.068413'
// let longitude = '118.522990'
// this.userLocation.latitude = latitude;
// this.userLocation.longitude = longitude;
// // 存储经纬度到缓存中
// uni.setStorageSync('latitude', latitude)
// uni.setStorageSync('longitude', longitude)
// uni.setStorageSync('province', '福建省')
// uni.setStorageSync('city', '泉州市')
// uni.setStorageSync('district', '南安市')
// this.mapapiStation()
let app = this
// 获取经纬度
uni.authorize({
scope: 'scope.userLocation',
success() {
uni.getLocation({
type: 'wgs84',
success: (res) => {
app.userLocation.latitude = res.latitude;
app.userLocation.longitude = res.longitude;
// 存储经纬度到缓存中
uni.setStorageSync('latitude', res.latitude)
uni.setStorageSync('longitude', res.longitude)
app.getRegion(res.latitude, res.longitude).then(region => {
uni.setStorageSync('province', region.province)
uni.setStorageSync('city', region.city)
uni.setStorageSync('district', region.district)
app.mapapiStation(res.latitude, res.longitude)
}).catch(err => {
console.error('获取区域信息失败:', err);
});
},
fail: (err) => {
console.error('获取位置失败', err);
}
});
},
fail: (err) => {
console.error('授权失败', err);
}
});
},
// 获取加油站点
mapapiStation(latitude, longitude) {
let app = this
let key = '67NBZ-MHUH4-ODCU2-KNPZW-3OGJF-ARBRQ';
let url = `https://apis.map.qq.com/ws/place/v1/search?key=${key}&keyword=中国石油&boundary=nearby(${latitude},${longitude},1000)&page_size=20`;
return new Promise((resolve, reject) => {
uni.request({
url: url,
method: 'GET',
success: (res) => {
var markers = [] //地图markers标记点
var arr = res.data.data
for (var i = 0; i < arr.length; i++) {
markers.push({
iconPath: "/static/marker.png",
id: i,
name: arr[i].title,
latitude: arr[i].location.lat,
longitude: arr[i].location.lng,
address: arr[i].address,
distance: (arr[i]._distance/1000).toFixed(2),
width: 32,
height: 32
})
}
app.markers = markers
app.now = markers[0]
resolve(markers)
},
fail: (err) => {
console.error('请求失败:', err);
reject(err); // 请求失败处理
}
});
});
},
// 逆你解析地址
getRegion(latitude, longitude) {
let key = '67NBZ-MHUH4-ODCU2-KNPZW-3OGJF-ARBRQ'; // ETFBZ-F4XCW-YOQRI-RWBQM-BAZIQ-2VFS5
let url = `https://apis.map.qq.com/ws/geocoder/v1/?location=${latitude},${longitude}&key=${key}`;
return new Promise((resolve, reject) => {
uni.request({
url: url,
method: 'GET',
success: (res) => {
let data = res.data;
if (data.status === 0) { // 腾讯地图API通常使用0表示成功
let address = data.result.address; // 完整地址
let province = data.result.address_component.province; // 省
let city = data.result.address_component.city; // 市
let district = data.result.address_component.district; // 区/县
let region = {
address: address,
province: province,
city: city,
district: district,
};
resolve(region); // 成功时返回region对象
} else {
console.error('逆地理编码失败:', data.message);
reject(data.message); // 处理失败情况
}
},
fail: (err) => {
console.error('请求失败:', err);
reject(err); // 请求失败处理
}
});
});
},
// 获取站点列表
getStationList(pageNo = 1) {
const app = this
return new Promise((resolve, reject) => {
StationApi.list({ page: pageNo })
.then(result => {
const newList = result.data.list
app.markers = newList
app.getRegion(newList[0].latitude, newList[0].longitude).then(region => {
newList[0].address = region.address
}).catch(err => {
console.error('获取区域信息失败:', err);
});
app.now = newList[0]
resolve(newList)
})
})
},
// 更新每个标记点与用户位置之间的距离
updateDistances() {
this.markers = this.markers.map(marker => {
marker.distance = this.calculateDistance(
this.userLocation.latitude,
this.userLocation.longitude,
marker.latitude,
marker.longitude
);
return marker;
});
},
// 计算两点之间的距离
calculateDistance(lat1, lon1, lat2, lon2) {
const radLat1 = this.rad(lat1);
const radLat2 = this.rad(lat2);
const a = radLat1 - radLat2;
const b = this.rad(lon1) - this.rad(lon2);
let s = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 2), 2) +
Math.cos(radLat1) * Math.cos(radLat2) * Math.pow(Math.sin(b / 2), 2)));
s = s * 6378.137; // 地球半径(单位:公里)
s = Math.round(s * 10000) / 10000;
return s.toFixed(2);
},
rad(d) {
return d * Math.PI / 180.0;
},
// 标记点点击事件处理
onMarkerTap(event) {
const markerId = event.detail.markerId;
const marker = this.markers.find(m => m.id === markerId);
if (marker) {
// this.getRegion(marker.latitude, marker.longitude).then(region => {
// marker.address = region.address
// }).catch(err => {
// console.error('获取区域信息失败:', err);
// });
this.now = marker;
}
},
//右下角定位按钮的点击事件
clickcontrol(){
uni.createMapContext("map", this).moveToLocation({ //moveToLocation将地图中心移动到当前定位点,需要配合map组件的show-location使用
latitude: this.userLocation.latitude,
longitude: this.userLocation.longitude
});
},
// 导航到标记点
navigate() {
uni.openLocation({
latitude: Number(this.now.latitude),
longitude: Number(this.now.longitude),
name: this.now.name,
address: this.now.address,
scale: 18
});
},
// 跳转商品详情页
oil() {
this.$navTo('pages/goods/list?location=1')
}
}
};
</script>
<style>
.container {
display: flex;
flex-direction: column;
align-items: center;
}
.station-list {
width: 100%;
padding: 20rpx;
position: fixed;
bottom: 80rpx;
height: 300rpx;
}
.station-item {
padding: 20rpx;
background-color: #fff;
margin-bottom: 10rpx;
border-radius: 12rpx;
box-shadow: 0 0 10rpx rgba(0, 0, 0, 0.1);
height: 350rpx;
}
.station-info {
height: 250rpx;
display: flex;
flex-direction: column;
gap: 10rpx;
}
.station-name {
font-size: 35rpx;
font-weight: bold;
color: #333;
}
.station-distance {
font-size: 26rpx;
margin-top: 4rpx;
margin-right: 25rpx;
}
.station-oil {
display: flex;
gap: 12rpx;
}
.station-oil text{
background-color: #5983b1;
color: aliceblue;
padding: 4rpx 8rpx;
font-size: 12px;
border-radius: 4px;
}
.station-time-info{
display: flex;
align-items: center;
gap: 20rpx;
}
.station-time-info .time-info-time {
font-size: 26rpx;
color: #666;
}
.station-time-info .time-info-plan {
font-size: 24rpx;
display: flex;
gap: 10rpx;
}
.station-time-info .time-info-plan text{
padding: 4rpx;
border: 1rpx solid #007aff;
color: #007aff;
border-radius: 8rpx;
}
.navigate-button {
padding: 4rpx 14rpx;
background-color: #007aff;
color: #fff;
border: none;
border-radius: 4rpx;
font-size: 22rpx;
width: 150rpx;
}
.navigate-button:active {
background-color: #005bb5;
}
.station-action{
display: flex;
}
.img-map1{
width: 60rpx;
height: 60rpx;
position: fixed;
bottom: 47%;
left: 46%;
}
.img-map2{
width: 60rpx;
height: 60rpx;
position: fixed;
bottom: 30vh;
right: 2vh;
background-color: #FFFFFF;
border-radius: 5px;
}
</style>
+311
View File
@@ -0,0 +1,311 @@
<template>
<view class="container">
<!-- 页面头部 -->
<view class="header">
<view class="title">
<text>绑定您的手机号</text>
</view>
<view class="sub-title">
<text>为了更好的服务您,请绑定手机号</text>
</view>
</view>
<!-- 表单 -->
<view class="submit-form">
<!-- 手机号 -->
<view class="form-item">
<input class="form-item--input" type="number" v-model="mobile" maxlength="11" placeholder="请输入手机号码" />
</view>
<!-- 图形验证码 -->
<view class="form-item">
<input class="form-item--input" type="text" v-model="captchaCode" maxlength="5" placeholder="请输入图形验证码" />
<view class="form-item--parts">
<view class="captcha" @click="getCaptcha()">
<image class="image" :src="captcha.base64"></image>
</view>
</view>
</view>
<!-- 短信验证码 -->
<view class="form-item">
<input class="form-item--input" type="number" v-model="smsCode" maxlength="6" placeholder="请输入短信验证码" />
<view class="form-item--parts">
<view class="captcha-sms" @click="handelSmsCaptcha()">
<text v-if="!smsState" class="activate">获取验证码</text>
<text v-else class="un-activate">重新发送({{ times }})秒</text>
</view>
</view>
</view>
<!-- 确认绑定 -->
<view class="submit-button" @click="handleSubmit()">
<text>确认绑定</text>
</view>
</view>
</view>
</template>
<script>
import store from '@/store'
import * as UserApi from '@/api/user'
import * as CaptchaApi from '@/api/captcha'
import * as Verify from '@/utils/verify'
// 倒计时时长(秒)
const times = 60
// 表单验证场景
const GET_CAPTCHA = 10
const FORM_SUBMIT = 20
export default {
data() {
return {
// 正在加载
isLoading: false,
// 图形验证码信息
captcha: {},
// 短信验证码发送状态
smsState: false,
// 倒计时
times,
// 手机号
mobile: '',
// 图形验证码
captchaCode: '',
// 短信验证码
smsCode: ''
}
},
/**
* 生命周期函数--监听页面加载
*/
created() {
// 获取图形验证码
this.getCaptcha()
},
methods: {
// 获取图形验证码
getCaptcha() {
const app = this
CaptchaApi.image().then(result => app.captcha = result.data)
},
// 点击发送短信验证码
handelSmsCaptcha() {
const app = this
if (!app.isLoading && !app.smsState && app.formValidation(GET_CAPTCHA)) {
app.sendSmsCaptcha()
app.getCaptcha()
}
},
// 表单验证
formValidation(scene = GET_CAPTCHA) {
const app = this
// 验证获取短信验证码
if (scene === GET_CAPTCHA) {
if (!app.validteMobile(app.mobile) || !app.validteCaptchaCode(app.captchaCode)) {
return false
}
}
// 验证表单提交
if (scene === FORM_SUBMIT) {
if (!app.validteMobile(app.mobile) || !app.validteSmsCode(app.smsCode)) {
return false
}
}
return true
},
// 验证手机号
validteMobile(str) {
if (Verify.isEmpty(str)) {
this.$toast('请先输入手机号')
return false
}
if (!Verify.isMobile(str)) {
this.$toast('请输入正确格式的手机号')
return false
}
return true
},
// 验证图形验证码
validteCaptchaCode(str) {
if (Verify.isEmpty(str)) {
this.$toast('请先输入图形验证码')
return false
}
return true
},
// 验证短信验证码
validteSmsCode(str) {
if (Verify.isEmpty(str)) {
this.$toast('请先输入短信验证码')
return false
}
return true
},
// 请求发送短信验证码接口
sendSmsCaptcha() {
const app = this
app.isLoading = true
CaptchaApi.sendSmsCaptcha({
form: {
captchaKey: app.captcha.key,
captchaCode: app.captchaCode,
mobile: app.mobile
}
})
.then(result => {
// 显示发送成功
app.$toast(result.message)
// 执行定时器
app.timer()
})
.finally(() => app.isLoading = false)
},
// 执行定时器
timer() {
const app = this
app.smsState = true
const inter = setInterval(() => {
app.times = app.times - 1
if (app.times <= 0) {
app.smsState = false
app.times = times
clearInterval(inter)
}
}, 1000)
},
// 点击提交
handleSubmit() {
const app = this
if (!app.isLoading && app.formValidation(FORM_SUBMIT)) {
app.onSubmitEvent()
}
},
// 确认提交事件
onSubmitEvent() {
const app = this
app.isLoading = true
UserApi.bindMobile({ form: { smsCode: app.smsCode, mobile: app.mobile } })
.then(result => {
// 显示操作成功
app.$toast(result.message)
// 跳转回原页面
setTimeout(() => {
app.onNavigateBack(1)
}, 2000)
})
.finally(() => app.isLoading = false)
},
/**
* 提交成功-跳转回原页面
*/
onNavigateBack(delta) {
uni.navigateBack({
delta: Number(delta || 1)
})
}
}
}
</script>
<style lang="scss" scoped>
.container {
padding: 100rpx 60rpx;
min-height: 100vh;
background-color: #fff;
}
// 页面头部
.header {
margin-bottom: 50rpx;
.title {
color: #191919;
font-size: 50rpx;
}
.sub-title {
margin-top: 20rpx;
color: #b3b3b3;
font-size: 25rpx;
}
}
// 输入框元素
.form-item {
display: flex;
padding: 18rpx;
border-bottom: 1rpx solid #f3f1f2;
margin-bottom: 25rpx;
height: 96rpx;
&--input {
font-size: 26rpx;
letter-spacing: 1rpx;
flex: 1;
height: 100%;
}
&--parts {
min-width: 100rpx;
height: 100%;
}
// 图形验证码
.captcha {
height: 100%;
.image {
display: block;
width: 192rpx;
height: 100%;
}
}
// 短信验证码
.captcha-sms {
font-size: 22rpx;
line-height: 50rpx;
padding-right: 20rpx;
.activate {
color: #cea26a;
}
.un-activate {
color: #9e9e9e;
}
}
}
// 提交按钮
.submit-button {
width: 100%;
height: 86rpx;
margin-top: 70rpx;
// background: #cea26a;
background: linear-gradient(to right, #ecb53c, #ff9211);
text-align: center;
line-height: 86rpx;
color: #fff;
border-radius: 80rpx;
box-shadow: 0px 10px 20px 0px rgba(0, 0, 0, 0.1);
letter-spacing: 5rpx;
}
</style>
+720
View File
@@ -0,0 +1,720 @@
<template>
<view v-if="!isFirstload" class="container">
<!-- 页面头部 -->
<view class="main-header" :style="{ height: platform == 'H5' ? '260rpx' : '320rpx', paddingTop: platform == 'H5' ? '0' : '80rpx' }">
<image class="bg-image" src="/static/background/user-header2.png" mode="scaleToFill"></image>
<!-- 用户信息 -->
<view v-if="isLogin" class="user-info">
<view class="user-avatar" @click="handlePersonal()">
<avatar-image :url="userInfo.avatar_url" :width="100" />
</view>
<view class="user-content">
<!-- 会员昵称 -->
<view class="nick-name oneline-hide" @click="handlePersonal()">{{ userInfo.nick_name }}</view>
<!-- 会员等级 -->
<view v-if="userInfo.grade_id > 0 && userInfo.grade" class="user-grade">
<view class="user-grade_icon">
<image class="image" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAA0lBMVEUAAAD/tjL/tzH/uDP/uC7/tjH/tzH/tzL/tTH+tTL+tjP/tDD/tTD+tzD/tjL/szD/uDH/tjL/tjL+tjD/tjT/szb/tzL/tTL+uTH+tjL/tjL/tjL/tTT/tjL/tjL+tjH/uTL/vDD/tjL/tjH/tzL9uS//tTL/nBr/sS7/tjH/ujL/szD/uTv+rzf/tzL+tzH+vDP+uzL+tjP+ry7+tDL9ki/7szf/sEX/tTL/tjL+tjL/tTH/tTT/tzH/tzL/tjP/sTX/uTP/wzX+rTn/vDX9vC8m8ckhAAAAOXRSTlMAlnAMB/vjxKWGMh0S6drMiVxPRkEY9PLy0ru0sKagmo5+dGtgVCMgBP716eXWyMGxqJGRe2o5KSmFNjaYAAABP0lEQVQ4y8XS13KDMBAF0AWDDe4t7r3ETu9lVxJgJ/n/X8rKAzHG5TE+Twz3zki7I/g/KXdghIbGJewrU4yzn08Ebgl6TuZzzuOC6W5es3HX6qsSz3NFShRU0MpucytDmOSpu3yULx3CA9RD1HjVedc0jSjqm6ZzhUjDsFDQhSp/OKj5GQvg0+ZCOixsbtDLAeTTOm/yGi8GyIphIVsgH737FEDV44LJa88IRKK/SetrwT9G/GUIr6vXjoy4GXn7+RboVXnghuSjaoGecwQxL2su3CwAKlO+QFoqxI4FMctHQhQd2OhxTu184jWUlI+rMTBTn1/IQcJHQ6GQdZ7pWiDaNdhTt330efISeiqYwQEzQpTlsURJLhzkEmpCPsERfeIUVyXr6MNuIyp5uziW6xURtt7hhGwzmMNJExfO4Bd9X0ZPqAxdNwAAAABJRU5ErkJggg==">
</image>
</view>
<view class="user-grade_name">
<text>{{ userInfo.grade.name }}</text>
</view>
</view>
<!-- 会员无等级时显示手机号 -->
<view v-else class="mobile">{{ userInfo.mobile }}</view>
</view>
</view>
<!-- 未登录 -->
<view v-else class="user-info" @click="handleLogin">
<view class="user-avatar">
<avatar-image :width="100" />
</view>
<view class="user-content">
<view class="nick-name">未登录</view>
<view class="login-tips">点击登录账号</view>
</view>
</view>
</view>
<!-- 绑定手机号 -->
<view v-if="isLogin && !userInfo.mobile && setting[SettingKeyEnum.REGISTER.value].isManualBind" class="my-mobile"
@click="handleBindMobile()">
<view class="info">点击绑定手机号,确保账户安全</view>
<view class="btn-bind">去绑定</view>
</view>
<!-- 我的钱包 -->
<view class="my-asset">
<view class="asset-left flex-box dis-flex flex-x-around">
<view class="asset-left-item" style="max-width: 200rpx;" @click="onTargetWallet">
<view class="item-value dis-flex flex-x-center">
<text>{{ isLogin ? assets.balance : '--' }}</text>
</view>
<view class="item-name dis-flex flex-x-center">
<text class="oneline-hide">账户余额</text>
</view>
</view>
<view class="asset-left-item" @click="onTargetPoints">
<view class="item-value dis-flex flex-x-center">
<text>{{ isLogin ? assets.points : '--' }}</text>
</view>
<view class="item-name dis-flex flex-x-center">
<text class="oneline-hide">{{ setting[SettingKeyEnum.POINTS.value].points_name }}</text>
</view>
</view>
<view class="asset-left-item" @click="onTargetMyCoupon">
<view class="item-value dis-flex flex-x-center">
<text class="oneline-hide">{{ isLogin ? assets.coupon : '--' }}</text>
</view>
<view class="item-name dis-flex flex-x-center">
<text>优惠券</text>
</view>
</view>
</view>
<view class="asset-right">
<view class="asset-right-item" @click="onTargetWallet">
<view class="item-icon dis-flex flex-x-center">
<text class="iconfont icon-qianbao"></text>
</view>
<view class="item-name dis-flex flex-x-center">
<text>我的钱包</text>
</view>
</view>
</view>
</view>
<!-- 订单操作 -->
<view class="order-navbar">
<view class="order-navbar-item" v-for="(item, index) in orderNavbar" :key="index" @click="onTargetOrder(item)">
<view class="item-icon">
<text class="iconfont" :class="[`icon-${item.icon}`]"></text>
</view>
<view class="item-name">{{ item.name }}</view>
<view class="item-badge" v-if="item.count && item.count > 0">
<text v-if="item.count <= 99" class="text">{{ item.count }}</text>
<text v-else class="text">99+</text>
</view>
</view>
</view>
<!-- 我的服务 -->
<view class="my-service">
<view class="service-title">我的服务</view>
<view class="service-content clearfix">
<block v-for="(item, index) in service" :key="index">
<view v-if="item.type == 'link'" class="service-item" @click="handleService(item)">
<view class="item-icon">
<text class="iconfont" :class="[`icon-${item.icon}`]"></text>
</view>
<view class="item-name">{{ item.name }}</view>
<view class="item-badge" v-if="item.count && item.count > 0">
<text v-if="item.count <= 99" class="text">{{ item.count }}</text>
<text v-else class="text">99+</text>
</view>
</view>
<view v-if="item.type == 'button' && platform == 'MP-WEIXIN'" class="service-item">
<button class="btn-normal" :open-type="item.openType">
<view class="item-icon">
<text class="iconfont" :class="[`icon-${item.icon}`]"></text>
</view>
<view class="item-name">{{ item.name }}</view>
</button>
</view>
<view v-if="item.type == 'phone'" class="service-item">
<button class="btn-normal" @click="callPhone()">
<view class="item-icon">
<text class="iconfont" :class="[`icon-${item.icon}`]"></text>
</view>
<view class="item-name">{{ item.name }}</view>
</button>
</view>
</block>
</view>
</view>
<!-- 退出登录 -->
<view v-if="isLogin" class="my-logout">
<view class="logout-btn" @click="handleLogout()">
<text>退出登录</text>
</view>
</view>
</view>
</template>
<script>
import store from '@/store'
import AvatarImage from '@/components/avatar-image'
import { setCartTabBadge } from '@/core/app'
import SettingKeyEnum from '@/common/enum/setting/Key'
import SettingModel from '@/common/model/Setting'
import * as UserApi from '@/api/user'
import * as OrderApi from '@/api/order'
import { checkLogin } from '@/core/app'
// 订单操作
const orderNavbar = [
{ id: 'all', name: '全部订单', icon: 'qpdingdan' },
{ id: 'payment', name: '待支付', icon: 'daifukuan', count: 0 },
// { id: 'delivery', name: '待发货', icon: 'daifahuo', count: 0 },
// { id: 'received', name: '待收货', icon: 'daishouhuo', count: 0 },
{ id: 'progress', name: '进行中', icon: 'daifahuo', count: 0 },
{ id: 'complete', name: '已完成', icon: 'daishouhuo', count: 0 },
]
/**
* 我的服务
* id: 标识; name: 标题名称; icon: 图标; type 类型(link和button); url: 跳转的链接
*/
const service = [
{ id: 'address', name: '收货地址', icon: 'shouhuodizhi', type: 'link', url: 'pages/address/index' },
{ id: 'coupon', name: '领券中心', icon: 'lingquan', type: 'link', url: 'pages/coupon/index' },
// { id: 'myCoupon', name: '优惠券', icon: 'youhuiquan', type: 'link', url: 'pages/my-coupon/index' },
{ id: 'contact', name: '在线客服', icon: 'kefu', type: 'button', openType: 'contact' },
{ id: 'points', name: '我的积分', icon: 'jifen', type: 'link', url: 'pages/points/log' },
{ id: 'refund', name: '退换/售后', icon: 'shouhou', type: 'link', url: 'pages/refund/index', count: 0 },
{ id: 'orderCenter', name: '订单中心', icon: 'order-c', type: 'link', url: 'pages/order/center' },
{ id: 'help', name: '我的帮助', icon: 'bangzhu', type: 'link', url: 'pages/help/index' },
{ id: 'phone', name: '客服电话', icon: 'dianhua', type: 'phone', openType: 'contact' },
]
export default {
components: {
AvatarImage
},
data() {
return {
// 枚举类
SettingKeyEnum,
// 正在加载
isLoading: true,
// 首次加载
isFirstload: true,
// 是否已登录
isLogin: false,
// 系统设置
setting: {},
// 当前用户信息
userInfo: {},
// 账户资产
assets: { balance: '--', points: '--', coupon: '--' },
// 我的服务
service,
// 订单操作
orderNavbar,
// 当前用户待处理的订单数量
todoCounts: { payment: 0, deliver: 0, received: 0 },
tel: '18281516816'
}
},
/**
* 生命周期函数--监听页面显示
*/
onShow(options) {
this.onRefreshPage()
},
methods: {
// 刷新页面
onRefreshPage() {
// 更新购物车角标
setCartTabBadge()
// 判断是否已登录
this.isLogin = checkLogin()
// 获取页面数据
this.getPageData()
},
// 获取页面数据
getPageData(callback) {
const app = this
app.isLoading = true
Promise.all([app.getSetting(), app.getUserInfo(), app.getUserAssets(), app.getTodoCounts()])
.then(result => {
app.isFirstload = false
// 初始化我的服务数据
app.initService()
// 初始化订单操作数据
app.initOrderTabbar()
// 执行回调函数
callback && callback()
})
.catch(err => console.log('catch', err))
.finally(() => app.isLoading = false)
},
// 初始化我的服务数据
initService() {
const app = this
const newService = []
service.forEach(item => {
if (item.id === 'points') {
item.name = '我的' + app.setting[SettingKeyEnum.POINTS.value].points_name
}
// 数据角标
if (item.count != undefined) {
item.count = app.todoCounts[item.id]
}
newService.push(item)
})
app.service = newService
},
// 初始化订单操作数据
initOrderTabbar() {
const app = this
const newOrderNavbar = []
orderNavbar.forEach(item => {
if (item.count != undefined) {
item.count = app.todoCounts[item.id]
}
newOrderNavbar.push(item)
})
app.orderNavbar = newOrderNavbar
},
// 获取商城设置
getSetting() {
const app = this
return new Promise((resolve, reject) => {
SettingModel.data()
.then(setting => {
app.setting = setting
resolve(setting)
}).catch(reject)
})
},
// 获取当前用户信息
getUserInfo() {
const app = this
return new Promise((resolve, reject) => {
!app.isLogin ? resolve(null) : UserApi.info({}, { load: app.isFirstload })
.then(result => {
app.userInfo = result.data.userInfo
resolve(app.userInfo)
})
.catch(err => {
if (err.result && err.result.status == 401) {
app.isLogin = false
resolve(null)
} else {
reject(err)
}
})
})
},
// 获取账户资产
getUserAssets() {
const app = this
return new Promise((resolve, reject) => {
!app.isLogin ? resolve(null) : UserApi.assets({}, { load: app.isFirstload })
.then(result => {
app.assets = result.data.assets
resolve(app.assets)
})
.catch(err => {
if (err.result && err.result.status == 401) {
app.isLogin = false
resolve(null)
} else {
reject(err)
}
})
})
},
// 获取当前用户待处理的订单数量
getTodoCounts() {
const app = this
return new Promise((resolve, reject) => {
!app.isLogin ? resolve(null) : OrderApi.todoCounts({}, { load: app.isFirstload })
.then(result => {
app.todoCounts = result.data.counts
resolve(app.todoCounts)
})
.catch(err => {
if (err.result && err.result.status == 401) {
app.isLogin = false
resolve(null)
} else {
reject(err)
}
})
})
},
// 跳转到登录页
handleLogin() {
!this.isLogin && this.$navTo('pages/login/index')
},
// 跳转到绑定手机号页面
handleBindMobile() {
this.$navTo('pages/user/bind/index')
},
// 跳转到修改个人信息页
handlePersonal() {
this.$navTo('pages/user/personal/index')
},
// 退出登录
handleLogout() {
const app = this
uni.showModal({
title: '友情提示',
content: '您确定要退出登录吗?',
success(res) {
if (res.confirm) {
store.dispatch('Logout', {})
.then(result => app.onRefreshPage())
}
}
})
},
// 跳转到钱包页面
onTargetWallet() {
this.$navTo('pages/wallet/index')
},
// 跳转到订单页
onTargetOrder(item) {
this.$navTo('pages/order/index', { dataType: item.id })
},
// 跳转到我的积分页面
onTargetPoints() {
this.$navTo('pages/points/log')
},
// 跳转到我的优惠券页
onTargetMyCoupon() {
this.$navTo('pages/my-coupon/index')
},
// 跳转到服务页面
handleService({ url }) {
this.$navTo(url)
},
// 拨打电话
callPhone(){
console.log(this.setting.trade.phone);
let tel = this.tel
uni.makePhoneCall({
phoneNumber: this.setting.trade.phone
})
},
},
/**
* 下拉刷新
*/
onPullDownRefresh() {
// 获取首页数据
this.getPageData(() => {
uni.stopPullDownRefresh()
})
},
}
</script>
<style lang="scss" scoped>
// 页面头部
.main-header {
background-color: #fff;
// background-image: url('/static/background/user-header.png');
position: relative;
width: 100%;
height: 280rpx;
background-size: 100% 100%;
overflow: hidden;
display: flex;
align-items: center;
// padding-top: 40rpx;
padding-left: 30rpx;
.bg-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
}
.user-info {
display: flex;
height: 100rpx;
z-index: 1;
.user-content {
display: flex;
flex-direction: column;
justify-content: center;
margin-left: 30rpx;
color: #c59a46;
.nick-name {
font-size: 34rpx;
font-weight: bold;
max-width: 270rpx;
}
.mobile {
margin-top: 15rpx;
font-size: 28rpx;
}
.user-grade {
align-self: baseline;
display: flex;
align-items: center;
background: #3c3c3c;
margin-top: 12rpx;
border-radius: 10rpx;
padding: 4rpx 12rpx;
.user-grade_icon .image {
display: block;
width: 32rpx;
height: 32rpx;
}
.user-grade_name {
margin-left: 5rpx;
font-size: 26rpx;
color: #EEE0C3;
}
}
.login-tips {
margin-top: 12rpx;
font-size: 30rpx;
}
}
}
}
// 角标组件
.item-badge {
position: absolute;
top: 0;
right: 55rpx;
background: #fa2209;
color: #fff;
border-radius: 100%;
min-width: 38rpx;
height: 38rpx;
display: flex;
justify-content: center;
align-items: center;
padding: 1rpx;
font-size: 24rpx;
}
// 我的钱包
.my-asset {
display: flex;
background: #fff;
padding: 40rpx 0;
.asset-right {
width: 200rpx;
border-left: 1rpx solid #eee;
}
.asset-right-item {
text-align: center;
color: #545454;
.item-icon {
font-size: 44rpx;
}
.item-name {
margin-top: 14rpx;
font-size: 28rpx;
}
}
.asset-left-item {
max-width: 183rpx;
text-align: center;
color: #666;
padding: 0 16rpx;
.item-value {
font-size: 34rpx;
color: red;
}
.item-name {
margin-top: 6rpx;
}
.item-name {
margin-top: 14rpx;
font-size: 28rpx;
}
}
}
// 订单操作
.order-navbar {
display: flex;
margin: 20rpx auto 20rpx auto;
padding: 20rpx 0;
width: 94%;
box-shadow: 0 1rpx 5rpx 0px rgba(0, 0, 0, 0.05);
font-size: 30rpx;
border-radius: 5rpx;
background: #fff;
&-item {
position: relative;
width: 25%;
.item-icon {
text-align: center;
margin: 0 auto;
padding: 10rpx 0;
color: #545454;
font-size: 44rpx;
}
.item-name {
font-size: 28rpx;
color: #545454;
text-align: center;
margin-right: 10rpx;
}
}
}
// 我的服务
.my-service {
margin: 22rpx auto 22rpx auto;
padding: 22rpx 0;
width: 94%;
box-shadow: 0 1rpx 5rpx 0px rgba(0, 0, 0, 0.05);
border-radius: 5rpx;
background: #fff;
.service-title {
padding-left: 24rpx;
margin-bottom: 20rpx;
font-size: 30rpx;
}
.service-content {
margin-bottom: -20rpx;
.service-item {
position: relative;
width: 25%;
float: left;
margin-bottom: 30rpx;
.item-icon {
text-align: center;
margin: 0 auto;
padding: 14rpx 0;
color: #ff3800;
font-size: 44rpx;
}
.item-name {
font-size: 28rpx;
color: #545454;
text-align: center;
margin-right: 10rpx;
}
}
}
}
// 退出登录
.my-logout {
display: flex;
justify-content: center;
margin-top: 50rpx;
.logout-btn {
width: 60%;
margin: 0 auto;
font-size: 28rpx;
color: #616161;
border-radius: 20rpx;
border: 1px solid #dcdcdc;
padding: 16rpx 0;
text-align: center;
}
}
// 绑定手机号 样式1
.my-mobile {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16rpx 40rpx;
background: #FCEBD1;
.info {
color: #cd8c0c;
font-size: 28rpx;
}
.btn-bind {
padding: 8rpx 24rpx;
background-color: #EAB766;
color: #fff;
border-radius: 30rpx;
font-size: 26rpx;
text-align: center;
}
}
// 绑定手机号 样式2
.my-mobile2 {
display: flex;
justify-content: space-between;
align-items: center;
margin: 20rpx auto 20rpx auto;
padding: 12rpx 40rpx;
width: 94%;
box-shadow: 0 1rpx 5rpx 0px rgba(0, 0, 0, 0.05);
font-size: 30rpx;
border-radius: 5rpx;
background: #fff;
.info {
// color: #cd8c0c;
font-size: 26rpx;
}
.btn-bind {
padding: 8rpx 24rpx;
background-color: #EAB766;
color: #fff;
border-radius: 30rpx;
font-size: 26rpx;
text-align: center;
}
}
</style>
+240
View File
@@ -0,0 +1,240 @@
<template>
<view class="container" :style="appThemeStyle">
<!-- 标题 -->
<view class="page-title">个人信息</view>
<!-- 表单组件 -->
<view class="form-wrapper">
<u-form :model="form" ref="uForm" label-width="140rpx">
<u-form-item label="头像">
<button class="btn-normal" open-type="chooseAvatar" @click="onClickAvatar()" @chooseavatar="onChooseAvatar">
<avatar-image :url="avatarUrl" :width="100" />
</button>
</u-form-item>
<u-form-item label="昵称" prop="nickName">
<u-input v-model="form.nickName" type="nickname" maxlength="30" placeholder="请输入昵称" @input="onInputNickName"
@blur="onInputNickName" />
</u-form-item>
</u-form>
</view>
<!-- 操作按钮 -->
<view class="footer">
<view class="btn-wrapper">
<view class="btn-item btn-item-main" :class="{ disabled }" @click="handleSubmit()">保存</view>
</view>
</view>
</view>
</template>
<script>
import store from '@/store'
import AvatarImage from '@/components/avatar-image'
import * as UserApi from '@/api/user'
import * as UploadApi from '@/api/upload'
// 表单验证规则
const rules = {
nickName: [{
required: true,
message: '请输入用户昵称',
trigger: ['blur', 'change']
}]
}
export default {
components: {
AvatarImage
},
data() {
return {
// 按钮禁用
disabled: false,
// 头像路径 (用于显示)
avatarUrl: '',
// 临时图片 (用于上传)
tempFile: null,
// 表单数据
form: {
avatarId: '',
nickName: ''
},
// 验证规则
rules,
}
},
/**
* 生命周期函数--监听页面加载
*/
onLoad() {
// 获取当前用户信息
this.getUserInfo()
},
// 必须要在onReady生命周期,因为onLoad生命周期组件可能尚未创建完毕
onReady() {
this.$refs.uForm.setRules(this.rules)
},
methods: {
// 获取当前用户信息
getUserInfo() {
const app = this
UserApi.info()
.then(result => {
const userInfo = result.data.userInfo
app.avatarUrl = userInfo.avatar_url
app.form.avatarId = userInfo.avatar_id
app.form.nickName = userInfo.nick_name
})
},
// 点击头像按钮事件
onClickAvatar() {
// #ifdef MP-WEIXIN
return
// #endif
this.chooseImage()
},
// 选择头像事件 - 仅限微信小程序
// #ifdef MP-WEIXIN
onChooseAvatar({ detail }) {
const app = this
app.avatarUrl = detail.avatarUrl
app.tempFile = { path: app.avatarUrl }
},
// #endif
// 选择图片
chooseImage() {
const app = this
// 选择图片
uni.chooseImage({
count: 1,
sizeType: ['original', 'compressed'], // 可以指定是原图还是压缩图,默认二者都有
sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有
success({ tempFiles }) {
app.tempFile = tempFiles[0]
app.avatarUrl = app.tempFile.path
}
});
},
// 上传图片
uploadFile() {
const app = this
return UploadApi.image([app.tempFile])
.then(fileIds => {
app.form.avatarId = fileIds[0]
app.tempFile = null
return true
})
.catch(() => {
app.disabled = false
return false
})
},
// 确认修改
async handleSubmit() {
const app = this
// 判断是否重复提交
if (app.disabled === true) return
app.$refs.uForm.validate(async valid => {
if (!valid) return
// 按钮禁用
app.disabled = true
// 先上传头像图片
if (app.tempFile && !await app.uploadFile()) {
return
}
// 提交保存个人信息
UserApi.personal({ form: app.form })
.then(result => {
app.$toast(result.message)
setTimeout(() => {
app.disabled = false
uni.navigateBack()
}, 1500)
})
.catch(err => app.disabled = false)
})
},
// 绑定昵称输入框 (用于微信小程序端快速填写昵称能力)
onInputNickName(val) {
if (val) {
this.form.nickName = val
}
}
}
}
</script>
<style>
page {
background: #f7f8fa;
}
</style>
<style lang="scss" scoped>
.container {}
.page-title {
width: 94%;
margin: 0 auto;
padding-top: 40rpx;
font-size: 28rpx;
color: rgba(69, 90, 100, 0.6);
}
.form-wrapper {
margin: 20rpx auto 20rpx auto;
padding: 0 40rpx;
width: 94%;
box-shadow: 0 1rpx 5rpx 0px rgba(0, 0, 0, 0.05);
border-radius: 16rpx;
background: #fff;
}
/* 底部操作栏 */
.footer {
margin-top: 80rpx;
.btn-wrapper {
height: 100%;
// display: flex;
// align-items: center;
padding: 0 20rpx;
}
.btn-item {
flex: 1;
font-size: 28rpx;
height: 86rpx;
color: #fff;
border-radius: 50rpx;
display: flex;
justify-content: center;
align-items: center;
}
.btn-item-wechat {
background: #0ba90b;
margin-bottom: 26rpx;
}
.btn-item-main {
background: linear-gradient(to right, #f9211c, #ff6335);
color: #fff;
// 禁用按钮
&.disabled {
opacity: 0.6;
}
}
}
</style>
+127
View File
@@ -0,0 +1,127 @@
<template>
<view class="container">
<mescroll-body ref="mescrollRef" :sticky="true" @init="mescrollInit" :down="{ use: false }" :up="upOption"
@up="upCallback">
<view class="log-list">
<view v-for="(item, index) in list.data" :key="index" class="log-item">
<view class="item-left flex-box">
<view class="rec-status">
<text>{{ item.describe }}</text>
</view>
<view class="rec-time">
<text>{{ item.create_time }}</text>
</view>
</view>
<view class="item-right">
<text>{{ item.money > 0 ? '+' : '' }}{{ item.money }}元</text>
</view>
</view>
</view>
</mescroll-body>
</view>
</template>
<script>
import MescrollBody from '@/components/mescroll-uni/mescroll-body.vue'
import MescrollMixin from '@/components/mescroll-uni/mescroll-mixins'
import * as LogApi from '@/api/balance/log'
import { getEmptyPaginateObj, getMoreListData } from '@/core/app'
const pageSize = 15
export default {
components: {
MescrollBody
},
mixins: [MescrollMixin],
data() {
return {
// 余额账单明细列表
list: getEmptyPaginateObj(),
// 上拉加载配置
upOption: {
// 首次自动执行
auto: true,
// 每页数据的数量; 默认10
page: { size: pageSize },
// 数量要大于12条才显示无更多数据
noMoreSize: 12,
// 空布局
empty: {
tip: '亲,暂无账单明细'
}
}
}
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {},
methods: {
/**
* 上拉加载的回调 (页面初始化时也会执行一次)
* 其中page.num:当前页 从1开始, page.size:每页数据条数,默认10
* @param {Object} page
*/
upCallback(page) {
const app = this
// 设置列表数据
app.getLogList(page.num)
.then(list => {
const curPageLen = list.data.length
const totalSize = list.data.total
app.mescroll.endBySize(curPageLen, totalSize)
})
.catch(() => app.mescroll.endErr())
},
// 获取余额账单明细列表
getLogList(pageNo = 1) {
const app = this
return new Promise((resolve, reject) => {
LogApi.list({ page: pageNo })
.then(result => {
// 合并新数据
const newList = result.data.list
app.list.data = getMoreListData(newList, app.list, pageNo)
resolve(newList)
})
})
}
}
}
</script>
<style lang="scss" scoped>
page,
.container {
background: #fff;
}
.log-list {
padding: 0 30rpx;
}
.log-item {
font-size: 28rpx;
padding: 20rpx 20rpx;
line-height: 1.8;
border-bottom: 1rpx solid rgb(238, 238, 238);
display: flex;
justify-content: center;
align-items: center;
}
.rec-status {
color: #333;
.rec-time {
color: rgb(160, 160, 160);
font-size: 26rpx;
}
}
</style>
@@ -0,0 +1,286 @@
<template>
<view class="container" v-if="userInfo.user_id">
<view class="account-panel dis-flex flex-y-center">
<view class="panel-lable">
<text>账户余额</text>
</view>
<view class="panel-balance flex-box">
<text>¥{{ userInfo.balance }}</text>
</view>
</view>
<view class="recharge-panel">
<!-- <view class="recharge-label">
<text>实体卡号</text>
</view> -->
<!-- 实体卡号 -->
<!-- <view class="recharge-input">
<input type="text" placeholder="请输十二位实体卡号" v-model="number" @input="onChangeNumber" />
</view>
<view class="recharge-label"></view> -->
<view class="recharge-label">
<text>卡密</text>
</view>
<view class="recharge-input">
<input type="text" placeholder="请输入十六位卡密" v-model="key" @input="onChangeKey" />
<image class="scan" src="/static/order/scan.png" @click="scanCode"></image>
</view>
<!-- 确认按钮 -->
<view class="recharge-submit btn-submit">
<form @submit="onSubmit">
<button class="button" formType="submit" :disabled="disabled">确认兑换</button>
</form>
</view>
</view>
<!-- 兑换描述 -->
<view class="recharge-describe">
<view class="recharge-label">
<text>兑换说明</text>
</view>
<view class="content">
<text space="ensp">{{ setting.describe }}</text>
</view>
</view>
</view>
</template>
<script>
import * as UserApi from '@/api/user'
import * as CardApi from '@/api/card'
import SettingModel from '@/common/model/Setting'
import SettingKeyEnum from '@/common/enum/setting/Key'
export default {
data() {
return {
// 正在加载
isLoading: true,
// 会员信息
userInfo: {},
// 分销设置
setting: {},
// 按钮禁用
disabled: false,
// 卡号,卡密
number: '',
key: ''
}
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
// 获取页面数据
this.getPageData()
},
methods: {
// 卡号输入框
onChangeNumber(e) {
this.number = e.target.value
},
// 卡密输入框
onChangeKey(e) {
this.key = e.target.value
},
// 获取页面数据
getPageData() {
const app = this
app.isLoading = true
Promise.all([app.getUserInfo(), app.getSetting()])
.then(() => app.isLoading = false)
},
// 获取会员信息
getUserInfo() {
const app = this
return new Promise((resolve, reject) => {
UserApi.info()
.then(result => {
app.userInfo = result.data.userInfo
resolve(app.userInfo)
})
})
},
// 获取充值设置
getSetting() {
const app = this
return new Promise((resolve, reject) => {
SettingModel.item(SettingKeyEnum.EXCHANGE.value, false)
.then(data => {
app.setting = data
resolve(data)
})
})
},
scanCode() {
uni.scanCode({
success: (res) => {
// 处理扫码结果
this.key = res.result
},
fail: (err) => {
this.$error('扫码失败')
}
});
},
// 立即兑换
onSubmit(e) {
const app = this
// 按钮禁用
app.disabled = true
// 提交到后端
CardApi.submit({ card_number: app.number, secret_key: app.key })
.then(result => {
app.$success('兑换处理中,请稍后查看')
this.number = ''
this.key = ''
setTimeout(() => {
// 获取页面数据
app.getPageData()
}, 1500)}
)
.finally(() => app.disabled = false)
}
}
}
</script>
<style lang="scss" scoped>
page,
.container {
background: #fff;
}
.container {
padding-bottom: 70rpx;
}
/* 账户面板 */
.account-panel {
width: 650rpx;
height: 180rpx;
margin: 50rpx auto;
padding: 0 60rpx;
box-sizing: border-box;
border-radius: 12rpx;
color: #fff;
background: linear-gradient(-125deg, #a46bff, #786cff);
box-shadow: 0 5px 22px 0 rgba(0, 0, 0, 0.26);
}
.panel-lable {
font-size: 32rpx;
}
.recharge-label {
color: rgb(51, 51, 51);
font-size: 28rpx;
margin-bottom: 25rpx;
}
.panel-balance {
text-align: right;
font-size: 46rpx;
}
.recharge-panel {
margin-top: 60rpx;
padding: 0 60rpx;
}
/* 充值套餐 */
.recharge-plan {
.recharge-plan_item {
width: 192rpx;
padding: 15rpx 0;
float: left;
text-align: center;
color: #888;
border: 1rpx solid rgb(228, 228, 228);
border-radius: 10rpx;
margin: 0 20rpx 20rpx 0;
&:nth-child(3n + 0) {
margin-right: 0;
}
&.active {
color: #786cff;
border: 1rpx solid #786cff;
.plan_money {
color: #786cff;
}
}
}
}
.plan_money {
font-size: 32rpx;
color: rgb(82, 82, 82);
}
.plan_gift {
font-size: 25rpx;
}
.recharge-input {
margin-top: 25rpx;
display: flex;
align-items: center;
image{
width: 35px;
height: 35px;
}
input {
border: 1rpx solid rgb(228, 228, 228);
border-radius: 10rpx;
padding: 15rpx 16rpx;
font-size: 26rpx;
flex: 1;
}
}
/* 立即充值 */
.recharge-submit {
margin-top: 70rpx;
}
.btn-submit {
.button {
font-size: 30rpx;
background: #786cff;
border: none;
color: white;
border-radius: 50rpx;
padding: 0 120rpx;
line-height: 3;
}
.button[disabled] {
background: #a098ff;
border-color: #a098ff;
color: white;
}
}
/* 充值说明 */
.recharge-describe {
margin-top: 50rpx;
padding: 0 60rpx;
.content {
font-size: 26rpx;
line-height: 1.6;
color: #888;
}
}
</style>
+130
View File
@@ -0,0 +1,130 @@
<template>
<view class="container">
<mescroll-body ref="mescrollRef" :sticky="true" @init="mescrollInit" :down="{ use: false }" :up="upOption"
@up="upCallback">
<view class="log-list">
<view v-for="(item, index) in list.data" :key="index" class="log-item ">
<view :class="['item-left', 'flex-box', item.state === 10 ? 'red' : '']">
<view class="rec-status">
<text>{{ item.card_number }} {{ item.describe }}</text>
</view>
<view class="rec-time">
<text>{{ item.create_time }}</text>
</view>
</view>
<view :class="['item-right', item.state === 10 ? 'red' : '']">
<text>{{ item.money }}元</text>
</view>
</view>
</view>
</mescroll-body>
</view>
</template>
<script>
import MescrollBody from '@/components/mescroll-uni/mescroll-body.vue'
import MescrollMixin from '@/components/mescroll-uni/mescroll-mixins'
import * as LogApi from '@/api/card/log'
import { getEmptyPaginateObj, getMoreListData } from '@/core/app'
const pageSize = 15
export default {
components: {
MescrollBody
},
mixins: [MescrollMixin],
data() {
return {
// 兑换明细列表
list: getEmptyPaginateObj(),
// 上拉加载配置
upOption: {
// 首次自动执行
auto: true,
// 每页数据的数量; 默认10
page: { size: pageSize },
// 数量要大于12条才显示无更多数据
noMoreSize: 12,
// 空布局
empty: {
tip: '亲,暂无兑换明细'
}
}
}
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {},
methods: {
/**
* 上拉加载的回调 (页面初始化时也会执行一次)
* 其中page.num:当前页 从1开始, page.size:每页数据条数,默认10
* @param {Object} page
*/
upCallback(page) {
const app = this
// 设置列表数据
app.getLogList(page.num)
.then(list => {
const curPageLen = list.data.length
const totalSize = list.data.total
app.mescroll.endBySize(curPageLen, totalSize)
})
.catch(() => app.mescroll.endErr())
},
// 获取余额账单明细列表
getLogList(pageNo = 1) {
const app = this
return new Promise((resolve, reject) => {
LogApi.list({ page: pageNo })
.then(result => {
// 合并新数据
const newList = result.data.list
app.list.data = getMoreListData(newList, app.list, pageNo)
resolve(newList)
})
})
}
}
}
</script>
<style lang="scss" scoped>
page,
.container {
background: #fff;
}
.log-list {
padding: 0 30rpx;
}
.log-item {
font-size: 28rpx;
padding: 20rpx 20rpx;
line-height: 1.8;
border-bottom: 1rpx solid rgb(238, 238, 238);
display: flex;
justify-content: center;
align-items: center;
}
.rec-status {
// color: #333;
.rec-time {
color: rgb(160, 160, 160);
font-size: 26rpx;
}
}
.red {
color: red;
}
</style>
+218
View File
@@ -0,0 +1,218 @@
<template>
<view class="container" v-if="!isLoading">
<view class="space-upper">
<view class="wallet-image">
<image src="/static/wallet.png" mode="widthFix"></image>
</view>
<view class="wallet-account">
<view class="wallet-account_balance">
<text>{{ userInfo.balance }}</text>
</view>
<view class="wallet-account_lable">
<text>账户余额(元)</text>
</view>
</view>
</view>
<view class="space-lower">
<view class="space-lower_item btn-recharge btn-group">
<view v-if="setting.is_entrance" class="btn-submit" @click="onTargetRecharge()">充 值</view>
<view v-if="share.is_entrance" class="btn-submit" @click="onTargetShare()">分 享</view>
<view v-if="exchange.is_entrance" class="btn-submit" @click="onTargetExchange()">实体卡兑换</view>
</view>
<view class="space-lower_item item-lable dis-flex flex-x-around">
<view class="lable-text" @click="onTargetBalanceLog()">
<text>账单详情</text>
</view>
<view class="lable-text" @click="onTargetExchangeLog()">
<text>兑换记录</text>
</view>
</view>
</view>
</view>
</template>
<script>
import * as UserApi from '@/api/user'
import SettingModel from '@/common/model/Setting'
import SettingKeyEnum from '@/common/enum/setting/Key'
export default {
data() {
return {
// 正在加载
isLoading: true,
// 会员信息
userInfo: {},
// 充值设置
setting: {},
// 实体卡兑换设置
exchange: {},
// 分享设置
share: {},
}
},
/**
* 生命周期函数--监听页面加载
*/
onShow(options) {
// 获取页面数据
this.getPageData()
},
methods: {
// 获取页面数据
getPageData() {
const app = this
app.isLoading = true
Promise.all([app.getUserInfo(), app.getSetting(), app.getExchange(), app.getShare()])
.then(() => app.isLoading = false)
},
// 获取会员信息
getUserInfo() {
const app = this
return new Promise((resolve, reject) => {
UserApi.info()
.then(result => {
app.userInfo = result.data.userInfo
resolve(app.userInfo)
})
})
},
// 获取充值设置
getSetting() {
const app = this
return new Promise((resolve, reject) => {
SettingModel.item(SettingKeyEnum.RECHARGE.value, false)
.then(data => {
app.setting = data
resolve(data)
})
})
},
// 获取兑换设置
getExchange() {
const app = this
return new Promise((resolve, reject) => {
SettingModel.item(SettingKeyEnum.EXCHANGE.value, false)
.then(data => {
app.exchange = data
resolve(data)
})
})
},
// 获取分享设置
getShare() {
const app = this
return new Promise((resolve, reject) => {
SettingModel.item(SettingKeyEnum.SHARE.value, false)
.then(data => {
app.share = data
resolve(data)
})
})
},
// 跳转分享页面
onTargetShare() {
this.$navTo('pages/wallet/share/index')
},
// 跳转兑换页面
onTargetExchange() {
this.$navTo('pages/wallet/exchange/index')
},
// 跳转充值页面
onTargetRecharge() {
this.$navTo('pages/wallet/recharge/index')
},
// 跳转充值记录页面
onTargetRechargeOrder() {
this.$navTo('pages/wallet/recharge/order')
},
// 跳转账单详情页面
onTargetBalanceLog() {
this.$navTo('pages/wallet/balance/log')
},
// 跳转兑换详情页面
onTargetExchangeLog() {
this.$navTo('pages/wallet/exchange/log')
}
}
}
</script>
<style>
page {
background: #fff;
}
</style>
<style lang="scss" scoped>
.container {
background: #fff;
}
.space-upper {
padding: 150rpx 0;
text-align: center;
}
.wallet-image image {
width: 360rpx;
height: 261.72rpx;
}
.wallet-account {
margin-top: 20rpx;
}
.wallet-account_balance {
font-size: 56rpx;
}
.wallet-account_lable {
margin-top: 14rpx;
color: #cec1c1;
font-size: 26rpx;
}
.space-lower {
margin-top: 30rpx;
padding: 0 80rpx;
}
.btn-group{
display: flex;
gap: 10px;
}
.btn-recharge .btn-submit {
width: 460rpx;
height: 84rpx;
margin: 0 auto;
border-radius: 50rpx;
background: #786cff;
color: white;
font-size: 30rpx;
display: flex;
justify-content: center;
align-items: center;
}
.item-lable {
margin-top: 80rpx;
font-size: 28rpx;
color: rgb(94, 94, 94);
padding: 0 100rpx;
}
</style>
@@ -0,0 +1,300 @@
<template>
<view class="container" v-if="userInfo.user_id">
<view class="account-panel dis-flex flex-y-center">
<view class="panel-lable">
<text>账户余额</text>
</view>
<view class="panel-balance flex-box">
<text>¥{{ userInfo.balance }}</text>
</view>
</view>
<view class="recharge-panel">
<view class="recharge-label">
<text>充值金额</text>
</view>
<view class="recharge-plan clearfix">
<block v-for="(item, index) in planList" :key="index">
<view class="recharge-plan_item" :class="{ active: selectedPlanId == item.plan_id }" @click="onSelectPlan(item.plan_id)">
<view class="plan_money">
<text>{{ item.money }}</text>
</view>
<view class="plan_gift" v-if="item.gift_money > 0">
<text>送{{ item.gift_money }}</text>
</view>
</view>
</block>
</view>
<!-- 手动充值输入框 -->
<view class="recharge-input" v-if="setting.is_custom == 1">
<input type="digit" placeholder="请输入充值金额" v-model="inputValue" @input="onChangeMoney" />
</view>
<!-- 确认按钮 -->
<view class="recharge-submit btn-submit">
<form @submit="onSubmit">
<button class="button" formType="submit" :disabled="disabled">立即充值</button>
</form>
</view>
</view>
<!-- 充值描述 -->
<view class="recharge-describe">
<view class="recharge-label">
<text>充值说明</text>
</view>
<view class="content">
<text space="ensp">{{ setting.describe }}</text>
</view>
</view>
</view>
</template>
<script>
import * as UserApi from '@/api/user'
import * as RechargeApi from '@/api/recharge'
import * as PlanApi from '@/api/recharge/plan'
import SettingModel from '@/common/model/Setting'
import SettingKeyEnum from '@/common/enum/setting/Key'
import { wxPayment } from '@/core/app'
export default {
data() {
return {
// 正在加载
isLoading: true,
// 会员信息
userInfo: {},
// 充值设置
setting: {},
// 充值方案列表
planList: [],
// 按钮禁用
disabled: false,
// 当前选中的套餐id
selectedPlanId: 0,
// 自定义金额
inputValue: '',
}
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
// 获取页面数据
this.getPageData()
},
methods: {
/**
* 选择充值套餐
*/
onSelectPlan(planId) {
this.selectedPlanId = planId
this.inputValue = ''
},
// 金额输入框
onChangeMoney(e) {
this.inputValue = e.target.value
this.selectedPlanId = 0
},
// 获取页面数据
getPageData() {
const app = this
app.isLoading = true
Promise.all([app.getUserInfo(), app.getSetting(), app.getPlanList()])
.then(() => app.isLoading = false)
},
// 获取充值方案列表
getPlanList() {
const app = this
return new Promise((resolve, reject) => {
PlanApi.list()
.then(result => {
app.planList = result.data.list
resolve(app.planList)
})
})
},
// 获取会员信息
getUserInfo() {
const app = this
return new Promise((resolve, reject) => {
UserApi.info()
.then(result => {
app.userInfo = result.data.userInfo
resolve(app.userInfo)
})
})
},
// 获取充值设置
getSetting() {
const app = this
return new Promise((resolve, reject) => {
SettingModel.item(SettingKeyEnum.RECHARGE.value, false)
.then(data => {
app.setting = data
resolve(data)
})
})
},
// 立即充值
onSubmit(e) {
const app = this
// 按钮禁用
app.disabled = true
// 提交到后端
RechargeApi.submit({ planId: app.selectedPlanId, customMoney: app.inputValue })
.then(result => app.wxPayment(result.data.payment))
.finally(() => app.disabled = false)
},
// 发起微信支付
wxPayment(option) {
const app = this
wxPayment(option)
.then(() => {
app.$success('支付成功')
setTimeout(() => {
// 获取页面数据
app.getPageData()
}, 1500)
})
.catch(err => app.$error('订单未支付'))
}
}
}
</script>
<style lang="scss" scoped>
page,
.container {
background: #fff;
}
.container {
padding-bottom: 70rpx;
}
/* 账户面板 */
.account-panel {
width: 650rpx;
height: 180rpx;
margin: 50rpx auto;
padding: 0 60rpx;
box-sizing: border-box;
border-radius: 12rpx;
color: #fff;
background: linear-gradient(-125deg, #a46bff, #786cff);
box-shadow: 0 5px 22px 0 rgba(0, 0, 0, 0.26);
}
.panel-lable {
font-size: 32rpx;
}
.recharge-label {
color: rgb(51, 51, 51);
font-size: 28rpx;
margin-bottom: 25rpx;
}
.panel-balance {
text-align: right;
font-size: 46rpx;
}
.recharge-panel {
margin-top: 60rpx;
padding: 0 60rpx;
}
/* 充值套餐 */
.recharge-plan {
.recharge-plan_item {
width: 192rpx;
padding: 15rpx 0;
float: left;
text-align: center;
color: #888;
border: 1rpx solid rgb(228, 228, 228);
border-radius: 10rpx;
margin: 0 20rpx 20rpx 0;
&:nth-child(3n + 0) {
margin-right: 0;
}
&.active {
color: #786cff;
border: 1rpx solid #786cff;
.plan_money {
color: #786cff;
}
}
}
}
.plan_money {
font-size: 32rpx;
color: rgb(82, 82, 82);
}
.plan_gift {
font-size: 25rpx;
}
.recharge-input {
margin-top: 25rpx;
input {
border: 1rpx solid rgb(228, 228, 228);
border-radius: 10rpx;
padding: 15rpx 16rpx;
font-size: 26rpx;
}
}
/* 立即充值 */
.recharge-submit {
margin-top: 70rpx;
}
.btn-submit {
.button {
font-size: 30rpx;
background: #786cff;
border: none;
color: white;
border-radius: 50rpx;
padding: 0 120rpx;
line-height: 3;
}
.button[disabled] {
background: #a098ff;
border-color: #a098ff;
color: white;
}
}
/* 充值说明 */
.recharge-describe {
margin-top: 50rpx;
padding: 0 60rpx;
.content {
font-size: 26rpx;
line-height: 1.6;
color: #888;
}
}
</style>
@@ -0,0 +1,127 @@
<template>
<view class="container">
<mescroll-body ref="mescrollRef" :sticky="true" @init="mescrollInit" :down="{ use: false }" :up="upOption"
@up="upCallback">
<view class="log-list">
<view v-for="(item, index) in list.data" :key="index" class="log-item">
<view class="item-left flex-box">
<view class="rec-status">
<text>{{ '充值成功' }}</text>
</view>
<view class="rec-time">
<text>{{ item.pay_time }}</text>
</view>
</view>
<view class="item-right">
<text>+{{ item.actual_money }}元</text>
</view>
</view>
</view>
</mescroll-body>
</view>
</template>
<script>
import MescrollBody from '@/components/mescroll-uni/mescroll-body.vue'
import MescrollMixin from '@/components/mescroll-uni/mescroll-mixins'
import * as OrderApi from '@/api/recharge/order'
import { getEmptyPaginateObj, getMoreListData } from '@/core/app'
const pageSize = 15
export default {
components: {
MescrollBody
},
mixins: [MescrollMixin],
data() {
return {
// 余额账单明细列表
list: getEmptyPaginateObj(),
// 上拉加载配置
upOption: {
// 首次自动执行
auto: true,
// 每页数据的数量; 默认10
page: { size: pageSize },
// 数量要大于12条才显示无更多数据
noMoreSize: 12,
// 空布局
empty: {
tip: '亲,暂无充值记录'
}
}
}
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {},
methods: {
/**
* 上拉加载的回调 (页面初始化时也会执行一次)
* 其中page.num:当前页 从1开始, page.size:每页数据条数,默认10
* @param {Object} page
*/
upCallback(page) {
const app = this
// 设置列表数据
app.getLogList(page.num)
.then(list => {
const curPageLen = list.data.length
const totalSize = list.data.total
app.mescroll.endBySize(curPageLen, totalSize)
})
.catch(() => app.mescroll.endErr())
},
// 获取余额账单明细列表
getLogList(pageNo = 1) {
const app = this
return new Promise((resolve, reject) => {
OrderApi.list({ page: pageNo })
.then(result => {
// 合并新数据
const newList = result.data.list
app.list.data = getMoreListData(newList, app.list, pageNo)
resolve(newList)
})
})
}
}
}
</script>
<style lang="scss" scoped>
page,
.container {
background: #fff;
}
.log-list {
padding: 0 30rpx;
}
.log-item {
font-size: 28rpx;
padding: 20rpx 20rpx;
line-height: 1.8;
border-bottom: 1rpx solid rgb(238, 238, 238);
display: flex;
justify-content: center;
align-items: center;
}
.rec-status {
color: #333;
.rec-time {
color: rgb(160, 160, 160);
font-size: 26rpx;
}
}
</style>
+269
View File
@@ -0,0 +1,269 @@
<template>
<view class="container" v-if="userInfo.user_id">
<view class="account-panel dis-flex flex-y-center">
<view class="panel-lable">
<text>账户余额</text>
</view>
<view class="panel-balance flex-box">
<text>¥{{ userInfo.balance }}</text>
</view>
</view>
<view class="recharge-panel">
<view class="recharge-label">
<text>分享金额</text>
</view>
<!-- 手动分享输入框 -->
<view class="recharge-input">
<input type="digit" placeholder="请输入分享金额" v-model="inputValue" @input="onChangeMoney" />
</view>
<view class="recharge-label"></view>
<view class="recharge-label">
<text>分享手机号</text>
</view>
<view class="recharge-input">
<input type="text" placeholder="请输入手机号码" v-model="tel" @input="onChangeTel" />
</view>
<!-- 确认按钮 -->
<view class="recharge-submit btn-submit">
<form @submit="onSubmit">
<button class="button" formType="submit" :disabled="disabled">确认分享</button>
</form>
</view>
</view>
<!-- 分享描述 -->
<view class="recharge-describe">
<view class="recharge-label">
<text>分享说明</text>
</view>
<view class="content">
<text space="ensp">{{ setting.describe }}</text>
</view>
</view>
</view>
</template>
<script>
import * as UserApi from '@/api/user'
import * as RechargeApi from '@/api/recharge'
import SettingModel from '@/common/model/Setting'
import SettingKeyEnum from '@/common/enum/setting/Key'
export default {
data() {
return {
// 正在加载
isLoading: true,
// 会员信息
userInfo: {},
// 分享设置
setting: {},
// 按钮禁用
disabled: false,
// 自定义金额
inputValue: '',
tel: ''
}
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
// 获取页面数据
this.getPageData()
},
methods: {
// 金额输入框
onChangeMoney(e) {
this.inputValue = e.target.value
this.selectedPlanId = 0
},
// 手机号输入框
onChangeTel(e) {
this.tel = e.target.value
},
// 获取页面数据
getPageData() {
const app = this
app.isLoading = true
Promise.all([app.getUserInfo(), app.getSetting()])
.then(() => app.isLoading = false)
},
// 获取会员信息
getUserInfo() {
const app = this
return new Promise((resolve, reject) => {
UserApi.info()
.then(result => {
app.userInfo = result.data.userInfo
resolve(app.userInfo)
})
})
},
// 获取充值设置
getSetting() {
const app = this
return new Promise((resolve, reject) => {
SettingModel.item(SettingKeyEnum.SHARE.value, false)
.then(data => {
app.setting = data
resolve(data)
})
})
},
// 立即充值
onSubmit(e) {
const app = this
// 按钮禁用
app.disabled = true
// 提交到后端
RechargeApi.share({ customMoney: app.inputValue, tel: app.tel })
.then(result => {
app.$success('分享成功')
this.inputValue = ''
this.tel = ''
setTimeout(() => {
// 获取页面数据
app.getPageData()
}, 1500)}
)
.finally(() => app.disabled = false)
}
}
}
</script>
<style lang="scss" scoped>
page,
.container {
background: #fff;
}
.container {
padding-bottom: 70rpx;
}
/* 账户面板 */
.account-panel {
width: 650rpx;
height: 180rpx;
margin: 50rpx auto;
padding: 0 60rpx;
box-sizing: border-box;
border-radius: 12rpx;
color: #fff;
background: linear-gradient(-125deg, #a46bff, #786cff);
box-shadow: 0 5px 22px 0 rgba(0, 0, 0, 0.26);
}
.panel-lable {
font-size: 32rpx;
}
.recharge-label {
color: rgb(51, 51, 51);
font-size: 28rpx;
margin-bottom: 25rpx;
}
.panel-balance {
text-align: right;
font-size: 46rpx;
}
.recharge-panel {
margin-top: 60rpx;
padding: 0 60rpx;
}
/* 充值套餐 */
.recharge-plan {
.recharge-plan_item {
width: 192rpx;
padding: 15rpx 0;
float: left;
text-align: center;
color: #888;
border: 1rpx solid rgb(228, 228, 228);
border-radius: 10rpx;
margin: 0 20rpx 20rpx 0;
&:nth-child(3n + 0) {
margin-right: 0;
}
&.active {
color: #786cff;
border: 1rpx solid #786cff;
.plan_money {
color: #786cff;
}
}
}
}
.plan_money {
font-size: 32rpx;
color: rgb(82, 82, 82);
}
.plan_gift {
font-size: 25rpx;
}
.recharge-input {
margin-top: 25rpx;
input {
border: 1rpx solid rgb(228, 228, 228);
border-radius: 10rpx;
padding: 15rpx 16rpx;
font-size: 26rpx;
}
}
/* 立即充值 */
.recharge-submit {
margin-top: 70rpx;
}
.btn-submit {
.button {
font-size: 30rpx;
background: #786cff;
border: none;
color: white;
border-radius: 50rpx;
padding: 0 120rpx;
line-height: 3;
}
.button[disabled] {
background: #a098ff;
border-color: #a098ff;
color: white;
}
}
/* 充值说明 */
.recharge-describe {
margin-top: 50rpx;
padding: 0 60rpx;
.content {
font-size: 26rpx;
line-height: 1.6;
color: #888;
}
}
</style>
+41
View File
@@ -0,0 +1,41 @@
<template>
<view>
<!-- web-view 组件用于加载网页 -->
<web-view :src="webUrl" @message="onMessage"></web-view>
</view>
</template>
<script>
export default {
data() {
return {
// 外部网页的URL
webUrl: ''
}
},
onLoad(options) {
console.log(options)
this.webUrl = options.url
},
methods: {
// onLoad(event) {
// console.log('Webview loaded:', event);
// // 检查 event.detail 是否存在并获取新 URL
// if (event && event.detail && event.detail.src) {
// this.webUrl = event.detail.src;
// console.log('New URL:', this.webUrl);
// } else {
// console.warn('Load event does not contain expected src property');
// }
// },
onError(event) {
console.error('Error loading webview:', event);
// 你可以根据 event.detail 进一步处理具体错误
}
}
}
</script>
<style>
/* 样式可以根据需要自定义 */
</style>