Commit 1842bd1a by yewills

feat: 初始项目,叠加业务需求,新增的文件

parent c1dbe7b8
<template>
<view class="comment-item">
<view class="top-box">
<image
class="head-image"
:src="comment.head || 'https://static.ydlcdn.com/m/images/global/default.png'"
/>
<view class="top-right-box">
<view class="name-box">
<view class="name">
<text>{{ nickName }}</text>
<image
v-if="comment.sequence > 0"
mode="widthFix"
class="great-image"
src="//static.ydlcdn.com/m/images/zx/experts/detail/label_quality_cmt.png"
/>
</view>
<view class="create-time">
{{ $dayjs(comment.createTime).format('YYYY-MM-DD') }}
</view>
</view>
<view class="theme-box">
<text class="label">评价:</text>
<text class="value">{{ commentResultText }}</text>
<text class="label">咨询主题:</text>
<text class="value">{{ comment.categoryName }}</text>
</view>
</view>
</view>
<view class="comment-content">
<toggle-fold-text
:line="5"
:line-height="22"
:text="comment.content || ''"
>
content
</toggle-fold-text>
</view>
<view
v-if="comment.tags"
class="tag-box"
>
<text
v-for="(tag, index) in comment.tags.split(',')"
:key="index"
class="tag-item"
>
{{ tag }}
</text>
</view>
<view
v-if="comment.expertReply === 2"
class="reply-box"
>
<toggle-fold-text
:line="5"
:line-height="22"
:text="`${comment.replyName} 回复: ${comment.replycontent}`"
>
content
</toggle-fold-text>
</view>
</view>
</template>
<script>
import { ToggleFoldText } from '@/components/toggle-fold-text.vue'
export default {
name: 'CommentItem',
components: {
ToggleFoldText,
},
props: {
comment: {
type: Object,
required: true,
},
},
data() {
return {}
},
computed: {
nickName() {
if (this.comment.nickName === '匿名用户') return this.comment.nickName
return `${this.comment.nickName.slice(0, 1)}**`
},
commentResultText() {
const { type, pleased } = this.comment
if (type === 2) {
return pleased === 1 ? '满意' : '不满意'
} else if (type === 1) {
return pleased === 1 || pleased === 2 || pleased === 3 ? '满意' : '不满意'
} else {
return ''
}
},
},
mounted() {},
methods: {},
}
</script>
<style lang="less" scoped>
.comment-item {
padding: 20px 0;
.top-box {
display: flex;
align-items: center;
.head-image {
width: 36px;
height: 36px;
border-radius: 50%;
margin-right: 8px;
}
.top-right-box {
flex: 1;
.name-box {
display: flex;
align-items: center;
justify-content: space-between;
.name {
font-size: 13px;
line-height: 20px;
margin-right: 8px;
color: #9d9ea7;
display: flex;
align-items: center;
.great-image {
width: 70px;
margin-left: 4px;
}
}
.create-time {
font-size: 12px;
color: #9d9ea7;
opacity: 0.5;
}
}
.theme-box {
display: flex;
font-size: 13px;
line-height: 20px;
.label {
color: #9d9ea7;
}
.value {
color: #1c1f28;
+ .label {
margin-left: 24px;
}
}
}
}
}
.comment-content {
margin-top: 7px;
/deep/ .toggle-fold-text {
font-size: 14px;
line-height: 22px;
color: #1c1f28;
}
}
.tag-box {
margin-top: 5px;
.tag-item {
color: #999;
font-size: 12px;
font-weight: 400;
margin: 4px 16px 0 0;
}
}
.reply-box {
margin-top: 14px;
background: #f6f7f9;
padding: 10px 14px;
border-radius: 4px;
color: #1c1f28;
font-size: 14px;
font-weight: 400;
line-height: 22px;
}
+ .comment-item {
border-top: 1px solid #efeff1;
}
}
</style>
<template>
<view class="consult">
<view class="consult-item">
<view class="head-box">
<image
class="head-cover"
mode="aspectFill"
:src="item.confidedIcon"
/>
<view class="p-line-status">
<!-- 1 在线 2离线 -->
{{ lineStatus[item.confideLine] }}
</view>
<!-- 播放按钮 -->
<view
v-if="item.confideVoice"
class="player-btn"
@click.stop="playAudio(item)"
>
<img
class="player-icon"
:src="
isPlayer
? 'https://static.ydlcdn.com/mini/mini_confide/confide_icon_pause.png'
: 'https://static.ydlcdn.com/mini/mini_confide/confide_icon_music.png'
"
alt=""
/>
</view>
</view>
<view class="item-con">
<view class="h2">
<view class="item-con-title">
<text class="dib vm fs32 c-24 mr10 b">{{ item.confidedName }}</text>
<!-- 是否实习 -->
<img
v-if="item.abilityLevel === 1"
class="internship-icon"
src="https://static.ydlcdn.com/m/images/zx/experts/detail/internship.png"
alt=""
/>
</view>
</view>
<view class="p-comment">
<view class="comment">
<text class="num">{{ item.confideConnection }}</text>
<text class="c">接通率</text>
</view>
<view class="comment">
<text class="num">{{ item.listenOrderNum }}</text>
<text class="c">服务人次</text>
</view>
<view class="comment">
<text class="num">{{ item.confidePraiseScore }}</text>
<text class="c">评分</text>
</view>
</view>
<view class="tags-box dix alc">
<!-- {{ JSON.stringify(item.feature_tags) }} -->
<view
v-if="item.confidedTag && item.confidedTag.length > 0"
class="tags"
>
{{ item.confidedTag.join(' | ') }}
</view>
<view
v-if="item.confideFee"
class="way"
>
<view class="price">
<text class="num">{{ item.confideFee }}</text>
<text class="unit"></text>
<text class="time">/25分钟</text>
</view>
</view>
</view>
<view
class="chat"
:style="{ background: chat_status_color[item.chat_status] }"
>
<text
v-if="item.confideLine === 1"
class="chat-btn on-line"
></text>
<text
v-else-if="item.confideLine === 2"
class="chat-btn off-line"
>
{{ item.confideSex === 1 ? '他' : '她' }}上线
</text>
<text
v-else-if="item.confideLine === 3"
class="chat-btn on-call"
>
通话中
</text>
</view>
</view>
</view>
<!-- 倾诉语 -->
<view
v-if="item.confideContent"
class="desc"
>
{{ item.confideContent }}
</view>
</view>
</template>
<script>
export default {
props: {
item: {
type: Object,
default() {
return {}
},
},
currentDoctor: {
type: Number,
default: -1,
},
playStatus: {
type: Boolean,
default: false,
},
},
data() {
return {
lineStatus: {
1: '在线',
2: '离线',
3: '通话中',
},
isPlayer: false,
}
},
watch: {
// 播放音频倾诉师和当前倾诉师相同时,更新播放状态
currentDoctor(v) {
if (v === this.item.confidedId) {
this.isPlayer = this.playStatus
} else {
this.isPlayer = false
}
},
// 播放状态更新时若播放音频倾诉师和当前倾诉师相则更新
playStatus(v) {
if (this.currentDoctor === this.item.confidedId) {
this.isPlayer = this.playStatus
} else {
this.isPlayer = false
}
},
},
methods: {
playAudio(item) {
this.$emit('player', {
...item,
isPlayer: this.isPlayer,
})
this.isPlayer = !this.isPlayer
},
},
}
</script>
<style lang="less" scoped>
.consult {
padding: 20px 0;
border-bottom: 0.5px solid #efeff0;
.consult-item {
display: flex;
position: relative;
.head {
&-cover {
width: 90px;
height: 90px;
border-radius: 8px;
display: block;
}
&-box {
position: relative;
width: 90px;
height: 90px;
flex-shrink: 0;
.p-line-status {
position: absolute;
top: 0;
left: 0;
opacity: 0.95;
min-width: 40px;
background-color: rgba(0, 0, 0, 0.5);
font-size: 11px;
text-align: center;
color: white;
border-radius: 8px 0 8px 0;
line-height: 18px;
font-weight: 300;
}
.player-btn {
position: absolute;
bottom: 6px;
right: 6px;
display: flex;
align-items: center;
justify-content: center;
background: -webkit-gradient(linear, 0% 0%, 100% 0%, from(#ff62a5), to(#ff8960));
width: 24px;
height: 24px;
border-radius: 100%;
}
.player-icon {
width: 16px;
height: 16px;
vertical-align: middle;
}
}
}
.item-con {
position: relative;
margin-left: 12px;
flex-grow: 1;
.sex-icon {
display: inline-block;
width: 13px;
height: 13px;
vertical-align: middle;
}
.internship-icon {
display: inline-block;
width: 28px;
height: 17px;
vertical-align: middle;
}
.h2 {
display: flex;
align-items: center;
margin-bottom: 4px;
display: flex;
justify-content: space-between;
}
.lab {
border-radius: 2px;
background-color: #f6f6f7;
padding: 0 4px;
font-size: 10px;
color: #62636f;
font-weight: 300;
}
.place {
display: flex;
align-items: center;
image {
width: 12px;
height: 12px;
}
.area1 {
color: #62636f;
font-size: 11px;
font-weight: 300;
}
}
.p-personal {
margin-bottom: 4px;
line-height: 18px;
color: rgba(130, 131, 140, 100);
font-size: 13px;
overflow: hidden;
display: -webkit-box;
/*! autoprefixer: off */
-webkit-box-orient: vertical;
/* autoprefixer: on */
-webkit-line-clamp: 2;
word-break: break-all;
}
.p-comment {
display: flex;
.comment {
color: #1c1f28;
margin-right: 16px;
text-align: center;
.num {
font-size: 14px;
font-weight: 700;
display: block;
margin-bottom: 2px;
}
.c {
margin-left: 2px;
color: #82838c;
font-size: 11px;
}
}
}
}
.tags-box {
margin-top: 8px;
justify-content: space-between;
.tags {
height: 18px;
border-radius: 4px;
line-height: 17px;
color: #9d9ea9;
font-size: 11px;
white-space: nowrap;
max-width: 140px;
overflow: hidden;
text-overflow: ellipsis;
}
}
.way {
display: flex;
align-items: center;
position: relative;
.price {
color: #82838c;
margin-right: 12px;
font-size: 12px;
white-space: nowrap;
.num {
font-size: 18px;
color: #ff5b06;
}
.unit {
color: #ff5b06;
margin: 0 1px;
}
}
}
.chat {
position: absolute;
top: 0;
right: 0;
display: flex;
align-items: center;
justify-content: center;
.chat-btn {
display: block;
width: 40px;
height: 40px;
border-radius: 20px;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
}
.on-line {
background: #32d296 url('https://static.ydlcdn.com/mini/mini_confide/confide_phone.png')
no-repeat center;
background-size: 24px 24px;
}
.off-line {
font-size: 11px;
line-height: 15px;
padding: 4px;
border: 0.5px solid #efeff0;
}
.on-call {
font-size: 12px;
line-height: 20px;
color: #ffffff;
background: #ff8c00;
}
}
}
.desc {
margin: 16px auto 0 auto;
font-size: 14px;
line-height: 18px;
max-width: 100%;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
color: #999;
}
}
</style>
<template>
<view class="c-screen">
<view class="top-pos-wrap border-b1px box-c">
<view class="search-box dix alc">
<view
class="search-body dix alc flex1"
@tap="handleJumpSearch"
>
<image
mode="aspectFill"
class="icon-search"
src="https://static.ydlcdn.com/weixin/image/icon_search.png"
/>
<input
v-model="searchWord"
class="search-input"
disabled
type="text"
confirm-type="search"
placeholder="请输入倾诉师姓名"
/>
</view>
<view
v-if="searchWord"
class="search-ft"
@tap="handleCancel"
>
<text class="btn">取消</text>
</view>
</view>
<view class="menu-wrap">
<view
:class="[
'item',
showQuery === queryType.SORT ? 'active' : '',
selectSort.id !== 1 ? 'selected' : '',
]"
:data-type="queryType.SORT"
@tap="toggleTab"
>
<text class="text ellipsis">
{{ selectSort.id !== 1 ? selectSort.name : '排序' }}
</text>
<view class="arrow"></view>
</view>
<view
:class="[
'item',
showQuery === queryType.AGE_SEX ? 'active' : '',
showAgeSex || ageSelect.length + (sexSelect ? 1 : 0) ? 'selected' : '',
]"
:data-type="queryType.AGE_SEX"
@tap="toggleTab"
>
<text class="text ellipsis">{{ showAgeSex || '性别年龄' }}</text>
<view class="arrow"></view>
</view>
<view
:class="[
'item',
showQuery === queryType.GOOD ? 'active' : '',
showGood || directionSelect.length ? 'selected' : '',
]"
:data-type="queryType.GOOD"
@tap="toggleTab"
>
<text class="text ellipsis">{{ showGood || '擅长方向' }}</text>
<view class="arrow"></view>
</view>
</view>
</view>
<view v-show="showQuery !== 0">
<view
v-if="showQuery === queryType.AGE_SEX"
class="screen-box border-t1px screen-them"
>
<view class="other-box">
<view class="screen-title">{{ sexType.title }}</view>
<radio-group class="screen-con">
<label
v-for="item in sexType.data"
:key="item.name"
:class="['screen-item', cacheSex === item.id ? 'active' : '']"
:data-value="item.id"
@click="handleSexRadio"
>
<radio
class="y-check"
:value="item.id"
:checked="cacheSex === item.id"
/>
<view class="screen">{{ item.name }}</view>
</label>
</radio-group>
<view class="screen-title">{{ ageType.title }}</view>
<checkbox-group
data-type="Age"
class="screen-con"
@change="screenChange"
>
<label
v-for="item in ageType.data"
:key="item.name"
:class="['screen-item', filter.isIncludes(cacheAge, String(item.id)) ? 'active' : '']"
>
<checkbox
class="y-check"
:value="item.id"
:checked="filter.isIncludes(cacheAge, String(item.id))"
/>
<view class="screen">{{ item.name }}</view>
</label>
</checkbox-group>
</view>
<view class="sure-btn bottom h136">
<view class="flex1 pr20 flexs0 bottom-btn">
<button
class="y-btn y-btn_default btn-reset"
@tap="resetScreen"
>
重置
</button>
</view>
<view class="flex1 bottom-btn">
<button
class="y-btn flex1 y-btn_primary btn-sure"
@tap="sureScreen"
>
确定
</button>
</view>
</view>
</view>
<view
v-else-if="showQuery === queryType.SORT"
class="screen-box border-t1px screen-sort"
>
<view
v-for="item in sortLib.data"
:key="item.id"
:data-value="item"
:class="['sort-item', item.id === selectSort.id ? 'selected' : '']"
@tap="toggleSort"
>
{{ item.name }}
</view>
</view>
<view
v-else-if="showQuery === queryType.GOOD"
class="screen-box border-t1px screen-other"
>
<scroll-view
scroll-y
class="other-box"
>
<view class="screen-title">{{ goodDirection.title }}</view>
<checkbox-group
data-type="Direction"
class="screen-con"
@change="screenChange"
>
<label
v-for="item in goodDirection.data"
:key="item.name"
:class="[
'screen-item',
filter.isIncludes(cacheDirection, String(item.id)) ? 'active' : '',
]"
>
<checkbox
class="y-check"
:value="item.id"
:checked="filter.isIncludes(cacheDirection, String(item.id))"
/>
<view class="screen">{{ item.name }}</view>
</label>
</checkbox-group>
</scroll-view>
<view class="sure-btn bottom">
<view class="flex1 pr20 flexs0 bottom-btn">
<button
class="y-btn y-btn_default btn-reset"
@tap="resetScreen('direction')"
>
重置
</button>
</view>
<view class="flex1 bottom-btn">
<button
class="y-btn y-btn_primary btn-sure"
@tap="sureScreen"
>
确定
</button>
</view>
</view>
</view>
<view
class="weui-mask screen-mask"
@tap="closeTab"
></view>
</view>
</view>
</template>
<script>
/* eslint-disable no-undef */
import filter from '@/utils/filter'
import { sortLib, goodDirection, sexType, ageType } from '@/utils/enums'
export default {
name: 'ExpertScreenComponent',
props: {
// url参数
urlParams: {
type: Object,
default() {
return {}
},
},
},
data() {
return {
goodDirection,
filter,
sexType,
ageType,
sortLib, // 排序
queryType: {
AGE_SEX: 1,
SORT: 3,
GOOD: 4,
},
showQuery: 0, // 显示筛选弹框
searchWord: '',
selectSort: {
name: '综合排序',
id: 1,
},
ageSelect: [],
sexSelect: '',
directionSelect: [],
cacheSex: '',
cacheAge: [],
cacheDirection: [],
}
},
computed: {
// 擅长方向tab下文案展示处理
showGood() {
// 若tab下只选择一项,需要处理展示选中的名称,否则展示默认文案
if (this.directionSelect.length === 1) {
return this.goodDirection.data.filter(item => {
return item.id === +this.directionSelect[0]
})[0].name
}
return false
},
// 性别,年龄tab下文案展示处理
showAgeSex() {
const { ageSelect, sexSelect } = this
const oArr = Array.prototype.concat(ageSelect, [sexSelect])
const { sexType, ageType } = this
// 若tab下只选择一项,需要处理展示选中的名称,否则展示默认文案
if (oArr.length === 1) {
if (ageSelect.length) {
return ageType.data.filter(item => {
return item.id === +oArr[0]
})[0].name
// 资质title
} else if (sexSelect) {
return sexType.data.filter(item => {
return item.id === +oArr[0]
})[0].name
}
}
return false
},
},
watch: {
// 监听url参数
urlParams(v) {
if (v) {
this.searchWord = v.searchWord || ''
}
},
},
mounted() {
this.getScreenEnums()
},
methods: {
// 获取筛选项枚举
async getScreenEnums() {
// dmp接口入参配置
const res = await this.$request
.get('auth/listen/home', {
params: {
ffrom: 'AppletWechatListen',
uid: this.$store.state.user.uid || '',
accessToken: this.$store.state.user.accessToken || '',
},
})
.finally(() => (this.loading = false))
const screenOption = res.find(item => item.type === 4)
const good = screenOption.body.find(item => item.filterType === 3)
const sexAge = screenOption.body.find(item => item.filterType === 2)
const sort = screenOption.body.find(item => item.filterType === 1)
this.goodDirection = good.group[0]
this.sortLib = sort.group[0]
this.sexType = sexAge.group[0]
this.ageType = sexAge.group[1]
},
// 跳转搜索页,需要存储原先参数
handleJumpSearch() {
const { searchWord = '' } = this
uni.navigateTo({
url: `/pages/home/expert-search${searchWord ? `?searchWord=${searchWord}` : ''}`,
})
},
// 取消
handleCancel() {
uni.reLaunch({
url: '/pages/home/home',
})
},
dealValues(arr) {
const resArr = []
arr.forEach(element => {
resArr.push(element.value)
})
return resArr.join(',')
},
// 触发父组件筛选
emitScreen() {
const data = this.getParams()
this.$emit('screenDoctor', data)
this.closeTab()
},
// 获取查询参数
getParams() {
const {
searchWord = '',
selectSort = {},
sexSelect = '',
ageSelect = [],
directionSelect = [],
} = this
console.log(sexSelect, 'aaaaaaaaaaa')
const obj = {
keywords: searchWord || '', // 搜索词
sortType: selectSort.id || '', // 排序方式
sexType: sexSelect || '', // 性别
ageType: ageSelect.join('-') || '', // 年龄
goodType: directionSelect.join('-') || '', // 擅长方向
}
return obj
},
// 切换顶部栏类目
toggleTab(e) {
const currentType = e.currentTarget.dataset.type
const screenType = currentType === this.showQuery ? 0 : currentType
if (screenType === 4) {
const { ageSelect, sexSelect, directionSelect } = this
this.cacheAge = ageSelect
this.cacheSex = sexSelect
this.cacheDirection = directionSelect
}
this.showQuery = +screenType
// 筛选弹窗打开关闭时控制页面是否可以滚动
if (screenType === 0) {
this.$emit('update:allowHomeScroll', true)
} else {
this.$emit('update:allowHomeScroll', false)
}
},
toggleSort(e) {
this.selectSort = e.currentTarget.dataset.value
this.emitScreen()
},
sureScreen() {
// 困扰 tab 选择主题后确定处理逻辑
const { cacheAge, cacheSex, cacheDirection } = this
this.ageSelect = cacheAge || []
this.sexSelect = cacheSex || ''
this.directionSelect = cacheDirection || []
this.emitScreen()
},
// 性别选项为单选操作,绑定点击事件
handleSexRadio(e) {
const value = e.currentTarget.dataset.value
if (value === this.sexSelect) {
this.sexSelect = ''
this.cacheSex = ''
} else {
this.sexSelect = value
this.cacheSex = value
}
},
screenChange(e) {
// 筛选tab下选择选项处理
const types = e.currentTarget.dataset.type
const values = e.detail.value
this[`cache${types}`] = values
},
resetScreen(type) {
if (type === 'direction') {
this.directionSelect = []
this.cacheDirection = []
} else {
this.ageSelect = []
this.sexSelect = ''
this.cacheAge = []
this.cacheSex = ''
}
},
// 关闭筛选重置数据
closeTab() {
this.showQuery = 0
// 筛选弹窗打开关闭时控制页面是否可以滚动
this.$emit('update:allowHomeScroll', true)
},
},
}
</script>
<style lang="less" scoped>
.c-screen {
.top-pos-wrap {
position: fixed;
z-index: 108;
top: 0;
left: 0;
right: 0;
background: #fff;
height: 94px;
padding-bottom: 10px;
.menu-wrap {
position: relative;
z-index: 110;
display: flex;
font-size: 14px;
width: 100%;
padding-top: 10px;
padding-bottom: 10px;
.item {
display: flex;
align-items: center;
justify-content: center;
flex-grow: 1;
text-align: center;
opacity: 0.8;
.text {
margin-right: 5px;
}
.arrow {
box-sizing: content-box;
width: 0px;
height: 0px;
border-left: 5px solid transparent;
border-top: 5px solid #d8d8d8;
border-right: 5px solid transparent;
}
&.selected {
color: #1da1f2;
font-weight: 500;
opacity: 1;
}
&.active {
font-weight: 500;
opacity: 1;
}
&.active .arrow {
border-top: 5px solid #1da1f2;
}
}
}
.quick-solt {
display: flex;
justify-content: space-between;
font-size: 11px;
padding-top: 2px;
.item {
width: 77px;
border-radius: 11px;
height: 22px;
line-height: 22px;
background: #f5f5f5;
text-align: center;
color: #666;
&.active {
color: #1da1f2;
background: #e8f6ff;
border: 0.5px solid #1da1f2;
}
}
}
}
.cate-view {
height: 220px;
overflow-y: auto;
}
.border-b1px:after {
content: ' ';
position: absolute;
bottom: 0;
left: 0;
right: 0;
border-bottom: 1px solid #f0f0f0;
transform: scaleY(0.5);
}
.border-t1px:after {
content: ' ';
position: absolute;
top: 0;
left: 0;
right: 0;
border-top: 1px solid #f0f0f0;
transform: scaleY(0.5);
}
.search-box {
background: #fff;
/* background: #efeef3; */
padding: 14px 0 2px;
/* border-bottom: 0.5px solid #f1f1f1; */
.search-body {
background: #f5f6f9;
padding: 7px 15px;
border-radius: 19px;
input {
margin-left: 7px;
width: 100%;
font-size: 14px;
color: rgba(0, 0, 0, 0.7);
&::first-line {
color: rgba(0, 0, 0, 0.7);
}
}
}
}
.icon-search {
width: 16px;
height: 16px;
}
.search-ft {
height: 36px;
line-height: 36px;
margin-left: 8px;
.btn {
font-size: 15px;
}
}
.icon_search-sub {
width: 36px;
height: 36px;
margin-left: 8px;
}
.screen-box {
position: fixed;
z-index: 120;
top: 94px;
left: 0;
right: 0;
width: 100%;
color: #242424;
background: #fff;
.other-box {
height: 100%;
}
}
.screen-them,
.screen-other {
padding-left: 20px;
padding-right: 20px;
padding-bottom: 100px;
}
.screen-title {
font-size: 16px;
padding: 24px 5px 8px 5px;
font-weight: bold;
}
.icon-arrow {
width: 16px;
float: right;
}
.screen-title-esp {
padding-top: 12px;
}
.icon-zx {
width: 18px;
height: 18px;
display: inline-block;
vertical-align: middle;
margin-top: -4px;
}
.screen-con {
display: flex;
flex-wrap: wrap;
.fwlx-item {
box-sizing: border-box;
min-width: 78px;
height: 34px;
padding: 0 10px;
display: inline-block;
line-height: 34px;
text-align: center;
font-weight: 400;
color: #242424;
margin: 0 5px 10px;
font-size: 14px;
background: #f7f7f7;
border-radius: 4px;
border: 1px solid #f7f7f7;
font-weight: 300;
&.is-hide {
display: none;
}
}
.screen-item {
width: 25%;
text-align: center;
font-size: 14px;
margin-top: 10px;
padding-left: 5px;
padding-right: 5px;
&-esp {
text-align: center;
font-size: 14px;
margin-top: 10px;
padding-left: 5px;
padding-right: 5px;
}
&.w50 {
width: 50%;
}
.screen {
padding: 7px 2px;
border-radius: 4px;
background: #f7f7f7;
border: 1px solid #f7f7f7;
white-space: nowrap;
text-align: center;
}
}
.fwlx-item.active {
background: #e8f6ff;
color: #1da1f2;
border: 1px solid #1da1f2;
}
.screen-item.active .screen,
.screen-item-esp.active .screen-esp {
background: #e8f6ff;
color: #1da1f2;
border: 1px solid #1da1f2;
}
}
.screen-esp {
border-radius: 4px;
background: #f7f7f7;
border: 1px solid #f7f7f7;
min-width: 78px;
padding: 7px 0;
text-align: center;
display: inline-block;
.long {
padding: 7px 8px;
}
}
.sure-btn {
display: flex;
padding-left: 5px;
padding-right: 5px;
&.bottom {
position: absolute;
box-sizing: border-box;
width: 100%;
bottom: 0;
height: 68px;
left: 0;
padding: 0 16px;
background: #fff;
box-shadow: 0 -1px 2px 0 rgba(0, 0, 0, 0.06);
button {
height: 44px;
line-height: 44px;
font-size: 16px;
&[disabled] {
background: #60bdf5;
color: rgba(255, 255, 255, 0.6);
}
}
}
}
.bottom-btn {
padding-top: 8px;
}
.btn-reset {
border: 1px solid rgba(235, 235, 235, 1);
color: #242424;
background: #fff;
border-radius: 8px;
}
.btn-sure {
border-radius: 8px;
}
.screen-mask {
z-index: 101;
&.to-top {
z-index: 10;
}
}
.screen-sort {
padding-top: 10px;
padding-bottom: 10px;
.sort-item {
padding: 10px;
font-size: 15px;
text-align: center;
&.selected {
color: #1da1f2;
font-weight: bold;
}
}
}
.good-at-person-box {
position: relative;
}
.fold-box-esp {
position: relative;
height: 29px;
}
.fold-box-con {
position: absolute;
bottom: 0;
width: 100%;
text-align: center;
}
.fold-box {
position: absolute;
bottom: 0;
width: 100%;
height: 40px;
background: linear-gradient(
to bottom,
rgba(255, 255, 255, 0.6) 30%,
rgba(255, 255, 255, 0.9) 50%,
rgba(255, 255, 255, 1) 80%,
rgba(255, 255, 255, 1) 100%
); /* 标准的语法 */
.fold-text {
color: #666;
font-size: 12px;
margin-right: 4px;
display: inline-block;
vertical-align: middle;
}
.fold-icon {
width: 10px;
height: 10px;
display: inline-block;
vertical-align: middle;
}
}
.h136 {
height: 68px;
}
.search-tips {
padding: 14px 20px 0;
background-color: #fff;
line-height: 1;
font-size: 15px;
.bd {
display: flex;
align-items: center;
padding-top: 10px;
.span {
color: #666;
margin-right: 6px;
}
.em {
height: 28px;
line-height: 28px;
padding: 0 12px;
background-color: #f9f9f9;
border: 1px solid rgba(235, 235, 235, 1);
border-radius: 4px;
font-size: 14px;
}
}
.hd {
display: flex;
align-items: center;
color: #666;
font-size: 14px;
.span {
font-weight: bold;
color: #1c1f28;
}
}
}
}
</style>
<template>
<view
:class="orderClassName"
@click="navToConfide"
>
<view class="order-status">
{{ isNoPay ? '待支付' : orderState[order.lifecycle].key }}
</view>
<view class="base-info">
<image
v-if="order.doctor"
:src="order.doctor.smallImage"
class="head-icon"
></image>
<view class="info-right">
<view class="name-and-price">
<text class="name">{{ order.doctor && order.doctor.name }}</text>
<view class="price">
<text class="price-unit">¥</text>
<text>
{{
order.couponMoney > order.orderMoney
? 0
: (order.orderMoney * 100 - order.couponMoney * 100) / 100
}}
</text>
</view>
</view>
<view :class="orderStatusName">
<view class="circle"></view>
<text class="online-status-text">
{{ order.onlineStatus ? (order.busyStatus ? '通话中' : '在线') : '离线' }}
</text>
<text
v-if="order.couponMoney > 0"
class="coupon-money"
>
红包已减¥{{ order.couponMoney }}
</text>
</view>
</view>
</view>
<view
v-if="order.lifecycle === 2 && order.listenRemainingTime"
class="duration-box"
>
<view class="duration-time">
<text class="label">剩余时长:</text>
<text class="value">
{{ formatDuration(order.listenRemainingTime.remainingTime) }}
</text>
</view>
<view class="expire-time">
<text class="label">过期时间:</text>
<text class="value">
{{ $dayjs(order.listenRemainingTime.expiredTime).format('YYYY-MM-DD H:mm:ss') }}
</text>
</view>
</view>
<view class="order-info-bottom">
<text class="create-time">下单时间 {{ $dayjs(order.createTime).format('YYYY-MM-DD') }}</text>
<template v-if="order.payStatus !== 2 && order.lifecycle !== 5">
<view
class="order-button default"
@click.stop="handleCancel()"
>
取消订单
</view>
<view
class="order-button pink"
@click.stop="handlePay"
>
立即支付
</view>
</template>
<template v-else>
<template v-if="order.lifecycle === 1 || order.lifecycle === 0">
<view
class="order-button default"
@click.stop="handleCancel(true)"
>
取消订单
</view>
<view
class="order-button primary"
@click.stop="navToConfide"
>
立即倾诉
</view>
</template>
<view
v-else-if="order.lifecycle === 2"
class="order-button primary"
@click.stop="navToConfide"
>
继续倾诉
</view>
<template v-else-if="order.lifecycle === 3">
<!-- 评价按钮需要提示下载app,要接入开关控制,暂时不显示 -->
<!-- <view
class="order-button default comment"
@click.stop="handleCommentClick"
>
评价
</view> -->
<view
class="order-button default"
@click.stop="navToConfide"
>
再次倾诉
</view>
</template>
<view
v-else
class="order-button default"
@click.stop="navToConfide"
>
再次倾诉
</view>
</template>
</view>
</view>
</template>
<script>
export default {
name: 'OrderItem',
props: {
order: {
required: true,
type: Object,
},
},
data() {
return {
orderState: {
0: { key: '未支付', text: '去支付', className: 'state-toBePay' },
1: { key: '待服务', text: '立即倾诉', className: 'state-server' },
2: { key: '服务中', text: '继续倾诉', className: 'state-server' },
3: { key: '待评价', text: '去评价', className: 'state-server' },
4: { key: '已结束', text: '再次倾诉', className: 'state-end' },
5: { key: '已关闭', text: '再次倾诉', className: 'state-end' },
6: { key: '已退款', text: '再次倾诉', className: 'state-end' },
},
payLoading: false, // 支付按钮loading
}
},
computed: {
orderClassName() {
const customClass =
this.orderState[
this.order.lifecycle < 0 || this.order.lifecycle > 6 ? 5 : this.order.lifecycle
].className
return `order-item ${customClass}`
},
orderStatusName() {
const customStatus = this.order.onlineStatus
? this.order.busyStatus
? 'calling'
: 'online'
: 'offline'
return `online-status ${customStatus}`
},
// 是否显示待支付
isNoPay() {
return (
(this.order.payStatus !== 2 && this.order.lifecycle !== 5) ||
(this.order.lifecycle === 0 && this.order.payStatus === 2)
)
},
},
methods: {
// 跳转倾诉主页
navToConfide() {
uni.navigateTo({
url: `/pages/confide/confide?listenerId=${this.order.listener.id}`,
})
},
// 取消订单,effct:是否涉及到金额会退
handleCancel(effect = false) {
const cancelTips =
effect || this.order.couponMoney
? '订单取消后将被关闭, 红包及支付金额将退回到您的账户'
: '订单取消后将被关闭'
this.$emit('cancel', { cancelTips, order: this.order })
},
handleCommentClick() {},
// TODO 跳转支付
async handlePay() {
if (this.payLoading) return
this.payLoading = true
try {
const res = await this.$request.get('/auth/order/listen/pay-body', {
params: {
uid: this.$store.state.user.uid,
listenerUid: (this.order.listener && this.order.listener.uid) || '',
},
})
this.payLoading = false
const price = this.order.orderMoney || 0
const coupon = (res.coupon && res.coupon.couponMoney) || 0
const balance = res.availableMoney || 0
// 将listenerId通过事件传给父组件,用于支付成功后的页面跳转
this.$emit('pay', this.order.listener && this.order.listener.id)
uni.navigateTo({
url: `/pages/pay/pay?price=${price}&coupon=${coupon}&balance=${balance}&payId=${this.order.payId}&orderId=${this.order.id}&from=orderList`,
})
} catch (e) {
this.payLoading = false
console.log(e)
}
},
// 时间格式化
formatDuration(duration) {
const s = duration % 60
const h = Math.floor(duration / 3600)
const m = Math.floor((duration - h * 3600) / 60)
return `${h ? h + '小时' : ''}${m ? m + '分' : ''}${s}秒`
},
},
}
</script>
<style lang="less" scoped>
.order-item {
background: #ffffff;
border-radius: 4px;
padding: 0 10px;
margin-bottom: 12px;
box-sizing: border-box;
.order-status {
height: 40px;
line-height: 40px;
color: #242424;
font-size: 14px;
position: relative;
&:after {
content: ' ';
position: absolute;
bottom: 0;
left: 0;
right: 0;
border-bottom: 1px solid #f7f7f7;
transform: scaleY(0.5);
}
}
&.state-end .order-status {
color: #999999;
}
.base-info {
display: flex;
align-items: center;
padding: 10px 0;
.head-icon {
width: 45px;
height: 45px;
border-radius: 4px;
margin-right: 10px;
}
.info-right {
flex: 1;
.name-and-price {
display: flex;
align-items: center;
.name {
flex: 1;
color: #242424;
font-weight: bold;
font-size: 14px;
}
.price {
display: flex;
align-items: flex-start;
color: #242424;
font-size: 18px;
.price-unit {
margin-right: 1px;
margin-top: 3px;
font-size: 11px;
}
}
}
.online-status {
display: flex;
align-items: center;
.circle {
width: 5px;
height: 5px;
border-radius: 5px;
margin-right: 3px;
}
.online-status-text {
flex: 1;
font-size: 11px;
}
.coupon-money {
color: #999999;
font-size: 11px;
}
&.calling {
.circle {
background: linear-gradient(180deg, #ff9500, #ffba15);
}
.online-status-text {
color: #ff9500;
}
}
&.online {
.circle {
background: linear-gradient(180deg, #45e6ab, #33d69b);
}
.online-status-text {
color: #242424;
}
}
&.offline {
.circle {
background: #999999;
}
.online-status-text {
color: #999999;
}
}
}
}
}
.duration-box {
background-color: #fafafa;
padding-top: 8px;
.duration-time,
.expire-time {
font-size: 11px;
display: flex;
justify-content: space-between;
line-height: 20px;
.label {
color: #999;
}
.value {
color: rgba(0, 0, 0, 0.65);
}
}
}
.order-info-bottom {
display: flex;
align-items: center;
background-color: #fafafa;
padding: 10px 0;
.create-time {
flex: 1;
font-size: 11px;
color: #999;
}
.order-button {
font-size: 13px;
height: 26px;
line-height: 26px;
border-radius: 13px;
padding: 0 15px;
color: #666;
border: 1px solid #666;
position: relative;
+ .order-button {
margin-left: 10px;
}
&.default {
box-sizing: border-box;
}
&.pink {
color: #fff;
border-color: transparent;
background: linear-gradient(180deg, #f96184, #ff477e);
}
&.primary {
color: #fff;
border: 1px solid transparent;
background: linear-gradient(0, #1da1f2, #23b2fa);
}
&.comment {
content: ' ';
position: absolute;
right: 0;
top: 0;
width: 9px;
height: 9px;
border-radius: 50%;
background-color: #f71500;
}
}
}
}
</style>
<template>
<view class="toggle-fold-text">
<view
:id="id"
class="text-content"
:style="{
'white-space': 'pre',
overflow: foldStatus ? 'hidden' : 'auto',
'text-overflow': 'ellipsis',
display: '-webkit-box',
'-webkit-line-clamp': foldStatus ? `${line}` : '',
'-webkit-box-orient': 'vertical',
}"
>
{{ text }}
</view>
<view class="fold-box">
<view
v-if="foldVisible"
class="fold-content"
@click="toggleFold"
>
<text>{{ foldStatus ? '展开' : '收起' }}</text>
<image
mode="widthFix"
:class="{
arrow: true,
reverse: !foldStatus,
}"
src="//static.ydlcdn.com/m/images/zx/experts/detail/comment-arrow-down.png"
></image>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'ToggleFoldText',
props: {
lineHeight: {
required: true,
type: Number,
},
line: {
required: true,
type: Number,
},
text: {
required: true,
type: String,
},
},
data() {
return {
// 是否是收起状态
foldStatus: false,
// 是否显示展开收起按钮
foldVisible: false,
id: `text-${+new Date()}`,
}
},
mounted() {
// 获取高度判断是否要收起
const query = uni.createSelectorQuery().in(this)
query
.select(`#${this.id}`)
.boundingClientRect(data => {
this.foldVisible = data.height > this.lineHeight * this.line
this.foldStatus = true
})
.exec()
},
methods: {
toggleFold() {
this.foldStatus = !this.foldStatus
},
},
}
</script>
<style lang="less" scoped>
.toggle-fold-text {
.fold-box {
margin-top: 4px;
display: flex;
justify-content: flex-end;
.fold-content {
width: 60px;
display: flex;
align-items: center;
font-size: 14px;
color: #1da1f2;
.arrow {
width: 12px;
margin-left: 4px;
margin-bottom: 4px;
&.reverse {
margin-top: 4px;
margin-bottom: 0;
transform: rotate(180deg);
}
}
}
}
}
</style>
<template>
<view class="confide-page">
<u-navbar
:left-icon="leftIcon"
fixed
placeholder
@leftClick="handleNavBarBack"
>
<view
slot="center"
class="nav-bar-center-box"
>
<text class="name">{{ doctor.name || '' }}</text>
<image
v-if="doctor.abilityLevel === 1"
class="internship"
mode="widthFix"
src="//static.ydlcdn.com/m/images/zx/experts/detail/internship.png"
></image>
</view>
</u-navbar>
<u-loading-page
v-if="!initStatus"
loading-text="正在加载..."
loading-mode="spinner"
:loading="true"
></u-loading-page>
<template v-else>
<view v-if="!!detail">
<view class="confide-content">
<view class="base-info">
<view class="info-top">
<image
class="head-image"
:src="doctor.head || 'https://static.ydlcdn.com/m/images/global/default.png'"
@click="handleHeadImageClick"
></image>
<view class="info-top-right">
<view class="right-item">
<view class="value">{{ listenerInfo.confideConnection || 0 }}</view>
<view class="label">接通率</view>
</view>
<view class="right-item">
<view class="value">{{ listenerInfo.listenNums || 0 }}</view>
<view class="label">倾诉人次</view>
</view>
<view
class="right-item"
@click="handleLoadCommentClick"
>
<view class="value">{{ doctor.feedbackScore || 0 }}</view>
<view class="label">评分</view>
</view>
</view>
</view>
<!-- <view class="info-center">
<text class="name">{{ doctor.name || '' }}</text>
<image
v-if="doctor.abilityLevel === 1"
class="internship"
mode="widthFix"
src="//static.ydlcdn.com/m/images/zx/experts/detail/internship.png"
></image>
</view> -->
<view class="info-bottom">
<view
v-if="listenerInfo.tags"
class="tags"
>
{{ listenerInfo.tags.replace(/,/g, ' | ') }}
</view>
<view class="price-box">
<text class="price-value">{{ listenerInfo.listenFee }}</text>
<text class="price-unit"></text>
<text>/25分钟</text>
</view>
</view>
</view>
<view class="more-info">
<view>
<view class="info-title">详细介绍</view>
<toggle-fold-text
:line="5"
:line-height="22"
:text="doctor.description || ''"
></toggle-fold-text>
</view>
<view>
<view class="info-title">聆听寄语</view>
<toggle-fold-text
:line="5"
:line-height="22"
:text="listenerInfo.desc || ''"
></toggle-fold-text>
</view>
<view>
<view class="info-title">教育背景</view>
<view
v-for="(item, index) in doctor.eduInfo || []"
:key="index"
class="info-desc"
>
{{ item.school }}{{ item.profession }}{{ item.educationName }}
</view>
</view>
<view>
<view class="info-title">专业资质</view>
<view
v-for="(item, index) in doctor.dtNationalCertificationInfo || []"
:key="index"
class="info-desc"
>
{{ item.title }}({{ item.identifierNo }})
</view>
</view>
</view>
<view
v-if="comments.length > 0"
class="comment-box"
>
<view class="comment-title">
<text>{{ doctor.name || '' }}的综合评分</text>
<text class="score">{{ doctor.feedbackScore || 0 }}</text>
</view>
<view class="comment-list">
<comment-item
v-for="(comment, index) in comments"
:key="index"
:comment="comment"
></comment-item>
</view>
<view class="loadmore-box">
<u-loadmore
:status="commentLoadStatus"
loadmore-text="查看更多评价"
@loadmore="handleLoadCommentClick"
/>
</view>
</view>
</view>
<view class="confide-button-box">
<!-- 离线 -->
<view
v-if="detail.listenStatus === 2"
class="confide-button offline"
@click="handleConfideButtonClick"
>
<text class="text">暂未上线</text>
</view>
<!-- 通话中 -->
<view
v-else-if="detail.listenStatus === 3"
class="confide-button busy"
@click="handleConfideButtonClick"
></view>
<!-- 在线 -->
<view
v-else
class="confide-button online"
@click="handleConfideButtonClick"
>
<u-loading-icon v-if="confideLoading"></u-loading-icon>
<view
v-else
class="online-box"
>
<image
v-if="continueConfide()"
class="phone-image"
src="//static.ydlcdn.com/mini/mini_confide/confide_phone.png"
/>
<template v-else>
<view class="price">
<text class="price-unit">¥</text>
<text>{{ price }}</text>
</view>
<text class="text">立即倾诉</text>
</template>
</view>
</view>
<view class="tip-box">
<template v-if="continueConfide()">
<view class="remaining-text">
剩余{{ formatRemainingTime(remainingTime.remainingTime) }}
</view>
<view>
{{ $dayjs(remainingTime.expiredTime).format('MM月DD日 HH:mm') }}
前可续拨
</view>
</template>
<template v-else>
<view
v-if="
detail.couponMoney &&
detail.couponMoney.max &&
detail.couponMoney.max.couponMoney &&
detail.listenStatus !== 2 &&
detail.listenStatus !== 3
"
class="coupon-text"
>
红包已优惠{{ detail.couponMoney.max.couponMoney }}
</view>
<view>通话全程保密,请放心倾诉</view>
</template>
</view>
</view>
<u-modal
:show="callTipVisible"
confirm-text="我知道了"
:close-on-click-overlay="true"
@confirm="handleCall"
@close="callTipVisible = false"
>
<view class="call-tip-box">
<image
class="call-image"
src="//static.ydlcdn.com/m/images/zx/experts/detail/listen.png"
mode="widthFix"
/>
<view class="text">号码加密 隐私保护</view>
<view class="sub-text">号码已通过“虚拟中间号”拨打咨询师</view>
</view>
</u-modal>
</view>
</template>
</view>
</template>
<script>
import { ToggleFoldText } from '@/components/toggle-fold-text.vue'
import { CommentItem } from '@/components/comment-item.vue'
import { hostPrefix } from '@/config'
import { throttle } from 'lodash'
import { setTrackData } from '@/utils/util'
export default {
name: 'ConfidePage',
components: {
ToggleFoldText,
CommentItem,
},
data() {
return {
listenerId: '', // 倾诉id
detail: null, // 详情数据
comments: [], // 评论列表
commentLoadStatus: 'loadmore', // 评论加载状态 loadmore: 加载更多 nomore: 没有更多
remainingTime: {}, // 剩余倾诉时间
payInfo: null, // 下单信息
confideLoading: false, // 倾诉按钮loading状态
callTipVisible: false, // 号码保护提示
phoneNumber: '', // 倾诉拨打的手机号
initStatus: false, // 初始化接口加载是否完成
call: false, // 是否触发打电话
leftIcon: 'arrow-left', // 导航栏左边图标
}
},
onLoad(options) {
this.listenerId = options.listenerId
this.call = options.call === '1'
// 订阅支付成功和登录成功事件
uni.$on('paySuccess', this.handlePaySuccess)
uni.$on('loginSuccess', this.handleLoginSuccess)
// 页面访问埋点
setTrackData({
events: [
{
event_id: 'page_visit',
event_custom_properties: {
part: `listener_detail_${this.listenerId}`,
position: '',
element: '',
},
},
],
})
},
onUnload() {
// 取消订阅支付成功和登录成功事件
uni.$off('paySuccess', this.handlePaySuccess)
uni.$off('loginSuccess', this.handleLoginSuccess)
},
computed: {
// 倾诉信息
listenerInfo() {
return (this.detail && this.detail.listener) || {}
},
// 专家信息
doctor() {
return this.listenerInfo.doctor || {}
},
// 红包金额
coupon() {
const { coupon } = this.payInfo || {}
return (
(coupon && coupon.couponMoney) ||
(this.detail &&
this.detail.couponMoney &&
this.detail.couponMoney.max &&
this.detail.couponMoney.max.couponMoney) ||
0
)
},
// 扣除红包后的金额
price() {
const listenFee = this.listenerInfo.listenFee
return this.coupon > 0 ? (this.coupon >= listenFee ? 0 : listenFee - this.coupon) : listenFee
},
},
async mounted() {
// 获取详情信息
await this.getDetailInfo()
// 判断navbar左侧图标是返回还是主页
const pages = getCurrentPages()
this.leftIcon = pages.length > 1 ? 'arrow-left' : 'home'
// 评论列表,入参doctorId需要等详情接口返回数据
this.getComments()
// 获取倾诉剩余时间,入参doctorId需要等详情接口返回数据
await this.getRemainingTime()
// 接口调用完成,初始化结束
this.initStatus = true
// 订单列表支付成功回到当前页触发打电话
if (this.call) {
this.handleSubmit()
}
},
methods: {
// 预览头像
handleHeadImageClick() {
uni.previewImage({
urls: [this.doctor.head || 'https://static.ydlcdn.com/m/images/global/default.png'],
indicator: 'none',
})
},
// 导航栏返回
handleNavBarBack() {
if (this.leftIcon === 'home') {
uni.switchTab({
url: '/pages/home/home',
})
} else {
uni.navigateBack()
}
},
// 详情信息
async getDetailInfo() {
try {
const res = await this.$request.get('/auth/listen/detail', {
params: {
id: this.listenerId,
uid: this.$store.state.user.uid,
accessToken: this.$store.state.user.accessToken,
listenVersion: '2.2', // 版本,2.2代表新的,返回数据会比老的少很多,部分字段的值也和老的不一样
},
})
this.detail = res || {}
} catch (e) {
console.log(e)
}
},
// 评论列表
async getComments() {
try {
const res = await this.$request.post('/consult/expert-page/evaluates', {
businessId: this.doctor.doctorId,
page: 1,
pageRows: 3,
tag: '',
sort: 0,
type: 0,
})
const list = (res && res.evaluates && res.evaluates.list) || []
// 判断是否还有下一页
this.commentLoadStatus =
list.length < ((res && res.evaluates && res.evaluates.total) || [])
? 'loadmore'
: 'nomore'
// 默认只展示3条评论
this.comments = list.slice(0, 3)
} catch (e) {
console.log(e)
}
},
// 获取倾诉剩余时间
async getRemainingTime() {
try {
const { remainingTime } = await this.$request.get('/auth/listen/dialchangestatus', {
params: {
doctorId: this.doctor.doctorId,
listenVersion: '2.2',
},
})
this.remainingTime = remainingTime || {}
} catch (e) {
console.log(e)
}
},
// 时间转成 mm分钟ss秒 格式
formatRemainingTime(time) {
const m = Math.floor(time / 60)
const s = time % 60
return `${m ? `${m < 10 ? '0' + m : m}分钟` : ''}${s < 10 ? '0' + s : s}秒`
},
// 更多评价点击事件,跳转h5页面
handleLoadCommentClick() {
const url = `${hostPrefix}/comment/evaList/${this.doctor.doctorId}?listenerId=${this.listenerId}`
uni.navigateTo({
url: `/pages/web/web?title=${
this.doctor.name ? `${this.doctor.name}的评价` : ''
}&loadUrl=${encodeURIComponent(url)}`,
})
},
// 是否是继续倾诉状态
continueConfide() {
return (
this.remainingTime.expiredTime &&
this.$dayjs(this.remainingTime.expiredTime).isAfter(this.$dayjs()) &&
this.remainingTime.remainingTime
)
},
// 倾诉按钮点击事件
handleConfideButtonClick: throttle(
function () {
if (this.confideLoading) return
// 未登录跳转登录
if (!this.$store.getters.isLogin) {
return this.handleLogin()
}
// 倾诉按钮点击的埋点
setTrackData({
events: [
{
event_id: 'content_click',
event_custom_properties: {
part: `listener_detail_${this.listenerId}`,
position: 'foot_floatingr',
element: 'listen_button',
content:
this.detail.listenStatus === 2
? '离线'
: this.detail.listenStatus === 3
? '通话中'
: '拨打',
},
},
],
})
// listenStatus 1=在线 2-离线 3-通话中
if (this.detail.listenStatus === 2) {
return uni.showToast({
title: '专家已离线',
icon: 'none',
})
}
if (this.detail.listenStatus === 3) {
return uni.showToast({
title: '正在通话中',
icon: 'none',
})
}
this.handleSubmit()
},
300,
{ leading: true, trailing: false },
),
// 跳转登录
handleLogin() {
uni.navigateTo({
url: '/pages/login/login',
})
},
// 提交订单
async handleSubmit() {
const params = {
uid: this.$store.state.user.uid,
accessToken: this.$store.state.user.accessToken,
id: this.listenerId,
type: 1,
isNeedDail: false,
ffrom: 'AppletWechatListen',
}
this.confideLoading = true
try {
const res = await this.$request.get('/auth/listen/submitOrderAndPay', {
params,
})
const { dialStatus, dialReason } = res.dialDetail
if (dialStatus === 100005) {
// 用户未登录
this.handleLogin()
} else if (dialStatus === 100007) {
// 余额不足
this.payInfo = res || {}
// TODO 支付参数
uni.navigateTo({
url: `/pages/pay/pay?price=${this.listenerInfo.listenFee || 0}&coupon=${
this.coupon
}&balance=${
(this.payInfo.dialDetail && this.payInfo.dialDetail.userBalance) || 0
}&payId=${this.payInfo.payId}&orderId=${this.payInfo.listenOrderId}&from=confide`,
})
} else if (dialStatus === 100008) {
// 用户未绑定手机号 这里不会出现这种场景,因为用户都是用手机号登录的
uni.showToast({
title: '未绑定手机号',
icon: 'none',
})
} else if (dialStatus === 0) {
await this.handleDial()
} else {
uni.showToast({
title: dialReason,
icon: 'none',
})
}
this.confideLoading = false
} catch (e) {
console.log(e)
this.confideLoading = false
}
},
// 调用打电话接口
async handleDial() {
const params = {
uid: this.$store.state.user.uid,
accessToken: this.$store.state.user.accessToken,
id: this.listenerId,
type: 1,
isNeedDail: true,
ffrom: 'AppletWechatListen',
}
try {
const { dialDetail } = await this.$request.get('/auth/listen/dial', {
params,
})
const { dialStatus, dialReason, phoneNu } = dialDetail
if (dialStatus === 0) {
// 可以拨打电话,显示提示弹窗
this.phoneNumber = phoneNu
this.callTipVisible = true
} else {
uni.showToast({
title: dialReason,
icon: 'none',
})
}
} catch (e) {
console.log(e)
}
},
// 打电话
handleCall() {
this.callTipVisible = false
uni.makePhoneCall({
phoneNumber: this.phoneNumber,
})
},
// 支付成功,刷新接口数据,触发倾诉按钮,从支付页回到当前页
async handlePaySuccess(from) {
if (from === 'confide') {
setTimeout(() => {
uni.navigateBack()
}, 1500)
await this.getDetailInfo()
this.handleSubmit()
}
},
// 登录成功刷新接口数据
async handleLoginSuccess() {
this.initStatus = false
await this.getDetailInfo()
await this.getRemainingTime()
this.initStatus = true
},
},
// 分享给朋友
onShareAppMessage() {
setTrackData({
events: [
{
event_id: 'common_click',
event_custom_properties: {
part: `listener_detail_${this.listenerId}`,
position: 'top_column',
element: 'share_friends',
},
},
],
})
return {
title: `${this.doctor.name}的倾诉主页`,
path: `/pages/confide/confide?fromOpenId=${this.$store.state.user.openId}&listenerId=${this.listenerId}`,
}
},
// 分享到朋友圈
onShareTimeline() {
setTrackData({
events: [
{
event_id: 'common_click',
event_custom_properties: {
part: `listener_detail_${this.listenerId}`,
position: 'top_column',
element: 'share_moments',
},
},
],
})
return {
title: `${this.doctor.name}的倾诉主页`,
path: `/pages/confide/confide?fromOpenId=${this.$store.state.user.openId}&listenerId=${this.listenerId}`,
}
},
}
</script>
<style lang="less" scoped>
.confide-page {
height: 100%;
overflow: auto;
.nav-bar-center-box {
display: flex;
align-items: center;
.name {
font-weight: bold;
font-size: 18px;
}
.internship {
width: 28px;
margin-left: 5px;
}
}
/deep/ .u-loading-page {
justify-content: flex-start;
.u-loading-page__warpper {
margin-top: 10px;
flex-direction: row;
.u-loading-page__warpper__loading-icon {
margin-bottom: 0;
margin-right: 8px;
}
.u-loading-page__warpper__text {
font-size: 14px !important;
color: rgb(96, 98, 102) !important;
}
}
}
.confide-content {
box-sizing: border-box;
padding: 16px 16px 200px;
.base-info {
.info-top {
display: flex;
align-items: center;
justify-content: space-between;
.head-image {
width: 60px;
height: 60px;
border-radius: 8px;
}
.info-top-right {
display: flex;
align-items: center;
.right-item {
text-align: center;
.label {
font-size: 12px;
color: #565656;
}
.value {
color: #212121;
font-size: 24px;
line-height: 36px;
height: 36px;
}
+ .right-item {
margin-left: 25px;
}
}
}
}
.info-center {
display: flex;
align-items: center;
margin-top: 20px;
.name {
font-size: 20px;
font-weight: bold;
line-height: 30px;
}
.internship {
width: 28px;
margin-left: 5px;
}
}
.info-bottom {
display: flex;
margin-top: 20px;
align-items: flex-start;
justify-content: space-between;
.tags {
background-color: rgba(194, 197, 204, 0.1);
color: rgb(148, 149, 160);
font-size: 12px;
line-height: 18px;
padding: 4px 6px;
margin-right: 20px;
}
.price-box {
display: flex;
align-items: flex-end;
color: rgb(170, 174, 186);
font-size: 12px;
line-height: 18px;
white-space: nowrap;
.price-value {
color: rgb(254, 96, 64);
font-size: 23px;
line-height: 23px;
}
.price-unit {
color: rgb(254, 96, 64);
}
}
}
}
.more-info {
margin-top: 10px;
margin-bottom: 40px;
overflow: hidden;
.info-title {
margin-top: 24px;
font-size: 16px;
line-height: 24px;
font-weight: bold;
}
/deep/ .toggle-fold-text,
.info-desc {
font-size: 14px;
line-height: 22px;
color: #6d6d6d;
}
}
.comment-box {
.comment-title {
font-size: 20px;
line-height: 30px;
font-weight: bold;
color: #242424;
.score {
color: #1da1f2;
margin-left: 5px;
}
}
}
}
.confide-button-box {
position: fixed;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.8), #fff);
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
.confide-button {
width: 105px;
height: 105px;
color: #fff;
font-size: 30px;
line-height: 30px;
background-repeat: no-repeat;
background-size: cover;
background-position: 50%;
display: flex;
align-items: center;
justify-content: center;
/deep/ .u-loading-icon .u-loading-icon__spinner {
color: #fff !important;
}
&.offline {
// offline.png中有‘喊他上线’文案,小程序中没有私聊功能,所以换成没有文案的图
// background-image: url('//static.ydlcdn.com/mini/mini_confide/confide_offline.png');
background-image: url('//static.ydlcdn.com/mini/mini_confide/confide_online.png');
.text {
font-size: 14px;
line-height: 20px;
font-weight: 400;
}
}
&.busy {
background-image: url('//static.ydlcdn.com/mini/mini_confide/confide_busy.png');
}
&.online {
background-image: url('//static.ydlcdn.com/mini/mini_confide/confide_online.png');
}
.online-box {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
&.paying {
background-repeat: no-repeat;
background-size: cover;
background-position: 50%;
background-image: url('//static.ydlcdn.com/mini/mini_confide/confide_paying.png');
}
.phone-image {
width: 35px;
height: 35px;
}
.price {
font-size: 28px;
font-weight: bold;
position: relative;
font-family: 'DINCondBold';
transform: translate(4px, 0);
.price-unit {
font-size: 12px;
line-height: 12px;
position: absolute;
left: -8px;
top: 4px;
}
}
.text {
font-size: 12px;
line-height: 14px;
margin-top: 2px;
font-weight: 400;
}
}
}
.tip-box {
margin-top: 10px;
font-size: 13px;
color: #666;
line-height: 20px;
text-align: center;
.remaining-text {
color: #242424;
font-weight: bold;
}
.coupon-text {
color: red;
font-weight: bold;
}
}
}
.call-tip-box {
display: flex;
flex-direction: column;
align-items: center;
.call-image {
width: 60px;
margin-bottom: 24px;
}
.text {
color: #242424;
font-size: 18px;
font-weight: bold;
line-height: 25px;
}
.sub-text {
color: #888;
margin-top: 4px;
font-size: 13px;
line-height: 18px;
}
}
}
</style>
<template>
<view class="box-c">
<!-- 搜索框 -->
<view class="search">
<view class="search-bd">
<image
mode="aspectFill"
class="icon-glass"
src="https://static.ydlcdn.com/weixin/image/icon_search.png"
/>
<input
v-model="searchWord"
:contenteditable="true"
placeholder-style="color:#adb4be"
type="text"
confirm-type="search"
placeholder="请输入倾诉师姓名"
focus
@confirm="handleSearch"
@input="handleChange"
/>
<view
v-if="searchWord"
class="clear"
@tap="handleClear"
>
<image
src="https://static.ydlcdn.com/weixin/image/close.png"
mode="aspectFill"
/>
</view>
</view>
<view
class="search-ft"
@tap="cancelSearch"
>
<text class="btn">取消</text>
</view>
</view>
<!-- 搜索结果 -->
<view
v-if="result.length"
class="result"
>
<view
v-for="item in result"
:key="item.id"
:data-item="item.doctor_name"
class="item"
@tap="handleSelect"
>
<image
mode="aspectFill"
class="icon-glass"
src="https://static.ydlcdn.com/weixin/image/icon_search.png"
/>
<rich-text
class="text"
:nodes="item.keyword"
></rich-text>
</view>
</view>
<!-- 搜索历史&热门搜索 -->
<div
v-else
class="recommend"
>
<div
v-if="searchHistory.length > 0"
class="history"
>
<div class="recommend-title">
<span>搜索历史</span>
<image
class="icon-delete"
src="https://static.ydlcdn.com/hlwyyCt/images/address/icon_del.png"
mode="aspectFill"
@click="deleteSearchHistory"
/>
</div>
<div class="recommend-list">
<view
v-for="item in searchHistory.slice(0, 9)"
:key="item"
:data-item="item"
class="item"
@tap="handleSelect"
>
{{ item }}
</view>
</div>
</div>
</div>
</view>
</template>
<script>
/* eslint-disable no-undef */
export default {
data() {
return {
searchWord: '',
searchHistory: [], // 搜索历史
hotSearch: [], // 热门搜索
result: [],
}
},
onLoad(options) {
// 做回显
if (options.searchWord) {
this.searchWord = options.searchWord
}
const searchHistory = uni.getStorageSync('expertSearchHistory')
if (searchHistory && searchHistory !== 'undefined') {
this.searchHistory = searchHistory
}
},
methods: {
// 获取关联词
getkeywordList() {
const keyword = this.searchWord
this.$request
.post('smart-rank/v1/search', {
filter: {
__keywords: keyword,
},
limit: 20,
options: {
search_scene_id: 'listener_suggest_search',
},
count: true,
})
.then(res => {
const { objects = [] } = res
this.result = objects.map(n => {
return {
...n,
keyword: n.doctor_name.replace(
new RegExp(keyword, 'gi'),
`<em class="keyword">${keyword}</em>`,
),
}
})
})
},
// 输入的搜索内容跳转结果页
jumpSearch(search) {
uni.reLaunch({
url: `/pages/home/home?searchWord=${search}`,
})
// 保存选择的搜索结果,用于展示搜索历史
let searchHistory = uni.getStorageSync('expertSearchHistory')
const searchWord = search
// 若搜索内容为空,不记录
if (!searchWord) {
return
}
if (searchHistory) {
const search = searchHistory.find(w => w === searchWord)
if (!search) {
searchHistory.unshift(searchWord)
}
} else {
searchHistory = []
searchHistory.unshift(searchWord)
}
uni.setStorageSync('expertSearchHistory', searchHistory)
},
// 删除搜索历史
deleteSearchHistory() {
uni.removeStorageSync('expertSearchHistory')
this.searchHistory = []
},
// 输入,防抖
handleChange(e) {
const searchWord = e.detail.value.trim()
this.searchWord = searchWord
this.timer && clearTimeout(this.timer)
this.timer = setTimeout(() => {
if (searchWord === '') {
this.result = []
} else {
this.getkeywordList()
}
}, 100)
},
// 清空
handleClear() {
this.searchWord = ''
this.result = []
},
// 取消搜索
cancelSearch() {
uni.navigateBack()
},
// 搜索
handleSearch() {
const val = this.searchWord.trim()
this.jumpSearch(val)
},
// 联想词点击
handleSelect(e) {
const item = e.currentTarget.dataset.item
this.jumpSearch(item)
},
},
}
</script>
<style lang="less" scoped>
.search {
display: flex;
align-items: center;
padding-top: 28rpx;
.search-bd {
display: flex;
align-items: center;
flex: 1;
background: #f5f6f9;
padding: 14rpx 30rpx;
padding-right: 0;
border-radius: 38rpx;
input {
margin-left: 16rpx;
width: 100%;
font-size: 28rpx;
caret-color: #1da1f2;
input::first-line {
color: #1da1f2;
}
}
.icon-glass {
min-width: 32rpx;
max-width: 32rpx;
height: 32rpx;
}
.clear {
display: flex;
align-items: center;
padding-left: 16rpx;
padding-right: 30rpx;
image {
width: 32rpx;
height: 32rpx;
}
}
}
.search-ft {
height: 72rpx;
line-height: 72rpx;
margin-left: 16rpx;
.btn {
font-size: 30rpx;
}
}
}
.result {
margin-top: 20rpx;
.item {
display: flex;
align-items: center;
padding-top: 24rpx;
padding-bottom: 24rpx;
font-size: 28rpx;
.icon-glass {
min-width: 24rpx;
max-width: 24rpx;
height: 24rpx;
}
.text {
padding-left: 8rpx;
max-width: 98%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
</style>
<style lang="less">
.result .item .keyword {
color: #1da1f2;
font-style: normal;
}
.recommend {
padding-top: 60rpx;
.history {
padding-bottom: 20rpx;
}
&-title {
font-size: 28rpx;
line-height: 40rpx;
font-weight: 700;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 10rpx;
margin-bottom: 20rpx;
.icon-delete {
width: 32rpx;
height: 34rpx;
opacity: 0.6;
}
}
&-list {
display: flex;
flex-wrap: wrap;
max-height: 170rpx;
overflow: hidden;
.item {
box-sizing: border-box;
height: 68rpx;
padding: 0 20rpx;
display: inline-block;
line-height: 68rpx;
text-align: center;
color: #242424;
margin: 0 10rpx 20rpx;
font-size: 28rpx;
background: #f7f7f7;
border-radius: 8rpx;
border: 1rpx solid #f7f7f7;
max-width: 270rpx;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
&.hot-list {
max-height: 250rpx;
}
}
}
</style>
<template>
<view class="login-box">
<u-navbar
title="登录注册"
:title-style="{ 'font-weight': 'bold', 'font-size': '18px' }"
fixed
placeholder
@leftClick="handleBack"
></u-navbar>
<image
class="logo"
src="https://static.ydlcdn.com/weixin/image/ydl_consult_logo.png"
></image>
<text class="title">壹点倾诉</text>
<view class="protocol">
<image
:src="
checked
? 'https://static.ydlcdn.com/weixin/image/checked.png'
: 'https://static.ydlcdn.com/weixin/image/check.png'
"
@click="switchChecked"
></image>
<text>阅读并同意</text>
<text
class="link"
@click="handleUserProtocolClick"
>
《壹点灵用户使用协议》
</text>
<text></text>
<text
class="link"
@click="handlePrivateProtocolClick"
>
《隐私保护政策》
</text>
</view>
<view class="button-box">
<phone-login
v-if="checked"
class="login"
text="微信授权登录"
@success="handleLoginSuccess"
></phone-login>
<button
v-else
class="login"
@click="showLoginTip"
>
微信授权登录
</button>
</view>
</view>
</template>
<script>
/* eslint-disable no-undef */
import { hostPrefix, ydlH5Prefix } from '@/config.js'
import PhoneLogin from '@/components/phone-login.vue'
export default {
name: 'LoginPage',
components: {
PhoneLogin,
},
data() {
return {
checked: true, // 是否勾选协议
required: false, // 来源页面是否强制登录
}
},
onLoad(options) {
this.required = options.required === '1'
},
methods: {
// 勾选切换
switchChecked() {
this.checked = !this.checked
},
// 跳转用户协议
handleUserProtocolClick() {
uni.navigateTo({
url: `/pages/web/web?title=用户注册协议&loadUrl=${encodeURIComponent(
`${ydlH5Prefix}/SDUserProtol`,
)}`,
})
},
// 跳转隐私协议
handlePrivateProtocolClick() {
uni.navigateTo({
url: `/pages/web/web?title=隐私保护政策&loadUrl=${encodeURIComponent(
`${hostPrefix}/Protol/yinsi-m`,
)}`,
})
},
// 未勾选提示
showLoginTip() {
uni.showToast({
title: '请同意服务条款',
icon: 'none',
})
},
// 登录成功,更新store数据
async handleLoginSuccess() {
const appId = uni.getAccountInfoSync().miniProgram.appId
const res = await this.$request.get(`/mini/wx/user/${appId}/loginUser`)
this.$store.commit('user/setUserInfo', res)
uni.$emit('loginSuccess')
uni.showToast({
title: '登录成功',
duration: 1500,
icon: 'success',
mask: true,
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
},
// 导航栏返回
handleBack() {
uni.navigateBack({
delta: this.required ? 2 : 1,
})
},
},
}
</script>
<style lang="scss" scoped>
.login-box {
display: flex;
flex-direction: column;
align-items: center;
padding: 0 10px;
height: 100%;
.logo {
width: 70px;
height: 70px;
margin-top: 70px;
}
.title {
margin-top: 20px;
line-height: 20px;
color: #0c1d31;
font-size: 20px;
font-weight: 600;
}
.protocol {
margin-top: 63px;
display: flex;
align-items: center;
flex-wrap: wrap;
font-size: 12px;
color: #495c72;
line-height: 17px;
}
.protocol image {
width: 15px;
height: 15px;
padding: 4px;
transform: translateY(-1px);
}
.protocol .link {
color: #1ea1f1;
}
.button-box {
margin-top: 14px;
width: 100%;
.login,
::v-deep .login button {
max-width: 324px;
width: 100%;
height: 48px;
line-height: 48px;
border-radius: 24px;
background-color: #1ea1f1 !important;
color: #fff;
font-size: 16px;
}
}
}
</style>
<template>
<view class="order-page">
<u-empty
v-if="initStatus && orderList.length === 0"
mode="list"
icon="https://static.ydlcdn.com/mini/mini_confide/list_empty.png"
text="暂无数据"
margin-top="40px"
text-color="rgb(96, 98, 102)"
></u-empty>
<view
v-else
class="content-box"
>
<order-item
v-for="item in orderList"
:key="item.id"
:order="item"
@cancel="handleCancel"
@pay="handlePay"
></order-item>
<view class="loadmore-box">
<u-loadmore :status="loadingStatus" />
</view>
</view>
<u-modal
:show="cancelVisible"
title="取消倾诉订单"
confirm-text="确定取消"
cancel-text="再想想"
:show-cancel-button="true"
width="72vw"
@cancel="cancelVisible = false"
@confirm="handleCancelConfirm"
>
<view class="slot-content">
<view class="logout-tip-text">{{ cancelTips }}</view>
</view>
</u-modal>
</view>
</template>
<script>
import OrderItem from '@/components/order-item.vue'
import { setTrackData } from '@/utils/util'
export default {
name: 'OrderPage',
components: {
OrderItem,
},
data() {
return {
orderList: [], // 订单列表
loadingStatus: 'loadmore', // 接口状态:loading: 加载中 loadmore: 加载更多 nomore: 没有更多
pageNo: 1,
pageSize: 10,
cancelVisible: false, // 取消订单提示弹窗
cancelTips: '', // 提示内容
order: {}, // 当前操作的订单
initStatus: false, // 初始化接口加载是否完成
listenerId: '', // 倾诉id
}
},
// 监听支付成功事件
onLoad() {
uni.$on('paySuccess', this.handlePaySuccess)
},
// 移除支付成功事件
onUnload() {
uni.$off('paySuccess', this.handlePaySuccess)
},
mounted() {
// 页面访问埋点
setTrackData({
events: [
{
event_id: 'page_visit',
event_custom_properties: {
part: 'my_listen_order',
position: '',
element: '',
},
},
],
})
// 获取订单列表
this.getOrderList()
},
methods: {
// 订单列表
async getOrderList() {
if (this.loadingStatus !== 'loadmore') return
this.loadingStatus = 'loading'
const { pageNo, pageSize } = this
const { uid, accessToken } = this.$store.state.user
try {
const { total, list } = await this.$request.get('/auth/order/listen/list', {
params: {
pageNo,
pageSize,
uid,
accessToken,
},
})
this.initStatus = true
if (pageNo === 1) {
this.orderList = [...list]
} else {
this.orderList.push(...list)
}
this.loadingStatus = this.orderList.length < total ? 'loadmore' : 'nomore'
} catch (e) {
console.log(e)
this.loadingStatus = 'nomore'
}
},
// 取消订单
handleCancel({ cancelTips, order }) {
this.cancelTips = cancelTips
this.order = order
this.cancelVisible = true
},
// 确认取消订单
async handleCancelConfirm() {
uni.showLoading({
title: '加载中',
mask: true,
icon: 'none',
})
try {
await this.$request.get('/auth/order/listen/orderCancel', {
params: {
listenOrderId: this.order.id,
},
})
this.$set(
this.orderList.find(item => item.id === this.order.id),
'lifecycle',
5,
)
uni.showToast({
title: '订单取消成功',
icon: 'none',
})
this.cancelVisible = false
} catch (e) {
console.log(e)
uni.hideLoading()
}
},
// 接收从订单组件传过来的 listenerId, 用于支付成功的回调中传参
handlePay(listenerId) {
this.listenerId = listenerId
},
// 支付成功回调
handlePaySuccess(from) {
if (from === 'orderList') {
setTimeout(() => {
uni.reLaunch({
url: `/pages/confide/confide?listenerId=${this.listenerId}&call=1`,
})
}, 1500)
}
},
},
// 下拉刷新,这里不要和 scrollView 一起使用,scrollView会限制高度,和下拉刷新冲突
async onPullDownRefresh() {
// 字段重置
this.pageNo = 1
this.loadingStatus = 'loadmore'
// 重新请求接口
await this.getOrderList()
// 停止下拉刷新
uni.stopPullDownRefresh()
},
// 上拉加载下一页
onReachBottom() {
console.log('bottom')
if (this.loadingStatus === 'loadmore') {
this.pageNo++
this.getOrderList()
}
},
}
</script>
<style lang="less" scoped>
.order-page {
.content-box {
padding: 10px 16px;
}
.loadmore-box {
margin-top: 20px;
}
}
</style>
<style>
page {
background: #f0f0f0;
}
</style>
<template>
<view class="pay-page">
<view class="order-info">
<view class="info-item">
<view class="label">倾诉服务</view>
<text class="value">{{ price }}</text>
</view>
<view class="info-item">
<view class="label">优惠</view>
<text class="value">{{ coupon > 0 ? `-¥${coupon}` : '暂无可用优惠' }}</text>
</view>
<view class="info-item">
<view class="label">可用余额</view>
<text class="value">{{ balance }}</text>
</view>
<view class="info-item">
<view class="label">还需支付</view>
<text class="value finally">{{ payAmount }}</text>
</view>
</view>
<view class="tips">
<view class="tips-content">付款保障</view>
<view class="tips-content">1.专业可信赖,国家认证团队,7*24小时在线</view>
<view class="tips-content">2.11指导,量身定制解决方案,服务客户300多万</view>
<view class="tips-content">3.安全保障,严格隐私保护,不满意100%退款</view>
</view>
<view class="footer">
<button
class="pay-button"
:loading="loading"
@click="pay"
>
确认支付
</button>
</view>
</view>
</template>
<script>
import { setTrackData } from '@/utils/util'
export default {
name: 'OrderPay',
data() {
return {
price: '0', // 原价
coupon: '0', // 优惠金额
balance: '0', // 余额
orderId: '', // 订单id
payId: '', // 支付id
loading: false,
from: '', // 来源页面
}
},
computed: {
// 实际支付金额
payAmount() {
return Math.max(this.price - this.coupon - this.balance, 0).toFixed(2)
},
},
onLoad(options) {
// 赋值
const { price, coupon, balance, orderId, payId, from } = options
this.price = price
this.coupon = coupon
this.balance = balance
this.orderId = orderId
this.payId = payId
this.from = from
// 页面访问埋点
setTrackData({
events: [
{
event_id: 'page_visit',
event_custom_properties: {
part: 'order_middle_page',
position: '',
element: '',
},
},
],
})
},
methods: {
// 调用后端接口得到支付参数
async pay() {
if (this.loading) {
return
}
setTrackData({
events: [
{
event_id: 'content_click',
event_custom_properties: {
part: 'order_middle_page',
position: 'foot_column',
element: 'confirm_payment',
content: this.orderId,
},
},
],
})
this.loading = true
const { uid, accessToken, openId } = this.$store.state.user
// 组合支付从余额里面扣除的金额
const payBalance = Math.min(Math.max(0, this.price - this.coupon), this.balance)
// 支付方式,1: 余额 5: 微信 7: 微信 + 余额
const payType = this.payAmount > 0 ? (this.balance > 0 ? 7 : 5) : 1
try {
const wxUrl = encodeURIComponent('http://m.ydl.com?backPayId=' + this.payId)
const res = await this.$request.post(
'/auth/cashierV2/unityPay',
{
payId: this.payId,
orderId: this.orderId,
payAmount: this.payAmount,
openId: openId,
payType,
payBalance,
payChannel: 'WX_MINI_APP',
quitUrl: wxUrl,
returnUrl: wxUrl,
uid: uid || '',
accessToken: accessToken || '',
},
{
raw: true,
},
)
this.loading = false
if (+res.code === 200) {
// 金额为0,直接调用成功方法
if (+this.payAmount === 0) {
this.handleSuccess()
return
}
// 唤起微信支付
this.requestPayment(res.data.content)
} else if (res.msg === '201 商户订单号重复') {
// 用户余额发生变化导致实际支付金额发生变化,需要取消订单后重新下单,后期等后端优化
uni.showModal({
title: '提示',
content: '金额发生变化,请取消订单后重新下单',
confirmText: '好的',
showCancel: false,
})
} else {
uni.showToast({
title: res.msg || res.errMsg,
icon: 'none',
})
}
} catch (e) {
this.loading = false
}
},
// 微信支付
requestPayment(data) {
const { nonceStr, signType, timeStamp, paySign } = data
uni.requestPayment({
timeStamp,
nonceStr,
signType,
package: data.package,
paySign: paySign,
success: () => {
this.handleSuccess()
},
fail: () => {
uni.showToast({
title: '支付失败',
icon: 'none',
})
},
})
},
handleSuccess() {
// 支付成功埋点
setTrackData({
events: [
{
event_id: 'payment_succ_page_visit',
event_custom_properties: {
part: 'order_middle_page',
position: '',
element: '',
order_id: this.orderId,
},
},
],
})
// 触发支付成功事件
uni.$emit('paySuccess', this.from)
uni.showToast({
title: '支付成功',
duration: 1500,
})
},
},
}
</script>
<style lang="less" scoped>
.pay-page {
padding: 10px 0;
height: 100%;
background: #f8f8f8;
overflow: auto;
}
.order-info {
margin: 0 16px;
border-radius: 8px;
background: #fff;
padding: 10px 16px;
.info-item {
display: flex;
align-items: center;
justify-content: space-between;
color: #666;
font-size: 13px;
line-height: 30px;
.value {
&.finally {
font-size: 18px;
color: red;
}
}
}
}
.tips {
margin: 16px;
.tips-content {
font-size: 13px;
line-height: 18px;
font-weight: 500;
color: #999;
margin-bottom: 8px;
}
}
.footer {
height: 99px;
position: fixed;
bottom: 0;
left: 0;
width: 100%;
box-sizing: border-box;
padding-top: 19px;
box-shadow: 0px -1px 0px 0px rgba(235, 235, 235, 1);
.pay-button {
background-color: #ff6b5d;
border: none;
color: #fff;
font-size: 17px;
font-weight: 500;
border-radius: 8px;
margin: 0 16px;
&:after {
border: none;
}
}
}
</style>
<template>
<web-view
v-if="!!loadUrl"
:src="loadUrl"
></web-view>
</template>
<script>
export default {
name: 'WebPage',
data() {
return {
loadUrl: '',
options: {},
}
},
onLoad(options) {
// 监听登录成功事件
uni.$on('loginSuccess', this.handleLoginSuccess)
this.options = options || {}
// 设置标题
if (this.options.title) {
uni.setNavigationBarTitle({
title: this.options.title,
})
}
},
// 移除登录成功事件
onUnload() {
uni.$off('loginSuccess', this.handleLoginSuccess)
},
mounted() {
this.init()
},
methods: {
// 初始化
init() {
// 判断即将进入的页面是否需要登录
if (this.options.login && !this.$store.getters.isLogin) {
uni.navigateTo({
url: '/pages/login/login?required=1',
})
} else {
// 配置 webview 的 url 地址
let url = decodeURIComponent(this.options.loadUrl || this.options.load_url)
const query = {
uid: this.$store.state.user.uid || '',
accessToken: this.$store.state.user.accessToken || '',
appId: uni.getAccountInfoSync().miniProgram.appId,
openId: this.$store.state.user.openId,
isFromMin: 'weapp',
miniType: 'confide',
timestamp: +new Date(),
}
// 更新 url 中的参数
Object.keys(query).forEach(prop => {
url = this.changeURLArg(url, prop, query[prop])
})
this.loadUrl = url
console.log(this.loadUrl)
}
},
// 更新 url 中的参数
changeURLArg(url, arg, value) {
const pattern = arg + '=([^&]*)'
const replaceText = arg + '=' + value
if (url.match(pattern)) {
const exp = new RegExp(`(${arg}=)([^&]*)`, 'gi')
return url.replace(exp, replaceText)
} else {
if (url.match('[\?]')) {
return url + '&' + replaceText
} else {
return url + '?' + replaceText
}
}
},
// 登录成功后重新初始化
handleLoginSuccess() {
this.init()
},
},
}
</script>
export default {
split: function (str, reg) {
return typeof str !== 'string' ? [] : str.trim().length === 0 ? [] : str.split(reg)
},
toFixed: function (str, num) {
return parseFloat(str).toFixed(num)
},
floor: function (num) {
return Math.floor(parseFloat(num))
},
isIncludes: function (arr, str) {
return arr.indexOf(str) > -1
},
// 截取字符
geSliceStr: function (v) {
return v.length > 5 ? v.slice(0, 5) + '...' : v
},
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment