初始版本

This commit is contained in:
贾祥聪
2025-08-19 14:16:51 +08:00
commit f937a1f9b9
4373 changed files with 359728 additions and 0 deletions

38
uniapp/src/App.vue Normal file
View File

@@ -0,0 +1,38 @@
<script setup lang="ts">
import { onLaunch } from '@dcloudio/uni-app'
import { useAppStore } from './stores/app'
import { useUserStore } from './stores/user'
import { useThemeStore } from './stores/theme'
import { apiIndexVisit } from '@/api/app'
import { getClient } from '@/utils/client'
import { useLocation } from '@/hooks/useLocation'
const { getConfig, getSystemInfo } = useAppStore()
const { getUser } = useUserStore()
const { getTheme } = useThemeStore()
const { getLocationData } = useLocation()
onLaunch(async () => {
await getTheme()
await getConfig()
getSystemInfo()
getLocationData()
// #ifdef H5
// const { status, page_status, page_url } = appStore.getH5Config
// if (status == 0) {
// if (page_status == 1) return (location.href = page_url)
// uni.reLaunch({ url: '/pages/empty/empty' })
// }
// #endif
await getUser()
await apiIndexVisit({
terminal: getClient()
})
})
</script>
<style lang="scss">
page {
height: 100%;
background-color: #f6f7f8;
}
</style>

76
uniapp/src/api/account.ts Normal file
View File

@@ -0,0 +1,76 @@
import request from '@/utils/request'
import { client } from '@/utils/client'
/** Login Start **/
/**
* @param { Object } params
* @return { Promise }
* @description 微信登录
*/
export const apiWechatAuthLogin = (params: any) =>
request.post({ url: '/login/oaLogin', data: params })
/**
* @param { Object } params
* @return { Promise }
* @description 手机号登录--发送验证码
*/
export const apiCaptchaLogin = (params: any) =>
request.post({ url: '/login/captcha', data: params })
/**
* @param { Object } params
* @return { Promise }
* @description 手机号密码/手机号验证码登录
*/
export const login = (params: any) =>
request.post({ url: '/login/account', data: { ...params, terminal: client } })
// 小程序-登录
export function mnpLogin(data: Record<string, any>) {
return request.post({ url: '/login/mnpLogin', data })
}
//注册
export function register(data: Record<string, any>) {
return request.post({ url: '/login/register', data: { ...data, channel: client } })
}
/**
* @param { Object } params
* @return { Promise }
* @description 小程序静默登录
*/
export const apiSilentLogin = (params: any) =>
request.post({ url: '/login/silentLogin', data: params })
/**
* @param { Object } params
* @return { Promise }
* @description 退出登录
*/
export const apiLoginOut = (params: any) => request.post({ url: '/login/logout', data: params })
/** Login End **/
/** Register Start **/
/**
* @param { Object } params
* @return { Promise }
* @description 手机号注册--发送验证码
*/
export const apiCaptchaRegister = (params: any) =>
request.post({ url: '/register/captcha', data: params })
/**
* @param { Object } params
* @return { Promise }
* @description 手机号注册
*/
export const apiAccountRegister = (params: any) =>
request.post({ url: '/register/register', data: params })
/** Register End **/
//更新微信小程序头像昵称
export function updateUser(data: Record<string, any>, header: any) {
return request.post({ url: '/login/updateUser', data, header })
}

143
uniapp/src/api/app.ts Normal file
View File

@@ -0,0 +1,143 @@
import request from '@/utils/request'
// #ifdef H5
import { getSignLink } from '@/hooks/wechat'
// #endif
//发送短信
export function smsSend(data: any) {
return request.post({ url: '/sms/sendCode', data: data })
}
/**
* @return { Promise }
* @description 获取公共配置
*/
export const apiConfigGet = () => request.get({ url: '/config/config' })
/**
* @param { Object } params
* @return { Promise }
* @description 访问量
*/
export const apiIndexVisit = (params: any) => request.post({ url: '/index/visit', data: params })
/**
* @param { Object } params
* @return { Promise }
* @description 访问量
*/
export const getTabbarConfig = () => request.post({ url: '/decorate/tabbar' })
/**
* @return { Promise }
* @description 获取公共配置
*/
// 获取政策协议
export const apiPolicyAgreement = () => request.get({ url: '/config/agreement' })
/**
* @return { Promise }
* @description 获取客服信息
*/
export const apiContactService = () => request.get({ url: '/user/customerService' })
/**
* @return { Promise }
* @description 微信消息订阅
*/
export const apiSubscribe = () => request.get({ url: '/subscribe/lists' })
/**
* @param { Object } params
* @return { Promise }
* @description 支付方式
*/
export const apiPayPayWay = (params: any) => request.get({ url: '/order/payWay', data: params })
/**
* @param { Object } params
* @return { Promise }
* @description 预支付
*/
// 获取支付状态
export function getPayStatus(data: any) {
return request.get({ url: '/mall.pay/status', data: data }, { isAuth: true })
}
// 获取支付结果
export function getPayResult(params: any,token?:string) {
return request.get({ url: '/pay/getPayResult', data: params, header: {token:token} })
}
export const apiPayPrepay = (params: any) => request.post({ url: '/pay/prepay', data: params })
export const apiJumpPayPrepay = (params: any,token:string) => request.post({ url: '/pay/prepay', data: params, header: {token:token}})
/**
* @param { Object } params
* @return { Promise }
* @description 公众号登录
*/
export const apiOALogin = (params: any) => request.post({ url: '/login/oaLogin', data: params })
/**
* @param { Object } params
* @return { Promise }
* @description 公众号-获取授权url
*/
export function apiCodeUrlGet(data: Record<string, any>) {
return request.get({ url: '/login/codeUrl', data })
}
/**
* @param { Object } params
* @return { Promise }
* @description 微信sdk配置
*/
export const apiJsConfig = () =>
request.get({ url: '/wechat/jsConfig', data: { url: getSignLink() } })
/**
* @param { Object } params { address: 广州市番禺区xxx }
* @return { Promise }
* @description 地址逆解析
*/
export const getGeocoder = (params: any) => request.get({ url: '/index/geocoder', data: params })
/**
* @param { Object } params { location: xxx,xxx }
* @return { Promise }
* @description 地址逆解析
*/
export const getGeocoderCoordinate = (params: any) =>
request.get({ url: '/index/geocoderCoordinate', data: params })
export function uploadImage(file: any, token?: string) {
return request.uploadFile({
url: '/upload/image',
filePath: file,
name: 'file',
header: {
token
},
fileType: 'image'
})
}
export const getAddress = (params: any) =>
request.get({ url: '/index/address', data: params })
// 获取小程序码
export function getMiniQrCode(data: any) {
return request.get({ url: '/share/getMnpQrCode', data: data }, { isAuth: false })
}
// 海报商品信息
export function getPosterGoods(data: any) {
return request.get({ url: '/mall.goods/detail_base64', data: data }, { isAuth: true })
}

21
uniapp/src/api/coach.ts Normal file
View File

@@ -0,0 +1,21 @@
import request from '@/utils/request'
/**
* @return { Promise }
* @description 技能列表
*/
export const apiSkillLists = () => request.get({ url: '/coach/skillLists' })
/**
* @param { Object } params
* @return { Promise }
* @description 服务师傅
*/
export const apiCoachLists = (params: any) => request.get({ url: '/coach/lists', data: params })
/**
* @param { Object } params
* @return { Promise }
* @description 服务师傅详情
*/
export const apiCoachDetail = (params: any) => request.get({ url: '/coach/detail', data: params })

45
uniapp/src/api/goods.ts Normal file
View File

@@ -0,0 +1,45 @@
import request from '@/utils/request'
/**
* @param { Object } params
* @return { Promise }
* @description 获取商品详情
*/
export const apiGoodsDetail = (params) => request.get({ url: '/goods/detail', data: params })
/**
* @param { Object } params
* @return { Promise }
* @description 商品收藏
*/
export const apiGoodsCollection = (params) => request.post({ url: '/goods/collect', data: params })
/**
* @return { Promise }
* @description 获取预约时间
*/
export const apiAppointTime = (params: any) => request.get({ url: '/order/getCoachServerTime', data: params })
/**
* @param { Object } params
* @return { Promise }
* @description 获取商品评价列表
*/
export const apiEvaluateGoodsLists = (params: any) =>
request.get({ url: '/goods_comment/lists', data: params })
/**
* @param { Object } params
* @return { Promise }
* @description 获取商品评价分类列表
*/
export const apiEvaluateGoodsCategory = (params: any) =>
request.get({ url: '/goods_comment/commentCategory', data: params })
/**
* @param { Object } params
* @return { Promise }
* @description 获取商品评价详情
*/
export const apiEvaluateGoodsDetail = (params: any) =>
request.get({ url: '/goods_comment/commentDetail', data: params })

87
uniapp/src/api/order.ts Normal file
View File

@@ -0,0 +1,87 @@
import request from '@/utils/request'
/**
* @param { Object } params
* @return { Promise }
* @description 订单下单
*/
export const apiPlaceOrder = (params: any) =>
request.post({url: '/order/placeOrder', data: params})
/**
* @param { Object } params
* @return { Promise }
* @description 订单列表
*/
export const apiOrderLists = (params: any) => request.get({url: '/order/lists', data: params})
/**
* @param { Object } params
* @return { Promise }
* @description 订单详情
*/
export const apiOrderDetail = (params: any) => request.get({url: '/order/detail', data: params})
/**
* @param { Object } params
* @return { Promise }
* @description 取消订单
*/
export const apiOrderCancel = (params: any) => request.post({url: '/order/cancel', data: params})
/**
* @param { Object } params
* @return { Promise }
* @description 删除订单
*/
export const apiOrderDel = (params: any) => request.post({url: '/order/del', data: params})
/** 师傅订单服务 Start **/
/**
* @param { Object } params
* @return { Promise }
* @description 师傅服务列表
*/
export const apiStaffOrderLists = (params: any) =>
request.get({url: '/staff_order/lists', data: params})
/**
* @param { Object } params
* @return { Promise }
* @description 师傅服务详情
*/
export const apiStaffOrderDetail = (params: any) =>
request.get({url: '/staff_order/detail', data: params})
/**
* @param { Object } params
* @return { Promise }
* @description 确认服务
*/
export const apiStaffOrderConfirmService = (params: any) =>
request.post({url: '/staff_order/confirmService', data: params})
/**
* @param { Object } params
* @return { Promise }
* @description 核销订单
*/
export const apiStaffOrderVerification = (params: any) =>
request.post({url: '/staff_order/verification', data: params})
/** 师傅订单服务 Start **/
/**
* @param { Object } params
* @return { Promise }
* @description 补差价
*/
export const apiOrderGap = (params: any) =>
request.post({url: '/order/orderGap', data: params})
/**
* @param { Object } params
* @return { Promise }
* @description 加钟
*/
export const apiOrderAppend = (params: any) =>
request.post({url: '/order/orderAppend', data: params})

54
uniapp/src/api/shop.ts Normal file
View File

@@ -0,0 +1,54 @@
import request from '@/utils/request'
//首页数据
export function getIndex() {
return request.get({ url: '/index/index' })
}
/**
* @description 风格主题
* @return { Promise }
*/
export function getDecorateTheme() {
return request.get({ url: '/decorate/style' })
}
/**
* @description 服务列表
* @param { any } data
* @return { Promise }
*/
export function getServiceList(data: any) {
return request.get({ url: '/index/serverLists', data }, { ignoreCancel: true })
}
/**
* @description 热门搜索
* @return { Promise }
*/
export function getHotSearch() {
return request.get({ url: '/search/hotLists' })
}
/**
* @return { Promise }
* @description 商家分类列表
*/
export const apiSkillLists = () => request.get({ url: '/coach/skillLists' })
/**
* @param { Object } params
* @return { Promise }
* @description 商家
*/
export const apiCoachLists = (params: any) => request.get({ url: '/coach/lists', data: params })
/**
* @param { Object } params
* @return { Promise }
* @description 商家详情
*/
export const apiCoachDetail = (params: any) => request.get({ url: '/coach/detail', data: params })
// 门店列表
export const apishopLists = (params: any) => request.get({ url: '/shop/lists', data: params })
export const apishopDetail = (params: any) => request.get({ url: '/shop/detail', data: params })

90
uniapp/src/api/store.ts Normal file
View File

@@ -0,0 +1,90 @@
import request from '@/utils/request'
/**
* @param { Object } params
* @return { Promise }
* @description 装修数据
*/
export const getDecoratePage = (params: any) => request.get({ url: '/decorate/page', data: params })
/**
* @param { Object } params
* @return { Promise }
* @description 获取最近城市
*/
export const getNearbyCity = (params: { longitude: string; latitude: string }) =>
request.get({ url: '/index/getNearbyCity', data: params }, { ignoreCancel: true })
/**
* @param { Object } params
* @return { Promise }
* @description 轮博图
*/
export const apiAdLists = (params: any) => request.get({ url: '/ad/lists', data: params })
/**
* @return { Promise }
* @description 服务分类
*/
export const apiCategoryLists = () => request.get({ url: '/goods_category/lists' })
/**
* @param { Object } params
* @return { Promise }
* @description 商品服务分类
*/
export const apiGoodsCategoryLists = (params: any) =>
request.get({ url: '/goods_category/otherLists', data: params })
/**
* @param { Object } params
* @return { Promise }
* @description 服务列表
*/
export const apiGoodsLists = (params: any) =>
request.get({ url: '/goods/lists', data: params }, { ignoreCancel: true })
/**
* @param { Object } params
* @return { Promise }
* @description 师傅详情
*/
export const apiStaffDetail = (params: any) => request.get({ url: '/staff/detail', data: params })
/**
* @param { Object } params
* @return { Promise }
* @description 获取地级市列表
*/
export const apiRegionCity = (params?: any) => request.get({ url: '/region/city', data: params })
/**
* @param { Object } params
* @return { Promise }
* @description 获取沿途地址
*/
export const getNearbyLocation = (params: any) => request.get({ url: '/index/getNearbyLocation', data: params })
/**
* @description 热门搜索
* @return { Promise }
*/
export function getHotSearch() {
return request.get({ url: '/search/hotLists' })
}
/**
* @description 搜索
* @param { string } keyword 关键词
* @return { Promise }
*/
export function getSearch(data: { keyword: string; pageNo: number; pageSize: number }) {
return request.get({ url: '/search', data })
}
/**
* @param { Object } params
* @return { Promise }
* @description 收藏接口
*/
export const putCollect = (params: { id: number, type: 1 | 2 | 3}) => request.post({ url: '/index/collect', data: params })

139
uniapp/src/api/user.ts Normal file
View File

@@ -0,0 +1,139 @@
import request from '@/utils/request'
/**
* @description 用户中心
*/
export const getUserCenter = (header?: any) => request.get({ url: '/user/center', header })
// 获取用户信息
export const apiUserInfoGet = (params?: any) => request.get({ url: '/user/info', data: params}, { isAuth: true })
// 设置用户信息
export const apiUserInfoSet = (params: any) =>
request.post({ url: '/user/setInfo', data: params }, { isAuth: true })
// 获取微信小程序手机号
export const apiWechatMobileGet = (params: any) =>
request.post({ url: '/user/getMobileByMnp', data: params }, { isAuth: true })
// 发送验证码-绑定手机号
export const userBindMobileCaptcha = (params: any) =>
request.post({ url: '/user/bindMobileCaptcha', data: params }, { isAuth: true })
// 绑定手机
export function userBindMobile(data: any, header?: any) {
return request.post({ url: '/user/bindMobile', data, header }, { isAuth: true })
}
// 微信电话
export function userMnpMobile(data: any, header?: any) {
return request.post({ url: '/user/getMobileByMnp', data, header }, { isAuth: true })
}
// 绑定微信-小程序
export const apiBindwx = (params: any, header?: any) =>
request.post({ url: '/login/mnpAuthBind', data: params, header }, { isAuth: true })
// 绑定微信-公众号
export const apiBindoa = (params: any, header?: any) =>
request.post({ url: '/login/oaAuthBind', data: params, header }, { isAuth: true })
// 解绑 -- 测试用
export const apiUnBindwx = (params: any, header?: any) =>
request.post({ url: '/login/unbinding', data: params }, { isAuth: true })
// 是否设置登录密码
export const apiHasPassword = () => request.get({ url: '/user/hasPassword' }, { isAuth: true })
// 设置登录密码
export const apiSetPassword = (params: any) =>
request.post({ url: '/user/setPassword', data: params }, { isAuth: true })
// 更改密码
export function userChangePwd(data: any) {
return request.post({ url: '/user/changePassword', data }, { isAuth: true })
}
//忘记密码
export function forgotPassword(data: Record<string, any>) {
return request.post({ url: '/user/resetPassword', data })
}
/** Profile End **/
/** Address Start **/
/**
* @description 获取地址列表
*/
export const apiAddressLists = (params: any) =>
request.get({ url: '/user_address/lists', data: params })
/**
* @param { Object } params
* @description 获取地址详情
*/
export const apiAddressDetail = (params: any) =>
request.get({ url: '/user_address/detail', data: params })
/**
* @param { Object } params
* @description 编辑地址
*/
export const apiAddressEdit = (params: any) =>
request.post({ url: '/user_address/edit', data: params })
/**
* @param { Object } params
* @description 切换默认地址
*/
export const apiAddressEditDefault = (params: any) =>
request.post({ url: '/user_address/setDefault', data: params })
/**
* @param { Object } params
* @description 新增地址
*/
export const apiAddressAdd = (params: any) =>
request.post({ url: '/user_address/add', data: params })
/**
* @param { Object } params
* @description 删除地址
*/
export const apiAddressDel = (params: any) =>
request.post({ url: '/user_address/del', data: params })
/** Address End **/
/** Evaluate Start **/
/**
* @param { Object } params
* @description 获取我的评价列表
*/
export const apiEvaluateLists = (params: any) =>
request.get({ url: '/goods_comment/commentGoodsLists', data: params })
/**
* @param { Object } params
* @description 评价服务信息
*/
export const apiEvaluateGoodsInfo = (params: any) =>
request.get({ url: '/goods_comment/commentGoodsInfo', data: params })
/**
* @param { Object } params
* @description 提交评价信息
*/
export const apiEvaluateAdd = (params: any) =>
request.post({ url: '/goods_comment/add', data: params })
/** Evaluate End **/
/** Collect Start **/
/**
* @description 获取我的收藏列表
*/
export const apiCollectLists = (params: any) =>
request.get({ url: '/user/collectLists', data: params })
/** Collect End **/
export const getKefuConfig = (params?: any) =>
request.post({ url: '/config/getKefuConfig', data: params }, { isAuth: true })

32
uniapp/src/api/wallet.ts Normal file
View File

@@ -0,0 +1,32 @@
import request from '@/utils/request'
// 钱包信息
export const apiUserWallet = () => request.get({ url: '/user/wallet' })
// 账户明细列表
export const apiAccountLogLists = (params: any) =>
request.get({ url: '/account_log/lists', data: params }, { ignoreCancel: true })
// 充值
export const apiRecharge = (params: any) =>
request.post({ url: '/recharge/recharge', data: params })
// 充值记录列表
export const apiRechargeLogLists = (params: any) =>
request.get({ url: '/recharge/logLists', data: params })
// 获取提现配置
export const apiGetWithdrawConfig = (params: any) =>
request.get({ url: '/withdraw/getConfig', data: params })
// 提现申请
export const apiWithdrawApply = (params: any) =>
request.post({ url: '/withdraw/apply', data: params })
// 提现申请列表
export const apiWithdrawLists = (params: any) =>
request.get({ url: '/withdraw/lists', data: params })
// 提现申请详情
export const apiWithdrawDetail = (params: any) =>
request.get({ url: '/withdraw/detail', data: params })

View File

@@ -0,0 +1,52 @@
<template>
<page-meta :page-style="$theme.pageStyle">
<!-- #ifndef H5 -->
<navigation-bar :front-color="$theme.navColor" :background-color="$theme.navBgColor" />
<!-- #endif -->
</page-meta>
<view class="main">
<tabs
:current="current"
height="80"
bar-width="60"
:auth="true"
bgColor="#fff"
:activeColor="$theme.primaryColor"
inactiveColor="#666"
:itemWidth="150"
:is-scroll="false"
>
<tab v-for="(item, i) in tabList" :key="i" :name="item.name">
<view class="list">
<list :status="item.status" :i="i" :index="current" />
</view>
</tab>
</tabs>
</view>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import list from './components/list.vue'
import tab from '@/components/tab/tab.vue'
import tabs from '@/components/tabs/tabs.vue'
const tabList = ref<any>([
{
name: '支出',
status: 1
},
{
name: '收入',
status: 2
}
])
const current = ref<number>(0)
</script>
<style lang="scss" scoped>
.list {
height: calc(100vh - 44px - env(safe-area-inset-bottom));
}
</style>

View File

@@ -0,0 +1,111 @@
<template>
<z-paging ref="paging" v-model="balance.lists" @query="queryList" :fixed="false" height="100%">
<!-- 余额明细列表 -->
<view class="balance-details">
<view v-for="(item, index) in balance.lists" :key="index">
<view class="balance-details-item flex">
<view
class="flex-none flex items-center justify-center bg-primary-light-9 rounded-full"
style="width: 84rpx; height: 84rpx;"
>
<u-icon name="coupon-fill" :color="$theme.primaryColor" size="40"></u-icon>
</view>
<view class="flex-1 flex items-center justify-between ml-2">
<view>
<view class="flex-1 balance-details-item-text">
{{ item.change_type_desc }}
</view>
<view class="balance-details-item-time">{{ item.create_time }}</view>
</view>
<!-- <view class="balance-details-item-time">{{ item.remark }}</view>-->
<view v-if="item.action == 2" class="balance-details-item-amount-add text-primary">
+{{ item.change_amount }}
</view>
<view v-else class="balance-details-item-amount-reduce text-primary">
-{{ item.change_amount }}
</view>
</view>
</view>
</view>
</view>
</z-paging>
</template>
<script lang="ts" setup>
import { reactive, ref, shallowRef } from 'vue'
import { apiAccountLogLists } from '@/api/wallet'
import { onLoad } from '@dcloudio/uni-app'
const props = withDefaults(
defineProps<{
status: string | number
i: number
index: number
}>(),
{
status: ''
}
)
const balance = reactive({
lists: [] as any,
change_type: 1
})
// 下拉组件的Ref
const paging = shallowRef()
const queryList = async (page_no: number, page_size: number) => {
try {
const { lists } = await apiAccountLogLists({
page_no,
page_size,
action: props.status,
change_object: balance.change_type
})
paging.value.complete(lists)
} catch (e) {
paging.value.complete(false)
}
}
</script>
<style lang="scss" scoped>
.balance-details {
margin: 30rpx;
.balance-details-item {
background-color: #fff;
margin-bottom: 24rpx;
padding: 36rpx 20rpx;
border-radius: 20rpx;
.balance-details-item-text {
font-size: 30rpx;
font-weight: 400;
}
.balance-details-item-amount-add {
font-size: 40rpx;
font-weight: 900;
}
.balance-details-item-amount-reduce {
font-size: 40rpx;
font-weight: 900;
}
.balance-details-item-time {
font-size: 24rpx;
font-weight: 400;
color: #999;
margin-top: 20rpx;
}
}
}
.empty {
padding-top: 300rpx;
}
</style>

View File

@@ -0,0 +1,39 @@
<template>
<view class="p-[24rpx]">
<mp-html :content="agreementContent"></mp-html>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { apiPolicyAgreement } from '@/api/app'
import { AgreementEnum } from "@/enums/agreementEnums"
let agreementType = ref('') // 协议类型
const agreementContent = ref('') // 协议内容
const getData = async () => {
const res = await apiPolicyAgreement()
if( agreementType == AgreementEnum.SERVICE) {
agreementContent.value = res.service_agreement
uni.setNavigationBarTitle({
title: String(res.service_title)
})
}else {
agreementContent.value = res.privacy_agreement
uni.setNavigationBarTitle({
title: String(res.privacy_title)
})
}
}
onLoad((options: any) => {
if (options.type) {
agreementType = options.type
getData()
}
})
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,253 @@
<template>
<!-- Header Start -->
<scroll-view class="scroll-view-box" scroll-x="true">
<block v-for="(item, index) in date" :key="index">
<view
class="day text-base"
:class="{ active: dayIndex == index }"
@click="dayIndex = index"
>
<view>{{ item.week }}</view>
<view class="mt-[10rpx]">{{ item.date }}</view>
</view>
</block>
</scroll-view>
<!-- Header End -->
<!-- Main Start -->
<view class="time-box">
<block v-for="(item2, index2) in timeSlot" :key="index2">
<view
class="time-item"
:class="{
select: selectIndex == index2,
disabled: dayIndex === 0 && item2.disabled
}"
@click="selectAppoint(index2, dayIndex === 0 && item2.disabled)"
>
{{ item2.start_time }} - {{ item2.end_time }}
</view>
</block>
</view>
<!-- Main End -->
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue'
import { toast } from '@/utils/util'
import { apiAppointTime } from '@/api/goods'
import { onLoad, onShow } from '@dcloudio/uni-app'
interface DateObj {
year: number
week: string | undefined
date: string
}
interface TimeSlotObj {
start_time: string
end_time: string
disabled: boolean
}
// 日期
const date = ref<DateObj[]>([])
// 当天日期索引
const dayIndex = ref<number>(0)
// 时间段选择索引
const selectIndex = ref<number>(0)
// 预约时间段
const timeSlot = ref<TimeSlotObj[]>([])
// 商品数据
const goodsForm = ref<any | null>({
goods_num: '' as string,
id: '' as string
})
/**
* @param { number } index
* @param { boolean } disabled
* @return { void }
* @description 选择上门时间段
*/
const selectAppoint = (index: number, disabled: boolean) => {
if (disabled) return toast('服务时间段已过!请选择其他时间段')
selectIndex.value = index
// 缓存用户所选的预约时间
uni.setStorage({
key: 'selectDate',
data: [dayIndex.value, selectIndex.value]
})
const dateTime = date.value[dayIndex.value]
const timeSlotItem = timeSlot.value[index]
const appointTime = {
year: dateTime.year,
date: dateTime.date,
start_time: timeSlotItem.start_time,
end_time: timeSlotItem.end_time
}
if (goodsForm.value.id != '') {
const params = {
goodsData: goodsForm.value,
appointData: appointTime
}
goPage(`/pages/order_buy/index?params=${JSON.stringify(params)}`)
return
}
uni.$emit('appointTime', appointTime)
uni.navigateBack()
}
/**
* @return { Promise } void
* @description 请求获取预约时间
*/
const getAppointTime = async (): Promise<void> => {
const { appoint_time, order_time } = await apiAppointTime()
timeSlot.value = handleTimeSlot(appoint_time)
date.value = handleAppointDay(order_time)
// 获取用户所选的预约时间
uni.getStorage({
key: 'selectDate',
success: (res) => {
dayIndex.value = res.data[0]
selectIndex.value = res.data[1]
}
})
}
/**
* @param { TimeSlotObj[] } timeSlot
* @return { TimeSlotObj[] } 时间段
* @description 计算已过期时间
*/
const handleTimeSlot = (timeSlot: TimeSlotObj[]) => {
const time = new Date() // 时间戳转换成标准日期时间
const min = time.getMinutes()
const startTime = time.getHours() + '' + (min <= 9 ? '0' + min : min) // 获取时分并组合成标准格式(时分)
timeSlot.map((item) => {
const end = item.end_time.replace(':', '')
item.disabled = Number(startTime) - Number(end) >= 0
})
return timeSlot
}
/**
* @param { number } days
* @return { string } 日期
* @description 根据返回预约天数计算出日期
*/
const handleAppointDay = (days: number | string | undefined) => {
const timeArr = []
// 现在时间戳
const newTime = new Date().getTime()
for (let i = 0; i <= days; i++) {
// 获取天数时间戳
const millisecond = newTime + i * 24 * 60 * 60 * 1000
// 年
const year = new Date(millisecond).getFullYear()
// 月
const month = new Date(millisecond).getMonth() + 1
// 星期
const week = new Date(millisecond).getDay()
// 日
const day = new Date(millisecond).getDate()
// 未来天数的数组
timeArr.push({
year: year,
week: handleWeek(week, i),
date: (month <= 9 ? '0' + month : month) + '-' + (day <= 9 ? '0' + day : day)
})
}
return timeArr
}
/**
* @param { number } week
* @param { number } i
* @return { string } 日期
* @description 转换日期
*/
const handleWeek = (week: number | string | undefined, i: number) => {
if (i === 0) return '今天'
else if (i === 1) return '明天'
else if (week === 0) return '周日'
else if (week === 1) return '周一'
else if (week === 2) return '周二'
else if (week === 3) return '周三'
else if (week === 4) return '周四'
else if (week === 5) return '周五'
else if (week === 6) return '周六'
}
/**
* @param { string } url
* @return { void }
* @description 跳转页面方法
*/
const goPage = (url: string) => {
uni.redirectTo({ url: url })
}
onLoad((options) => {
if (options.params) {
const goodsData = JSON.parse(options.params)
goodsForm.value = goodsData
}
})
onShow(() => {
getAppointTime()
})
</script>
<style lang="scss">
.scroll-view-box {
height: 140rpx;
white-space: nowrap;
box-sizing: border-box;
background-color: #ffffff;
padding: 30rpx 20rpx 0 20rpx;
.day {
width: 85rpx;
text-align: center;
display: inline-block;
color: #222222;
margin: 0 20rpx;
}
.active {
color: #f36161;
border-bottom: 4px solid #f36161;
padding-bottom: 16rpx;
}
}
.time-box {
padding: 24rpx;
.time-item {
width: 220rpx;
height: 80rpx;
line-height: 80rpx;
text-align: center;
display: inline-block;
background-color: #ffffff;
margin: 0 20rpx 20rpx 0;
border-radius: 10rpx;
}
.time-item:nth-child(3n) {
margin-right: 0;
}
.select {
background-color: rgba(255, 244, 244, 1);
color: rgba(243, 97, 97, 1);
// border: 1px solid rgba(243, 97, 97, 1);
}
.disabled {
color: rgba(153, 153, 153, 1);
background-color: rgba(255, 255, 255, 1);
}
}
</style>

View File

@@ -0,0 +1,23 @@
<template>
<view class="as-us flex flex-1 flex-col items-center justify-center">
<image :src="appStore.config.web_logo" mode="" class="img"></image>
<view class="text-content mt-[20rpx]">当前版本{{ `v${appStore.config.version}` }}</view>
</view>
</template>
<script setup lang="ts">
import { useAppStore } from '@/stores/app'
const appStore = useAppStore()
</script>
<style lang="scss" scoped>
.as-us {
height: 100%;
.img {
width: 160rpx;
height: 160rpx;
border-radius: 20rpx;
margin-top: 96rpx;
}
}
</style>

View File

@@ -0,0 +1,146 @@
<template>
<view class="container">
<scroll-view
class="menu"
scroll-x="true"
:scroll-left="160 * idx"
v-if="menuData.sons.length"
>
<view
class="menu--item"
:class="{ active: categoryId === menuData.info.id }"
@click="changeTabs(menuData.info)"
>
<view class="flex justify-center">
<u-image src="@/bundle/static/images/mb-like.svg" width="56" height="56" />
</view>
<view class="black font-medium mt-[14rpx]">{{ '全部服务' }}</view>
</view>
<block v-for="item in menuData.sons" :key="item.id">
<view
class="menu--item"
:class="{ active: categoryId === item.id }"
@click="changeTabs(item)"
>
<view class="flex justify-center">
<u-image :src="item.image" width="56" height="56" />
</view>
<view class="black font-medium mt-[14rpx]">{{ item.name }}</view>
</view>
</block>
</scroll-view>
<view class="main">
<z-paging
ref="paging"
v-model="goodsData"
@query="queryList"
:fixed="false"
height="100%"
>
<view class="px-[24rpx]">
<goods-card :goodsList="goodsData"></goods-card>
</view>
</z-paging>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref, shallowRef } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { apiGoodsCategoryLists, apiGoodsLists } from '@/api/store'
import GoodsCard from '@/components/goods-card/index.vue'
const categoryId = ref<number>()
const menuData = ref<any>({
info: {},
sons: []
})
const goodsData = ref<any>([])
const paging = shallowRef()
// 记录当前选中商品下标
const idx = ref(0)
onLoad((options: any) => {
categoryId.value = options?.id * 1 || 0
getCategoryList()
})
// 切换菜单
const changeTabs = (event: any) => {
// 如果点击同一个2次的话那就是取消当前选择然后选择一级分类的商品
categoryId.value = event.id === categoryId.value ? menuData.value.info.id : event.id
paging.value.reload()
}
// 获取分类列表
const getCategoryList = async (): Promise<void> => {
try {
const res: any = await apiGoodsCategoryLists({ id: categoryId.value })
uni.setNavigationBarTitle({
title: res.info.name
})
menuData.value = res
if (menuData.value.sons.length) {
idx.value = menuData.value.sons.findIndex((item) => item.id === categoryId.value)
}
} catch (err) {
console.log(err)
}
}
// 获取商品列表
const queryList = async (pageNo: number, pageSize: number) => {
try {
const { lists } = await apiGoodsLists({
page_no: pageNo,
page_size: pageSize,
category_id: categoryId.value
})
paging.value.complete(lists)
} catch (e) {
console.log('报错=>', e)
//TODO handle the exception
paging.value.complete(false)
}
}
</script>
<style lang="scss">
.container {
display: flex;
height: 100vh;
overflow: hidden;
flex-direction: column;
.menu {
height: 188rpx;
white-space: nowrap;
box-sizing: border-box;
padding: 20rpx 0 20rpx 24rpx;
&--item {
width: 160rpx;
height: 148rpx;
padding: 20rpx 0;
margin-right: 20rpx;
display: inline-block;
text-align: center;
font-size: 26rpx;
border-radius: 10rpx;
background-color: #ffffff;
}
.active {
color: #f36161;
}
}
.main {
flex: 1;
min-height: 0;
overflow: scroll;
}
}
</style>

View File

@@ -0,0 +1,65 @@
<template>
<page-meta :page-style="$theme.pageStyle">
<!-- #ifndef H5 -->
<navigation-bar
:front-color="$theme.navColor"
:background-color="$theme.navBgColor"
/>
<!-- #endif -->
</page-meta>
<view
class="register min-h-full flex flex-col items-center px-[30rpx] pt-[60rpx] box-border"
>
<view class="w-full">
<view class="bg-white mb-[30rpx] rounded-[24rpx] px-[48rpx] py-[15rpx] flex items-center">
<u-input
class="flex-1"
v-model="data"
:border="false"
:placeholder="fieldType == FieldType.NICKNAME ? '请输入昵称' : '请输入账号'"
placeholder-style="color: #999"
/>
</view>
<view class="mt-[112rpx]">
<u-button type="primary" @click="handleConfirm" class="rounded-[24rpx]"> 确定 </u-button>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { forgotPassword } from '@/api/user'
import { reactive, ref } from 'vue'
import { FieldType } from '@/enums/appEnums'
import { apiUserInfoSet } from '@/api/user'
import { onLoad } from '@dcloudio/uni-app'
const fieldType = ref('') // 1-昵称 2-账号
const data = ref('')
const handleConfirm = async () => {
if (!data.value && fieldType.value == FieldType.NICKNAME) return uni.$u.toast('请输入昵称')
if (!data.value && fieldType.value == FieldType.USERNAME) return uni.$u.toast('请输入账号')
await apiUserInfoSet({
field: fieldType.value,
value: data.value
})
uni.$u.toast('操作成功')
uni.navigateBack()
}
onLoad((options) => {
data.value = options.data || ''
fieldType.value = options.type || ''
})
</script>
<style lang="scss">
page {
height: 100%;
}
</style>

View File

@@ -0,0 +1,119 @@
<template>
<u-popup
v-model="showPopup"
mode="bottom"
border-radius="12"
:closeable="true"
:mask-close-able="true"
>
<view style="padding: 30rpx; height: 1100rpx">
<view class="font-medium text-lg pb-4">
请选择所在城市
</view>
<!-- 搜索 -->
<u-search
placeholder="搜索城市名"
v-model="keyword"
shape="round"
:clearabled="true"
:animation="true"
:height="70"
bg-color="#f2f2f2"
@search="handleSearch"
@custom="handleSearch"
@clear="handleSearch"
></u-search>
<!-- 定位城市 -->
<view class="text-[24rpx] text-[#c8c9cc] mt-[30rpx] mb-[30rpx]">已开通以下城市</view>
<scroll-view scroll-y="true" :style="{ height: '400px' }">
<view>
<!-- 城市列表 -->
<view
v-for="(cityItem, cityIndex) in cityList"
:key="cityIndex"
class="anchor flex mb-[10rpx]"
>
<view class="font-bold text-[32rpx]">{{ cityIndex }}</view>
<view class="ml-[20rpx]">
<block v-for="(cityItem2, cityIndex2) in cityItem" :key="cityIndex2">
<view
@click="handleClick(cityItem2)"
class="pb-[40rpx] pt-[4rpx] w-[500rpx]"
>
<view v-if="location.city_id != cityItem2.city_id">
{{ cityItem2.name }}
</view>
<view v-else class="u-flex justify-between">
<view :style="{'color':$theme.primaryColor}">{{ cityItem2.name }}</view>
<u-icon name="checkmark" :color="$theme.primaryColor"></u-icon>
</view>
</view>
</block>
</view>
</view>
</view>
</scroll-view>
</view>
</u-popup>
</template>
<script lang="ts" setup>
import {computed, onMounted, ref} from 'vue'
import {apiRegionCity} from '@/api/store'
const props = defineProps({
show: {
type: Boolean
},
location: {
type: Object,
default: () => ({})
}
})
const emit = defineEmits<{
(event: 'update:show', show: boolean): void
(event: 'update:location', value: any): void
(event: 'update', value: any): void
}>()
// 搜索关键字
const keyword = ref<string | number>('')
// 城市列表
const cityList = ref<any>([])
const showPopup = computed({
get() {
return props.show
},
set(val) {
emit('update:show', val)
}
})
const handleSearch = async () => {
cityList.value = await apiRegionCity({
keyword: keyword.value
})
}
const handleClick = (item: any) => {
emit('update:location', {
city_id: item.city_id,
id: item.id,
longitude: item.gcj02_lng,
latitude: item.gcj02_lat,
name: item.name
})
emit('update', item)
showPopup.value = false
}
// 初始化城市数据
const initCityData = async (): Promise<void> => {
cityList.value = await apiRegionCity()
}
onMounted(() => { initCityData() })
</script>

View File

@@ -0,0 +1,305 @@
<template>
<view class="city-pages flex flex-col min-h-0 h-full">
<!-- 搜索框 -->
<view class="search flex items-center">
<!-- 左侧城市 -->
<view class="flex items-center search--city" @click.stop="showCityPicker = true">
<text class="mr-[15rpx]">{{ currentLocation.name }}</text>
<u-icon name="arrow-down" size="20"></u-icon>
</view>
<!-- 右侧搜索城市 -->
<input
type="text"
class="flex-1 search--search"
placeholder="请输入地点"
v-model="keyword"
@confirm="handleSearch"
@input="handleSearch"
/>
</view>
<!-- 地图组件 -->
<view class="mt-[30rpx] mb-[30rpx] map">
<map
id="myMap"
theme="normal"
style="width: 100%; height: 400rpx; border-radius: 20rpx; overflow: hidden"
show-location
:enable-overlooking="false"
:scale="13"
@regionchange="regionchange"
:latitude="currentLocation.latitude"
:longitude="currentLocation.longitude"
>
<cover-view class="iconImg">
<cover-image
class="img"
src="https://hellouniapp.dcloud.net.cn/static/location.png"
></cover-image>
</cover-view>
<!-- #ifdef MP -->
<cover-view class="origin" @click="originLocation">
<cover-image
class="origin_icon"
src="@/bundle/static/images/map/origin.png"
></cover-image>
<cover-view class="text-xs mt-[15rpx]">定位</cover-view>
</cover-view>
<!-- #endif -->
</map>
</view>
<view class="address flex flex-col min-h-0 flex-1 mt-4" v-if="addressList?.length">
<!-- 地址列表 -->
<scroll-view scroll-y="true" class="h-full" :show-scrollbar="false">
<view
class="address-item"
v-for="(item, index) in addressList"
:key="index"
@click="choiceAddress(item)"
>
<view class="u-flex justify-between">
<view class="u-flex">
<u-icon name="map"></u-icon>
<view class="font-bold ml-[10rpx] w-[500rpx] truncate">
{{ item.title }}
</view>
</view>
<view class="text-[#909399] text-[24rpx]">
{{ computeDistance(item._distance) }}
</view>
</view>
<view
class="text-[#909399] text-[24rpx] ml-[40rpx] mt-[10rpx] w-[500rpx] truncate"
>
{{ item.address }}
</view>
</view>
</scroll-view>
</view>
<!-- 无地址 -->
<view v-else style="height: 60vh" class="flex flex-col items-center justify-center pb-3">
<view class="mt-[40rpx]">
<u-image src="@/bundle/static/images/map/good.png" width="290" height="200" />
</view>
<view class="my-[30rpx] text-muted">
<text>没有数据哦~</text>
</view>
</view>
<!-- 定位提示弹窗 -->
<modal-popup
v-model:show="showLocationModal"
:title="errorTitle"
:content="errorContent"
@refresh=""
/>
<!-- 城市选择器 -->
<city-picker
v-model:show="showCityPicker"
v-model:location="currentLocation"
@update="getAddressList"
/>
</view>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { onLoad, onUnload } from '@dcloudio/uni-app'
import { useAppStore } from '@/stores/app'
import { locationState, useLocation } from '@/hooks/useLocation'
import { getNearbyLocation } from '@/api/store'
import CityPicker from './component/city-picker.vue'
import { useRoute, useRouter } from 'uniapp-router-next'
const route = useRoute()
const router = useRouter()
const appStore = useAppStore()
const { showLocationModal, errorTitle, errorContent, getLocationData, setLocationData } =
useLocation()
// 城市选择弹窗
const showCityPicker = ref(false)
//搜索关键词
const keyword = ref('')
//地址列表
const addressList = ref([])
// 当前选择城市
const currentLocation = ref(locationState)
// 创建map的上下文对象, 从而操控map组件
const mapCtx = uni.createMapContext('myMap')
//计算地址距离
const computeDistance = (distance: number) => {
let res = distance + 'm'
if (distance > 1000) {
res = (distance / 1000).toFixed(3) + 'km'
}
return res
}
const handleSearch = async () => {
uni.showLoading({
title: '搜索中...',
mask: true
})
await getAddressList()
uni.hideLoading()
}
// 滑动地图更新当前定位
const regionchange = (e: any) => {
// #ifdef WEB
if (e.detail.type == 'end' && (e.detail.causedBy == 'update' || e.detail.causedBy == 'drag')) {
mapCtx.getCenterLocation({
success: async (res: any) => {
currentLocation.value.latitude = res.latitude
currentLocation.value.longitude = res.longitude
getAddressList()
}
})
}
// #endif
// #ifdef MP-WEIXIN
if (e.type == 'end' && (e.causedBy == 'update' || e.causedBy == 'drag')) {
mapCtx.getCenterLocation({
success: async (res: any) => {
currentLocation.value.latitude = res.latitude
currentLocation.value.longitude = res.longitude
getAddressList()
}
})
}
// #endif
}
// 回到我的位置
const originLocation = async () => {
mapCtx.moveToLocation({
latitude: currentLocation.value.latitude as number,
longitude: currentLocation.value.longitude as number
})
await getLocationData()
getAddressList()
}
//选择地址
const choiceAddress = (res: any) => {
const latitude = res.location.lat
const longitude = res.location.lng
const info = {
name: currentLocation.value.name,
city_id: currentLocation.value.city_id,
latitude: latitude,
longitude: longitude,
address: res.address,
title: res.title
}
if (route.query.is_address) {
console.log('emit', info)
uni.$emit('city_select', info)
} else {
setLocationData(info)
}
uni.$emit('updateAddress')
router.navigateBack()
}
const getAddressList = async () => {
const data = await getNearbyLocation({
keyword: keyword.value,
latitude: currentLocation.value.latitude,
longitude: currentLocation.value.longitude
})
addressList.value = data.data
}
onLoad(async () => {
if (!currentLocation.value.city_id) {
await getLocationData()
await getAddressList()
} else {
await getAddressList()
}
})
</script>
<style lang="scss">
.city-pages {
padding: 24rpx;
.search {
padding: 15rpx 30rpx;
border-radius: 40rpx;
background-color: #ffffff;
&--city {
color: #333;
font-size: 28rpx;
}
&--search {
height: 50rpx;
font-size: 26rpx;
margin-left: 30rpx;
padding-left: 30rpx;
}
}
.map {
position: relative;
.iconImg {
position: absolute;
top: 46%;
left: 47%;
.img {
width: 50rpx;
height: 50rpx;
}
}
.origin {
position: absolute;
top: 10rpx;
right: 10rpx;
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background-color: rgba(255, 255, 255, 1);
text-align: center;
border: 1rpx solid rgba(187, 187, 187, 1);
.origin_icon {
position: relative;
top: 15%;
left: 30%;
width: 30rpx;
height: 30rpx;
}
}
}
.address {
background-color: #ffffff;
padding: 16rpx 24rpx;
border-radius: 20rpx;
.address-item {
padding-bottom: 30rpx;
margin: 20rpx 0;
border-bottom: 1px solid #f4f4f5;
}
.address-item:last-child {
border-bottom: none;
}
}
}
</style>

View File

@@ -0,0 +1,457 @@
<template>
<page-meta :page-style="$theme.pageStyle">
<!-- #ifndef H5 -->
<navigation-bar :front-color="$theme.navColor" :background-color="$theme.navBgColor" />
<!-- #endif -->
</page-meta>
<view class="coach-detail">
<!-- #ifndef H5 -->
<u-sticky h5-nav-height="0" bg-color="transparent">
<u-navbar
:is-back="true"
:is-fixed="true"
title="技师详情"
:immersive="true"
:border-bottom="false"
:title-bold="false"
:background="{ background: `rgba(256,256, 256, ${percent})` }"
:title-color="percent > 0.5 ? '#000' : !isEmpty ? '#000' : '#fff'"
:back-icon-color="percent > 0.5 ? '#000' : !isEmpty ? '#000' : '#fff'"
>
</u-navbar>
</u-sticky>
<!-- #endif -->
<view v-if="isEmpty">
<!-- 轮播图 -->
<l-swiper
:content="{
data: coachData.life_photo
}"
name="url"
height="750"
indicatorPos="bottomRight"
mode="number"
borderRadius="0"
/>
<!-- 商品信息 -->
<view class="coach-detail-info">
<view class="flex items-center bg-[#FEF4EB] px-[30rpx] py-[12rpx] rounded-t-lg" @click="goTocredential()">
<u-image mode="aspectFit" height="36" width="120" :src="anxingouLogo"></u-image>
<view class="text-[#CB9F5D] text-sm ml-4 tips">
<text>实名认证</text>
<text>资质认证</text>
<text>平台担保</text>
</view>
<u-icon color="#CB9F5D" class="ml-auto" size="28" name="arrow-right"></u-icon>
</view>
<view class="p-[24rpx] rounded-b-lg bg-white roun">
<view class="flex items-center justify-between">
<view class="text-xl font-bold">{{ coachData.name }}</view>
<view class="text-[#C38925]">最早可约{{ coachData.first_appoint }}</view>
</view>
<view class="text-info mt-4 introduct">
<text>{{ coachData.gender == 1 ? '男' : '女' }}</text>
<text>{{ coachData.age }}岁</text>
<text>{{ coachData.education }}</text>
<text>{{ coachData.nation }}</text>
<!-- <text>{{ coachData.shop_name }}</text> -->
</view>
<view class="text-xs mt-4 flex justify-between">
<view class="flex items-center">
<u-image height="24" width="24" :src="user"></u-image>
<view class="ml-1">
<text class="text-info">已服务</text>
<text class="text-primary">{{ coachData.order_num }}人</text>
</view>
</view>
<view class="flex items-center">
<u-image height="24" width="24" :src="favor"></u-image>
<view class="ml-1">
<text class="text-info">好评率</text>
<text class="text-primary">{{ coachData.good_comment }}</text>
</view>
</view>
<view class="flex items-center">
<u-image height="24" width="24" :src="distance"></u-image>
<view class="ml-1">
<text class="text-info">距离</text>
<text class="text-primary">{{ coachData.distance_desc }}</text>
</view>
</view>
</view>
<!-- 入驻商家信息 -->
<view v-if="Object.keys(coachData?.shop || {}).length" class="mt-[36rpx] bg-[#F6F7F8] py-[12rpx] pl-[12rpx] pr-[22rpx] flex items-center rounded-[20rpx]">
<u-image mode="aspectFill" height="92" width="92" :src="coachData?.shop?.logo" borderRadius="16" class="flex-none"></u-image>
<view class="w-full mx-[20rpx]">
<view class="text-base text-main mb-[8rpx] line-clamp-1">
{{ coachData?.shop?.name || ''}}
</view>
<view class="text-xs text-muted">
<span class="mr-[34rpx]">评分:{{ coachData?.shop?.good_comment || 0 }}</span>
<span>人均:¥{{ coachData?.shop?.consumption || 0 }}</span>
</view>
</view>
<view class="text-center">
<view class="text-base text-content">{{ coachData?.shop?.distance || 0 }}km</view>
<view class="text-xs text-muted">距离</view>
</view>
</view>
</view>
</view>
<view
v-if="coachData?.introduction"
class="bg-white rounded-[20rpx] p-[24rpx] mx-[30rpx] mt-[30rpx]"
>
<view class="text-lg font-medium">自我介绍</view>
<view class="mt-2 text-content">
{{ coachData.introduction }}
</view>
</view>
<view class="rounded-[20rpx] bg-white p-[24rpx] m-[30rpx]">
<tabs
:isScroll="false"
height="70"
bar-width="60"
font-size="32rpx"
:bold="true"
inactiveColor="#333"
:activeColor="$theme.primaryColor"
>
<tab name="服务项目">
<view class="mt-4" v-for="item in coachData.goods_lists" :key="item.id">
<project :data="item" :coach_id="coachId" />
</view>
</tab>
<tab name="用户评价">
<evaluate
v-for="item in coachData.comment_lists"
:index="item.id"
:data="item"
/>
</tab>
</tabs>
</view>
<view class="footer u-flex fixed">
<view class="flex-1">
<u-button type="primary" @click="handleCollection(coachData.is_collect)">
<u-icon
:name="coachData.is_collect ? 'star-fill' : 'star'"
color="#ffffff"
size="30"
class="mb-[1rpx]"
/>
<text class="text-base font-medium ml-1">加收藏</text>
</u-button>
</view>
</view>
</view>
<view v-else class="empty">
<u-empty
text="抱歉该技师不存在~"
:src="'/static/images/empty/collection.png'"
:icon-size="300"
color="#888888"
>
<template #bottom>
<view class="empty-bottom">
<button
class="bg-primary text-lg text-white leading-[80rpx] h-[80rpx]"
@click="router.reLaunch('/pages/index/index')"
>
去看看其它
</button>
</view>
</template>
</u-empty>
</view>
</view>
<!-- 返回顶部按钮 -->
<u-back-top
:scroll-top="scrollTop"
:top="100"
:customStyle="{
backgroundColor: '#FFF',
color: '#000',
boxShadow: '0px 3px 6px rgba(0, 0, 0, 0.1)'
}"
>
</u-back-top>
<!-- 定位提示弹窗 -->
<modal-popup
v-model:show="showLocationModal"
:title="errorTitle"
:content="errorContent"
@refresh="handleRefresh"
/>
</template>
<script lang="ts">
export default {
options: {
styleIsolation: 'shared'
}
}
</script>
<script setup lang="ts">
import { ref, computed, reactive, shallowRef, unref, nextTick, handleError } from 'vue'
import { onLoad, onPageScroll, onShow } from '@dcloudio/uni-app'
import { apiCoachDetail } from '@/api/coach'
import { useUserStore } from '@/stores/user'
import { useAppStore } from '@/stores/app'
import { useRouter } from 'uniapp-router-next'
import { locationState, useLocation } from '@/hooks/useLocation'
import anxingouLogo from '@/bundle/static/images/anxingouLogo.png'
import user from '@/bundle/static/icon/user.png'
import favor from '@/bundle/static/icon/favor.png'
import distance from '@/bundle/static/icon/distance.png'
import project from '@/components/project-card/index.vue'
import evaluate from '@/components/evaluate-card/index.vue'
import { putCollect } from '@/api/store'
const {
showLocationModal,
errorTitle,
errorContent,
getLocationData
} = useLocation()
type CoachType = {
age: number
comment_num: number
distance: number
distance_desc: string
education: string
first_appoint: string
gender: number
good_comment: string
id: number
introduction: string
life_photo: string[]
is_collect: number
name: string
nation: string
order_num: number
shop_name: string
skill_id: number
skill_name: string
work_photo: string
work_status: number
goods_lists: any
comment_lists: any
health_certificate: string
certification: string
id_card: string
shop: any
}
const router = useRouter()
const appStore = useAppStore()
const userStore = useUserStore()
const coachData = reactive<CoachType>({
age: -1,
comment_num: -1,
distance: -1,
distance_desc: '',
education: '',
first_appoint: '',
gender: -1,
good_comment: '',
id: -1,
introduction: '',
is_collect: 0,
life_photo: [],
name: '',
nation: '',
order_num: -1,
shop_name: '',
skill_id: -1,
skill_name: '',
work_photo: '',
work_status: -1,
goods_lists: [],
comment_lists: [],
health_certificate: '',
certification: '',
id_card: '',
shop: {}
})
const isLogin = computed(() => userStore.token)
const coachId = ref<number | string>('')
const scrollTop = ref<number>(0)
const percent = ref<number>(0)
const isEmpty = ref<boolean>(true)
// 去到认证页面
const goTocredential = () => {
router.navigateTo({
path: '/bundle/pages/master_worker_credential/index',
query: coachData
})
}
/**
* @description 获取商品详情
*/
const initializeCoachDetails = async (): Promise<void> => {
try {
const res: CoachType = await apiCoachDetail({
id: coachId.value,
longitude: locationState.longitude,
latitude: locationState.latitude,
city_id: locationState.city_id
})
if (res.life_photo) {
res.life_photo = res.life_photo.map((item: string) => {
return { url: item }
})
}
// @ts-ignore
Reflect.ownKeys(coachData).map((key: string) => (coachData[key] = res?.[key]))
console.log(coachData)
} catch (error) {
console.log(error)
isEmpty.value = false
}
}
// 收藏
const handleCollection = async (is_collect: number): Promise<void> => {
if (!isLogin.value) {
router.navigateTo('/pages/login/login')
return
}
try {
await putCollect({
id: unref(coachId) as number,
type: 1
})
if (is_collect) {
uni.$u.toast('取消收藏成功')
} else {
uni.$u.toast('收藏成功')
}
await initializeCoachDetails()
} catch (error) {
//TODO handle the exception
console.log('收藏请求错误', error)
}
}
// 刷新
const handleRefresh = async () => {
try {
await getLocationData()
} catch (error: any) {
// uni.$u.toast('获取定位失败,可能影响距离计算')
}
await initializeCoachDetails()
}
onLoad(async (options) => {
if (!locationState.latitude || !locationState.longitude) {
try {
await getLocationData()
} catch (error: any) {
// uni.$u.toast('获取定位失败,可能影响距离计算')
}
}
coachId.value = options?.id || 0
nextTick(() => {
initializeCoachDetails()
})
})
onPageScroll((event: any) => {
scrollTop.value = event.scrollTop
const top = uni.upx2px(100)
percent.value = event.scrollTop / top > 1 ? 1 : event.scrollTop / top
})
</script>
<style lang="scss" scoped>
.coach-detail {
padding-bottom: 200rpx;
:deep(.u-swiper-indicator) {
bottom: 100rpx !important;
}
.coach-detail-info {
position: relative;
z-index: 10;
margin: -70rpx 30rpx 0 30rpx;
border-radius: 20rpx;
.introduct {
text {
font-size: 24rpx;
border-radius: 8rpx;
margin-right: 14rpx;
padding: 4rpx 12rpx;
background-color: #f6f7f8;
}
}
.tips {
text {
position: relative;
margin-right: 20rpx;
&:not(:first-child) {
::before {
content: '·';
position: absolute;
left: -15rpx;
top: 50%;
transform: translateY(-50%);
}
}
}
}
}
}
// 收藏图标
.collection {
width: 44rpx;
height: 44rpx;
}
// 底部按钮
.footer {
left: 0;
bottom: 0;
width: 100%;
z-index: 10;
position: fixed;
padding: 20rpx 30rpx;
background-color: #ffffff;
box-shadow: 0 -4px 48px 0 #141a231f;
padding-bottom: calc(env(safe-area-inset-bottom) + 20rpx);
}
// 服务下架或不存在时
.empty {
padding-top: 200rpx;
.empty-bottom {
width: 90vw;
margin-top: 130rpx;
}
}
</style>

View File

@@ -0,0 +1,131 @@
<template>
<page-meta :page-style="$theme.pageStyle">
<!-- #ifndef H5 -->
<navigation-bar :front-color="$theme.navColor" :background-color="$theme.navBgColor" />
<!-- #endif -->
</page-meta>
<view class="collect-list flex flex-col min-h-0 h-full">
<u-tabs
:list="tabList"
bg-color="#ffffff"
height="80"
:active-color="$theme.primaryColor"
inactive-color="#666666"
:is-scroll="false"
@change="change"
:current="current - 1"
></u-tabs>
<view class="flex-1">
<z-paging
auto-show-back-to-top
ref="paging"
v-model="dataList"
:fixed="false"
:empty-view-img="EmptyOrder"
height="100%"
@query="queryList"
>
<template v-for="item in dataList" :key="item.id值">
<view v-if="current == 1" class="">
<!-- 技师卡片 -->
<coach-card :data="item" type="1" @confirm="navigateToCoach"></coach-card>
</view>
<view v-if="current == 2" class="mx-[30rpx] mt-[30rpx] p-[12rpx] rounded-[20rpx] bg-white">
<!-- 项目卡片 -->
<project-card :data="item"></project-card>
</view>
<view v-if="current == 3" class="">
<shopLists :data="item" type="1"></shopLists>
</view>
</template>
</z-paging>
</view>
</view>
</template>
<script lang="ts" setup>
import { nextTick, ref, shallowRef } from 'vue'
import { apiCollectLists } from '@/api/user'
import EmptyOrder from '@/static/images/empty/order.png'
import { useRoute, useRouter } from 'uniapp-router-next'
import { locationState } from '@/hooks/useLocation'
import { onLoad } from '@dcloudio/uni-app'
import CoachCard from '@/components/master-worker-card/index.vue'
import ProjectCard from '@/components/project-card/index.vue'
import shopLists from '@/components/shop-lists/index.vue'
const route = useRoute()
const router = useRouter()
const dataList = ref<any>([])
const paging = shallowRef<any>(null)
const tabList = ref<any>([
{
name: '技师',
type: 1
},
{
name: '项目',
type: 2
},
{
name: '店铺',
type: 3
}
])
const current = ref<number>(1)
const change = (index: any) => {
current.value = Number(index + 1)
console.log(current.value)
paging.value?.reload()
}
// 初始化收藏
const queryList = async (page_no: number, page_size: number) => {
try {
console.log(456789, current.value)
const { lists } = await apiCollectLists({
type: current.value,
page_no: page_no || 1,
page_size: page_size || 10,
longitude: locationState.longitude,
latitude: locationState.latitude
})
paging.value.complete(lists)
} catch (e) {
console.log('报错=>', e)
paging.value.complete(false)
}
}
const navigateToCoach = (id: number) => {
router.navigateTo({
path: '/bundle/pages/coach_details/index',
query: {
id: id
}
})
}
onLoad(() => {
if (Number(route.query.type)) {
current.value = Number(route.query.type)
}
})
</script>
<style lang="scss">
.collect {
border-radius: 14rpx;
background-color: white;
margin: 20rpx 20rpx 0 20rpx;
padding: 30rpx;
}
.empty {
padding-top: 300rpx;
}
</style>

View File

@@ -0,0 +1,254 @@
<template>
<view class="service pt-20">
<view class="service-contain">
<view class="header-image">
<u-image
:src="getImageUrl('/resource/image/shopapi/default/service.png')"
width="100rpx"
height="100rpx"
shape="circle"
style="margin-top: -50rpx"
class="circle"
></u-image>
</view>
<!-- way==1 二维码客服 -->
<template v-if="serviceData.way == 1">
<view class="lg mt-[40rpx] flex justify-center" v-if="serviceData.remarks">
{{ serviceData.remarks }}
</view>
<view class="code flex justify-center">
<u-image
:src="serviceData.qr_code"
width="320rpx"
height="320rpx"
border-radius="20"
></u-image>
</view>
<view
class="mt-[20rpx] mb-[20rpx] xs muted flex justify-center"
v-if="serviceData.phone"
>
客服电话{{ serviceData.phone }}
</view>
<view
class="xs mt-[20rpx] muted mb-[20rpx] flex justify-center"
v-if="serviceData.business_time"
>
服务时间: {{ serviceData.business_time }}
</view>
</template>
<!-- logo -->
<template v-if="serviceData.way != 1">
<view class="flex lg justify-center mt-[40rpx]">
{{ appStore.config.shop_name }}
</view>
<view class="code flex justify-center">
<u-image
:src="appStore.config.shop_logo"
width="320rpx"
height="320rpx"
border-radius="20"
></u-image>
</view>
</template>
<!-- way==2 电话客服 -->
<template v-if="serviceData.way == 2">
<view class="mt-[40rpx] flex justify-center" v-if="serviceData.phone">
<view style="text-align: center">
<view> 拨打客服热线 </view>
<view style="text-decoration: underline" @click="handleCall">
{{ serviceData.service_phone }}
</view>
</view>
</view>
</template>
</view>
<view class="pt-[20rpx]" v-if="serviceData.way == 4">
<!-- #ifdef MP-WEIXIN -->
<button
open-type="contact"
class="copy-btn icon-item text-white rounded-[20rpx] flex justify-center items-center"
style="line-height: 88rpx"
>
<text style="line-height: 32rpx" class="text-[28rpx]">联系微信小程序客服</text>
</button>
<!-- #endif -->
</view>
<view class="pt-[20rpx]" v-else>
<view
class="rounded-[20rpx] copy-btn flex justify-center items-center text-white lg"
v-if="serviceData.way == 1"
@click="saveImageQr"
>
<text class="">保存二维码</text>
</view>
<view
class="rounded-[20rpx] copy-btn flex justify-center items-center text-white lg"
v-if="serviceData.way == 2"
@click="handleCall"
>
<text class="">拨打电话</text>
</view>
<view
class="rounded-[20rpx] copy-btn flex justify-center items-center text-white lg"
v-if="serviceData.way == 3"
@click="handleService"
>
<text class="">联系企业微信客服</text>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { toast } from '@/utils/util'
import { getKefuConfig } from '@/api/user'
import { getClient } from '@/utils/client'
import { serviceEnum } from '@/enums/appEnums'
import { useAppStore } from '@/stores/app'
interface serviceDataObj {
business_time: string
enterprise_id: string
kefu_link: string
name: string
phone: string
qr_code: string
remarks: string
service_phone: string
way: string | number
}
const { getImageUrl } = useAppStore()
const appStore = useAppStore()
const serviceData = ref<serviceDataObj>({
business_time: '',
enterprise_id: '',
kefu_link: '',
name: '',
phone: '',
qr_code: '',
remarks: '',
service_phone: '',
way: ''
})
// 获取客服信息
const getContactService = async (): Promise<void> => {
const res = await getKefuConfig()
serviceData.value = res[serviceEnum[getClient()]]
}
// 保存二维码
const saveImageQr = async (): Promise<void> => {
//#ifdef H5
toast('长按图片保存')
//#endif
//#ifndef H5
try {
const res = await uni.getImageInfo({ src: serviceData.value.qr_code })
try {
await uni.saveImageToPhotosAlbum({ filePath: res.path })
toast('保存成功')
} catch (e) {
const modelRes = await uni.showModal({
title: '图片保存失败',
content: '请确认是否已开启授权'
})
if (modelRes.confirm) uni.openSetting()
}
} catch (err) {
toast('请在小程序后台配置downloadFile')
}
//#endif
}
//拨打电话
const handleCall = () => {
if (!serviceData.value.service_phone) {
toast('请在后台配置客服电话号码')
return
}
uni.makePhoneCall({
phoneNumber: serviceData.value.service_phone,
success(res) {
console.log(res)
},
fail(err) {
console.log(err)
}
})
}
const handleService = () => {
// #ifdef MP-WEIXIN
wx.openCustomerServiceChat({
extInfo: { url: serviceData.value.kefu_link },
corpId: serviceData.value.enterprise_id,
success(res: any) {
console.log(res)
},
fail(err: any) {
console.log(err)
toast('请在后台配置企业微信客服')
}
})
// #endif
// #ifdef H5
if (!serviceData.value.kefu_link) {
toast('请在后台配置企业微信客服')
return
}
window.open(serviceData.value.kefu_link, '_self')
// #endif
}
getContactService()
</script>
<style lang="scss">
.header-image {
display: flex;
justify-content: center;
.circle {
border-radius: 50%;
border: 6rpx solid #ffffff;
}
}
.service {
width: 100vw;
height: 100vh;
background-image: url(../../static/images/service_bg.jpg);
background-size: 100% auto;
&-contain {
width: 620rpx;
// height: 750rpx;
margin: auto;
margin-bottom: 40rpx;
padding-bottom: 80rpx;
border-radius: 10px;
background-color: #ffffff;
.code {
border-radius: 20rpx;
padding-top: 60rpx;
}
.phone {
padding: 0 20rpx;
text-decoration: underline;
}
}
}
.copy-btn {
margin: 0 80rpx;
height: 88rpx;
// @include background_linear(90deg, 0, 100%);
background: #000722;
}
</style>

View File

@@ -0,0 +1,33 @@
<template>
<view class="card">
<evaluate-card :data="commentData" :border="false"> </evaluate-card>
</view>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { apiEvaluateGoodsDetail } from '@/api/goods'
import EvaluateCard from '@/components/evaluate-card/index.vue'
const commentId = ref<number>(0)
const commentData = ref<any>([])
const getData = async () => {
commentData.value = await apiEvaluateGoodsDetail({ id: commentId.value })
}
onLoad((options: any) => {
commentId.value = options.id || 0
getData()
})
</script>
<style lang="scss">
.card {
border-radius: 20rpx;
background-color: white;
margin: 20rpx 20rpx 0 20rpx;
padding: 10rpx 30rpx 30rpx 30rpx;
}
</style>

View File

@@ -0,0 +1,118 @@
<template>
<view class="card">
<view class="card--header flex justify-between col-start">
<view class="flex">
<u-image :src="user.avatar" width="80" height="80" border-radius="50%"></u-image>
<view class="ml-[20rpx]">
<view class="text-base normal font-medium">{{ user.nickname }}</view>
<view class="text-muted text-xs mt-[10rpx]">{{ create_time }}</view>
</view>
</view>
<view class="flex">
<!-- <u-rate
:count="5"
v-model="service_comment"
size="28"
inactive-icon="star-fill"
active-color="#ff9600"
></u-rate> -->
<!-- <view class="ml-[20rpx] lighter text-xs">
<text v-if="service_comment == 5">非常好</text>
<text v-if="service_comment == 4"></text>
<text v-if="service_comment == 3">一般</text>
<text v-if="service_comment == 2"></text>
<text v-if="service_comment == 1">非常差</text>
</view> -->
</view>
</view>
<view class="card--main">
<view class="content">
{{ comment }}
</view>
<view class="flex flex-wrap">
<block v-for="(item3, index) in goods_comment_image" :key="index">
<view
class="mt-[10rpx]"
:class="{ 'mr-[10rpx]': (index + 1) % 4 != 0 }"
@click.stop="previewImage(index)"
>
<u-image
:src="item3.uri"
width="150"
height="150"
border-radius="14rpx"
></u-image>
</view>
</block>
</view>
<view class="reply normal text-base mt-[20rpx]" v-if="reply">
<text class="font-medium">商家回复: </text>
<text class="text-[#909399]">
{{ reply }}
</text>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
const props = withDefaults(
defineProps<{
goods_id: string | number
comment: string | null
goods_comment_image: string | null
reply: string | null
create_time: string | null
service_comment: string | number | any
user: any
}>(),
{
goods_id: '',
comment: '',
goods_comment_image: '',
reply: '',
create_time: '',
service_comment: '',
user: {
avatar: '',
nickname: ''
}
}
)
// 查看评价图片
const previewImage = (current: number) => {
uni.previewImage({
current,
urls: props.goods_comment_image.map((el) => el.uri)
})
}
</script>
<style lang="scss" scoped>
.card {
border-radius: 20rpx;
background-color: white;
margin: 20rpx 20rpx 0 20rpx;
padding: 30rpx;
&--header {
width: 100%;
}
&--main {
.content {
padding: 20rpx 0;
font-size: 28rpx;
color: #222222;
}
.reply {
word-break: break-all;
padding: 24rpx 20rpx;
background-color: #f6f6f6;
border-radius: 10rpx;
}
}
}
</style>

View File

@@ -0,0 +1,77 @@
<template>
<z-paging
auto-show-back-to-top
:auto="i == index"
ref="paging"
v-model="dataList"
:data-key="i"
@query="queryList"
:fixed="false"
height="100%"
>
<block v-for="(item, index) in dataList" :key="index">
<Card
:comment="item.comment"
:goods_comment_image="item.goods_comment_image"
:reply="item.reply"
:create_time="item.create_time"
:goods_id="item.goods_id"
:service_comment="item.service_comment"
:user="item.user"
></Card>
</block>
</z-paging>
</template>
<script lang="ts" setup>
import { ref, watch, nextTick, shallowRef, unref } from 'vue'
import Card from './card.vue'
import { apiEvaluateGoodsLists } from '@/api/goods'
const paging = shallowRef<any>(null)
const dataList = ref<any>([])
const isFirst = ref<boolean>(true)
const props = withDefaults(
defineProps<{
goods_id: number
i: number
index: number
cid: number
}>(),
{
goods_id: 0,
cid: 0
}
)
watch(
() => props.index,
async () => {
await nextTick()
if (props.i == props.index && unref(isFirst)) {
isFirst.value = false
paging.value?.reload()
}
},
{ immediate: true }
)
const queryList = async (pageNo: number, pageSize: number) => {
try {
const { lists } = await apiEvaluateGoodsLists({
goods_id: props.goods_id,
pageNo,
pageSize,
id: props.cid
})
paging.value.complete(lists)
} catch (e) {
console.log('报错=>', e)
//TODO handle the exception
paging.value.complete(false)
}
}
</script>
<style scoped></style>

View File

@@ -0,0 +1,188 @@
<template>
<page-meta :page-style="$theme.pageStyle">
<!-- #ifndef H5 -->
<navigation-bar
:front-color="$theme.navColor"
:background-color="$theme.navBgColor"
/>
<!-- #endif -->
</page-meta>
<view class="goods-comment flex flex-col min-h-0 h-full">
<u-sticky h5-nav-height="0" bg-color="transparent">
<u-navbar
:is-back="true"
:is-fixed="true"
title="用户评价"
:border-bottom="false"
:title-bold="false"
:background="{ background: `rgba(256,256, 256, 0)` }"
:title-color="'#000'"
>
</u-navbar>
</u-sticky>
<view class="flex flex-wrap gap-y-2 gap-x-2 p-[30rpx]">
<view
v-for="(item, index) in tabList"
:key="item.id"
class="px-[26rpx] py-[8rpx] rounded-full"
:class="{
'bg-primary-light-9 text-primary': current == index,
'bg-white text-main': current != index
}"
@click="handleChange(index)"
>
<u-icon size="28" name="photo" v-if="item.name == '有图'"></u-icon>
<u-icon size="28" name="thumb-up" v-if="item.name == '好评'"></u-icon>
{{ item.name }}
{{ item.count }}
</view>
</view>
<view class="flex-1">
<z-paging
auto-show-back-to-top
:auto="true"
ref="paging"
v-model="dataList"
:fixed="false"
height="100%"
:empty-view-img="EmptyEvaluate"
@query="queryList"
>
<view
v-for="(item, index) in dataList"
:key="index"
class="m-[24rpx] px-[24rpx] py-[30rpx] bg-white rounded-[20rpx]"
>
<view class="flex items-center w-full">
<u-image
:src="item.user.avatar"
width="80"
height="80"
borderRadius="50%"
class="flex-none"
></u-image>
<view class="flex flex-col justify-between ml-2 w-full">
<view class="text-base font-medium w-full">
{{ item.user.nickname }}
</view>
<view class="mt-1 text-muted text-xs">
{{ item.create_time }}
</view>
</view>
<u-rate
:count="5"
v-model="item.service_comment"
:disabled="true"
class="ml-auto"
inactive-color="#eaeaeb"
inactiveIcon="star-fill"
active-color="#d86930"
></u-rate>
</view>
<view class="mt-3 break-words text-content"> {{ item.comment }}</view>
<view class="mt-3 grid gap-2 grid-cols-4">
<view
v-for="(commentImage, index) in item.goods_comment_image"
:key="index"
class="mt-[10rpx]"
:class="{ 'mr-[10rpx]': (index + 1) % 4 != 0 }"
@click.stop="previewImage(item.goods_comment_image, index)"
>
<u-image
:src="commentImage.uri"
width="150"
height="150"
border-radius="14rpx"
></u-image>
</view>
</view>
</view>
</z-paging>
</view>
</view>
</template>
<script lang="ts" setup>
import {ref, computed, shallowRef} from 'vue'
import {onLoad} from '@dcloudio/uni-app'
import {apiEvaluateGoodsCategory, apiEvaluateGoodsLists} from '@/api/goods'
import {useUserStore} from '@/stores/user'
import {useRoute} from "uniapp-router-next";
import EmptyEvaluate from '@/static/images/empty/evaluate.png'
const route = useRoute()
// 是否登录
const userStore = useUserStore()
const isLogin = computed(() => userStore.token)
const tabList = ref<any>([])
const current = ref<number>(0)
const paging = shallowRef<any>(null)
const dataList = ref<any>([])
const handleChange = (index: number) => {
current.value = Number(index)
paging.value.reload()
}
const getData = async () => {
const data = await apiEvaluateGoodsCategory({
goods_id: route.query.id
})
tabList.value = [...data.comment]
paging.value.reload()
}
const queryList = async (page_no: number, page_size: number) => {
try {
const {lists} = await apiEvaluateGoodsLists({
goods_id: route.query.id,
page_no: page_no || 1,
page_size: page_size || 10,
id: tabList.value[current.value].id
})
paging.value.complete(lists)
} catch (e) {
console.log('报错=>', e)
//TODO handle the exception
paging.value.complete(false)
}
}
const previewImage = (url: any, index: number | string) => {
uni.previewImage({
current: index,
urls: url.map((item: any) => item.uri)
})
}
onLoad(() => {
getData()
})
</script>
<style lang="scss">
.container {
display: flex;
height: 100vh;
overflow: hidden;
flex-direction: column;
}
.main {
flex: 1;
min-height: 0;
overflow: scroll;
swiper {
height: 100%;
}
}
.List {
height: calc(100vh - 86px - env(safe-area-inset-bottom));
}
</style>

View File

@@ -0,0 +1,198 @@
<template>
<z-paging
auto-show-back-to-top
:auto="i == index"
ref="paging"
v-model="dataList"
:data-key="i"
@query="queryList"
:fixed="false"
height="100%"
>
<block v-for="(item2, index) in dataList" :key="index">
<view class="card">
<view class="u-flex justify-between">
<view class="text-[#909399]">上门服务</view>
<view>{{ item2.is_comment ? '已评价' : '待评价' }}</view>
</view>
<view class="u-flex justify-between mt-[20rpx] mb-[20rpx]">
<view class="font-bold text-3xl">
<text>{{ item2.appoint_date }}</text>
<text class="text-primary ml-[20rpx]">
{{ item2.appoint_time }}
</text>
</view>
<view
class="u-flex"
style="background-color: #f6f7f8; padding: 6rpx 10rpx; border-radius: 40rpx"
>
<u-image
:src="item2.coach_info.work_photo"
width="40"
height="40"
shape="circle"
></u-image>
<view class="ml-[16rpx]">
{{ item2.coach_info.name }}
</view>
</view>
</view>
<view class="goods flex">
<u-image
:src="item2.goods_snap.image"
width="136"
height="136"
border-radius="20rpx"
></u-image>
<view class="flex-1 ml-[20rpx]">
<view class="text-base font-medium text-main line-clamp-1">
{{ item2.goods_snap.name }}
</view>
<view class="flex justify-between items-center flex-1 mt-1">
<view class="text-xs text-muted">
服务时间{{ item2.goods_snap.duration }}分钟
</view>
<view class="text-muted"> x{{ item2.goods_snap.goods_num || 1 }} </view>
</view>
<view class="mt-1">
<price
:content="item2.goods_snap.price"
main-size="32rpx"
minor-size="20rpx"
font-weight="900"
color="#E86016"
></price>
</view>
</view>
</view>
<view class="footer flex justify-between items-center">
<view class="text-primary font-bold text-2xl">¥{{ item2.goods_price }}</view>
<view>
<u-button
v-if="item2.is_comment == 0"
type="primary"
size="medium"
@click="
router.navigateTo(
`/bundle/pages/evaluate_submit/index?order_goods_id=${item2.id}`
)
"
>
去评价
</u-button>
<u-button
v-else
type="primary"
size="medium"
@click="
router.navigateTo(
`/bundle/pages/evaluate_detail/index?id=${item2.id}`
)
"
>
查看评价
</u-button>
</view>
</view>
</view>
</block>
</z-paging>
</template>
<script lang="ts" setup>
import { ref, watch, nextTick, shallowRef, unref } from 'vue'
import { apiEvaluateLists } from '@/api/user'
import { useRouter } from 'uniapp-router-next'
import { onShow } from '@dcloudio/uni-app'
const props = withDefaults(
defineProps<{
type: number
count: number
i: number
index: number
}>(),
{
type: 0,
count: 0
}
)
const router = useRouter()
const paging = shallowRef<any>(null)
const dataList = ref<any>([])
const isFirst = ref<boolean>(true)
watch(
() => props.index,
async () => {
await nextTick()
if (props.i == props.index && unref(isFirst)) {
// isFirst.value = false
paging.value?.reload()
}
},
{ immediate: true, deep: true }
)
const queryList = async (page_no: number, page_size: number) => {
try {
const { lists } = await apiEvaluateLists({
type: props.type,
page_no,
page_size
})
paging.value.complete(lists)
} catch (e) {
console.log('报错=>', e)
paging.value.complete(false)
}
}
onShow(async () => {
// 已评价onShow 不刷新
if (Number(props.index) !== 1) {
await nextTick()
paging.value?.reload()
}
})
</script>
<style scoped lang="scss">
.card {
border-radius: 20rpx;
background-color: white;
margin: 20rpx 20rpx 0 20rpx;
padding: 30rpx 24rpx;
.goods {
padding: 20rpx;
border-radius: 20rpx;
background-color: #f7f7f7;
.tag {
color: #909399;
font-size: 20rpx;
padding: 4rpx 10rpx;
background-color: #ffffff;
border-radius: 4rpx;
display: inline-block;
margin-right: 10rpx;
margin-top: 10rpx;
}
.tag:last-child {
margin-right: 0;
}
}
.footer {
border-top: 1px solid #f7f7f7;
margin-top: 30rpx;
padding: 20rpx 0 0;
}
}
</style>

View File

@@ -0,0 +1,474 @@
<template>
<view class="tabs">
<u-sticky :enable="isFixed" :bg-color="stickyBgColor" :offset-top="top" :h5-nav-height="0">
<view
:id="id"
:style="{
background: bgColor
}"
>
<scroll-view
:style="{ height: height + 'rpx' }"
scroll-x
class="scroll-view"
:scroll-left="scrollLeft"
scroll-with-animation
>
<view class="scroll-box" :class="{ 'tabs-scorll-flex': !isScroll }">
<view
class="tab-item line1"
:id="'tab-item-' + index"
v-for="(item, index) in list"
:key="index"
@tap="clickTab(index)"
:style="[tabItemStyle(index)]"
>
<u-badge
:count="item[count] || item['dot'] || 0"
:offset="offset"
size="mini"
></u-badge>
{{ item[name] || item['name'] }}
</view>
<view v-if="showBar" class="tab-bar" :style="[tabBarStyle]"></view>
</view>
</scroll-view>
</view>
</u-sticky>
<view
class="tab-content"
@touchstart="onTouchStart"
@touchmove="onTouchMove"
@touchcancel="onTouchEnd"
@touchend="onTouchEnd"
>
<!-- <view class="tab-track" :class="{'tab-animated': animated}" :style="[trackStyle]"> -->
<view>
<slot v-if="isLogin && auth"></slot>
<u-empty
v-else
:src="'/static/images/empty/order.png'"
text="您还没有登录~"
mode="data"
:icon-size="300"
margin-top="300"
color="#888888"
>
<template #bottom>
<view class="mt-4">
<u-button
shape="circle"
@click="goPage('/pages/login/login')"
:ripple="true"
:hair-line="false"
type="info"
>
去登录</u-button
>
</view>
</template>
</u-empty>
</view>
<!-- </view> -->
</view>
</view>
</template>
<script lang="ts" setup>
import { getRect } from '@/utils/util'
import {
ref,
reactive,
computed,
watch,
provide,
nextTick,
onMounted,
getCurrentInstance
} from 'vue'
import { useTouch } from '@/hooks/useTouch'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
// 是否登录
const isLogin = computed(() => userStore.token)
// Touch 钩子
const { touch, resetTouchStatus, touchStart, touchMove } = useTouch()
const emit = defineEmits<{
(event: 'change', value: number): void
}>()
const props = withDefaults(
defineProps<{
isScroll?: boolean // 导航菜单是否需要滚动如只有2或者3个的时候就不需要滚动了此时使用flex平分tab的宽度
current?: number | string // 当前活动tab的索引
height?: number | string // 导航栏的高度和行高
fontSize?: number | string // 字体大小
duration?: number | string // 过渡动画时长, 单位ms
activeColor?: number | string // 选中项的主题颜色
inactiveColor?: number | string // 未选中项的颜色
barWidth?: number | string // 菜单底部移动的bar的宽度单位rpx
barHeight?: number // 移动bar的高度
gutter?: number | string // 单个tab的左或有内边距左右相同
bgColor?: number | string // 导航栏的背景颜色
name?: string // 读取传入的数组对象的属性(tab名称)
count?: string // 读取传入的数组对象的属性(徽标数)
offset?: number[] // 徽标数位置偏移
bold?: boolean // 活动tab字体是否加粗
activeItemStyle?: any // 当前活动tab item的样式
showBar?: boolean // 是否显示底部的滑块
barStyle?: any // 底部滑块的自定义样式
itemWidth?: string // 标签的宽度
isFixed?: boolean // 吸顶是否固定
top?: number | string // 吸顶顶部距离
stickyBgColor?: string // 吸顶颜色
swipeable: boolean // 是否允许滑动切换
// animated: boolean // 切换动画
auth?: boolean // 权限登录验证
}>(),
{
isScroll: true,
current: 0,
height: 80,
fontSize: 28,
duration: 0.3,
activeColor: '#2073F4',
inactiveColor: '#333',
barWidth: 40,
barHeight: 4,
gutter: 30,
bgColor: '#FFFFFF',
name: 'name',
count: 'count',
offset: [5, 20],
bold: true,
activeItemStyle: {},
showBar: true,
barStyle: {},
itemWidth: 'auto',
isFixed: false,
top: 0,
stickyBgColor: '#FFFFFF',
swipeable: true,
// animated: true
auth: false
}
)
const list = ref<any>([])
const childrens = ref<any>([])
const scrollLeft = ref<number>(0) // 滚动scroll-view的左边滚动距离
const tabQueryInfo = ref<any>([]) // 存放对tab菜单查询后的节点信息
const componentWidth = ref<number>(0) // 屏幕宽度单位为px
const scrollBarLeft = ref<number>(0) // 移动bar需要通过translateX()移动的距离
const parentLeft = ref<number>(0) // 父元素(tabs组件)到屏幕左边的距离
const id = ref<string>('cu-tab') // id值
const currentIndex = ref<any>(props.current)
const barFirstTimeMove = ref<boolean>(true) // 滑块第一次移动时(页面刚生成时),无需动画,否则给人怪异的感觉
const swiping = ref<boolean>(false)
//@ts-ignore
const ctx = getCurrentInstance()
// 监听tab的变化重新计算tab菜单的布局信息因为实际使用中菜单可能是通过
// 后台获取的如新闻app顶部的菜单获取返回需要一定时间所以list变化时重新获取布局信息
watch(
() => list.value,
async (n, o) => {
// list变动时重制内部索引否则可能导致超出数组边界的情况
if (!barFirstTimeMove.value && n.length !== o.length) {
currentIndex.value = 0
}
// 用$nextTick等待视图更新完毕后再计算tab的局部信息否则可能因为tab还没生成就获取就会有问题
await nextTick()
init()
}
)
watch(
() => props.current,
(nVal, oVal) => {
// 视图更新后再执行移动操作、
nextTick(() => {
currentIndex.value = nVal
scrollByIndex()
})
},
{ immediate: true }
)
// 移动bar的样式
const tabBarStyle = computed(() => {
const style = {
width: props.barWidth + 'rpx',
transform: `translate(${scrollBarLeft.value}px, -100%)`,
// 滑块在页面渲染后第一次滑动时,无需动画效果
'transition-duration': `${barFirstTimeMove.value ? 0 : props.duration}s`,
'background-color': props.activeColor,
height: props.barHeight + 'rpx',
opacity: barFirstTimeMove.value ? 0 : 1,
// 设置一个很大的值,它会自动取能用的最大值,不用高度的一半,是因为高度可能是单数,会有小数出现
'border-radius': `${props.barHeight / 2}px`
}
Object.assign(style, props.barStyle)
return style
})
// tab的样式
const tabItemStyle = computed(() => {
return (index) => {
let style: any = {
height: props.height + 'rpx',
'line-height': props.height + 'rpx',
'font-size': props.fontSize + 'rpx',
padding: props.isScroll ? `0 ${props.gutter}rpx` : '',
flex: props.isScroll ? 'auto' : '1',
width: `${props.itemWidth}rpx`
}
// 字体加粗
if (index == currentIndex.value && props.bold) style.fontWeight = 'bold'
if (index == currentIndex.value) {
style.color = props.activeColor
// 给选中的tab item添加外部自定义的样式
style = Object.assign(style, props.activeItemStyle)
} else {
style.color = props.inactiveColor
}
return style
}
})
// const trackStyle = computed(() => {
// if (!props.animated) return ''
// return {
// left: -100 * currentIndex.value + '%',
// 'transition-duration': props.duration + 's',
// '-webkit-transition-duration': props.duration + 's',
// }
// })
// 跳转页面
const goPage = (url: any) => {
uni.navigateTo({ url: url })
}
const updateTabs = () => {
list.value = childrens.value.map((item) => {
const { name, dot, active, inited } = item.event
const { updateRender } = item
return {
name,
dot,
active,
inited,
updateRender
}
})
// nextTick(() => {
// init()
// })
}
// 设置一个init方法方便多处调用
const init = async () => {
// 获取tabs组件的尺寸信息
const tabRect = await getRect('#' + id.value, false, ctx)
// tabs组件距离屏幕左边的宽度
parentLeft.value = tabRect.left
// tabs组件的宽度
componentWidth.value = tabRect.width
getTabRect()
}
// 点击某一个tab菜单
const clickTab = (index) => {
// 点击当前活动tab不触发事件
if (index == currentIndex.value) return
nextTick(() => {
currentIndex.value = index
scrollByIndex()
})
// 发送事件给父组件
emit('change', index)
}
// 查询tab的布局信息
const getTabRect = () => {
// 创建节点查询
const query: any = uni.createSelectorQuery().in(ctx)
// 历遍所有tab这里是执行了查询最终使用exec()会一次性返回查询的数组结果
for (let i = 0; i < list.value.length; i++) {
// 只要size和rect两个参数
query.select(`#tab-item-${i}`).fields({
size: true,
rect: true
})
}
// 执行查询,一次性获取多个结果
query.exec((res) => {
tabQueryInfo.value = res
// 初始化滚动条和移动bar的位置
scrollByIndex()
})
}
// 滚动scroll-view让活动的tab处于屏幕的中间位置
const scrollByIndex = () => {
// 当前活动tab的布局信息有tab菜单的width和left(为元素左边界到父元素左边界的距离)等信息
const tabInfo = tabQueryInfo.value[currentIndex.value]
if (!tabInfo) return
// 活动tab的宽度
const tabWidth = tabInfo.width
// 活动item的左边到tabs组件左边的距离用item的left减去tabs的left
const offsetLeft = tabInfo.left - parentLeft.value
// 将活动的tabs-item移动到屏幕正中间实际上是对scroll-view的移动
const scrollLefts = offsetLeft - (componentWidth.value - tabWidth) / 2
scrollLeft.value = scrollLefts < 0 ? 0 : scrollLefts
// 当前活动item的中点点到左边的距离减去滑块宽度的一半即可得到滑块所需的移动距离
const left = tabInfo.left + tabInfo.width / 2 - parentLeft.value
// 计算当前活跃item到组件左边的距离
scrollBarLeft.value = left - uni.upx2px(props.barWidth) / 2
// 第一次移动滑块的时候barFirstTimeMove为true放到延时中将其设置false
// 延时是因为scrollBarLeft作用于computed计算时需要一个过程需否则导致出错
if (barFirstTimeMove.value == true) {
setTimeout(() => {
barFirstTimeMove.value = false
}, 100)
}
// 更新子组件的显示
childrens.value.forEach((item, ind) => {
const active = ind === currentIndex.value
if (active !== item.event.active || !item.event.inited) {
item.updateRender(active)
}
})
}
// 子组件调用此函数而产生的事件通信
const handleChange = (event, updateRender) => {
childrens.value.push({ event: event, updateRender })
}
// 手指触摸
const onTouchStart = (event) => {
if (!props.swipeable) return
swiping.value = true
touchStart(event)
}
// 手指滑动
const onTouchMove = (event) => {
if (!props.swipeable || !swiping.value) return
touchMove(event)
}
// 手指滑动结束
const onTouchEnd = () => {
if (!props.swipeable || !swiping.value) return
const minSwipeDistance = 50
if (touch.direction === 'horizontal' && touch.offsetX >= minSwipeDistance) {
let index,
len = list.value.length,
curIndex = currentIndex.value
if (touch.deltaX <= 0) {
curIndex >= len - 1 ? (index = 0) : (index = curIndex + 1)
} else {
curIndex <= 0 ? (index = len - 1) : (index = curIndex - 1)
}
nextTick(() => {
currentIndex.value = index
scrollByIndex()
})
// 发送事件给父组件
emit('change', index)
}
swiping.value = false
}
onMounted(() => {
updateTabs()
})
provide('handleChange', handleChange)
provide('updateTabs', updateTabs)
</script>
<style lang="scss" scoped>
/* #ifndef APP-NVUE */
::-webkit-scrollbar,
::-webkit-scrollbar,
::-webkit-scrollbar {
display: none;
width: 0 !important;
height: 0 !important;
-webkit-appearance: none;
background: transparent;
}
/* #endif */
.scroll-box {
height: 100%;
position: relative;
/* #ifdef MP-TOUTIAO */
white-space: nowrap;
/* #endif */
}
.tab-fixed {
position: sticky;
top: 0;
width: 100%;
}
/* #ifdef H5 */
// 通过样式穿透隐藏H5下scroll-view下的滚动条
scroll-view ::v-deep ::-webkit-scrollbar {
display: none;
width: 0 !important;
height: 0 !important;
-webkit-appearance: none;
background: transparent;
}
/* #endif */
.scroll-view {
width: 100%;
white-space: nowrap;
position: relative;
}
.tab-item {
position: relative;
/* #ifndef APP-NVUE */
display: inline-block;
/* #endif */
text-align: center;
transition-property: background-color, color;
}
.tab-bar {
position: absolute;
bottom: 6rpx;
}
.tabs-scorll-flex {
display: flex;
justify-content: space-between;
}
// .tab-content {
// overflow: hidden;
// .tab-track {
// position: relative;
// width: 100%;
// height: 100%;
// }
// .tab-animated {
// display: flex;
// transition-property: left;
// }
// }
</style>

View File

@@ -0,0 +1,81 @@
<template>
<page-meta :page-style="$theme.pageStyle">
<!-- #ifndef H5 -->
<navigation-bar :front-color="$theme.navColor" :background-color="$theme.navBgColor" />
<!-- #endif -->
</page-meta>
<view class="container">
<!-- :auth="true" 是表示需要权限登录的 -->
<tabs
:current="current"
@change="handleChange"
height="80"
bar-width="60"
:barStyle="{ bottom: '0' }"
:auth="true"
:isScroll="false"
:activeColor="$theme.primaryColor"
>
<tab v-for="(item, i) in tabList" :key="i" :name="item.name">
<view class="List pt-[20rpx]" v-if="isLogin">
<List :type="item.type" :count="item.count" :i="i" :index="current"></List>
</view>
</tab>
</tabs>
</view>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import List from './components/list.vue'
import tab from '@/components/tab/tab.vue'
import tabs from './components/tabs.vue'
import { useUserStore } from '@/stores/user'
const tabList = ref([
{
name: '待评价',
type: 0,
count: 0
},
{
name: '已评价',
type: 1,
count: 0
}
])
const current = ref<number>(0)
const userStore = useUserStore()
// 是否登录
const isLogin = computed(() => userStore.token)
const handleChange = (index: number) => {
current.value = Number(index)
}
onLoad(async (options: { type?: any }) => {
current.value = options?.type * 1 || 0
})
</script>
<style lang="scss">
.container {
display: flex;
height: 100vh;
overflow: hidden;
flex-direction: column;
}
.main {
flex: 1;
min-height: 0;
overflow: scroll;
swiper {
height: 100%;
}
}
.List {
height: calc(100vh - 80rpx - env(safe-area-inset-bottom));
}
</style>

View File

@@ -0,0 +1,154 @@
<template>
<page-meta :page-style="$theme.pageStyle">
<!-- #ifndef H5 -->
<navigation-bar
:front-color="$theme.navColor"
:background-color="$theme.navBgColor"
/>
<!-- #endif -->
</page-meta>
<view class="comment-submit flex flex-col min-h-0 h-full">
<u-sticky h5-nav-height="0" bg-color="transparent">
<u-navbar
:is-back="true"
:is-fixed="true"
title="评价"
:border-bottom="false"
:title-bold="false"
:background="{ background: `rgba(256,256, 256, 0)` }"
:title-color="'#000'"
>
</u-navbar>
</u-sticky>
<!-- Header Start -->
<view class="comment-header flex flex-col justify-center items-center">
<view class="font-bold text-base mb-4">您觉得这次服务怎么样</view>
<u-rate
v-model="formData.service_comment"
:count="5"
:min-count="1"
inactive-icon="star-fill"
size="56"
active-color="#d86930"
inactive-color="#eaeaeb"
></u-rate>
<view class="mt-3 text-content text-xs">
<text v-if="formData.service_comment == 5">非常好很满意</text>
<text v-else-if="formData.service_comment == 4">比较满意</text>
<text v-else-if="formData.service_comment == 3">一般有待改进</text>
<text v-else-if="formData.service_comment == 2">体验不好</text>
<text v-else-if="formData.service_comment == 1">非常差不喜欢</text>
<text v-else>请选择评价</text>
</view>
</view>
<!-- Header End -->
<!-- Main Start -->
<view class="comment-main flex-1">
<view class="card">
<view class="content">
<u-input
v-model="formData.comment"
type="textarea"
height="200"
placeholder="技师服务如何?与您的期待相符嘛?快来评价吧!"
/>
</view>
<view class="mt-4">
<uploader
v-model="formData.image"
:deletable="true"
preview-size="140rpx"
:maxUpload="4"
image-fit="aspectFill"
:mutiple="true"
:showProgress="true"
v-model:isUploaded="isUploaded"
/>
</view>
</view>
</view>
<view class="comment-footer safe-area-inset-bottom">
<u-button
type="primary"
@click="submit"
>
提交评价
</u-button>
</view>
</view>
</template>
<script lang="ts" setup>
import { reactive,ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { apiEvaluateAdd } from '@/api/user'
import Uploader from '@/components/uploader/index.vue'
interface FormDataType {
order_goods_id: number
service_comment: number | string
comment: string | number
image: Array<string | null>
}
const formData = reactive<FormDataType>({
order_goods_id: 0,
service_comment: 5,
comment: '',
image: []
})
const isUploaded = ref(true)
const submit = async (): Promise<void> => {
if (!isUploaded.value) {
uni.$u.toast('图片上传中,请稍后')
return
}
try {
await apiEvaluateAdd(formData)
setTimeout(() => {
uni.navigateBack()
}, 1000)
} catch (error) {
console.log('提交评价: ', error)
}
}
onLoad((options: any) => {
formData.order_goods_id = options.order_goods_id || 0
})
</script>
<style lang="scss" scoped>
.comment-submit {
.comment-header {
margin: 50rpx 30rpx 0 30rpx;
padding: 28rpx 0;
border-radius: 24rpx;
background: linear-gradient(180deg, #fee5d766 -20.8%, #FFF 57.99%);
}
.comment-main {
margin: 30rpx;
.card {
border-radius: 24rpx;
background-color: #ffffff;
padding: 24rpx;
}
}
.comment-footer {
padding: 20rpx 30rpx;
flex-shrink: 0;
background: #FFF;
padding-bottom: calc(constant(safe-area-inset-bottom));
padding-bottom: calc(env(safe-area-inset-bottom));
box-shadow: 0 -4rpx 48rpx 0 #141a231f;
}
}
</style>

View File

@@ -0,0 +1,304 @@
<template>
<view class="container">
<!-- 搜索框 -->
<view class="flex search">
<!-- 左侧城市 -->
<view class="flex search--city" @click="goPage('/bundle/pages/city/index')">
<text class="mr-[15rpx]">{{ locationState.name }}</text>
<u-icon name="arrow-down" size="20"></u-icon>
</view>
<!-- 右侧搜索城市 -->
<input
type="text"
class="flex-1 search--search"
placeholder="搜索地点"
v-model="keyword"
@confirm="search"
/>
</view>
<!-- 地图组件 -->
<view class="mt-[30rpx] mb-[30rpx] map">
<map
id="myMap"
theme="normal"
style="width: 100%; height: 400rpx; border-radius: 20rpx; overflow: hidden"
show-location
:enable-overlooking="false"
:scale="13"
@regionchange="regionchange"
:latitude="latitude"
:longitude="longitude"
:markers="markersList"
>
<cover-view class="iconImg">
<cover-image
class="img"
src="https://hellouniapp.dcloud.net.cn/static/location.png"
></cover-image>
</cover-view>
<!-- #ifdef MP -->
<!-- <cover-view class="origin" @click="originLocation">
<cover-image
class="origin_icon"
src="@/bundle/static/images/map/origin.png"
></cover-image>
<cover-view class="text-xs mt-[15rpx]">定位</cover-view>
</cover-view> -->
<!-- #endif -->
</map>
</view>
<view class="address">
<!-- 地址列表 -->
<view v-if="addressList?.length">
<scroll-view scroll-y="true" class="scroll-Y" :show-scrollbar="false">
<view
class="address-item"
v-for="(item, index) in addressList"
:key="index"
@click="choiceAddress(item)"
>
<view class="u-flex justify-between">
<view class="u-flex">
<u-icon name="map"></u-icon>
<view class="font-bold ml-[10rpx] w-[500rpx] truncate">{{
item.title
}}</view>
</view>
<view class="text-[#909399] text-[24rpx]">{{ computeDistance(item._distance) }}</view>
</view>
<view
class="text-[#909399] text-[24rpx] ml-[40rpx] mt-[10rpx] w-[500rpx] truncate"
>{{ item.address }}</view
>
</view>
</scroll-view>
</view>
<!-- 无地址 -->
<view class="flex flex-col items-center pb-3" v-else>
<view class="mt-[40rpx]">
<u-image src="@/bundle/static/images/map/good.png" width="290" height="200" />
</view>
<view class="my-[30rpx]">
<text>没有数据哦</text>
</view>
</view>
</view>
<!-- 定位提示弹窗 -->
<modal-popup
v-model:show="showLocationModal"
:title="errorTitle"
:content="errorContent"
@refresh=""
/>
</view>
</template>
<script lang="ts" setup>
import { ref, watch, computed } from 'vue'
import { useAppStore } from '@/stores/app'
import { getAddress } from '@/api/app'
import { locationState, useLocation } from '@/hooks/useLocation'
const appStore = useAppStore()
const {
showLocationModal,
errorTitle,
errorContent,
getLocationData,
setLocationData
} = useLocation()
//经纬度
const latitude = ref()
const longitude = ref()
//搜索关键词
const keyword = ref('')
// 创建map的上下文对象, 从而操控map组件
const mapCtx = uni.createMapContext('myMap')
// 地图标记点
const markersList = ref<any>([])
// 获取地址列表
const addressList = ref<any>([])
const getAddressList = async () => {
const { data } = await getAddress({
keyword: keyword.value,
latitude: latitude.value,
longitude: longitude.value
})
addressList.value = data
renderPointToMap(addressList.value)
}
//计算地址距离
const computeDistance = (distance:number) => {
let res = distance + 'm'
if (distance > 1000) {
res = (distance / 1000).toFixed(3) + 'km'
}
return res
}
// 渲染所有地址
const renderPointToMap = (arr: any[]) => {
if (!arr.length) return
markersList.value = []
arr.forEach((item) => {
if (!item.status) return
markersList.value.push({
id: item.id,
latitude: +item.latitude,
longitude: +item.longitude,
iconPath: '/bundle/static/images/map/end_point_icon.png',
height: 30,
width: 20
})
})
}
// 滑动地图更新当前定位
const regionchange = (e: any) => {
// #ifdef WEB
if (e.detail.type == 'end' && (e.detail.causedBy == 'update' || e.detail.causedBy == 'drag')) {
mapCtx.getCenterLocation({
success: async (res: any) => {
latitude.value = res.latitude
longitude.value = res.longitude
getAddressList()
}
})
}
// #endif
// #ifdef MP-WEIXIN
if (e.type == 'end' && (e.causedBy == 'update' || e.causedBy == 'drag')) {
mapCtx.getCenterLocation({
success: async (res: any) => {
latitude.value = res.latitude
longitude.value = res.longitude
getAddressList()
}
})
}
// #endif
}
// 回到我的位置
// const originLocation = async () => {
// mapCtx.moveToLocation({latitude:latitude.value,longitude:longitude.value})
// await getLocationData()
// getAddressList()
// }
//搜索
const search = async () => {
uni.showLoading({
title: '搜索中...',
mask: true
})
await getAddressList()
uni.hideLoading()
}
//选择地址
const choiceAddress = (res: any) => {
const latitude = res.location.lat
const longitude = res.location.lng
const info = {
name: locationState.name,
city_id: locationState.city_id,
latitude: latitude,
longitude: longitude,
addressName: res.title
}
setLocationData(info)
uni.navigateBack()
}
// 监听位置变化并更新
watch(
() => locationState.latitude,
(val) => {
if (val) {
latitude.value = locationState.latitude
longitude.value = locationState.longitude
getAddressList()
}
},
{ immediate: true }
)
const goPage = (url: string) => {
uni.navigateTo({
url: url
})
}
</script>
<style lang="scss" scoped>
.container {
padding: 20rpx;
.search {
height: 100%;
padding: 15rpx 30rpx;
border-radius: 40rpx;
background-color: #ffffff;
&--city {
color: #222;
font-size: 28rpx;
}
&--search {
margin-left: 30rpx;
padding-left: 30rpx;
border-left: 1px solid #ccc;
}
}
.map {
position: relative;
.iconImg {
position: absolute;
top: 46%;
left: 47%;
.img {
width: 50rpx;
height: 50rpx;
}
}
.origin {
position: absolute;
top: 10rpx;
right: 10rpx;
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background-color: rgba(255, 255, 255, 1);
text-align: center;
border: 1rpx solid rgba(187, 187, 187, 1);
.origin_icon {
position: relative;
top: 15%;
left: 30%;
width: 30rpx;
height: 30rpx;
}
}
}
.address {
background-color: #ffffff;
padding: 30rpx 24rpx;
border-radius: 20rpx;
.address-item {
padding-bottom: 30rpx;
margin: 20rpx 0;
border-bottom: 1px solid #f4f4f5;
}
}
}
</style>

View File

@@ -0,0 +1,70 @@
<!-- 认证信息 -->
<template>
<view class="px-[30rpx] py-[20rpx] real_name_info">
<view class="bg-white rounded-lg">
<view class="flex items-center flex-1 p-[32rpx]">
<u-image :src="info?.work_photo" width="124" height="124" shape="circle"></u-image>
<view class="ml-3">
<view class="text-xl font-black line-clamp-1">
{{ info?.name || '-' }}
</view>
<view class="mt-[16rpx]">
{{ info?.id_card || '-' }}
</view>
</view>
</view>
</view>
<!-- 职业资格证 -->
<view class="bg-white rounded-lg px-[20rpx] py-[30rpx] mt-[20rpx]" v-if="info.certification">
<view class="title mb-2 flex items-center text-center">
<view class="block"></view>
<view class="ml-2">职业资格证</view>
</view>
<u-image :src="info.certification" width="650" height="340" mode="aspectFit"></u-image>
</view>
<!-- 健康证 -->
<view class="bg-white rounded-lg px-[20rpx] py-[30rpx] mt-[20rpx]" v-if="info.health_certificate">
<view class="title mb-2 flex items-center text-center">
<view class="block"></view>
<view class="ml-2">健康证</view>
</view>
<u-image :src="info.health_certificate" width="650" height="340" mode="aspectFit"></u-image>
</view>
</view>
</template>
<script lang="ts" setup>
import { onLoad } from '@dcloudio/uni-app'
import { useRoute } from 'uniapp-router-next'
import { ref } from 'vue'
const route = useRoute()
const info = ref({
work_photo: '',
name: '',
id_card: '',
certification: '',
health_certificate: '',
})
onLoad(() => {
console.log('route.query =>', route.query)
info.value = route.query
})
</script>
<style lang="scss" scoped>
.real_name_info {
.title {
.block {
width: 4rpx;
height: 28rpx;
background-color: #25C373;
}
}
}
</style>

View File

@@ -0,0 +1,23 @@
<template>
<view class="bottom fixed z-50 bottom-0 bg-white w-full pt-[20rpx] px-[30rpx]">
<!-- <u-button v-if="true" type="primary" @click="beClick">加入商家</u-button> -->
<!-- <u-button v-if="true" type="primary" plain @click="beClick">重新申请</u-button> -->
<u-button v-if="true" type="primary" @click="beClick">加收藏</u-button>
</view>
</template>
<script lang="ts" setup>
const emit = defineEmits(['click'])
//点击
const beClick = () => {
console.log(uni.$u.color['error'])
emit('click')
}
</script>
<style lang="scss" scoped>
.bottom {
padding-bottom: calc(env(safe-area-inset-bottom) + 20rpx);
}
</style>

View File

@@ -0,0 +1,10 @@
<template>
<view>
<evaluateCard></evaluateCard>
<evaluateCard></evaluateCard>
</view>
</template>
<script lang="ts" setup>
import evaluateCard from '@/components/evaluate-card/index.vue'
</script>

View File

@@ -0,0 +1,8 @@
<template>
<view class="py-[30rpx] px-[24rpx] bg-white rounded-lg mt-4 h-[1000rpx]">
<view class="text-lg font-normal font-bold">自我介绍</view>
<u-parse html=""></u-parse>
</view>
</template>
<script lang="ts" setup></script>

View File

@@ -0,0 +1,41 @@
<template>
<!-- <u-sticky h5-nav-height="0" bg-color="transparent"> -->
<!-- <u-navbar
:is-back="false"
title=""
:border-bottom="false"
:title-bold="true"
:fixed="false"
:background="{ background: 'rgba(256,256, 256, 1)' }"
> -->
<view>
<u-navbar
:is-back="true"
title="技师详情"
:border-bottom="false"
:title-bold="true"
:fixed="false"
title-color="#000"
back-icon-color="#000"
:background="{
background: percent == 0 ? 'rgba(256,256, 256, 0)' : 'rgba(256,256, 256, 1)'
}"
/>
<!-- <view class="absolute w-full z-[999] p-2">
<tips></tips>
</view> -->
</view>
<!-- </u-sticky> -->
</template>
<script lang="ts" setup>
import { watch } from 'vue'
// import tips from '../tips/index.vue'
const props = defineProps({
percent: {
type: Number,
defualt: 0
}
})
</script>

View File

@@ -0,0 +1,9 @@
<template>
<view>
<projectCard></projectCard>
</view>
</template>
<script lang="ts" setup>
import projectCard from '@/components/project-card/index.vue'
</script>

View File

@@ -0,0 +1,103 @@
<template>
<view>
<view class="flex items-center bg-[#FEF4EB] px-[30rpx] py-[12rpx] rounded-lg">
<u-image mode="aspectFit" height="36" width="120" :src="anxingouLogo"></u-image>
<view class="text-[#CB9F5D] text-sm ml-4 tips">
<text>资质认证</text>
<text>资质认证</text>
<text>资质认证</text>
</view>
<u-icon color="#CB9F5D" class="ml-auto" size="28" name="arrow-right"></u-icon>
</view>
<view class="px-[24rpx] rounded-lg bg-white">
<view class="flex items-center justify-between">
<view class="text-xl font-bold">王芳芳</view>
<view class="text-[#C38925]">最早可约16:00</view>
</view>
<view class="text-info mt-2 introduct">
<text>草本外敷</text>
<text>草本外敷</text>
<text>草本外敷</text>
</view>
<view class="text-xs mt-2 flex justify-between">
<view class="flex items-center">
<u-image height="24" width="24" :src="user"></u-image>
<view class="ml-1">
<text class="text-info">已服务</text>
<text class="text-primary">688</text>
</view>
</view>
<view class="flex items-center">
<u-image height="24" width="24" :src="favor"></u-image>
<view class="ml-1">
<text class="text-info">好评率</text>
<text class="text-primary">98%</text>
</view>
</view>
<view class="flex items-center">
<u-image height="24" width="24" :src="distance"></u-image>
<view class="ml-1">
<text class="text-info">距离</text>
<text class="text-primary">3.6km</text>
</view>
</view>
</view>
<view class="rounded-lg p-2 bg-page mt-4 flex">
<u-image src="" width="80" height="80" borderRadius="40"></u-image>
<view class="ml-2 flex flex-col justify-between flex-1 min-w-0">
<view class="text-sm">为乳糖中医馆</view>
<view class="text-xs text-info">
<text>评分4.5</text>
<text class="ml-4">人均¥188</text>
</view>
</view>
<view class="flex flex-col justify-between items-center text-info">
<view class="text-sm">4.2km</view>
<view class="text-xs">距离</view>
</view>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import price from '@/components/price/price.vue'
import user from '@/bundle/static/icon/user.png'
import distance from '@/bundle/static/icon/distance.png'
import favor from '@/bundle/static/icon/favor.png'
import anxingouLogo from '@/bundle/static/images/anxingouLogo.png'
</script>
<style lang="scss" scoped>
.introduct {
text {
position: relative;
margin-right: 20rpx;
&:not(:first-child) {
::before {
content: '|';
position: absolute;
left: -12rpx;
top: 50%;
transform: translateY(-50%);
}
}
}
}
.tips {
text {
position: relative;
margin-right: 20rpx;
&:not(:first-child) {
::before {
content: '·';
position: absolute;
left: -15rpx;
top: 50%;
transform: translateY(-50%);
}
}
}
}
</style>

View File

@@ -0,0 +1,24 @@
<template>
<view>
<u-swiper height="750" mode="number" indicatorPos="bottomRight" :list="list"></u-swiper>
</view>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const list = ref([
{
image: 'https://cdn.uviewui.com/uview/swiper/1.jpg',
title: '昨夜星辰昨夜风,画楼西畔桂堂东'
},
{
image: 'https://cdn.uviewui.com/uview/swiper/2.jpg',
title: '身无彩凤双飞翼,心有灵犀一点通'
},
{
image: 'https://cdn.uviewui.com/uview/swiper/3.jpg',
title: '谁念西风独自凉,萧萧黄叶闭疏窗,沉思往事立残阳'
}
])
</script>

View File

@@ -0,0 +1,11 @@
<template>
<view>
<view class="bg-warning-light-7 text-warning w-full p-2 rounded-lg">
温馨提示商家正在审核中
</view>
<view class="bg-error-light-7 text-error w-full p-2 rounded-lg">
拒绝原因的阿法甲联赛独立思考理解了啊极乐世界阿里贱啊弗雷斯科打卡了 历史简单快乐啊
</view>
</view>
</template>
<script lang="ts" setup></script>

View File

@@ -0,0 +1,58 @@
<template>
<view class="relative">
<navbar :percent="percent"></navbar>
<view class="absolute top-0 left-0 w-full z-[-1]">
<view class="top-0 left-0 w-full">
<wokerSwiper />
<view class="absolute w-full z-10 mt-[-50rpx] px-[30rpx] pb-[200rpx]">
<side></side>
<introduce></introduce>
<view class="rounded-lg bg-white p-2 mt-[24rpx] px-[20px]">
<tabs
:isScroll="false"
activeColor="#F36161"
inactiveColor="#666"
:current="current"
height="80"
bar-width="60"
>
<tab name="服务项目">
<project />
</tab>
<tab name="用户评价">
<evaluate />
</tab>
</tabs>
</view>
</view>
</view>
</view>
<bottom></bottom>
</view>
</template>
<script lang="ts" setup>
import navbar from './components/nav/index.vue'
import wokerSwiper from './components/swiper/index.vue'
import side from './components/side/index.vue'
import introduce from './components/introduce/index.vue'
import tab from '@/components/tab/tab.vue'
import tabs from '@/components/tabs/tabs.vue'
import project from './components/project/index.vue'
import evaluate from './components/evaluate/index.vue'
import bottom from './components/bottom/index.vue'
import { onPageScroll } from '@dcloudio/uni-app'
import { ref } from 'vue'
const scrollTop = ref<number>(0)
const percent = ref<number>(0)
const current = ref<number>(0)
onPageScroll((event: any) => {
scrollTop.value = event.scrollTop
const top = uni.upx2px(100)
percent.value = event.scrollTop / top > 1 ? 1 : event.scrollTop / top
// console.log(percent.value)
})
</script>

View File

@@ -0,0 +1,317 @@
<template>
<page-meta :page-style="$theme.pageStyle">
<!-- #ifndef H5 -->
<navigation-bar
:front-color="$theme.navColor"
:background-color="$theme.navBgColor"
/>
<!-- #endif -->
</page-meta>
<view class="order-pay flex flex-col min-h-0 h-full bg-white">
<view class="header flex justify-center items-center flex-col">
<view class="order-amount ">
<text style="font-size: 76rpx"></text>
{{ payState.order_amount || '100.00' }}
</view>
<view class="mt-2 text-muted text-xs">需要支付</view>
<!-- <view-->
<!-- class="box-time flex"-->
<!-- v-if="timeStamp >= 0 && params.from === 'order'"-->
<!-- >-->
<!-- 支付剩余时间-->
<!-- <u-count-down-->
<!-- :timestamp="timeStamp"-->
<!-- @end="handleCountDownEnd"-->
<!-- format="mm:ss"-->
<!-- :font-size="26"-->
<!-- :separator-size="26"-->
<!-- />-->
<!-- </view>-->
</view>
<view class="main">
<view class="text-main text-lg font-medium">支付方式</view>
<u-radio-group v-model="payWay" :key="payWay" class="w-full" :active-color="$theme.primaryColor">
<view
v-for="item in payData"
:key="item.id"
class="payment-item w-full"
@click="selectPayWay(item.pay_way)"
>
<label class="flex items-center justify-between w-full">
<view class="flex items-center">
<u-image
:src="item.image"
width="48"
height="48"
shape="circle"
/>
<view class="pl-[20rpx] u-flex">
<view class="text-[28rpx] text-[#333]">
{{ item.name }}
</view>
<view class="pl-[10rpx] text-[24rpx] text-muted">
{{ item.extra || '' }}
</view>
</view>
</view>
<view>
<u-radio :name="item.pay_way"></u-radio>
</view>
</label>
</view>
</u-radio-group>
</view>
<view class="footer w-full p-[20rpx] mb-[80rpx]">
<u-button
type="primary"
:loading="isLock"
@click="handlePay"
>
立即支付
</u-button>
</view>
</view>
<!-- 支付查询弹窗 -->
<payment-check
v-model:show="payState.showCheck"
:from="payState.from"
:order-id="payState.orderId"
@fail="handlePayResult(PayStatusEnum.FAIL)"
@success="handlePayResult(PayStatusEnum.SUCCESS)"
/>
<!-- 支付宝弹窗 -->
<payment-alipay
v-model:show="payState.showAlipay"
:alipay-link="alipayLink"
@check="payState.showCheck = true"
/>
<!-- 提示绑定微信弹窗 -->
<modal-popup
v-model:show="payState.showBindWx"
title="温馨提示"
content="当前账号未微信授权,请前往个人设置授权"
@confirm="router.navigateTo('/pages/user_set/user_set')"
>
</modal-popup>
</template>
<script setup lang="ts">
import {ref, reactive} from 'vue'
import {onLoad} from '@dcloudio/uni-app'
import {getToken} from '@/utils/auth'
import {apiPayPayWay, apiPayPrepay} from "@/api/app";
import {getClient} from "@/utils/client";
import {useLockFn} from "@/hooks/useLockFn";
import {pay, PayWayEnum} from "@/utils/pay/index";
import {series} from "@/utils/util";
import {useRoute, useRouter} from "uniapp-router-next";
import {useUserStore} from "@/stores/user";
import {PayStatusEnum} from "@/enums/appEnums";
import PaymentCheck from "@/components/payment/check.vue";
import PaymentAlipay from "@/components/payment/alipay.vue";
const router = useRouter()
const route = useRoute()
const token = getToken()
const userStore = useUserStore()
const params = ref<any>({
from: 'order',
order_id: '',
icon: '',
order_amount: ''
})
const payData: any = ref({
cancel_time: '',
order_amount: ''
})
// 支付方式:1-微信支付;2-支付宝支付;3-余额支付;
const payWay = ref()
const timeStamp = ref<number | string | undefined>()
//支付宝支付链接
const alipayLink = ref('')
// 支付状态
const payState = reactive({
order_amount: '',
orderId: '',
from: 'order',
showBindWx: false,
showCheck: false,
showAlipay: false,
showPaySuccess: false
})
// 选择支付方式
const selectPayWay = (pay: number) => {
// console.log('pay', pay)
payWay.value = pay
}
const initPlaceOrder = async () => {
let payInfo = await apiPayPayWay({
from: payState.from || 'order',
order_id: payState.orderId,
scene: getClient()
})
payData.value = payInfo.lists
payState.order_amount = payInfo.order_amount;
// timeStamp.value = (payData.value.cancel_time - Date.now() / 1000) * 1000
const checkPay = payData.value.find((item: any) => item.is_default) || payData.value[0]
payWay.value = checkPay?.pay_way
}
const placeOrderAfter = (() => {
const checkIsBindWx = async () => {
if (
userStore.userInfo.pay_auth == 0 &&
payWay.value == PayWayEnum.WECHAT
) {
payState.showBindWx = true
return Promise.reject('请先绑定微信后支付')
}
}
const alipayment = async () => {
//支付宝支付 --- 逻辑
if (payWay.value == PayWayEnum.ALIPAY) {
// #ifdef MP-WEIXIN
alipayLink.value = `${import.meta.env.VITE_APP_BASE_URL || ''}mobile/bundle/pages/toAlipay/toAlipay?order_id=${payState.orderId}&from=${payState.from}&pay_way=${payWay.value}&key=${token}`
payState.showAlipay = true
return Promise.reject('微信不允许使用支付宝支付')
// #endif
//#ifdef H5
const ua = navigator.userAgent.toLowerCase()
if (ua.match(/MicroMessenger/i)) {
//微信浏览器环境
router.reLaunch({
path: '/bundle/pages/toAlipay/toAlipay',
query: {
order_id: payState.orderId,
from: payState.from,
pay_way: payWay.value,
key: token
}
})
return Promise.reject('微信不允许使用支付宝支付')
}
//#endif
}
}
const payment = async () => {
try {
// uni.showLoading({
// title: '正在支付中'
// })
const data = await apiPayPrepay({
from: payState.from,
pay_way: payWay.value,
order_id: payState.orderId
})
const res = await pay.payment(
data.pay_way,
data?.config || data?.payurl || data?.qrcode
)
console.log('res', res)
// 支付结果
handlePayResult(res)
} catch (error) {
return Promise.reject(error)
} finally {
// uni.hideLoading()
}
}
return series(checkIsBindWx, alipayment, payment)
})()
const {isLock, lockFn: handlePay} = useLockFn(async () => {
try {
// 支付处理
await placeOrderAfter()
} catch (err) {
console.log('支付', err)
}
})
const handlePayResult = (status: PayStatusEnum) => {
switch (status) {
case PayStatusEnum.SUCCESS:
router.redirectTo({
path: '/bundle/pages/payment_result/payment_result',
query: {
order_id: payState.orderId,
from: payState.from
}
})
break
case PayStatusEnum.FAIL:
router.redirectTo({
path: '/bundle/pages/payment_result/payment_result',
query: {
order_id: payState.orderId,
from: payState.from
}
})
break
}
}
onLoad(async (option: any) => {
if (option?.order_amount) {
payState.order_amount = option?.order_amount
payWay.value = 2
}
payState.orderId = option.order_id
payState.from = option.from
console.log(option)
await initPlaceOrder()
})
</script>
<style lang="scss" scoped>
.header {
margin: 20rpx 20rpx 0;
height: 340rpx;
.order-amount {
color: #ea6a57;
font-size: 80rpx;
font-weight: bold;
}
&-time {
border-radius: 200rpx;
background-color: rgba(255, 255, 255, 1);
padding: 10rpx 48rpx;
margin-top: 24rpx;
font-size: 24rpx;
}
}
.main {
margin: 24rpx;
.payment-item {
margin-top: 30rpx;
padding: 30rpx;
border-radius: 20rpx;
background-color: #f6f7f8;
}
}
.footer {
width: 100%;
position: fixed;
bottom: 0;
padding-bottom: env(safe-area-inset-bottom);
// background-color: #ffffff;
//box-shadow: 0 -4px 48px 0 #141a231f;
}
</style>

View File

@@ -0,0 +1,154 @@
<template>
<page-meta :page-style="$theme.pageStyle">
<!-- #ifndef H5 -->
<navigation-bar
:front-color="$theme.navColor"
:background-color="$theme.navBgColor"
/>
<!-- #endif -->
</page-meta>
<page-status :status="status">
<view class="payment-result flex flex-col min-h-0 h-full bg-white">
<view
class="flex flex-col justify-center"
style="height: 40vh;"
>
<!-- Header -->
<view class="text-center py-[60rpx]">
<image
style="width: 150rpx; height: 150rpx;"
:src="resultStatus[payResult.pay_status].image"
/>
<view
style="font-size: 40rpx;"
class="mt-4 text-main font-medium"
>
{{ resultStatus[payResult.pay_status].text }}
</view>
<view class="mt-2 text-content text-xs">
{{ resultStatus[payResult.pay_status].desc }}
</view>
</view>
</view>
<!-- Footer -->
<view
class="mx-[30rpx] pt-[30rpx]"
style="padding: 50rpx 40px;"
>
<template v-if="payForm.from !== PayFromType.USERRECHARGE">
<view class="flex-1">
<u-button
type="primary"
@click="router.switchTab('/pages/order/index')"
>
查看订单
</u-button>
</view>
</template>
<template v-else>
<view class="flex-1">
<u-button
type="primary"
@click="router.redirectTo('/bundle/pages/user_wallet/user_wallet')"
>
我的钱包
</u-button>
</view>
</template>
<view class="flex-1 mt-4">
<u-button
type="default"
@click="router.reLaunch('/pages/index/index')"
>
返回首页
</u-button>
</view>
</view>
<!-- Component -->
<!-- <page-status ref="pageRef"></page-status>-->
</view>
</page-status>
</template>
<script lang="ts" setup>
import {onLoad} from '@dcloudio/uni-app'
import {PageStatusEnum, PayFromType} from '@/enums/appEnums'
import {reactive, shallowRef, unref, ref} from 'vue'
import GoodsAbnormalImage from '@/static/images/empty/order.png'
import IconWait from '@/static/images/icon/icon_wait.png'
import IconSuccess from '@/static/images/icon/icon_pay_success.png'
import {useUserStore} from "@/stores/user";
import {useRoute, useRouter} from "uniapp-router-next";
import {getPayResult} from "@/api/app";
const userStore = useUserStore()
const route = useRoute()
const router = useRouter()
const status = ref(PageStatusEnum.LOADING)
const pageRef = shallowRef()
const payForm = reactive({
from: '',
order_id: '',
})
const payResult = reactive<any>({
pay_status: 1, // 0-待支付 1-已支付
total_amount: 0,
pay_time: '-',
pay_way_desc: '-',
sn: '-',
})
const resultStatus = [
{
text: '等待支付',
desc: '超时订单将自动取消',
image: IconWait,
},
{
text: '支付成功',
desc: '您已完成支付',
image: IconSuccess,
},
]
// 初始化订单详情
const initOrderDetail = async (): Promise<void> => {
try {
const data = await getPayResult({
order_id: payForm.order_id,
from: payForm.from
})
Reflect.ownKeys(payResult).map((item: any) => {
payResult[item] = data[item]
})
status.value = PageStatusEnum.NORMAL
} catch (error) {
console.log('查询支付结果', error)
status.value = PageStatusEnum.ERROR
}
}
onLoad(async () => {
try {
if (!route.query.order_id) {
throw new Error('参数有误')
}
payForm['from'] = route.query['from'] as string
payForm['order_id'] = route.query['order_id'] as string
await initOrderDetail()
} catch (error) {
unref(pageRef).show({
text: error,
src: GoodsAbnormalImage,
})
console.log('初始化支付页面', error)
}
})
</script>

View File

@@ -0,0 +1,68 @@
<template>
<view class="user-recharge">
<z-paging
auto-show-back-to-top
ref="paging"
v-model="dataList"
@query="queryList"
:fixed="false"
height="100%"
>
<template v-for="(item, index) in dataList" :key="index">
<view class="wrapper">
<view class="left">
<view class="title text-lg">余额充值</view>
<view class="time">{{ item.create_time }}</view>
</view>
<view class="right"> +{{ item.order_amount }} </view>
</view>
</template>
</z-paging>
</view>
</template>
<script lang="ts" setup>
import { ref, shallowRef } from 'vue'
import { apiRechargeLogLists } from '@/api/wallet'
const paging = shallowRef<any>(null)
const dataList = ref<any>([])
const queryList = async (page_no: number, page_size: number) => {
try {
const { lists } = await apiRechargeLogLists({
page_no,
page_size
})
paging.value.complete(lists)
} catch (e) {
console.log('报错=>', e)
//TODO handle the exception
paging.value.complete(false)
}
}
</script>
<style lang="scss" scoped>
.user-recharge {
height: calc(100vh - env(safe-area-inset-bottom));
padding-top: 15rpx;
.wrapper {
padding: 20rpx 21rpx;
background-color: rgba(255, 255, 255, 1);
border-top: 1px solid rgba(234, 234, 234, 1);
display: flex;
justify-content: space-between;
.left {
.time {
margin-top: 10rpx;
font-size: 24rpx;
color: #999;
}
}
.right {
color: #ff2c3c;
font-size: 38rpx;
}
}
}
</style>

View File

@@ -0,0 +1,112 @@
<template>
<view class="suggest">
<!-- 历史搜索 -->
<view class="history" v-if="his_search.length">
<view class="flex justify-between pb-[6rpx] pt-[26rpx]">
<view class="text-lg font-medium">历史搜索</view>
<view class="text-xs text-muted" @click="() => emit('clear')">清空</view>
</view>
<view class="w-full">
<block v-for="(hisItem, index) in his_search" :key="index">
<view class="keyword truncate" @click="handleHistoreSearch(hisItem)">{{
hisItem
}}</view>
</block>
</view>
</view>
<!-- 热门搜索 -->
<view class="hot" v-if="hot_search.status == 1 && searchData.length">
<view
class="flex items-center font-medium pb-[6rpx] text-lg"
style="color: #DE3927;"
>
<image
style="width: 44rpx; height: 44rpx"
src="@/bundle/static/images/search_hot.png"
>
</image>
<text class="ml-2">热门搜索</text>
</view>
<view class="grid grid-cols-2 gap-y-4 w-full mt-2 p-[30rpx] bg-white rounded-lg">
<block v-for="(hotItem, index) in searchData" :key="index">
<view
class="w-full truncate max-w-full"
@click="handleHistoreSearch(hotItem.name)"
>
<text
class="text-base font-bold"
:style="{
'color': index<=2 ? '#DE3927' : '#999999'
}"
>
{{ index + 1 }}
</text>
<text class="ml-2">
{{ hotItem.name }}
</text>
</view>
</block>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
const emit = defineEmits<{
(event: 'search', value: string): void
(event: 'clear', value: void): void
}>()
const props = withDefaults(
defineProps<{
hot_search?: {
data: any[]
status: number
}
his_search?: string[]
}>(),
{
hot_search: () => ({
data: [],
status: 0
}),
his_search: () => []
}
)
const searchData = computed(() => {
return props.hot_search.data.filter((item) => item.name)
})
const handleHistoreSearch = (text: string) => {
emit('search', text)
}
</script>
<style lang="scss" scoped>
.suggest {
height: 100%;
margin: 0 30rpx;
.keyword {
display: inline-block;
margin: 24rpx 16rpx 0 0;
padding: 8rpx 24rpx;
border-radius: 26rpx;
background-color: #ffffff;
}
.hot {
margin: 30rpx 0;
padding: 24rpx;
border-radius: 24rpx;
background: url("@/bundle/static/images/search_bg.png");
background-size: cover;
background-repeat: no-repeat;
}
}
</style>

View File

@@ -0,0 +1,410 @@
<template>
<page-meta :page-style="$theme.pageStyle">
<!-- #ifndef H5 -->
<navigation-bar
:front-color="$theme.navColor"
:background-color="$theme.navBgColor"
/>
<!-- #endif -->
</page-meta>
<view class="search flex flex-col min-h-0 h-full">
<!-- #ifndef H5 -->
<u-sticky h5-nav-height="0" bg-color="transparent">
<u-navbar
:is-back="true"
:is-fixed="true"
:border-bottom="false"
title="搜索"
title-color="#000"
:title-bold="false"
:background="{ background: `rgba(256,256, 256, ${search.searching ? 1 : 0})` }"
>
</u-navbar>
</u-sticky>
<!-- #endif -->
<!-- 搜索框 --- 未搜索 -->
<view
class="p-[30rpx]"
:style="{
'background-color': !search.searching ? 'transparent' : '#ffffff'
}"
>
<view
class="search-input flex"
:style="{
'background-color': !search.searching ? '#ffffff' : '#F6F7F8'
}"
>
<view class="search-input-icon flex items-center flex-none">
<u-icon name="search"></u-icon>
</view>
<view class="search-input-wrapper flex items-center flex-1">
<input
class="search-input-inner flex-1"
placeholder="请输入关键词"
v-model="keyword"
@confirm="handleSearch"
@change="handleSearch"
:focus="true"
/>
<view
v-if="keyword.length"
class="flex-none px-3 text-muted"
@click="clearSearch"
>
<u-icon name="close-circle" size="34"></u-icon>
</view>
</view>
<button
class="search-input-btn flex-none"
@click="handleSearch"
>
搜索
</button>
</view>
</view>
<!-- 搜索 -->
<view class="search-content flex flex-col min-h-0 h-full">
<!-- 未搜索 -->
<suggest
v-show="!search.searching"
:hot_search="search.hot_search"
:his_search="search.his_search"
@clear="handleClear"
@search="(val) => {
keyword = val;
handleSearch()
}"
></suggest>
<!-- 搜索结果 -->
<view
class="flex flex-col min-h-0 h-full"
v-show="search.searching"
>
<!-- 检索下拉组件 列表 -->
<view class="bg-white">
<u-dropdown
ref="uDropdownRef"
menu-icon="arrow-down-fill"
menu-icon-size="20"
border-radius="18"
duration="0"
:active-color="$theme.primaryColor"
>
<u-dropdown-item :title="sortOptions[sort].label">
<view class="bg-white rounded-b-xl pt-4">
<view
v-for="(item, index) in sortOptions"
:key="index"
class="pb-4 text-content"
:class="{
'text-primary': index === sort
}"
@click="handleFiltrate('sort', index)"
>
<text class="ml-4">
{{ item.label }}
</text>
</view>
</view>
</u-dropdown-item>
<u-dropdown-item
title="销量"
>
<view class="bg-white rounded-b-xl pt-4">
<view
v-for="(item, index) in salesOptions"
:key="index"
class="pb-4 text-content"
:class="{
'text-primary': item.value === order_sales
}"
@click="handleFiltrate('order_sales', item.value)"
>
<text class="ml-4">
{{ item.label }}
</text>
</view>
</view>
</u-dropdown-item>
<u-dropdown-item
title="价格"
>
<view class="bg-white rounded-b-xl pt-4">
<view
v-for="(item, index) in salesOptions"
:key="index"
class="pb-4 text-content"
:class="{
'text-primary': item.value === price
}"
@click="handleFiltrate('price', item.value)"
>
<text class="ml-4">
{{ item.label }}
</text>
</view>
</view>
</u-dropdown-item>
<u-dropdown-item
:title="listOptions[list].label"
>
<view class="bg-white rounded-b-xl pt-4">
<view
v-for="(item, index) in listOptions"
:key="index"
class="pb-4 pl-4 text-content"
:class="{
'text-primary': item.value === list
}"
@click="handleFiltrate('list', item.value)"
>
<u-icon
:name="item.icon"
size="28"
></u-icon>
<text class="ml-2">
{{ item.label }}展示
</text>
</view>
</view>
</u-dropdown-item>
</u-dropdown>
</view>
<!-- 显示页面 列表 -->
<view class="h-full">
<z-paging
ref="paging"
v-model="search.result.content.goods_list"
:fixed="false"
height="100%"
:empty-view-img="EmptySearch"
@query="queryList"
>
<area-goods
:content="search.result.content"
:styles="search.result.styles"
/>
</z-paging>
</view>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import {ref, reactive, shallowRef} from 'vue'
import Suggest from './component/suggest.vue'
import {HISTORY} from '@/enums/constantEnums'
import {getHotSearch, getServiceList} from '@/api/shop'
import {locationState} from "@/hooks/useLocation";
import cache from '@/utils/cache'
import AreaGoods from "@/components/widgets/area-goods/area-goods.vue";
import EmptySearch from "@/static/images/empty/search.png";
interface Search {
hot_search: {
data: any[]
status: number
}
his_search: string[]
result: any
searching: boolean
}
const search = reactive<Search>({
hot_search: {
data: [],
status: 1
},
his_search: [],
// 这里复用了首页装修的商品组组件数据。
result: {
content: {
enabled: 1, // 是否启用
type: 1, // 1-自动获取
goods_type: 1, // 1-单列 2-双列 3-横向滑动 4-大图模式
show_title: 1, // 商品标题
show_server: 1, // 服务
show_price: 1, // 商品价格
show_scribing_price: 1, // 划线价格
show_sales: 1, // 销量
btn_text: '立即下单', // 按钮文字
show_num: 1, // 显示数量
goods_list: [] // 商品数据
},
styles: {
title_color: '#000000', // 标题颜色
server_color: '#888888', // 服务颜色
price_color: '#fc5447', // 价格颜色
scribing_price_color: '#888888', // 划线价格颜色
sales_color: '#888888', // 销量颜色
btn_radius: 10, // 按钮圆角
root_bg_color: '', // 根背景颜色
component_bg_color: '#ffffff', // 组件背景颜色
goods_bg_color: '', // 商品背景颜色
padding_top: 12, // 上内边距
padding_horizontal: 12, // 左右内边距
padding_bottom: 12, // 下内边距
goods_horizontal: 12, // 商品左右内边距
goods_vertical: 12, // 商品上下内边距
border_top_radius: 20,
border_bottom_radius: 20,
image_radius: 8
}
},
searching: false
})
const keyword = ref<string>('')
const order_sales = ref('') // 销量排序
const price = ref('') // 价格排序
const sort = ref('') // 综合排序
const list = ref('card') // 列表展示
const paging = shallowRef()
const uDropdownRef = shallowRef()
const sortOptions = {
'': {value: '', label: '综合排序'},
// 'desc': { value: 'desc', label:'距离优先'},
// 'asc': {value: 'asc', label: '销量优先'},
'comment_sales': {value: 'desc', label: '好评优先'}
}
const salesOptions = [
{value: '', label: '默认'},
{value: 'desc', label: '从高到低'},
{value: 'asc', label: '从低到高'}
]
const listOptions = {
card: {value: 'card', icon: 'grid', label: '卡片'},
list: {value: 'list', icon: 'list-dot', label: '列表'}
}
const handleFiltrate = (type: string, value: string) => {
switch (type) {
case 'sort':
sort.value = value
paging.value.refresh()
break
case 'order_sales':
order_sales.value = value
paging.value.refresh()
break
case 'price':
price.value = value
paging.value.refresh()
break
case 'list':
list.value = value
value === 'card' ? (search.result.content.goods_type = 1) : (search.result.content.goods_type = 2)
break
}
uDropdownRef.value?.close()
}
const handleSearch = () => {
if (keyword.value.trim() === '') {
return
}
if (keyword.value) {
if (!search.his_search.includes(keyword.value)) {
search.his_search.unshift(keyword.value)
cache.set(HISTORY, search.his_search)
}
}
paging.value.reload()
search.searching = true
}
const clearSearch = () => {
keyword.value = ''
search.searching = false
}
const getHotSearchFunc = async () => {
try {
search.hot_search = await getHotSearch()
} catch (e) {
//TODO handle the exception
console.log('获取热门搜索失败=>', e)
}
}
const handleClear = async (): Promise<void> => {
const resModel: any = await uni.showModal({
title: '温馨提示',
content: '是否清空历史记录?'
})
if (resModel.confirm) {
cache.set(HISTORY, '')
search.his_search = []
}
}
const queryList = async (page_no: number, page_size: number) => {
try {
const {lists} = await getServiceList({
keyword: keyword.value,
city_id: locationState.city_id,
order_sales: order_sales.value,
price: price.value,
comment_sales: sort.value,
page_no,
page_size
})
paging.value.complete(lists)
} catch (e) {
console.log('报错=>', e)
//TODO handle the exception
paging.value.complete(false)
}
}
getHotSearchFunc()
search.his_search = cache.get(HISTORY) || []
</script>
<style lang="scss" scoped>
.search {
.search-input {
background-color: #ffffff;
border-radius: 50px;
padding: 10rpx 20rpx;
.search-input-icon {
color: #666666;
padding: 0 14rpx 0 14rpx;
}
.search-input-wrapper {
}
.search-input-inner {
}
.search-input-btn {
height: 60rpx;
line-height: 60rpx;
font-size: 28rpx;
@apply bg-primary text-white rounded-full;
}
}
&-content {
&-s {
height: 100%;
}
}
}
</style>

View File

@@ -0,0 +1,125 @@
<template>
<page-meta :page-style="$theme.pageStyle">
<!-- #ifndef H5 -->
<navigation-bar :front-color="$theme.navColor" :background-color="$theme.navBgColor" />
<!-- #endif -->
</page-meta>
<view class="h-full pt-[120rpx]">
<view class="top-search">
<u-search
v-model="keyword"
placeholder="请输入关键词"
:height="80"
:animation="true"
@search="search"
@custom="search"
@clear="queryList"
></u-search>
</view>
<view class="coachList">
<z-paging
:auto="true"
auto-show-back-to-top
ref="paging"
v-model="dataList"
:fixed="false"
height="100%"
@query="queryList"
>
<view v-for="item in dataList" :key="item.id">
<coach-card :data="item" type="1" @confirm="navigateToCoach"></coach-card>
</view>
<!-- <template #empty>
<view class="">
暂无可服务技师
</view>
</template> -->
</z-paging>
</view>
</view>
</template>
<script lang="ts" setup>
import { nextTick, ref, shallowRef } from 'vue'
import { apiCollectLists } from '@/api/user'
import { useRoute, useRouter } from 'uniapp-router-next'
import { locationState } from '@/hooks/useLocation'
import { onLoad } from '@dcloudio/uni-app'
import CoachCard from '@/components/master-worker-card/index.vue'
import ProjectCard from '@/components/project-card/index.vue'
import shopLists from '@/components/shop-lists/index.vue'
import { apiCoachLists } from '@/api/coach'
const route = useRoute()
const router = useRouter()
const dataList = ref<any>([])
const paging = shallowRef<any>(null)
// 搜索关键字
const keyword = ref<string | number>('')
// 处理搜索
const search = () => {
paging.value?.reload()
}
const shop_id = ref('')
const goods_id = ref('')
// 初始化收藏
const queryList = async (page_no: number, page_size: number) => {
try {
const { lists } = await apiCoachLists({
shop_id: shop_id.value,
goods_id: goods_id.value,
keyword: keyword.value,
page_no: page_no || 1,
page_size: page_size || 10,
longitude: locationState.longitude,
latitude: locationState.latitude
})
paging.value.complete(lists)
} catch (e) {
console.log('报错=>', e)
paging.value.complete(false)
}
}
const navigateToCoach = (id: number) => {
router.navigateTo({
path: '/pages/order_buy/index',
query: {
goods_id: goods_id.value,
coach_id: id,
}
})
}
onLoad(() => {
shop_id.value = route.query.shop_id
goods_id.value = route.query.goods_id
})
</script>
<style lang="scss" scoped>
.empty {
padding-top: 300rpx;
}
.top-search {
left: 0;
top: 0;
width: 100%;
z-index: 10;
position: fixed;
padding: 20rpx 30rpx;
height: 120rpx;
background-color: #ffffff;
}
.coachList {
height: calc(100vh - 120rpx - env(safe-area-inset-bottom));
}
</style>

View File

@@ -0,0 +1,114 @@
<template>
<view class="card" @click.stop="goPage">
<view class="card--header flex justify-between">
<view class="order-sn">订单编号{{ orderInfo.sn }}</view>
<view class="status">{{ orderInfo.order_status_desc }}</view>
</view>
<block v-for="item3 in orderInfo.order_goods" :key="item3.id">
<view class="card--main flex">
<u-image :src="item3.goods_image" width="160" height="160"></u-image>
<view class="ml-[20rpx] service-text">
<view class="service-text--name truncate">{{ item3.goods_name }}</view>
<view class="mt-[16rpx]"
>预约时间: {{ orderInfo.appoint_time }} {{ orderInfo.appoint_week }}</view
>
<view class="mt-[16rpx]">实付金额: ¥{{ orderInfo.order_amount }}</view>
</view>
</view>
</block>
<view class="card--footer flex justify-between">
<view class="text-primary">
<template v-if="orderInfo.order_cancel_time">
<view class="flex" v-if="timeStamp >= 0">
<u-count-down
:timestamp="timeStamp"
format="mm:ss"
:font-size="26"
:separator-size="26"
@end="timeStamp = 0"
/>
<text class="ml-[10rpx]">后自动取消</text>
</view>
</template>
</view>
<view>
<slot></slot>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref, reactive, watchEffect } from 'vue'
const props = withDefaults(
defineProps<{
orderInfo?: any // 底部
}>(),
{
orderInfo: {}
}
)
const timeStamp = ref<number | null>(0)
watchEffect(() => {
// 获取倒计时段
const endTimestamp = props.orderInfo.order_cancel_time
const startTimestamp = new Date().getTime() / 1000
timeStamp.value = (endTimestamp - startTimestamp) * 1000
})
const goPage = () => {
uni.navigateTo({
url: `/bundle/pages/service_order_detail/index?id=${props.orderInfo.id}`
})
}
</script>
<style lang="scss" scoped>
.card {
border-radius: 14rpx;
background-color: #ffffff;
margin: 20rpx 20rpx 0 20rpx;
&--header {
padding: 24rpx 30rpx;
font-size: 26rpx;
border-bottom: 1px solid #e5e5e5;
.order-sn {
color: #222222;
}
.status {
color: #f36161;
}
}
&--main {
padding: 30rpx;
color: #555555;
font-size: 26rpx;
.service-text {
&--name {
width: 460rpx;
font-weight: 500;
color: #222222;
font-size: 32rpx;
// line-1
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}
&--footer {
padding: 20rpx 30rpx;
font-size: 26rpx;
border-top: 1px solid #e5e5e5;
}
}
</style>

View File

@@ -0,0 +1,84 @@
<template>
<z-paging
auto-show-back-to-top
:auto="i == index"
ref="paging"
v-model="dataList"
:data-key="i"
@query="queryList"
:fixed="false"
height="100%"
>
<block v-for="(item, index) in dataList" :key="index">
<order-card :orderInfo="item">
<order-footer
:orderId="item?.id"
:cancel="item.cancel_btn"
:evaluate="item.comment_btn"
:contact="item.contact_btn"
:pay="item.pay_btn"
:mobile="item?.staff?.mobile"
:confirmService="item.confirm_service_btn"
:verification="item.verification_btn"
:orderStatus="item.order_status"
:goods_image="item.order_goods[0].goods_image"
:goods_name="item.order_goods[0].goods_name"
:type="2"
@refresh="queryList"
/>
</order-card>
</block>
</z-paging>
</template>
<script lang="ts" setup>
import { ref, watch, nextTick, shallowRef, unref } from 'vue'
import orderCard from './order-card.vue'
import orderFooter from '@/components/order-footer/index.vue'
import { apiStaffOrderLists } from '@/api/order'
import { onShow } from '@dcloudio/uni-app'
const props = withDefaults(
defineProps<{
order_status: number
i: number
index: number
}>(),
{
order_status: 0
}
)
const paging = shallowRef<any>(null)
const dataList = ref<any>([])
const isFirst = ref<boolean>(true)
watch(
() => props.index,
async () => {
await nextTick()
if (props.i == props.index && unref(isFirst)) {
isFirst.value = false
paging.value?.reload()
}
},
{ immediate: true }
)
const queryList = async (pageNo: number, pageSize: number) => {
try {
const { lists } = await apiStaffOrderLists({
order_status: props.order_status,
pageNo,
pageSize
})
paging.value.complete(lists)
} catch (e) {
console.log('报错=>', e)
//TODO handle the exception
paging.value.complete(false)
}
}
</script>
<style scoped></style>

View File

@@ -0,0 +1,91 @@
<template>
<view class="order-list">
<!-- :auth="true" 是表示需要权限登录的 -->
<tabs
:current="current"
@change="handleChange"
height="80"
bar-width="60"
:barStyle="{ bottom: '0' }"
:auth="true"
activeColor="#F36161"
>
<tab v-for="(item, i) in tabList" :key="i" :name="item.name">
<view class="orderList pt-[20rpx]" v-if="isLogin">
<orderList
:order_status="item.order_status"
:i="i"
:index="current"
></orderList>
</view>
</tab>
</tabs>
</view>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import orderList from './components/order-list.vue'
import tab from '@/components/tab/tab.vue'
import tabs from '@/components/tabs/tabs.vue'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
// 是否登录
const isLogin = computed(() => userStore.token)
const tabList = ref<any>([
{
name: '全部',
order_status: 0
},
{
name: '预约中',
order_status: 1
},
{
name: '服务中',
order_status: 2
},
{
name: '已完成',
order_status: 3
},
{
name: '已关闭',
order_status: 4
}
])
const current = ref<number>(0)
const handleChange = (index: number) => {
current.value = Number(index)
}
onLoad(async (options: { type?: any }) => {
current.value = options?.type * 1 || 0
})
</script>
<style lang="scss">
.container {
display: flex;
height: 100vh;
overflow: hidden;
flex-direction: column;
}
.main {
flex: 1;
min-height: 0;
overflow: scroll;
swiper {
height: 100%;
}
}
.orderList {
height: calc(100vh - 86px - env(safe-area-inset-bottom));
}
</style>

View File

@@ -0,0 +1,216 @@
<template>
<uni-transition
mode-class="zoom-in"
needLayout="true"
:show="orderData.order_goods.length"
:duration="500"
>
<view class="order_detail">
<view class="flex pb-[30rpx]">
<image
v-if="orderData.order_status === 0"
class="header-image"
src="/static/images/icon_pay.png"
/>
<image
v-if="orderData.order_status === 1 || orderData.order_status === 2"
class="header-image"
src="/static/images/icon_wait.png"
/>
<image
v-if="orderData.order_status === 3"
class="header-image"
src="/static/images/icon_success.png"
/>
<image
v-if="orderData.order_status === 4"
class="header-image"
src="@/static/images/icon_close.png"
/>
<text class="statusDec ml-[15rpx]">{{ orderData.order_status_desc }}</text>
</view>
<!-- 地址卡片 -->
<view class="card">
<view class="card--header">
<view class="title">{{ orderData.contact }} {{ orderData.mobile }}</view>
</view>
<view class="text-sm text-muted">
{{ orderData.province }}
{{ orderData.city }}
{{ orderData.district }}
{{ orderData.address }}
</view>
</view>
<!-- 商品卡片 -->
<view class="card">
<view class="goods-item">
<u-image
:src="orderData.order_goods[0]?.goods_image"
width="160"
height="160"
border-radius="4"
/>
<view class="ml-[20rpx] mt-[4rpx]">
<view class="flex justify-between title">
<view class="goods-item--name truncate">{{
orderData.order_goods[0]?.goods_name
}}</view>
<text>x{{ orderData.order_goods[0]?.goods_num }}</text>
</view>
<view class="mt-[24rpx]">
<price
:price="orderData.order_goods[0]?.goods_price"
:desc="orderData.order_goods[0]?.unit_name"
/>
</view>
</view>
</view>
</view>
<!-- 上门时间 -->
<view class="card normal text-base flex justify-between">
<view>上门时间</view>
<view>
{{ orderData.appoint_time }} {{ orderData.appoint_week }}
{{ orderData.door_time }}
</view>
</view>
<!-- 服务金额 -->
<view class="card normal text-base">
<view class="flex justify-between">
<view>服务金额</view>
<view>¥{{ orderData.order_amount }}</view>
</view>
<view class="mt-[30rpx] flex justify-between">
<view>实付金额</view>
<view class="text-primary">¥{{ orderData.total_amount }}</view>
</view>
</view>
<!-- 备注 -->
<view class="card flex justify-between flex-wrap normal text-base">
<view>备注</view>
<view style="width: 100%; word-wrap: break-word">{{ orderData.user_remark }}</view>
</view>
<!-- 订单信息 -->
<view class="card normal text-base">
<view class="flex justify-between">
<view>订单编号</view>
<view>{{ orderData.sn }}</view>
</view>
<view class="mt-[30rpx] flex justify-between">
<view>支付方式</view>
<view>{{ orderData.pay_way_desc }}</view>
</view>
<view class="mt-[30rpx] flex justify-between">
<view>下单时间</view>
<view>{{ orderData.create_time }}</view>
</view>
</view>
</view>
</uni-transition>
<view
class="footer flex justify-end"
v-if="orderData.confirm_service_btn || orderData.verification_btn"
>
<order-footer
:orderId="orderData.id"
:confirmService="orderData.confirm_service_btn"
:verification="orderData.verification_btn"
:goods_image="orderData.order_goods[0].goods_image"
:goods_name="orderData.order_goods[0].goods_name"
@refresh="initOrderDetail"
/>
</view>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { apiStaffOrderDetail } from '@/api/order'
import OrderFooter from '@/components/order-footer/index.vue'
const orderData = ref<any>({
order_goods: [],
order_amount: '',
total_amount: '',
total_goods_price: ''
})
const orderId = ref<number | string>('')
const initOrderDetail = async (): Promise<void> => {
orderData.value = await apiStaffOrderDetail({ id: orderId.value })
}
onLoad((options) => {
orderId.value = parseInt(options?.id)
initOrderDetail()
})
</script>
<style lang="scss">
.order_detail {
height: 100%;
padding: 30rpx 24rpx;
padding-bottom: 140rpx;
background: linear-gradient(to bottom, #f36161 200rpx, transparent 0);
.statusDec {
color: rgba(255, 255, 255, 1) !important;
}
.header-image {
width: 44rpx;
height: 44rpx;
}
.card {
padding: 24rpx;
margin-bottom: 20rpx;
background-color: #fff;
border-radius: 10rpx;
&--header {
padding-bottom: 20rpx;
}
.title {
color: #000;
font-size: 28rpx;
.num {
color: #666;
font-size: 28rpx;
}
}
}
.goods-item {
display: flex;
&--name {
width: 430rpx;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}
.footer {
left: 0;
bottom: 0;
width: 100%;
height: 108rpx;
position: fixed;
padding: 20rpx 30rpx;
background-color: #fff;
box-shadow: 2rpx 2rpx 22rpx rgba($color: #000, $alpha: 0.2);
.btn {
width: 320rpx;
}
}
</style>

View File

@@ -0,0 +1,53 @@
<template>
<view class="main">
<view class="credential mb-[30rpx]">
<view class="flex items-center pt-[10rpx] pb-[30rpx]">
<view class="w-[6rpx] h-[32rpx]" :style="{'background-color':$theme.primaryColor}"></view>
<text class="ml-[10rpx] text-2xl">营业执照</text>
</view>
<u-image
:src="business_license"
height="260rpx"
mode="heightFix"
border-radius="16rpx"
class="credential-img"
@click="previewImage(business_license)"
></u-image>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import {onLoad} from '@dcloudio/uni-app'
const business_license = ref()
//预览图片
const previewImage = (url:string) => {
uni.previewImage({
urls: [url]
})
}
onLoad((options) => {
business_license.value = options?.business_license || ''
})
</script>
<style lang="scss" scoped>
.main {
padding: 20rpx 20rpx 40rpx;
.info {
padding: 30rpx 24rpx 40rpx;
background-color: #ffffff;
border-radius: 20rpx;
}
.credential {
padding: 20rpx;
background-color: #ffffff;
border-radius: 20rpx;
text-align: center;
}
}
</style>

View File

@@ -0,0 +1,93 @@
<template>
<u-popup
v-model="showPopup"
mode="bottom"
border-radius="12"
:closeable="true"
:mask-close-able="true"
>
<view style="padding: 30rpx; min-height: 100rpx">
<view class="font-medium text-lg pb-4 text-center">
{{ data.name }}
</view>
<view>
<view class="flex items-center">
<view class="w-[6rpx] h-[28rpx]" :style="{'background-color':$theme.primaryColor}"></view>
<text class="ml-[10rpx]">商家介绍</text>
</view>
<view class="mt-[20rpx] text-muted text-sm">{{ data.synopsis }}</view>
</view>
<view class="mt-[30rpx] mb-[50rpx]">
<view class="flex items-center">
<view class="w-[6rpx] h-[28rpx]" :style="{'background-color':$theme.primaryColor}"></view>
<text class="ml-[10rpx]">商家信息</text>
</view>
<view class="mt-[20rpx] text-muted text-sm">
<view class="u-flex justify-between w-full mb-[24rpx]">
<view class="u-flex flex-1 w-full truncate">
<view><u-icon name="map" color="#909399"></u-icon></view>
<view class="text-sm text-muted ml-[6rpx]">{{ data.shop_address_detail }}</view>
</view>
<!-- <view @click="openLocation()"><u-icon name="phone-fill" :color="$theme.primaryColor"></u-icon></view> -->
</view>
<view class="u-flex justify-between w-full mb-[24rpx]">
<view class="u-flex flex-1 w-full truncate">
<view><u-icon name="clock" color="#909399"></u-icon></view>
<view class="text-sm text-muted ml-[6rpx]">{{ '营业时间:'+data.business_time_desc }}</view>
</view>
</view>
<view class="u-flex justify-between w-full mb-[24rpx]">
<view class="u-flex flex-1 w-full truncate" @click="router.navigateTo({path: '/bundle/pages/shop_credential/index?business_license='+data.business_license})">
<view><u-icon name="order" color="#909399"></u-icon></view>
<view class="text-sm text-muted mx-[6rpx]">查看营业资质</view>
<view><u-icon name="arrow-right" color="#909399" size="20rpx"></u-icon></view>
</view>
</view>
</view>
</view>
</view>
</u-popup>
</template>
<script lang="ts" setup>
import {computed, onMounted, ref} from 'vue'
import { useRoute, useRouter } from 'uniapp-router-next'
const router = useRouter()
const props = defineProps({
show: {
type: Boolean
},
data: {
type: Object,
default: () => ({})
}
})
const emit = defineEmits<{
(event: 'update:show', show: boolean): void
(event: 'update:data', value: any): void
}>()
const showPopup = computed({
get() {
return props.show
},
set(val) {
emit('update:show', val)
}
})
//查看位置
const openLocation = () => {
uni.openLocation({
latitude: parseFloat(props.data.latitude as string),
longitude: parseFloat(props.data.longitude as string),
name: props.data.shop_address_detail,
address: props.data.region_desc,
success: function () {
showPopup.value = false
}
});
}
</script>

View File

@@ -0,0 +1,392 @@
<template>
<page-meta :page-style="$theme.pageStyle">
<!-- #ifndef H5 -->
<navigation-bar :front-color="$theme.navColor" :background-color="$theme.navBgColor" />
<!-- #endif -->
</page-meta>
<view class="coach-detail">
<!-- #ifndef H5 -->
<u-sticky h5-nav-height="0" bg-color="transparent">
<u-navbar
:is-back="true"
:is-fixed="true"
title="商家详情"
:immersive="true"
:border-bottom="false"
:title-bold="false"
:background="{ background: `rgba(256,256, 256, ${percent})` }"
:title-color="percent > 0.5 ? '#000' : !isEmpty ? '#000' : '#fff'"
:back-icon-color="percent > 0.5 ? '#000' : !isEmpty ? '#000' : '#fff'"
>
</u-navbar>
</u-sticky>
<!-- #endif -->
<view v-if="isEmpty">
<!-- 轮播图 -->
<l-swiper
:content="{
data: shopData.shop_image
}"
name="url"
height="750"
indicatorPos="bottomRight"
mode="number"
borderRadius="0"
/>
<view v-if="!shopData.shop_image?.length" class="h-[100rpx]"></view>
<!-- 商品信息 -->
<view class="coach-detail-info">
<view class="p-[24rpx] rounded-b-lg bg-white roun rounded-t-lg">
<view
class="flex items-center justify-between"
@click="showIntroducePopup = true"
>
<view class="text-xl font-bold truncate">{{ shopData.name }}</view>
<view><u-icon name="arrow-right"></u-icon></view>
</view>
<view class="py-[10rpx]">
<view>
<u-rate
:count="5"
v-model="shopData.good_comment"
size="24"
disabled
active-color="#e86016"
></u-rate>
<text class="start-text ml-1 text-xs" v-if="shopData.good_comment">
{{ shopData.good_comment.toFixed(1) }}
</text>
</view>
<view
class="text-info mt-1"
style="font-size: 24rpx"
v-if="shopData.consumption"
>{{ '¥' + shopData.consumption + '/人' }}</view
>
</view>
<view class="py-[10rpx]">
<view class="u-flex">
<view
:style="{
color: shopData.work_status ? $theme.primaryColor : '#dd6161'
}"
>{{ shopData.work_status_desc }}</view
>
<view class="text-muted ml-[20rpx]">{{
shopData.business_time_desc
}}</view>
</view>
</view>
<view class="u-flex flex-wrap text-sm text-muted">
<view
class="service-desc u-flex"
v-for="(item, index) in shopData.category_name"
:key="index"
>
<text>{{ item }}</text
><text class="line" v-if="item">|</text>
</view>
</view>
<view class="text-xs mt-4 u-flex justify-between">
<view class="flex-1 mr-[10rpx]">
<view class="text-[26rpx] mb-[6rpx]"
>{{ shopData.shop_address_detail
}}{{ shopData.shop_address_detail }}</view
>
<view class="text-muted text-sm u-flex">
<u-icon name="map" color="#909399"></u-icon>
<view>{{ shopData.region_desc }}</view>
</view>
</view>
<view class="text-[22rpx] u-flex text-center">
<view class="mr-[40rpx]" @click="call(shopData.mobile)">
<view>
<u-image
width="40rpx"
height="40rpx"
src="@/bundle/static/images/icon_phone.png"
></u-image>
</view>
<view>电话</view>
</view>
<view @click="openLocation()">
<view>
<u-image
width="40rpx"
height="40rpx"
src="@/bundle/static/images/icon_address.png"
></u-image>
</view>
<view>导航</view>
</view>
</view>
</view>
</view>
</view>
<view class="rounded-[20rpx] bg-white p-[24rpx] m-[30rpx]">
<tabs
show-bar
:isScroll="false"
height="70"
bar-width="60"
font-size="32rpx"
:bold="true"
inactiveColor="#333"
:activeColor="$theme.primaryColor"
>
<tab name="服务项目">
<view class="mt-4" v-for="item in shopData.goods_lists" :key="item.id">
<project :data="item" :shop_id="shopId" :is_shop="1"/>
</view>
</tab>
<tab name="用户评价">
<evaluate
v-for="item in shopData.comment_lists"
:index="item.id"
:data="item"
/>
</tab>
</tabs>
</view>
<view class="footer u-flex fixed">
<view class="flex-1">
<u-button type="primary" @click="handleCollection(shopData.is_collect)">
<u-icon
:name="shopData.is_collect ? 'star-fill' : 'star'"
color="#ffffff"
size="30"
class="mb-[1rpx]"
/>
<text class="text-base font-medium ml-1">加收藏</text>
</u-button>
</view>
</view>
</view>
<view v-else class="empty">
<u-empty
text="抱歉该商家不存在~"
:src="'/static/images/empty/collection.png'"
:icon-size="300"
color="#888888"
>
<template #bottom>
<view class="empty-bottom">
<button
class="bg-primary text-lg text-white leading-[80rpx] h-[80rpx]"
@click="router.reLaunch('/pages/shop/index')"
>
去看看其它
</button>
</view>
</template>
</u-empty>
</view>
</view>
<!-- 返回顶部按钮 -->
<u-back-top
:scroll-top="scrollTop"
:top="100"
:customStyle="{
backgroundColor: '#FFF',
color: '#000',
boxShadow: '0px 3px 6px rgba(0, 0, 0, 0.1)'
}"
>
</u-back-top>
<!-- 简介 -->
<introduce-popup v-model:show="showIntroducePopup" v-model:data="shopData" />
</template>
<script lang="ts">
export default {
options: {
styleIsolation: 'shared'
}
}
</script>
<script setup lang="ts">
import { ref, computed, reactive, shallowRef, unref } from 'vue'
import { onLoad, onPageScroll, onShow } from '@dcloudio/uni-app'
import { apishopDetail } from '@/api/shop'
import { useUserStore } from '@/stores/user'
import { useAppStore } from '@/stores/app'
import { useRouter } from 'uniapp-router-next'
import { locationState } from '@/hooks/useLocation'
import project from '@/components/project-card/index.vue'
import evaluate from '@/components/evaluate-card/index.vue'
import { putCollect } from '@/api/store'
import IntroducePopup from './component/introduce.vue'
import { getClient } from '@/utils/client'
const router = useRouter()
const appStore = useAppStore()
const userStore = useUserStore()
const shopData = ref<any>({})
const isLogin = computed(() => userStore.token)
const shopId = ref<number | string>('')
const scrollTop = ref<number>(0)
const percent = ref<number>(0)
const isEmpty = ref<boolean>(true)
const showIntroducePopup = ref(false)
/**
* @description 获取商品详情
*/
const initializeCoachDetails = async (): Promise<void> => {
try {
shopData.value = await apishopDetail({
id: shopId.value,
longitude: locationState.longitude,
latitude: locationState.latitude,
terminal: getClient()
})
shopData.value.shop_image = shopData.value.shop_image.map((item) => {
return item.uri
})
} catch (error) {
console.log(error)
isEmpty.value = false
}
}
// 收藏
const handleCollection = async (is_collect: number): Promise<void> => {
if (!isLogin.value) {
router.navigateTo('/pages/login/login')
return
}
try {
await putCollect({
id: unref(shopId) as number,
type: 3
})
if (is_collect) {
uni.$u.toast('取消收藏成功')
} else {
uni.$u.toast('收藏成功')
}
await initializeCoachDetails()
} catch (error) {
//TODO handle the exception
console.log('收藏请求错误', error)
}
}
//拨打电话
const call = (phoneNumber: string) => {
console.log('phoneNumber', phoneNumber)
uni.makePhoneCall({
phoneNumber: phoneNumber
})
}
//查看位置
const openLocation = () => {
uni.openLocation({
latitude: parseFloat(shopData.value.latitude as string),
longitude: parseFloat(shopData.value.longitude as string),
name: shopData.value.shop_address_detail,
address: shopData.value.region_desc,
success: function () {}
})
}
onLoad((options) => {
shopId.value = options?.id || 0
initializeCoachDetails()
})
onPageScroll((event: any) => {
scrollTop.value = event.scrollTop
const top = uni.upx2px(100)
percent.value = event.scrollTop / top > 1 ? 1 : event.scrollTop / top
})
</script>
<style lang="scss" scoped>
.coach-detail {
padding-bottom: 200rpx;
:deep(.u-swiper-indicator) {
bottom: 100rpx !important;
}
.coach-detail-info {
position: relative;
z-index: 10;
margin: -70rpx 30rpx 0 30rpx;
border-radius: 20rpx;
.introduct {
text {
font-size: 24rpx;
border-radius: 8rpx;
margin-right: 14rpx;
padding: 4rpx 12rpx;
background-color: #f6f7f8;
}
}
.tips {
text {
position: relative;
margin-right: 20rpx;
&:not(:first-child) {
::before {
content: '·';
position: absolute;
left: -15rpx;
top: 50%;
transform: translateY(-50%);
}
}
}
}
}
}
.service-desc .line {
margin: 0 10rpx;
}
.service-desc:last-child .line {
display: none;
}
// 收藏图标
.collection {
width: 44rpx;
height: 44rpx;
}
// 底部按钮
.footer {
left: 0;
bottom: 0;
width: 100%;
z-index: 10;
position: fixed;
padding: 20rpx 30rpx;
background-color: #ffffff;
box-shadow: 0 -4px 48px 0 #141a231f;
padding-bottom: calc(env(safe-area-inset-bottom) + 20rpx);
}
// 服务下架或不存在时
.empty {
padding-top: 200rpx;
.empty-bottom {
width: 90vw;
margin-top: 130rpx;
}
}
</style>

View File

@@ -0,0 +1,90 @@
<template>
<view class="container">
<image v-if="imageShow" src="@/bundle/static/images/Alipay.png" class="img"></image>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { onLoad,onShow,onUnload } from '@dcloudio/uni-app'
import { apiJumpPayPrepay,getPayResult } from '@/api/app'
const order_id = ref()
const from = ref()
const pay_way = ref()
const token = ref()
let imageShow = ref(false)
const chatInterval = ref()
onLoad((param) => {
order_id.value = param.order_id
from.value = param.from
pay_way.value = param.pay_way
token.value = param.key
})
onShow(async () => {
//#ifdef H5
let ua = navigator.userAgent.toLowerCase();
if (ua.match(/MicroMessenger/i)) {
imageShow.value = true
chatInterval.value = setInterval(() => {
const payResult = getPayResult({from:from.value,order_id:order_id.value},token.value)
.then(function (response) {
console.log(response)
if(response.pay_status == 1) {
clearInterval(chatInterval.value)
uni.showModal({
showCancel: false,
content: '支付成功',
success: () => {
if(from.value == 'order') {
const param = JSON.stringify({
order_id: order_id.value,
from: from.value
})
uni.reLaunch({
url: `/bundle/pages/payment_result/payment_result?param=${param}`
})
}
if(from.value == 'recharge') {
uni.reLaunch({
url: '/bundle/pages/user_wallet/user_wallet?isPay=true'
})
}
}
})
}
})
}, 1000)
return
}
//不是微信游览器
const res = await apiJumpPayPrepay({
from: from.value,
pay_way: pay_way.value,
order_id: order_id.value
},token.value)
console.log(res)
const alipayPage = document.createElement('div')
alipayPage.innerHTML = res.config
document.body.appendChild(alipayPage)
document.forms[0].submit()
//#endif
})
onUnload((param) => {
clearInterval(chatInterval.value)
})
</script>
<style lang="scss" scoped>
page {
height: 100%;
background-color: black;
}
.container {
padding: 0 40rpx;
display: flex;
justify-content: end;
.img {
height: 750rpx;
}
}
</style>

View File

@@ -0,0 +1,292 @@
<template>
<page-meta :page-style="$theme.pageStyle">
<!-- #ifndef H5 -->
<navigation-bar :front-color="$theme.navColor" :background-color="$theme.navBgColor" />
<!-- #endif -->
</page-meta>
<view class="user-address flex flex-col min-h-0 h-full">
<u-sticky h5-nav-height="0" bg-color="transparent">
<u-navbar
:is-back="true"
:is-fixed="true"
title="地址管理"
:border-bottom="false"
:title-bold="false"
:background="{ background: '#f6f7f8' }"
title-color="#000000"
>
</u-navbar>
</u-sticky>
<view class="flex-1">
<z-paging
v-model="addressList"
ref="paging"
:fixed="false"
height="100%"
auto-show-back-to-top
:empty-view-img="EmptyAddress"
:hide-no-more-inside="true"
hide-empty-view
@query="queryList"
>
<view
class="address-item u-flex justify-between mt-[20rpx]"
v-for="(item, index) in addressList"
:key="item.id"
:data-id="item.id"
@click.stop="onSelect"
>
<view class="flex-1" >
<view class="sm text-muted text-base mb-[20rpx]">
{{ item.province }}
{{ item.city }}
{{ item.district }}
<text class="default" v-if="item.is_default">默认地址</text>
</view>
<view>
<text class="text-base font-bold">{{ item.address }} {{ item?.house_number }}</text>
</view>
<view class="mt-[10rpx] sm text-muted text-[24rpx]">
{{ item.contact }} {{ item.gender_desc }} {{ item.mobile }}
</view>
</view>
<view class="flex">
<view class="mr-[40rpx]" @click.stop="goEditAddress(item.id)">
<u-icon name="edit-pen" color="#909399" :size="40"></u-icon>
</view>
<view class="" @click.stop="openDelAddress(item.id)">
<u-icon name="trash" color="#909399" :size="40"></u-icon>
</view>
</view>
</view>
<!-- 不可用-不在开通范围-->
<template v-if="disableServerList.length">
<view class="mx-[30rpx] mt-[30rpx]" >
以下地址暂不在开通范围
</view>
<view class="address-item u-flex justify-between mt-[20rpx]" v-for="item in disableServerList" :key="item.id">
<view class="flex-1 text-[#CCCCCC]">
<view class="sm text-muted text-base mb-[20rpx]">
{{ item.province }}
{{ item.city }}
{{ item.district }}
</view>
<view>
<text class="text-base font-bold">{{ item.address }} {{ item?.house_number }}</text>
</view>
<view class="mt-[10rpx] sm text-muted text-[24rpx]">
{{ item.contact }} {{ item.gender_desc }} {{ item.mobile }}
</view>
</view>
<view class="flex">
<view class="mr-[40rpx]" @click.stop="goEditAddress(item.id)">
<u-icon name="edit-pen" color="#CCCCCC" :size="40"></u-icon>
</view>
<view class="" @click.stop="openDelAddress(item.id)">
<u-icon name="trash" color="#CCCCCC" :size="40"></u-icon>
</view>
</view>
</view>
</template>
<!-- 不可用 - 不在服务范围 -->
<template v-if="disableDistanceList.length">
<view class="mx-[30rpx] mt-[30rpx]" >
以下地址暂不在服务范围
</view>
<view class="address-item u-flex justify-between mt-[20rpx]" v-for="item in disableDistanceList" :key="item.id">
<view class="flex-1 text-[#CCCCCC]">
<view class="sm text-muted text-base mb-[20rpx]">
{{ item.province }}
{{ item.city }}
{{ item.district }}
</view>
<view>
<text class="text-base font-bold">{{ item.address }} {{ item?.house_number }}</text>
</view>
<view class="mt-[10rpx] sm text-muted text-[24rpx]">
{{ item.contact }} {{ item.gender_desc }} {{ item.mobile }}
</view>
</view>
<view class="flex">
<view class="mr-[40rpx]" @click.stop="goEditAddress(item.id)">
<u-icon name="edit-pen" color="#CCCCCC" :size="40"></u-icon>
</view>
<view class="" @click.stop="openDelAddress(item.id)">
<u-icon name="trash" color="#CCCCCC" :size="40"></u-icon>
</view>
</view>
</view>
</template>
</z-paging>
</view>
<view class="flex items-center justify-between bg-white footer">
<view>
<u-button
type="primary"
@click="router.navigateTo('/bundle/pages/user_address_edit/index')"
>
+ 新增地址
</u-button>
</view>
</view>
</view>
<!-- 删除提示弹窗 -->
<modal-popup
v-model:show="deleteState.showPopup"
title="温馨提示"
content="确定要删除这个地址吗?"
@confirm="deleteAddress"
/>
</template>
<script lang="ts" setup>
import { shallowRef, ref, reactive } from 'vue'
import { onLoad, onShow, onUnload } from '@dcloudio/uni-app'
import { apiAddressLists, apiAddressEditDefault, apiAddressDel } from '@/api/user'
import { useRouter } from 'uniapp-router-next'
import EmptyAddress from '@/static/images/empty/address.png'
import { location, useLocation } from '@/hooks/useLocation'
const router = useRouter()
const goodsId = ref(null)
const coachId = ref(null)
const paging = shallowRef<any>(null)
const type = ref(false)
const addressList = ref<any>([]) // 地址列表-可用
const disableDistanceList = ref<any>([]) // 地址列表-不在服务范围
const disableServerList = ref<any>([]) // 地址列表-不在开通范围
const isIOS = uni.getSystemInfoSync().system.includes('iOS')
// 删除地址状态
const deleteState = reactive({
showPopup: false,
address_id: -1
})
// 选择当前地址作为支付地址
const onSelect = async (event: any) => {
const id = event?.currentTarget?.dataset?.id
console.log('id=>', id)
try {
if (type.value) {
uni.$emit('address', id)
router.navigateBack()
} else {
await apiAddressEditDefault({ id })
router.redirectTo('/bundle/pages/user_address/index')
}
} catch (error) {
console.log(error, '设置默认地址捕捉错误')
}
}
// 编辑地址
const goEditAddress = (params = '') => {
router.navigateTo({
path: '/bundle/pages/user_address_edit/index',
query: {
id: params
}
})
}
// 删除地址
const openDelAddress = (id: number) => {
deleteState.showPopup = true
deleteState.address_id = id
}
const deleteAddress = async () => {
await apiAddressDel({
id: deleteState.address_id
})
paging.value.refresh()
}
// 获取地址列表
const queryList = async (page_no: number, page_size: number) => {
try {
const res = await apiAddressLists({
page_no,
page_size,
// longitude: location.longitude,
// latitude: location.latitude,
goods_id: goodsId.value,
coach_id: coachId.value
})
console.log(res, addressList.value)
addressList.value = res.usable
disableDistanceList.value = res.distance_disable
disableServerList.value = res.server_disable
paging.value.setLocalPaging(res.usable)
} catch (e) {
console.log('报错=>', e)
paging.value.setLocalPaging(false)
}
}
onLoad((options: any) => {
if (options.type) {
type.value = options.type
}
if (options.goodsId) {
goodsId.value = options.goodsId
}
if (options.coachId) {
coachId.value = options.coachId
}
})
onShow(() => {
paging.value?.refresh()
})
onUnload(() => {
uni.$emit('changeAddress')
})
</script>
<style lang="scss" scoped>
.user-address {
.address-item {
border-radius: 24rpx;
background-color: #ffffff;
margin: 30rpx 30rpx;
padding: 28rpx 36rpx 28rpx 20rpx;
.default {
padding: 2px 10rpx;
margin-left: 20rpx;
border: 1px solid var(--color-primary);
border-radius: 6rpx;
font-size: 22rpx;
color: var(--color-primary);
}
.setting {
height: 90rpx;
width: 100rpx;
line-height: 90rpx;
text-align: right;
color: #f36161;
font-size: 26rpx;
}
}
.footer {
height: 118rpx;
line-height: 118rpx;
padding: 0 30rpx;
box-sizing: content-box;
padding-bottom: env(safe-area-inset-bottom);
> view {
width: 100%;
}
}
}
</style>

View File

@@ -0,0 +1,361 @@
<template>
<page-meta :page-style="$theme.pageStyle">
<!-- #ifndef H5 -->
<navigation-bar :front-color="$theme.navColor" :background-color="$theme.navBgColor" />
<!-- #endif -->
</page-meta>
<view class="user-add__edit flex flex-col min-h-0 h-full">
<u-sticky h5-nav-height="0" bg-color="transparent">
<u-navbar
:is-back="true"
:is-fixed="true"
:title="!addressId ? '新增地址' : '编辑地址'"
:border-bottom="false"
:title-bold="false"
:background="{ background: '#f6f7f8' }"
title-color="#000000"
>
</u-navbar>
</u-sticky>
<!-- Mian Start -->
<view class="main flex-1">
<view class="bg-white m-[30rpx]" style="border-radius: 24rpx">
<u-form
:model="formData"
ref="formRef"
:border-bottom="false"
:error-type="['message', 'toast']"
>
<!-- 地址 -->
<view class="card">
<u-form-item prop="region">
<view class="flex flex-1 items-center" @click="chooseLocation">
<view class="label"> 地址</view>
<view class="flex-1">
<span v-if="formData.region">
{{ formData.region }}
</span>
<span v-else class="text-muted">请选择地址</span>
<!-- <u-input
v-model="formData.region"
placeholder="请选择省市区"
:disabled="true"
/> -->
</view>
<!--#ifdef H5 -->
<!-- <view
class="absolute w-full h-full opacity-0"
@click="chooseLocation"
></view> -->
<!-- #endif -->
<u-icon name="arrow-right" size="22" color="#888888"></u-icon>
</view>
</u-form-item>
</view>
<!-- 门牌号 -->
<view class="card">
<u-form-item prop="house_number">
<view class="flex w-full col-start">
<view class="label"> 门牌号</view>
<view class="flex-1 pt-[6rpx]">
<u-input
v-model="formData.house_number"
placeholder="请输入门牌号"
/>
</view>
</view>
</u-form-item>
</view>
<!-- 联系人 -->
<view class="card">
<u-form-item prop="contact">
<view class="flex">
<view class="label"> 联系人</view>
<view class="flex-1">
<u-input
v-model="formData.contact"
placeholder="请输入联系人"
/>
<view>
<u-radio-group
v-model="formData.gender"
size="24"
labelSize="24"
width="150"
:active-color="$theme.primaryColor"
>
<u-radio :name="1" size="28" labelSize="24">
<text class="mt-2">先生</text>
</u-radio>
<u-radio :name="2" size="28" labelSize="24">
<text class="mt-2">女士</text>
</u-radio>
</u-radio-group>
</view>
</view>
</view>
</u-form-item>
</view>
<!-- 手机号 -->
<view class="card">
<u-form-item prop="mobile">
<view class="flex">
<view class="label"> 手机号</view>
<u-input
v-model="formData.mobile"
placeholder="请输入联系人手机号"
/>
</view>
</u-form-item>
</view>
</u-form>
</view>
<!-- 是否默认地址 -->
<view
class="flex justify-between items-center bg-white mx-[30rpx] px-[20rpx] py-[30rpx]"
style="border-radius: 24rpx"
>
<view>
<view class="text-base font-bold text-main">设为默认地址</view>
<view class="text-xs text-muted mt-2">提示:下单时会优先使用该地址</view>
</view>
<view class="pr-4">
<u-switch
v-model="formData.is_default"
size="40"
:active-color="$theme.primaryColor"
:active-value="1"
:inactive-value="0"
></u-switch>
</view>
</view>
</view>
<!-- Mian End -->
<!-- Footer Start -->
<view class="flex p-[20rpx] pt-[30rpx] bg-white safe-area-inset-bottom">
<view class="flex-1">
<u-button type="primary" @click="onSubmit"> 保存 </u-button>
</view>
</view>
<!-- Footer End -->
</view>
</template>
<script lang="ts" setup>
// +----------------------------------------------------------------------
// | LikeShop100%开源免费商用电商系统
// +----------------------------------------------------------------------
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
// | 开源版本可自由商用可去除界面版权logo
// | 商业版本务必购买商业授权,以免引起法律纠纷
// | 禁止对系统程序代码以任何目的,任何形式的再发布
// | Gitee下载https://gitee.com/likeshop_gitee/likeshop
// | 访问官网https://www.likemarket.net
// | 访问社区https://home.likemarket.net
// | 访问手册http://doc.likemarket.net
// | 微信公众号:好象科技
// | 好象科技开发团队 版权所有 拥有最终解释权
// +----------------------------------------------------------------------
// | Author: LikeShopTeam
// +----------------------------------------------------------------------
import { onLoad, onReady, onUnload } from '@dcloudio/uni-app'
import { ref, reactive, computed } from 'vue'
import { toast } from '@/utils/util'
import { getGeocoderCoordinate } from '@/api/app'
import { apiAddressDetail, apiAddressEdit, apiAddressAdd, apiAddressDel } from '@/api/user'
import router from '@/router'
type AddressType = {
contact: string // 联系方式
mobile: string | number // 手机号码
province: string | number // 省
province_id: string | number // 省
city: string | number // 市
city_id: string | number // 市
district: string | number // 区
district_id: string | number // 区
address: string // 详细地址
is_default: number | boolean // 是否默认
gender: number // 性别
region: string
longitude: string // 经度
latitude: string // 纬度
house_number: string // 门牌号
}
const formData = ref<AddressType>({
contact: '',
mobile: '',
province: '',
province_id: '',
city: '',
city_id: '',
district: '',
district_id: '',
address: '',
gender: 1,
is_default: 0,
region: '',
longitude: '',
latitude: '',
house_number: '' // 门牌号
})
const formRef = ref()
const addressId = ref<string | number>('')
const rules = ref<object>({
contact: [
{ required: true, message: '请输入联系人', trigger: ['change', 'blur'] },
{ min: 1, max: 20, message: '输入长度不得超过20位', trigger: ['blur', 'change'] }
],
mobile: [
{ required: true, message: '请输入手机号码', trigger: ['change', 'blur'] },
{
pattern: /^1[3-9]\d{9}$/,
transform(value: any) {
return String(value)
},
message: '请输入正确的手机号'
}
],
region: [{ required: true, message: '请选择地址', trigger: ['change', 'blur'] }],
house_number: [{ required: true, message: '请输入门牌号', trigger: ['change', 'blur'] }]
})
// 获取地址详情
const getAddressDetail = async (): Promise<void> => {
formData.value = await apiAddressDetail({ id: addressId.value })
formData.value.region = `${formData.value.city} ${formData.value.district} ${formData.value.address}`
}
// 处理地址
const getAddressInfo = async (address: string, title: string): Promise<void> => {
// 获取 区或县 后面的详细地址
const result = address.match(/(?:.*?(?:县|区))?(.*)/)
const part = result?.[1]?.trim() || '' // 无行政区时返回原地址
console.log('part =>', part)
formData.value.address = part + ' ' + title
console.log('formData.value.address =>', formData.value.address)
}
// 校验
const onSubmit = () => {
formRef.value.validate((valid: boolean) => {
if (!valid) return false
if (!addressId.value) handleAddressAdd()
else handleAddressEdit()
})
}
// 添加地址
const handleAddressAdd = async (): Promise<void> => {
await apiAddressAdd({ ...formData.value })
setTimeout(() => {
uni.navigateBack()
}, 300)
}
// 编辑地址
const handleAddressEdit = async (): Promise<void> => {
await apiAddressEdit({ ...formData.value })
setTimeout(() => {
uni.navigateBack()
}, 300)
}
// 选择省市区
const chooseLocation = () => {
router.navigateTo({
path: '/bundle/pages/city/index',
query: {
is_address: 1
}
})
}
// 逆解析地址
const getLocation = async (latitude: string | number, longitude: string | number) => {
try {
const res = await getGeocoderCoordinate({
location: `${latitude},${longitude}`
})
const data = res.result
console.log('逆解析地址 =>', res.result)
handleAddressInfo(data)
} catch (error) {
console.log('地址逆解析', error)
}
}
// 处理接口返回的地址数据
const handleAddressInfo = (event: any) => {
let city_id = event.ad_info.city_code.substr(3, 6)
if (city_id == 110000 || city_id == 310000 || city_id == 210000 || city_id == 410000) {
city_id = city_id * 1
city_id += 100
}
formData.value.city_id = city_id + ''
formData.value.province_id = formData.value.city_id.substr(0, 3) + '000'
formData.value.district_id = event.ad_info.adcode
formData.value.region = `${event.ad_info.city} ${event.ad_info.district} ${formData.value.address}`
formData.value.longitude = event.location.lng
formData.value.latitude = event.location.lat
}
// 删除地址
const handleAddressDel = async (): Promise<void> => {
try {
await apiAddressDel({ id: addressId.value })
setTimeout(() => {
uni.navigateBack()
}, 300)
} catch (error) {
console.log(error)
}
}
onLoad((options: { id?: number }) => {
addressId.value = Number(options.id)
if (options.id) {
getAddressDetail()
}
})
uni.$on('city_select', (value: any) => {
console.log('选择地址 ==>', value)
getAddressInfo(value.address || '', value.title)
getLocation(value.latitude, value.longitude)
})
onReady(() => {
formRef.value?.setRules(rules.value)
})
onUnload(() => {
console.log('注销?')
uni.$off(['city_select'])
})
</script>
<style lang="scss" scoped>
.user-add__edit {
.card {
padding: 0 24rpx;
border-radius: 14rpx;
background-color: #ffffff;
.label {
width: 160rpx;
color: #333333;
font-size: 28rpx;
font-weight: 900;
line-height: 70rpx;
}
}
.is-default {
image {
width: 34rpx;
height: 34rpx;
}
}
}
</style>

View File

@@ -0,0 +1,461 @@
<template>
<page-meta :page-style="$theme.pageStyle">
<!-- #ifndef H5 -->
<navigation-bar
:front-color="$theme.navColor"
:background-color="$theme.navBgColor"
/>
<!-- #endif -->
</page-meta>
<view class="user-data p-[30rpx]">
<u-sticky h5-nav-height="0" bg-color="transparent">
<u-navbar
:is-back="true"
:is-fixed="true"
title="个人资料"
:border-bottom="false"
:title-bold="false"
:background="{ background: `rgba(256,256, 256, 0)` }"
:title-color="'#000'"
>
</u-navbar>
</u-sticky>
<!-- Main Start -->
<!-- 头部修改头像 -->
<!-- <view class="header flex flex-col justify-center items-center">
<button
class="relative flex flex-col items-center after:border-0"
hover-class="none"
open-type="chooseAvatar"
style="background-color: transparent"
@click="chooseAvatar"
@chooseavatar="chooseAvatar"
>
<image
class="absolute"
style="width: 52rpx; height: 52rpx; right: 30rpx; bottom: 70rpx;"
src="@/static/images/user_profile_photo.png"
></image>
<image :src="userInfo?.avatar" style="width: 192rpx; height: 192rpx"/>
<view
class="mt-[10rpx] text-center text-muted text-xs"
@click.stop="(showUserName = true), (newUsername = userInfo?.account)"
>
账号: {{ userInfo?.account }}
</view>
</button>
</view> -->
<!-- 头像 -->
<view
class="item rounded-t-[20rpx] mt-[20rpx] btn-border flex flex-1 justify-between items-center"
>
<button
class="relative flex flex-1 justify-between items-center after:border-0 px-[0rpx]"
hover-class="none"
open-type="chooseAvatar"
style="background-color: transparent"
@click="chooseAvatar"
@chooseavatar="chooseAvatar"
>
<view class="label text-left">头像</view>
<u-image :src="userInfo?.avatar" width="80" height="80" border-radius="50%"></u-image>
</button>
</view>
<view
class="item flex flex-1 justify-between items-center"
>
<view class="label">昵称</view>
<view class="content text-right pr-2" @click="toChangeAccount(userInfo?.nickname, FieldType.NICKNAME)">{{ userInfo?.nickname }}</view>
<u-icon name="arrow-right" size="22" color="#666" @click="toChangeAccount(userInfo?.nickname, FieldType.NICKNAME)"></u-icon>
</view>
<!-- 账号 -->
<view
class="item flex flex-1 justify-between items-center"
>
<view class="label">账号</view>
<view class="content text-right pr-2" @click="toChangeAccount(userInfo?.account, FieldType.USERNAME)">{{ userInfo?.account }}</view>
<u-icon name="arrow-right" size="22" color="#666" @click="toChangeAccount(userInfo?.account, FieldType.USERNAME)"></u-icon>
</view>
<!-- 性别 -->
<view class="item flex flex-1 justify-between items-center" @click="changeSex">
<view class="label">性别</view>
<view class="content text-right pr-2" v-if="userInfo?.sex === 0">未知</view>
<view class="content text-right pr-2" v-if="userInfo?.sex === 1">先生</view>
<view class="content text-right pr-2" v-if="userInfo?.sex === 2">女士</view>
<u-icon name="arrow-right" size="22" color="#666"></u-icon>
</view>
<!-- 手机号 -->
<view class="item flex flex-1 justify-between items-center">
<view class="label">手机号</view>
<view class="content text-right pr-2" @click="toBindMobile">{{
userInfo?.mobile == '' ? '未绑定手机号' : userInfo?.mobile
}}
</view>
<u-icon name="arrow-right" size="22" color="#666" @click="toBindMobile"></u-icon>
<!-- #ifdef MP-WEIXIN -->
<!-- <u-button
open-type="getPhoneNumber"
@getphonenumber="getPhoneNumber"
type="error"
shape="circle"
size="mini"
:plain="true"
class="btn"
>
{{ userInfo?.mobile == '' ? '绑定手机号' : '更换手机号' }}
</u-button> -->
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<!-- <u-button
@click="showMobilePop = true"
type="error"
size="mini"
shape="circle"
:plain="true"
>
{{ userInfo?.mobile == '' ? '绑定手机号' : '更换手机号' }}
</u-button> -->
<!-- #endif -->
</view>
<!-- 注册时间 -->
<view class="item rounded-b-[20rpx] flex flex-1 justify-between items-center">
<view class="label">注册时间</view>
<view class="content text-right">{{ userInfo?.create_time }}</view>
</view>
</view>
<!-- 昵称修改组件 -->
<u-popup
v-model="showNickName"
:closeable="true"
mode="center"
:maskCloseAble="false"
border-radius="20"
>
<view class="px-[50rpx] py-[40rpx] bg-white" style="width: 85vw">
<form @submit="changeNameConfirm">
<view class="mb-[70rpx] text-xl text-center">修改昵称</view>
<u-form-item borderBottom>
<input
class="nr h-[60rpx] w-full"
:value="userInfo.nickname"
name="nickname"
type="nickname"
placeholder="请输入昵称"
/>
</u-form-item>
<view class="mt-[80rpx]">
<button
class="bg-primary text-white w-full h-[80rpx] !text-lg !leading-[80rpx] rounded-full"
form-type="submit"
hover-class="none"
size="mini"
>
确定
</button>
</view>
</form>
</view>
</u-popup>
<!-- 账号修改组件 -->
<u-popup v-model="showUserName" :closeable="true" mode="center" border-radius="20">
<view class="px-[50rpx] py-[40rpx] bg-white" style="width: 85vw">
<view class="mb-[70rpx] text-xl text-center">修改账号</view>
<u-form-item borderBottom>
<u-input
class="flex-1"
v-model="newUsername"
placeholder="请输入账号"
:border="false"
/>
</u-form-item>
<view class="mt-[80rpx]">
<u-button @click="changeUserNameConfirm" type="primary" shape="circle">
确定
</u-button>
</view>
</view>
</u-popup>
<!-- 性别修改组件 -->
<u-picker
mode="selector"
v-model="showPicker"
confirm-color="#4173FF"
:default-selector="[0]"
:range="sexList"
@confirm="changeSexConfirm"
/>
<!-- 账号修改组件 -->
<u-popup v-model="showMobilePop" :closeable="true" mode="center" border-radius="20">
<view class="px-[50rpx] py-[40rpx] bg-white" style="width: 85vw">
<view class="mb-[70rpx] text-xl text-center">修改手机号码</view>
<u-form-item borderBottom>
<u-input
class="flex-1"
v-model="newMobile"
placeholder="请输入新的手机号码"
:border="false"
/>
</u-form-item>
<u-form-item borderBottom>
<u-input
class="flex-1"
v-model="mobileCode"
placeholder="请输入验证码"
:border="false"
/>
<view
class="border-l border-solid border-0 border-light pl-3 text-muted leading-4 ml-3 w-[180rpx]"
@click="sendSms"
>
<u-verification-code
ref="uCodeRef"
:seconds="60"
@change="codeChange"
change-text="x秒"
/>
{{ codeTips }}
</view>
</u-form-item>
<view class="mt-[80rpx]">
<u-button @click="changeCodeMobile" type="primary" shape="circle"> 确定</u-button>
</view>
</view>
</u-popup>
</template>
<script lang="ts" setup>
import {ref, shallowRef} from 'vue'
import {onShow, onUnload} from '@dcloudio/uni-app'
import {apiUserInfoGet, apiUserInfoSet, userBindMobile, apiWechatMobileGet} from '@/api/user'
import {smsSend} from '@/api/app'
import {FieldType, SMSEnum} from '@/enums/appEnums'
import {uploadFile} from '@/utils/util'
// 用户信息
const userInfo = ref<any>({})
// 用户信息的枚举
const fieldType = ref(FieldType.NONE)
//选择性别数据
const sexList = ref<Array<string> | null>(['男', '女'])
//显示昵称弹窗
const showNickName = ref<boolean | null>(false)
//显示账户弹窗
const showUserName = ref<boolean | null>(false)
//显示性别选择弹窗
const showPicker = ref<boolean | null>(false)
// 显示手机号验证码调整弹窗 非小程序才需要
const showMobilePop = ref<boolean | null>(false)
//新昵称
const newNickname = ref<string>('')
//新账号
const newUsername = ref<string>('')
//新的手机号码
const newMobile = ref<string>('')
//修改手机验证码
const mobileCode = ref<string>('')
const codeTips = ref('')
const uCodeRef = shallowRef()
// 跳转修改昵称、账号页面
const toChangeAccount = (data:string, type:number) => {
uni.navigateTo({
url: `/bundle/pages/change_account/index?data=${data}&type=${type}`
})
}
// 跳转更换手机号码页面
const toBindMobile = () => {
uni.navigateTo({
url: `/pages/bind_mobile/index?mobile=${userInfo.value.mobile}`
})
}
// 获取用户信息
const getUser = async (): Promise<void> => {
userInfo.value = await apiUserInfoGet()
}
// 获取验证码显示字段
const codeChange = (text: string) => {
codeTips.value = text
}
// 发送验证码
const sendSms = async () => {
if (!newMobile.value) return uni.$u.toast('请输入新的手机号码')
if (uCodeRef.value?.canGetCode) {
await smsSend({
scene: userInfo.value.mobile ? SMSEnum.CHANGE_MOBILE : SMSEnum.BIND_MOBILE,
mobile: newMobile.value
})
uni.$u.toast('发送成功')
uCodeRef.value?.start()
}
}
// 验证码修改手机号-非微信小程序
const changeCodeMobile = async () => {
await userBindMobile({
type: userInfo.value.mobile ? 'change' : 'bind',
mobile: newMobile.value,
code: mobileCode.value
})
uni.$u.toast('操作成功')
showMobilePop.value = false
getUser()
}
// 修改用户信息
const setUserInfoFun = async (value: string): Promise<void> => {
await apiUserInfoSet({
field: fieldType.value,
value: value
})
uni.$u.toast('操作成功')
getUser()
}
// 上传头像
const chooseAvatar = (e: any) => {
fieldType.value = FieldType.AVATAR
// #ifndef MP-WEIXIN
uni.navigateTo({
url: '/uni_modules/vk-uview-ui/components/u-avatar-cropper/u-avatar-cropper?destWidth=300&rectWidth=200&fileType=jpg'
})
// #endif
// #ifdef MP-WEIXIN
if (e.detail.avatarUrl) {
uploadAvatar(e.detail.avatarUrl)
}
// #endif
}
// 显示修改用户性别弹窗
const changeSex = () => {
showPicker.value = true
fieldType.value = FieldType.SEX
}
// 修改用户性别
const changeSexConfirm = (value) => {
setUserInfoFun(value[0] + 1)
showPicker.value = false
}
// 修改用户账号
const changeUserNameConfirm = async () => {
if (newUsername.value == '') return uni.$u.toast('账号不能为空')
if (newUsername.value.length > 10) return uni.$u.toast('账号长度不得超过十位数')
fieldType.value = FieldType.USERNAME
await setUserInfoFun(newUsername.value)
showUserName.value = false
}
// 修改用户昵称
const changeNameConfirm = async (e: any) => {
newNickname.value = e.detail.value.nickname
if (newNickname.value == '') return uni.$u.toast('昵称不能为空')
if (newNickname.value.length > 10) return uni.$u.toast('昵称长度不得超过十位数')
fieldType.value = FieldType.NICKNAME
await setUserInfoFun(newNickname.value)
showNickName.value = false
}
// 微信小程序 绑定||修改用户手机号
const getPhoneNumber = async (e): Promise<void> => {
const {encryptedData, iv, code} = e.detail
const data = {
code,
encrypted_data: encryptedData,
iv
}
if (encryptedData) {
await apiWechatMobileGet({
...data
})
uni.$u.toast('操作成功')
getUser()
}
}
const goPage = (url: string) => {
uni.navigateTo({
url: url
})
}
const uploadAvatar = (path: string) => {
uni.showLoading({
title: '正在上传中...',
mask: true
})
uploadFile(path)
.then((res) => {
uni.hideLoading()
setUserInfoFun(res.url)
})
.catch(() => {
uni.hideLoading()
uni.$u.toast('上传失败')
})
}
// 监听从裁剪页发布的事件,获得裁剪结果
uni.$on('uAvatarCropper', (path) => {
uploadAvatar(path)
})
onShow(async () => {
getUser()
})
onUnload(() => {
uni.$off('uAvatarCropper')
})
</script>
<style lang="scss" scoped>
.header {
width: 100%;
height: 400rpx;
image {
border-radius: 50%;
}
}
.item {
margin-top: 2rpx;
padding: 40rpx 20rpx;
background-color: #ffffff;
.label {
width: 150rpx;
@apply text-main text-base font-medium;
}
.content {
flex: 1;
width: 80%;
padding-right: 12rpx;
@apply text-content text-base font-medium text-right;
}
}
</style>

View File

@@ -0,0 +1,201 @@
<template>
<view class="user-recharge">
<view class="wrapper">
<view class="balance">充值金额</view>
<view class="flex items-center balance-recharge-input">
<text style="font-size: 46rpx">¥</text>
<input class="input" placeholder="0.00" type="digit" v-model="rechargeData.money" />
</view>
<view class="balance-recharge-tips mt-[20rpx]">
<view>当前可用余额为 ¥ {{ walletData.user_money }}</view>
</view>
</view>
<view class="px-[30rpx]">
<button class="btn text-lg text-white rounded-full" @click="handleRecharge()">
立即充值
</button>
<navigator
class="record"
hover-class="none"
url="/bundle/pages/recharge_record/recharge_record"
>
充值记录
</navigator>
</view>
</view>
<!-- 充值成功组件 -->
<u-popup v-model="rechargeSuccess" :closeable="true" mode="center" border-radius="14">
<view class="bg-white recharge-success-card" style="width: 70vw">
<view class="recharge-success">
<image src="@/static/images/recharge_uccess.png" />
<view class="recharge-success-text"> 充值成功 </view>
</view>
<view class="p-[40rpx]">
<u-button
@click="changeRechargeComplete"
type="primary"
:ripple="true"
:hair-line="false"
shape="circle"
>
完成
</u-button>
</view>
</view>
</u-popup>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { onShow, onUnload, onLoad } from '@dcloudio/uni-app'
import { apiRecharge, apiUserWallet } from '@/api/wallet'
import { PaymentStatusEnum } from '@/utils/enum'
const rechargeAmountLists = ref<Array<any>>([])
const walletData = ref({
user_money: '' // 可用余额
})
const rechargeData = ref({
money: '', // 充值金额
template_id: '', // 充值模板id
pay_way: 1 // 支付方式
})
// 充值弹窗
const rechargeSuccess = ref<boolean | null>(false)
// 获取钱包数据
const getWalletData = () => {
apiUserWallet('').then((res: any) => {
walletData.value.user_money = res.user_money
})
}
// 获取充值模板
// const getRechargeAmountLists = () => {
// apiRechargeTemplateLists('').then((res: any) => {
// rechargeAmountLists.value = res
// })
// }
// 充值
const handleRecharge = (id = '') => {
// await apiRecharge({ ...rechargeData.value }).then(() => {
// rechargeSuccess.value = true
// })
delete rechargeData.value.template_id
if (id !== '') {
rechargeData.value.template_id = id
delete rechargeData.value.money
}
apiRecharge({ ...rechargeData.value }).then((res: any) => {
rechargeData.value.template_id = ''
const params = {
from: res.from,
order_id: res.order_id
}
// 支付方式hooks
try {
// 支付方式hooks
uni.navigateTo({
url: `/bundle/pages/order_pay/order_pay?order_id=${params.order_id}&from=${params.from}&order_amount=${rechargeData.value.money}`
})
} catch (err) {
console.log('下单', err)
}
})
}
// const changeRechargeValue = (index: any) => {
// rechargeData.value.money = rechargeAmountLists.value[index].money
// }
const changeRechargeComplete = () => {
rechargeSuccess.value = false
}
// 跳转页面
const goPage = (url: any) => {
uni.navigateTo({ url: url })
}
onLoad(async (options) => {
// 监听全局duringPayment事件
uni.$on('duringPayment', (params) => {
if (params.result === PaymentStatusEnum['SUCCESS']) {
// uni.navigateBack()
// setTimeout(() => toast('支付成功'), 0.5 * 1000)
}
})
uni.$emit('send')
if (options.rechargeSuccess == 1) {
rechargeSuccess.value = true
}
})
onUnload(() => {
uni.$off(['duringPayment', 'send'])
})
onShow(() => {
getWalletData()
// getRechargeAmountLists()
})
</script>
<style lang="scss" scoped>
.user-recharge {
.wrapper {
margin: 20rpx 30rpx;
padding: 40rpx;
width: 690rpx;
height: 330rpx;
border-radius: 20rpx;
background: #fff;
.balance {
font-weight: 500;
font-size: 30rpx;
line-height: 36rpx;
color: #666;
margin-bottom: 38rpx;
}
.balance-recharge-input {
margin-top: 35rpx;
margin-right: 66rpx;
.input {
height: 94rpx;
text-align: left;
font-size: 66rpx;
margin-left: 30rpx;
position: relative;
z-index: 2;
}
border-bottom: 1rpx solid #ebebeb;
}
.balance-recharge-tips {
font-size: 24rpx;
color: #999;
}
}
.btn {
background: $u-type-primary;
}
.record {
margin-top: 24rpx;
font-size: 28rpx;
text-align: center;
color: #666;
}
}
</style>

View File

@@ -0,0 +1,223 @@
<template>
<page-meta :page-style="$theme.pageStyle">
<!-- #ifndef H5 -->
<navigation-bar
:front-color="$theme.navColor"
:background-color="$theme.navBgColor"
/>
<!-- #endif -->
</page-meta>
<view class="user-wallet flex flex-col min-h-0 h-full">
<view class="user-wallet-view flex flex-col min-h-0 h-full">
<u-sticky h5-nav-height="0" bg-color="transparent">
<u-navbar
:is-back="true"
:is-fixed="false"
title="我的钱包"
:immersive="false"
:border-bottom="false"
:title-bold="false"
:background="{ background: `rgba(256,256, 256, 0)` }"
title-color="#000"
back-icon-color="#000"
>
</u-navbar>
</u-sticky>
<view class="wrapper backdrop-blur-sm">
<view class="flex items-center">
<u-image
:src="userStore.userInfo.avatar"
width="60"
height="60"
border-radius="50%"
></u-image>
<view class="text-base text-white ml-2">
{{ userStore.userInfo.nickname }}
</view>
</view>
<view
class="flex justify-between"
style="margin-top: 32rpx"
>
<view>
<price
:content="walletData.total_money"
main-size="72rpx"
minor-size="48rpx"
font-weight="900"
color="#ffffff"
></price>
<view class="mt-1 text-base text-white font-medium">
当前余额()
</view>
</view>
<view
class="text-primary text-center text-xs bg-white inline"
style="height: 72rpx; margin-top: 30px; padding: 18rpx 28rpx; border-radius: 30px 0 0 30px;"
@click="router.navigateTo('/bundle/pages/account_detail/account_detail')"
>
<text>余额明细</text>
<u-icon name="arrow-right" size="24"></u-icon>
</view>
</view>
</view>
<view class="flex-1 m-[30rpx]">
<view class="title-bar text-lg font-medium">
<text class="ml-3">余额充值</text>
</view>
<view class="grid grid-cols-3 gap-y-2 gap-x-2 mt-4">
<view
v-for="(item, index) in template"
:key="item"
style="transition: all 0.5s linear"
class="py-[24rpx] text-center rounded-[20rpx]"
:class="{
'bg-primary text-white': currentIndex == index,
'bg-primary-light-9 text-primary': currentIndex != index
}"
@tap="currentIndex = index"
>
<price
:content="item"
main-size="48rpx"
minor-size="28rpx"
font-weight="900"
color="inherit"
></price>
</view>
</view>
<view
style="transition: all 0.5s linear"
class="flex items-center rounded-[20rpx] mt-4 p-[30rpx] bg-white text-primary"
>
<view class="text-[40rpx] font-bold">¥</view>
<input
type="digit"
v-model="inputMoney"
placeholder="请输入自定义金额"
class="text-2xl font-medium pl-3"
@focus="currentIndex = -1"
>
</view>
<view class="mt-4 text-muted text-xs font-medium leading-6">
<view>充值说明:</view>
<view>1.本次充值仅限于在平台上消费无法跨地区使用</view>
<view>2.若遇到充值未到账请联系客服</view>
</view>
</view>
<view
class="footer bg-white safe-area-inset-bottom p-[30rpx]"
style="box-shadow: 0 -4px 48px 0 #141a231f;"
>
<u-button
type="primary"
@click="handleRecharge"
>
<text class="text-base font-medium ml-1">充值</text>
</u-button>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import {ref} from 'vue'
import {onShow, onLoad} from '@dcloudio/uni-app'
import {apiRecharge, apiUserWallet} from '@/api/wallet'
import {useUserStore} from "@/stores/user";
import {useRouter} from "uniapp-router-next";
const router = useRouter()
const userStore = useUserStore()
const walletData = ref<any>({
user_money: '', // 可用余额
user_earnings: '', // 可提现余额
total_money: '', // 总资产
})
const template = ref([100, 200, 300, 400, 500, 600])
const inputMoney = ref('')
const currentIndex = ref(0)
const getWalletData = async () => {
walletData.value = await apiUserWallet()
}
const handleRecharge = async () => {
const i = currentIndex.value
const money = i == -1 ? inputMoney.value : template.value[i]
const res = await apiRecharge({
money: money,
template_id: ''
})
router.navigateTo({
path: '/bundle/pages/order_pay/order_pay',
query: {
from: res.from,
order_id: res.order_id,
order_amount: money
}
})
}
onShow(() => {
getWalletData()
})
</script>
<style lang="scss" scoped>
.user-wallet {
position: relative;
&::after {
content: '';
position: absolute;
top: -750px;
left: 50%;
transform: translateX(-50%);
width: 1000px;
height: 1000px;
z-index: 1;
border-radius: 1000px;
background: linear-gradient(292deg,var(--color-primary) 29.15%, var(--color-primary-light-7) 69.66%);
}
.user-wallet-view {
position: relative;
z-index: 10;
}
.wrapper {
height: 316rpx;
margin: 30rpx;
padding: 32rpx 0 52rpx 32rpx;
flex-shrink: 0;
border-radius: 32rpx;
border: 2rpx solid #ffffffcc;
background: #ffffff4d;
box-shadow: 0 6rpx 72rpx 0 #10653333;
backdrop-filter: blur(48rpx);
}
.title-bar {
position: relative;
&::before {
content: '';
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 70%;
border-radius: 4px;
@apply bg-primary;
}
}
}
</style>

View File

@@ -0,0 +1,342 @@
<template>
<view class="user-withdraw">
<view class="balance-withdrawal-card">
<u-tabs
:list="withdrawConfigData.type"
:is-scroll="true"
:current="operationCurrent"
@change="operationChange"
inactive-color="#666"
active-color="#33D192"
itemWidth="240"
/>
</view>
<!-- 微信收款码 -->
<template
v-if="
withdrawConfigData.type[operationCurrent]?.value ==
BalanceWithdrawalEnum.WECHAT_COLLECTION_CODE
"
>
<view class="payment-code-card">
<u-form :model="withdrawApplyData" ref="uForm" label-width="150rpx">
<u-form-item label="微信账号">
<u-input v-model="withdrawApplyData.account" placeholder="请输入微信账号" />
</u-form-item>
<u-form-item label="真实姓名">
<u-input
v-model="withdrawApplyData.real_name"
placeholder="请输入真实姓名"
/>
</u-form-item>
<u-form-item label="备注">
<u-input
v-model="withdrawApplyData.apply_remark"
placeholder="请输入备注(选填)"
/>
</u-form-item>
</u-form>
<view class="mt-[20rpx]">
<uploader v-model="withdrawApplyData.money_qr_code"></uploader>
<view class="ml-[10rpx]"> 微信收款码 </view>
</view>
</view>
</template>
<!-- 支付宝收款码 -->
<template
v-if="
withdrawConfigData.type[operationCurrent]?.value ==
BalanceWithdrawalEnum.ALIPAY_COLLECTION_CODE
"
>
<view class="payment-code-card">
<u-form :model="withdrawApplyData" ref="uForm" label-width="150rpx">
<u-form-item label="支付宝账号">
<u-input
v-model="withdrawApplyData.account"
placeholder="请输入支付宝账号"
/>
</u-form-item>
<u-form-item label="真实姓名">
<u-input
v-model="withdrawApplyData.real_name"
placeholder="请输入真实姓名"
/>
</u-form-item>
<u-form-item label="备注">
<u-input
v-model="withdrawApplyData.apply_remark"
placeholder="请输入备注(选填)"
/>
</u-form-item>
</u-form>
<view class="mt-[20rpx]">
<uploader v-model="withdrawApplyData.money_qr_code"></uploader>
<view class="ml-[10rpx]"> 支付宝收款码 </view>
</view>
</view>
</template>
<!-- 银行卡 -->
<template
v-if="
withdrawConfigData.type[operationCurrent]?.value == BalanceWithdrawalEnum.BANK_CARD
"
>
<view class="payment-code-card">
<u-form :model="withdrawApplyData" ref="uForm" label-width="150rpx">
<u-form-item label="银行卡账号">
<u-input
v-model="withdrawApplyData.account"
placeholder="请输入银行卡账号"
/>
</u-form-item>
<u-form-item label="持卡人姓名">
<u-input
v-model="withdrawApplyData.real_name"
placeholder="请输入持卡人姓名"
/>
</u-form-item>
<u-form-item label="提现银行">
<u-input v-model="withdrawApplyData.bank" placeholder="请输入提现银行" />
</u-form-item>
<u-form-item label="银行支行">
<u-input v-model="withdrawApplyData.subbank" placeholder="如:荔湾支行" />
</u-form-item>
<u-form-item label="备注">
<u-input
v-model="withdrawApplyData.apply_remark"
placeholder="请输入备注(选填)"
/>
</u-form-item>
</u-form>
</view>
</template>
<!-- 钱包余额 / 微信钱包 -->
<view class="wallet-balance-card">
<view class="wallet-balance-input flex">
<text style="font-size: 46rpx">¥</text>
<input
class="flex-1"
placeholder="0.00"
type="digit"
v-model="withdrawApplyData.money"
/>
<view class="withdrawal-text">
<view
class="all-withdrawal"
@click="withdrawApplyData.money = withdrawConfigData.user_earnings"
>
全部提现
</view>
<view class="can-withdrawal">
可提现余额 ¥ {{ withdrawConfigData.user_earnings }}
</view>
</view>
</view>
<view
class="wallet-balance-tips"
v-if="
withdrawConfigData.type[operationCurrent]?.value != BalanceWithdrawalEnum.WALLET
"
>
提示:提现需要扣除服务费{{ withdrawConfigData.percentage }}%
</view>
</view>
<view class="mt-[30rpx]">
<u-button
@click="handleWithdraw()"
:ripple="true"
:hair-line="false"
shape="circle"
type="primary"
hover-class="none"
>
确认提现
</u-button>
<view
class="withdrawal-record"
@click="goPage('/bundle/pages/withdraw_record/withdraw_record')"
>
提现记录
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { BalanceWithdrawalEnum } from '@/utils/enum'
import { apiGetWithdrawConfig, apiWithdrawApply } from '@/api/wallet'
import uploader from '@/components/uploader/index.vue'
import formCard from './components/form-card.vue'
const operationCurrent = ref<any>(0)
// 提现配置数据
const withdrawConfigData = ref({
user_earnings: '', // 可提现金额
min_withdraw: '', // 最低提现金额
max_withdraw: '', // 最高提现金额
percentage: '', // 手续费百分比
type: '' // 提现类型
})
// 提现申请数据
const withdrawApplyData = ref({
type: '', // 提现类型 1-余额 2-微信零钱 3-银行卡 4-微信收款码 5-支付宝收款码
money: '', // 提现金额
account: '', // 账号; 提现方式:银行卡、微信收款码、支付宝收款码 必填
real_name: '', // 真实姓名; 提现方式:银行卡、微信收款码、支付宝收款码 必填
money_qr_code: '', // 收款码 提现方式:微信收款码、支付宝收款码 必填
bank: '', // 提现银行 提现方式:银行卡 必填
subbank: '', // 提现支银行 提现方式:银行卡 必填
apply_remark: '' // 备注
})
const operationChange = (e: any) => {
operationCurrent.value = e
}
// 获取提现配置
const getWithdrawConfig = async () => {
const res = await apiGetWithdrawConfig('')
withdrawConfigData.value = res
}
// 提现
const handleWithdraw = () => {
if (withdrawApplyData.value.money == '') return uni.$u.toast('请输入提现金额')
const params = {
type: withdrawConfigData.value.type[operationCurrent.value]?.value,
money: withdrawApplyData.value.money,
account: withdrawApplyData.value.account,
real_name: withdrawApplyData.value.real_name,
money_qr_code: withdrawApplyData.value.money_qr_code.toString(),
bank: withdrawApplyData.value.bank,
subbank: withdrawApplyData.value.subbank,
apply_remark: withdrawApplyData.value.apply_remark
}
apiWithdrawApply({ ...params }).then((res: any) => {
uni.$u.toast('添加成功')
withdrawApplyData.value.money = ''
setTimeout(() => {
uni.navigateTo({
url: `/bundle/pages/withdrawal_details/withdrawal_details?id=${res.id}`
})
}, 1000)
})
}
// 跳转页面
const goPage = (url: any) => {
uni.navigateTo({ url: url })
}
onShow(() => {
getWithdrawConfig()
})
</script>
<style lang="scss" scoped>
.user-withdraw {
padding: 15rpx 30rpx;
// tab栏
.balance-withdrawal-card {
background-color: #fff;
border-radius: 20rpx;
padding: 10rpx;
}
// 收款码/银行卡
.payment-code-card {
background-color: #fff;
border-radius: 20rpx;
margin-top: 20rpx;
padding: 0 36rpx 30rpx 36rpx;
.u-list-item.data-v-f8c23944 {
background-color: #fff !important;
margin: 0;
border: 1rpx dashed #ccc;
margin-top: 20rpx;
padding-top: 20rpx;
}
}
// 钱包余额 / 微信钱包
.wallet-balance-card {
background-color: #fff;
border-radius: 20rpx;
margin-top: 20rpx;
padding: 35rpx 0 66rpx 66rpx;
.wallet-balance-input {
margin-top: 20rpx;
margin-right: 66rpx;
input {
height: 94rpx;
text-align: left;
font-size: 66rpx;
margin-left: 30rpx;
}
border-bottom: 1rpx solid #ebebeb;
.withdrawal-text {
font-size: 24rpx;
.all-withdrawal {
color: $u-type-primary;
display: subgrid;
display: flex;
justify-content: flex-end;
padding-bottom: 10rpx;
}
.can-withdrawal {
color: #999;
}
}
}
.wallet-balance-tips {
margin-top: 30rpx;
font-size: 24rpx;
color: #999;
font-weight: 400;
}
}
.withdrawal-record {
display: flex;
justify-content: center;
align-items: center;
font-size: 28rpx;
color: #666;
margin-top: 40rpx;
}
}
</style>

View File

@@ -0,0 +1,51 @@
<template>
<view>
<u-navbar
:is-back="true"
title="认证信息"
:border-bottom="false"
:fixed="false"
title-color="#000"
back-icon-color="#000"
:background="{
background: 'transparent'
}"
z-index="999"
/>
<view class="top absolute top-0 left-0 z[-1]">
<image class="w-[100vw]" mode="aspectFit" :src="logo"></image>
<view class="px-[20rpx]">
<view class="rounded-lg p-[20rpx] flex items-center bg-white">
<u-image src="" height="120" width="120" borderRadius="120"></u-image>
<view class="ml-2">
<view class="font-bold text-lg">杨业</view>
<view class="mt-2">2213123***************</view>
</view>
</view>
<view class="rounded-lg p-[20rpx] bg-white mt-4">
<view>职业资格证</view>
<view>
<u-image widht="100%" mode="aspectFit"></u-image>
</view>
</view>
<view class="rounded-lg p-[20rpx] bg-white mt-4">
<view>健康证</view>
<view>
<u-image widht="100%" mode="aspectFit"></u-image>
</view>
</view>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import logo from '@/bundle/static/images/anxingouTop.png'
import Nav from '@/components/navbar/navbar.vue'
</script>
<style lang="scss" scoped>
.top {
background: linear-gradient(0deg, #f6f7f8 0%, #fdeede 100%);
}
</style>

View File

@@ -0,0 +1,98 @@
<template>
<view class="withdrawal-record" v-for="(item, index) in withdrawApplyLists" :key="index">
<view class="withdrawal-record-item" @click="toDetail(item.id)">
<view class="flex">
<view class="flex-1 withdrawal-record-item-text">提现至{{ item.type_desc }}</view>
<view class="withdrawal-record-item-amount">+{{ item.money }}</view>
</view>
<view class="flex justify-between mt-[10rpx]">
<view class="withdrawal-record-item-time"> {{ item.create_time }} </view>
<template v-if="item.status == 1">
<view class="text-xs" style="color: #0cc267">{{ item.status_desc }}</view>
</template>
<template v-if="item.status == 2">
<view class="text-xs" style="color: #ff2c3c">{{ item.status_desc }}</view>
</template>
<template v-if="item.status == 3">
<view class="text-xs" style="color: #666">{{ item.status_desc }}</view>
</template>
<template v-if="item.status == 4">
<view class="text-xs" style="color: #ff2c3c">{{ item.status_desc }}</view>
</template>
</view>
<template v-if="item.verify_remark != null">
<template v-if="item.verify_remark != ''">
<view class="review-tips">审核提示{{ item.verify_remark }}</view>
</template>
</template>
</view>
</view>
</template>
<script lang="ts" setup>
const props = withDefaults(
defineProps<{
withdrawApplyLists: any
}>(),
{
withdrawApplyLists: []
}
)
const toDetail = (id: any) => {
uni.navigateTo({
url: `/bundle/pages/withdrawal_details/withdrawal_details?id=${id}`
})
}
</script>
<style lang="scss" scoped>
.withdrawal-record {
padding-top: 10rpx;
.withdrawal-record-item {
background-color: #fff;
padding: 20rpx 30rpx;
border-bottom: 1rpx solid #ebebeb;
.withdrawal-record-item-text {
font-size: 30rpx;
font-weight: 400;
}
.withdrawal-record-item-amount {
font-size: 34rpx;
font-weight: 400;
}
.withdrawal-record-item-time {
font-size: 24rpx;
font-weight: 400;
color: #999;
margin-top: 10rpx;
}
.review-tips {
font-size: 24rpx;
color: #ff2c3c;
margin-top: 10rpx;
}
.success {
font-size: 24rpx;
color: #0cc267;
}
.fail {
font-size: 24rpx;
color: #ff2c3c;
}
}
}
</style>

View File

@@ -0,0 +1,35 @@
<template>
<z-paging
auto-show-back-to-top
ref="paging"
v-model="dataList"
@query="queryList"
:fixed="false"
height="100%"
>
<Card :withdrawApplyLists="dataList" />
</z-paging>
</template>
<script lang="ts" setup>
import { ref, shallowRef } from 'vue'
import Card from './card.vue'
import { apiWithdrawLists } from '@/api/wallet'
const paging = shallowRef<any>(null)
const dataList = ref<any>([])
const queryList = async (page_no: number, page_size: number) => {
try {
const { lists } = await apiWithdrawLists({
page_no,
page_size
})
paging.value.complete(lists)
} catch (e) {
console.log('报错=>', e)
//TODO handle the exception
paging.value.complete(false)
}
}
</script>

View File

@@ -0,0 +1,23 @@
<template>
<view class="withdrawal-record">
<view class="List pt-[20rpx]" v-if="isLogin">
<List />
</view>
</view>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue'
import List from './components/list.vue'
import { useUserStore } from '@/stores/user'
// 是否登录
const userStore = useUserStore()
const isLogin = computed(() => userStore.token)
</script>
<style lang="scss" scoped>
.List {
height: calc(100vh - env(safe-area-inset-bottom));
}
</style>

View File

@@ -0,0 +1,307 @@
<template>
<view class="withdrawal-details">
<view class="bg-white withdrawal-details-item">
<view class="whether-pass" v-if="formData.status == 1">
<image src="@/bundle/static/images/icon_cashOut_wait.png" />
<view class="mt-[12rpx]">{{ formData.status_desc }}</view>
</view>
<view class="whether-pass" v-if="formData.status == 2">
<image src="@/bundle/static/images/icon_cashOut_wait.png" />
<view class="mt-[12rpx]">{{ formData.status_desc }}</view>
</view>
<view class="whether-pass" v-if="formData.status == 3">
<image src="@/bundle/static/images/icon_cashOut_success.png" />
<view class="mt-[12rpx]">{{ formData.status_desc }}</view>
</view>
<view class="whether-pass" v-if="formData.status == 4">
<image src="@/bundle/static/images/icon_cashOut_fail.png" />
<view class="mt-[12rpx]">{{ formData.status_desc }}</view>
</view>
<view class="withdrawal-money">
<view class="withdrawal-money-icon"> ¥ </view>
<view class="withdrawal-money-text"> {{ formData.money }} </view>
</view>
<view class="ml-[30rpx] mt-[20rpx] mr-[30rpx] pb-[20rpx]">
<view class="flex justify-between withdrawal-content">
<view>提现单号</view>
<view>{{ formData.sn }}</view>
</view>
<view class="flex justify-between withdrawal-content">
<view>申请时间</view>
<view>{{ formData.create_time }}</view>
</view>
<view class="flex justify-between withdrawal-content">
<view>提现至</view>
<view>{{ formData.type_desc }}</view>
</view>
<view class="flex justify-between withdrawal-content">
<view>服务费</view>
<view>{{ formData.handling_fee }}</view>
</view>
<view class="flex justify-between withdrawal-content">
<view>实际到账</view>
<view>{{ formData.left_money }}</view>
</view>
</view>
<!-- 银行卡提现 -->
<view
class="ml-[30rpx] mt-[20rpx] mr-[30rpx] pt-[20rpx]"
style="border-top: 1rpx solid #e5e5e5"
v-if="formData.type == 3"
>
<view class="flex justify-between withdrawal-content">
<view>银行卡账号</view>
<view>{{ formData.account }}</view>
</view>
<view class="flex justify-between withdrawal-content">
<view>持卡人姓名</view>
<view>{{ formData.real_name }}</view>
</view>
<view class="flex justify-between withdrawal-content">
<view>提现银行</view>
<view>{{ formData.bank }}</view>
</view>
<view class="flex justify-between withdrawal-content">
<view>银行支行</view>
<view>{{ formData.subbank }}</view>
</view>
<view class="flex justify-between withdrawal-content">
<view>备注说明</view>
<view>{{ formData.transfer_remark }}</view>
</view>
</view>
<!-- 支付宝收款码提现 -->
<view
class="ml-[30rpx] mt-[20rpx] mr-[30rpx] pt-[20rpx]"
style="border-top: 1rpx solid #e5e5e5"
v-if="formData.type == 5"
>
<view class="flex justify-between withdrawal-content">
<view>支付宝账号</view>
<view>{{ formData.account }}</view>
</view>
<view class="flex justify-between withdrawal-content">
<view>真实姓名</view>
<view>{{ formData.real_name }}</view>
</view>
<view class="flex justify-between withdrawal-content">
<view>支付宝收款码</view>
<u-image
height="160"
width="160"
:src="formData.money_qr_code"
@click="showImage([formData.money_qr_code])"
>
</u-image>
</view>
<view class="flex justify-between withdrawal-content">
<view>备注说明</view>
<view>{{ formData.transfer_remark }}</view>
</view>
</view>
<!-- 微信收款码提现 -->
<view
class="ml-[30rpx] mt-[20rpx] mr-[30rpx] pt-[20rpx]"
style="border-top: 1rpx solid #e5e5e5"
v-if="formData.type == 4"
>
<view class="flex justify-between withdrawal-content">
<view>微信账号</view>
<view>{{ formData.account }}</view>
</view>
<view class="flex justify-between withdrawal-content">
<view>真实姓名</view>
<view>{{ formData.real_name }}</view>
</view>
<view class="flex justify-between withdrawal-content">
<view>微信收款码</view>
<u-image
height="160"
width="160"
:src="formData.money_qr_code"
@click="showImage([formData.money_qr_code])"
>
</u-image>
</view>
<view class="flex justify-between withdrawal-content">
<view>备注说明</view>
<view>{{ formData.transfer_remark }}</view>
</view>
</view>
<!-- 转账凭证 -->
<view
class="mx-[30rpx] my-[20rpx] pt-[20rpx]"
style="border-top: 1rpx solid #e5e5e5"
v-if="formData.status == 3 || formData.status == 4"
>
<view class="flex justify-between withdrawal-content">
<view>转账凭证</view>
<u-image
height="160"
width="160"
:src="formData.transfer_voucher"
v-if="formData.transfer_voucher"
@click="showImage([formData.transfer_voucher])"
>
</u-image>
</view>
<view class="flex justify-between withdrawal-content">
<view>转账说明</view>
<view>{{ formData.transfer_remark || '-' }}</view>
</view>
</view>
<view class="check-withdrawal-record">
<button class="Btn" @click="toRecord">查看历史提现记录</button>
<view class="mt-[20rpx]">
<button class="Btn del_Btn" @click="toHome">返回首页</button>
</view>
</view>
</view>
<view class="review-success-tips">* 审核成功后约72小时内到账请留意账户明细</view>
</view>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { apiWithdrawDetail } from '@/api/wallet'
const withdrawId = ref<number | string>('')
const formData = ref({
type: '', // 提现类型1-钱包余额2-微信零钱3-银行卡;4-微信收款码;5-支付宝收款码
type_desc: '', // 提现类型描述
status: '', // 提现状态:1-待提现2-提现中3-提现成功4-提现失败
status_desc: '', // 提现状态描述
money: '', // 提现金额
money_qr_code: '', // 收款码
create_time: '', // 提现申请时间
sn: '', // 提现编号
handling_fee: '', // 服务费
account: '', // 提现账号
real_name: '', // 提现人姓名
bank: '', // 提现银行
subbank: '', // 提现支行
transfer_voucher: '', // 转账凭证
transfer_remark: '' // 转账备注
})
// 获取提现申请详情
const getWithdrawDetail = () => {
apiWithdrawDetail({ id: withdrawId.value }).then((res: any) => {
formData.value = res
})
}
const toRecord = () => {
uni.redirectTo({
url: '/bundle/pages/withdraw_record/withdraw_record'
})
}
const toHome = () => {
uni.reLaunch({
url: '/pages/index/index'
})
}
const showImage = (list: any) => {
uni.previewImage({
urls: list,
current: 1
})
}
onLoad((options) => {
withdrawId.value = options?.id || ''
getWithdrawDetail()
})
</script>
<style lang="scss" scoped>
.withdrawal-details {
padding: 20rpx;
.withdrawal-details-item {
.whether-pass {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
font-size: 34rpx;
font-weight: 500;
padding-top: 40rpx;
image {
width: 100rpx;
height: 100rpx;
}
}
.withdrawal-money {
display: flex;
justify-content: center;
align-items: center;
color: #ff2c3c;
margin-top: 20rpx;
padding-bottom: 52rpx;
.withdrawal-money-icon {
font-size: 30rpx;
}
.withdrawal-money-text {
font-size: 46rpx;
}
}
.withdrawal-content {
padding-bottom: 34rpx;
}
.check-withdrawal-record {
padding: 40rpx 0rpx 50rpx 0;
margin: 0 30rpx;
border-top: 1px solid #ebebeb;
.Btn {
@apply bg-white text-base text-black leading-[72rpx] h-[72rpx] rounded-full;
border: 1px solid $u-type-primary;
background-color: $u-type-primary;
color: white;
text-align: center;
}
.del_Btn {
border: 1px solid $u-type-primary;
background-color: #c9c9c900;
color: $u-type-primary;
}
}
}
.review-success-tips {
display: flex;
justify-content: center;
align-items: center;
font-size: 24rpx;
color: #999;
margin-top: 30rpx;
}
}
button::after {
border: none;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 748 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 636 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 621 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="28" height="28" style="border-color: rgba(0,0,0,0);border-width: bpx;border-style: undefined" filter="none">
<g>
<path d="M25.467 11.733c3.239 0 4.442 2.018 3.739 4.629l-1.438 5.403-1.44 5.402-0.183 0.693h-23.611v-15.235h8.621l2.496-7.532c0.060-0.285 0.195-0.631 0.452-0.981 0.477-0.649 1.219-1.045 2.187-1.045 1.039 0 1.78 0.517 2.165 1.323 0.212 0.446 0.279 0.869 0.279 1.195v6.149h6.733zM24.709 25.994v0zM25.963 21.285l1.44-5.405c0.409-1.521-0.043-2.281-1.936-2.281h-8.599v-8.016c-0.008-0.143-0.043-0.276-0.101-0.397l0.003 0.006c-0.089-0.185-0.197-0.26-0.48-0.26-0.357 0-0.548 0.102-0.683 0.284-0.057 0.075-0.102 0.163-0.129 0.259l-0.001 0.006-0.035 0.141-2.939 8.869h-8.102v11.502h20.309l1.253-4.708zM8.8 13.733v13.867h1.867v-13.867z" fill="rgba(243.015,96.9,96.9,1)"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 877 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -0,0 +1,90 @@
<template>
<view class="agreement" :class="{ shake: isShake }">
<view>
<u-checkbox v-model="isActive" shape="circle">
<view class="text-base flex">
已阅读并同意
<view class="text-primary" @click.stop>
<router-navigate
class="text-primary"
to="/bundle/pages/agreement/agreement?type=service"
>
服务协议
</router-navigate>
</view>
<view class="text-primary" @click.stop>
<router-navigate
class="text-primary"
to="/bundle/pages/agreement/agreement?type=privacy"
>
隐私协议
</router-navigate>
</view>
</view>
</u-checkbox>
</view>
</view>
<modal-popup
v-model:show="showPopup"
title="服务协议及隐私协议"
@confirm="isActive = true"
>
<template #content>
<view>
为了更好的保障您的权益请您阅读并同意 <text class="text-primary">服务协议隐私政策</text>
</view>
</template>
</modal-popup>
</template>
<script lang="ts" setup>
import { useAppStore } from '@/stores/app'
import { computed, ref } from 'vue'
import ModalPopup from '../modal-popup/modal-popup.vue'
const appStore = useAppStore()
const showPopup = ref(false)
const isActive = ref(false)
const isShake = ref(false)
const checkAgreement = () => {
if (!isActive.value) {
showPopup.value = true
isShake.value = true
setTimeout(() => {
isShake.value = false
}, 1000)
}
return isActive.value
}
defineExpose({
checkAgreement
})
</script>
<style lang="scss">
.shake {
animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
transform: translate3d(0, 0, 0);
}
@keyframes shake {
10%,
90% {
transform: translate3d(-1px, 0, 0);
}
20%,
80% {
transform: translate3d(2px, 0, 0);
}
30%,
50%,
70% {
transform: translate3d(-4px, 0, 0);
}
40%,
60% {
transform: translate3d(4px, 0, 0);
}
}
</style>

View File

@@ -0,0 +1,107 @@
<template>
<button
class="avatar-upload p-0 m-0 rounded inline-flex flex-col items-center"
hover-class="none"
open-type="chooseAvatar"
@click="chooseAvatar"
@chooseavatar="chooseAvatar"
>
<image
:style="styles"
class="w-full h-full"
mode="heightFix"
:src="modelValue"
v-if="modelValue"
/>
<slot v-else>
<view
:style="styles"
class="border border-dotted border-light flex w-full h-full flex-col items-center justify-center text-muted text-xs box-border rounded"
>
<u-icon name="plus" :size="36"/>
添加图片
</view>
</slot>
<slot name="footer"></slot>
</button>
</template>
<script lang="ts" setup>
import {addUnit} from '@/utils/util'
import {isBoolean} from 'lodash'
import {computed, CSSProperties, onUnmounted} from 'vue'
import {useRouter} from 'uniapp-router-next'
const router = useRouter()
const props = defineProps({
modelValue: {
type: String
},
size: {
type: [String, Number],
default: 140
},
round: {
type: [Boolean, String, Number],
default: false
},
border: {
type: Boolean,
default: true
}
})
const emit = defineEmits<{
(event: 'update:modelValue', value: string): void
(event: 'upload', value: string): void
}>()
const styles = computed<CSSProperties>(() => {
const size = addUnit(props.size)
return {
width: size,
height: size,
borderRadius: isBoolean(props.round)
? props.round
? '50%'
: ''
: addUnit(props.round)
}
})
const chooseAvatar = async (e: any) => {
// #ifndef MP-WEIXIN
router.navigateTo({
path: '/uni_modules/vk-uview-ui/components/u-avatar-cropper/u-avatar-cropper?destWidth=300&rectWidth=200&fileType=jpg'
})
// #endif
// #ifdef MP-WEIXIN
const path = e.detail?.avatarUrl
if (path) {
uploadImageIng(path)
}
// #endif
}
const uploadImageIng = async (file: string) => {
emit('update:modelValue', file)
emit('upload', file)
}
// 监听从裁剪页发布的事件,获得裁剪结果
uni.$on('uAvatarCropper', (path) => {
// console.log(path)
uploadImageIng(path)
})
onUnmounted(() => {
uni.$off('uAvatarCropper')
})
</script>
<style lang="scss" scoped>
.avatar-upload {
background: #fff;
overflow: hidden;
&::after {
border: none;
}
}
</style>

View File

@@ -0,0 +1,82 @@
<template>
<view class="mt-[20rpx] pb-[20rpx]" :class="{ item: border }">
<view class="flex items-center justify-between">
<view class="flex items-center">
<u-image :src="data.avatar" width="80" height="80" borderRadius="50%"></u-image>
<view class="flex flex-col justify-between ml-2">
<view class="text-base font-medium">
{{ data.nickname }}
</view>
<view class="mt-1 text-muted text-xs">
{{ data.create_time }}
</view>
</view>
</view>
<u-rate
:count="5"
v-model="data.service_comment"
:disabled="true"
class="ml-auto"
inactive-color="#eaeaeb"
inactiveIcon="star-fill"
active-color="#d86930"
></u-rate>
</view>
<view class="mt-3 break-words text-content comment"> {{ data.comment }}</view>
<view class="mt-3 grid gap-2 grid-cols-4">
<view
v-for="(commentImage, index) in data.goods_comment_image"
:key="index"
class="mt-[10rpx]"
:class="{ 'mr-[10rpx]': (index + 1) % 4 != 0 }"
@click.stop="previewImage(data.goods_comment_image, index)"
>
<u-image
:src="commentImage.uri"
width="150"
height="150"
border-radius="14rpx"
></u-image>
</view>
</view>
<view class="reply mt-3" v-if="data.reply">
<span class="text-sm">商家回复</span>
<span class="text-sm text-content">{{ data.reply }}</span>
</view>
</view>
</template>
<script lang="ts" setup>
defineProps<{
data: any
border: true
}>()
const previewImage = (url: any, index: number | string) => {
uni.previewImage({
current: index,
urls: url.map((item: any) => item.uri)
})
}
</script>
<style lang="scss" scoped>
.item {
border-bottom: 2rpx solid #f5f5f5;
}
.comment {
line-height: 42rpx;
letter-spacing: 3rpx;
}
.reply {
line-height: 40rpx;
letter-spacing: 3rpx;
padding: 10rpx 20rpx;
border-radius: 4rpx;
background: #f3f3f3;
}
</style>

View File

@@ -0,0 +1,93 @@
<template>
<template v-if="type == 'list'">
<block v-for="goodsItem in goodsList" :key="goodsItem.id">
<view
class="goods-item goods-list w-[340rpx] h-[480rpx] mt-[20rpx] flex bg-white"
@click="goPage(goodsItem.id)"
>
<u-image :src="goodsItem.image" width="340" height="340"></u-image>
<view class="goods-name truncate pl-[20rpx] pt-[20rpx]">
{{ goodsItem.name }}
</view>
<view class="pl-[20rpx] mt-[10rpx]">
<price
:price="goodsItem?.price"
:desc="goodsItem.unit_desc || goodsItem?.unit_name"
></price>
</view>
</view>
</block>
</template>
<template v-if="type == 'row'">
<block v-for="goodsItem in goodsList" :key="goodsItem.id">
<view
class="goods-item w-[220rpx] h-[310rpx] mr-[20rpx] flex bg-white"
@click="goPage(goodsItem.id)"
>
<u-image :src="goodsItem.image" width="220" height="200"></u-image>
<view class="goods-name truncate pt-[20rpx]">
{{ goodsItem.name }}
</view>
<view class="mt-[10rpx]">
<price
:price="goodsItem?.price"
:desc="goodsItem.unit_desc || goodsItem?.unit_name"
></price>
</view>
</view>
</block>
</template>
</template>
<script lang="ts" setup>
/** Props Start **/
const props = withDefaults(
defineProps<{
type: string // 列表类型
goodsList?: any // 商品数据
}>(),
{
type: 'list',
goodsList: []
}
)
/** Props End **/
/** Methods Start **/
/**
* @param { string } url
* @return { void }
* @description 跳转到商品
*/
const goPage = (id: number | string) => {
uni.navigateTo({
url: `/pages/goods/index?id=${id}`
})
}
/** Methods End **/
</script>
<style lang="scss" scoped>
.goods-item {
display: inline-block;
overflow: hidden;
border-radius: 8rpx;
.goods-name {
width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-size: 28rpx;
color: #222222;
}
}
.goods-list {
margin-right: 20rpx;
}
.goods-list:nth-child(2n) {
margin-right: 0;
}
</style>

View File

@@ -0,0 +1,97 @@
<template>
<u-swiper
v-if="lists.length"
:list="lists"
:mode="mode"
:height="height"
:effect3d="effect3d"
:indicator-pos="indicatorPos"
:autoplay="autoplay"
:interval="interval"
:duration="duration"
:circular="circular"
:borderRadius="borderRadius"
:current="current"
:name="name"
:bg-color="bgColor"
@click="handleClick"
@change="handleChange"
>
</u-swiper>
</template>
<script setup lang="ts">
import {useAppStore} from "@/stores/app";
import {navigateTo, navigateToMiniProgram, LinkTypeEnum} from "@/utils/util";
import {watchEffect, computed} from "vue";
import {useRouter} from "uniapp-router-next";
const emit = defineEmits(["change"]);
const props = withDefaults(
defineProps<{
content?: any; // 轮播图数据
mode?: string; // 指示器模式 rect / dot / number / none
height?: string; // 轮播图组件高度
indicatorPos?: string; // 指示器的位置 topLeft / topCenter / topRight / bottomLeft / bottomRight
effect3d?: boolean; // 是否开启3D效果
autoplay?: boolean; // 是否自动播放
interval?: number | string; // 自动轮播时间间隔单位ms
duration?: number | string; // 切换一张轮播图所需的时间单位ms
circular?: boolean; // 是否衔接播放
current?: number; // 默认显示第几项
name?: string; // 显示的属性
borderRadius?: string; //轮播图圆角值单位rpx
bgColor?: string; // 背景颜色
}>(),
{
content: {
data: [],
},
mode: "round",
indicatorPos: "bottomCenter",
height: "340",
effect3d: false,
autoplay: true,
interval: "2500",
duration: 300,
circular: true,
current: 0,
name: "image",
borderRadius: "0",
bgColor: "#f3f4f6",
}
);
const {getImageUrl} = useAppStore();
watchEffect(() => {
try {
const content = props?.content;
const len = content?.data?.length;
if (!len) return;
for (let i = 0; i < len; i++) {
const item = content.data[i];
item.image = getImageUrl(item.image);
}
emit("change", 0);
} catch (error) {
//TODO handle the exception
console.log("轮播图数据错误", error);
}
});
const lists = computed(() => props.content.data || []);
const router = useRouter();
const handleClick = (index: number) => {
const link = props.content.data[index]?.link;
if (!link) return
navigateTo(link);
};
const handleChange = (index: number) => {
emit("change", index);
};
</script>
<style></style>

View File

@@ -0,0 +1,82 @@
<template>
<u-swiper
v-if="lists.length"
:list="lists"
:mode="mode"
:height="height"
:effect3d="effect3d"
:indicator-pos="indicatorPos"
:autoplay="autoplay"
:interval="interval"
:circular="circular"
:border-radius="borderRadius"
:current="current"
:name="name"
@click="handleClick"
>
</u-swiper>
</template>
<script setup lang="ts">
import { useAppStore } from '@/stores/app'
import { navigateTo } from '@/utils/util'
import { watchEffect, computed } from 'vue'
const props = withDefaults(
defineProps<{
content?: any // 轮播图数据
mode?: string // 指示器模式 rect / dot / number / none
height?: string // 轮播图组件高度
indicatorPos?: string // 指示器的位置 topLeft / topCenter / topRight / bottomLeft / bottomRight
effect3d?: boolean // 是否开启3D效果
autoplay?: boolean // 是否自动播放
interval?: number | string // 自动轮播时间间隔单位ms
circular?: boolean // 是否衔接播放
current?: number // 默认显示第几项
name?: string // 显示的属性
borderRadius?: number | string //圆角
}>(),
{
content: {
data: []
},
mode: 'round',
indicatorPos: 'bottomCenter',
height: '340',
effect3d: false,
autoplay: true,
interval: '2500',
circular: true,
current: 0,
name: 'image',
borderRadius: 0
}
)
const { getImageUrl } = useAppStore()
watchEffect(() => {
try {
const content = props?.content
const len = content?.data?.length
if (!len) return
for (let i = 0; i < len; i++) {
const item = content.data[i]
item.image = getImageUrl(item.image)
}
} catch (error) {
//TODO handle the exception
console.log('轮播图数据错误', error)
}
})
const lists = computed(() => props.content.data || [])
const handleClick = (index: number) => {
const link = props.content.data[index]?.link
if (!link) return
navigateTo(link)
}
</script>
<style></style>

View File

@@ -0,0 +1,73 @@
<template>
<view
class="mx-[30rpx] mt-[30rpx] p-[12rpx] rounded-[20rpx] bg-white flex "
@click="emits('confirm', data.id)"
>
<u-image width="188" height="188" borderRadius="12px" :src="data.work_photo"></u-image>
<view class="ml-3 flex-1 flex flex-col">
<view class="flex justify-between relative pt-1">
<text class="font-bold text-lg">{{ data.name }}</text>
<view
v-if="type == 2"
class="absolute px-[20rpx] py-[10rpx] text-xs"
:class="{
'text-primary bg-primary-light-9': data.work_status == 1,
'text-[#f6f7f8] text-content': data.work_status != 1
}"
style="top: -12rpx; right: -12rpx; border-radius: 0 24rpx 0 24rpx"
>
<text v-if="data.work_status == 1"> 最早可约{{ data.first_appoint }} </text>
<text v-else>休息中</text>
</view>
<text v-if="type == 1 && data.work_status == 1" class="text-[20rpx] text-[#C38925]">
最早可约{{ data.first_appoint }}
</text>
<text v-if="type == 1 && data.work_status != 1" class="text-[20rpx] text-muted">
已休息
</text>
</view>
<view class="text-xs flex n mt-2">
<view>
<text class="text-info">已服务</text>
<text>{{ data.order_num }}</text>
<text class="text-info"></text>
</view>
<view class="ml-2">
<text class="text-info">好评率</text>
<text>{{ data.good_comment }}</text>
</view>
<view class="ml-auto">
<text class="text-info">{{ data.distance_desc }}</text>
</view>
</view>
<view class="mt-4 flex justify-between">
<view class="flex items-center">
<view class="text-info flex items-center">
<u-image :src="icon_star" width="32" height="32"></u-image>
<text class="ml-1">{{ data.collect_num }}</text>
</view>
<view class="text-info flex items-center ml-4">
<u-image :src="icon_flower" width="32" height="32"></u-image>
<text class="ml-1">{{ data.comment_num }}</text>
</view>
</view>
<u-button class="mx-[0rpx]" type="primary" size="mini" :disabled="data.work_status != 1">
立即下单
</u-button>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import icon_star from '@/static/images/icon/icon_star.png'
import icon_flower from '@/static/images/icon/icon_flower.png'
const emits = defineEmits<{
(e: 'confirm', id: number): void
}>()
const props = defineProps<{
data: any
type: '1' | '2'
}>()
</script>

View File

@@ -0,0 +1,130 @@
<template>
<u-popup
v-model="showPopup"
mode="center"
:mask-close-able="false"
:customStyle="{
'background': `none`
}"
:closeable="closeable"
@close="emits('close')"
>
<view
style="width: 600rpx;border-radius: 20rpx;"
class="modal-popup p-[40rpx] text-center"
>
<view class="py-2 font-medium text-2xl text-black relative z-10">
{{ title }}
</view>
<view
class="py-[16px] text-base text-content relative z-10"
style="width: 500rpx; margin: 0 auto;"
>
<slot name="content">{{ content }}</slot>
</view>
<slot name="footer">
<view
class="flex gap-[20rpx] mt-[40rpx]"
>
<view class="flex-1">
<u-button
@click="cancel"
>
取消
</u-button>
</view>
<view class="flex-1">
<u-button
type="primary"
@click="confirm"
>
确定
</u-button>
</view>
</view>
</slot>
</view>
</u-popup>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
const props = defineProps<{
show: boolean
title: string
content: string
closeable: boolean
}>()
const emits = defineEmits<{
(event: 'update:show', show: boolean): void
(event: 'update', value: any): void
(event: 'refresh'): void
(event: 'close'): void
(event: 'cancel'): void
(event: 'confirm'): void
}>()
const showPopup = computed({
get() {
return props.show
},
set(val) {
emits('update:show', val)
}
})
const cancel = () => {
showPopup.value = false
emits('cancel')
}
const confirm = async () => {
showPopup.value = false
emits('confirm')
// 检测定位权限, 如果是就打开设置
if (props.title === '定位权限未授权') {
const settings = await uni.openSetting()
if (settings.authSetting['scope.userLocation']) {
emits('refresh') // 重新获取定位
}
}
}
</script>
<style lang="scss" scoped>
.modal-popup {
position: relative;
background: linear-gradient(to bottom, var(--color-primary-light-9) 0%, #fff 50%, #fff 100%);
z-index: 2; // 确保内容的z-index高于伪元素
&::before,
&::after {
content: '';
position: absolute;
width: 160px;
height: 160px;
opacity: 0.3;
border-radius: 50%;
z-index: 1; // 确保伪元素的z-index低于内容
}
&::before {
left: -50px;
top: -90px;
background: linear-gradient(200deg, var(--color-primary-light-3) 0%, #fff 100%);
}
&::after {
right: -50px;
top: -90px;
background: linear-gradient(150deg, var(--color-primary-light-3) 0%, #fff 100%);
}
}
</style>

View File

@@ -0,0 +1,203 @@
<template>
<view class="navbar" @tap.stop="navbar.showFloat = false">
<u-navbar
:background="background"
:title="title"
:title-color="titleColor"
:border-bottom="borderBottom"
:immersive="immersive"
:title-bold="true"
:is-back="false"
>
<view class="navbar-left flex py-[12rpx] px-[25rpx] rounded-[30rpx] ml-[20rpx]">
<u-icon :name="backIcon" :size="36" @click="goBack"></u-icon>
<view class="line"></view>
<view class="navbar-lists" @tap.stop="navbar.showFloat = !navbar.showFloat">
<u-icon :name="IconList" :size="32"></u-icon>
<view class="navbar-float" v-show="navbar.showFloat">
<view
v-for="(item, index) in navbar.list"
:key="index"
:to="item.url"
:open-type="item.type"
class="float-item"
hover-class="none"
@click="gotoPage(item)"
>
<view class="flex items-center justify-center">
<u-icon :name="item.icon" :size="44"></u-icon>
<text class="ml-[20rpx]">{{ item.name }}</text>
</view>
</view>
</view>
</view>
</view>
</u-navbar>
<view class="mask" v-show="navbar.showFloat" @touchstart="navbar.showFloat = false"> </view>
</view>
</template>
<script lang="ts" setup>
import { reactive, computed } from 'vue'
import IconBack from '@/static/images/icon/icon_back.png'
import IconList from '@/static/images/icon/icon_list.png'
import IconHome from '@/static/images/icon/icon_home.png'
import IconSearch from '@/static/images/icon/icon_search.png'
import IconUser from '@/static/images/icon/icon_nav_user.png'
import IconCart from '@/static/images/icon/icon_cart.png'
// import { useRouter } from 'uniapp-router-next'
import { navigateTo } from '@/utils/util'
// const router = useRouter()
const props = defineProps({
// 导航标题
title: {
type: String
},
// 导航标题颜色
titleColor: {
type: String,
default: '#000000'
},
// 导航的背景颜色
background: {
type: Object,
default: () => ({
background: '#ffffff'
})
},
// 是否显示底部边框
borderBottom: {
type: Boolean,
default: false
},
immersive: {
type: Boolean,
default: false
}
})
const navbar = reactive({
isIndex: false,
showFloat: false,
list: [
{
url: '/pages/index/index',
name: '首页',
icon: IconHome,
type: 'switchTab'
},
{
url: '/pages/search/search',
name: '搜索',
icon: IconSearch,
type: 'navigate'
},
{
url: '/pages/shop_cart/shop_cart',
name: '购物车',
icon: IconCart,
type: 'switchTab'
},
{
url: '/pages/user/user',
name: '个人中心',
icon: IconUser,
type: 'switchTab'
}
]
})
const backIcon = computed(() => (navbar.isIndex ? IconHome : IconBack))
const goBack = () => {
if (!navbar.isIndex) {
uni.navigateBack()
return
}
// router.reLaunch('/pages/index/index')
}
const gotoPage = (item: any) => {
if (item.type == 'switchTab') {
uni.switchTab({
url: item.url
})
} else {
uni.navigateTo({
url: item.url
})
//navigateTo(item.url, 'navigateTo')
}
}
setTimeout(() => {
const pages = getCurrentPages()
if (pages.length == 1) {
navbar.isIndex = true
}
})
</script>
<style lang="scss" scoped>
.navbar {
.navbar-left {
background: rgba(255, 255, 255, 0.3);
border: 1rpx solid rgba(0, 0, 0, 0.1);
.line {
width: 1px;
height: 36rpx;
background: rgba(0, 0, 0, 0.2);
margin: 0 25rpx;
}
.navbar-lists {
display: flex;
justify-content: center;
position: relative;
.navbar-float {
position: absolute;
top: 40px;
width: 258rpx;
padding: 0 24rpx;
background: #fff;
border-radius: 14rpx;
box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.06);
&::before {
content: '';
display: block;
position: absolute;
left: 50%;
width: 0;
height: 0;
border: 14rpx solid transparent;
border-bottom-color: #fff;
transform: translate(-50%, -100%);
}
.float-item {
padding: 20rpx 0;
display: flex;
align-items: center;
&:not(:last-of-type) {
border-bottom: 1px solid #e5e5e5;
}
}
}
}
}
.mask {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
}
}
</style>

View File

@@ -0,0 +1,65 @@
<template>
<navigator :url="`/pages/news_detail/news_detail?id=${newsId}`">
<view class="news-card flex bg-white px-[20rpx] py-[32rpx]">
<view class="mr-[20rpx]" v-if="item.image">
<u-image :src="item.image" width="240" height="180"></u-image>
</view>
<view class="news-card-content flex flex-col justify-between flex-1">
<view class="news-card-content-title text-lg font-medium">{{ item.title }}</view>
<view class="news-card-content-intro text-gray-400 text-sm mt-[16rpx]">
{{ item.desc }}
</view>
<view class="text-muted text-xs w-full flex justify-between mt-[12rpx]">
<view>{{ item.create_time }}</view>
<view class="flex items-center">
<image
src="/static/images/icon/icon_visit.png"
class="w-[30rpx] h-[30rpx]"
></image>
<view class="ml-[10rpx]">{{ item.click }}</view>
</view>
</view>
</view>
</view>
</navigator>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const props = withDefaults(
defineProps<{
item: any
newsId: number
}>(),
{
item: {},
newsId: ''
}
)
</script>
<style lang="scss" scoped>
.news-card {
border-bottom: 1px solid #f8f8f8;
&-content {
&-title {
-webkit-line-clamp: 2;
overflow: hidden;
word-break: break-all;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
}
&-intro {
-webkit-line-clamp: 1;
overflow: hidden;
word-break: break-all;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
}
}
}
</style>

View File

@@ -0,0 +1,154 @@
<template>
<u-popup
v-model="showPopup"
mode="bottom"
:customStyle="{
'background': `none`
}"
:closeable="true"
:mask-close-able="false"
@close="showPopup = false"
>
<view
style="border-radius: 20rpx 20rpx 0 0;"
class="append-popup bg-white p-[30rpx] text-center"
>
<view class="py-2 font-medium text-2xl text-black">
{{ title }}
</view>
<view class="mt-3">
<view
class="flex text-base bg-[#f6f7f8]"
style="border-radius: 20rpx; padding: 20rpx 24rpx;"
>
<view class="flex-none">
<u-image
:src="goods.goods_image"
width="136"
height="136"
border-radius="20"
/>
</view>
<view class="ml-2 flex-1 text-left">
<view class="line-clamp-1 text-base font-bold text-main">
{{ goods.goods_name }}
</view>
<view class="mt-1 text-xs text-muted">
服务时间:{{ duration }}分钟
</view>
<view class="u-flex justify-between mt-1 ">
<price
:content="goodsPrice"
main-size="32rpx"
minor-size="20rpx"
font-weight="900"
color="#E86016"
></price>
<u-number-box
bg-color=""
v-model="goodsNum"
@change="getOrderInfo"
></u-number-box>
</view>
</view>
</view>
<view class="mt-4 text-left">
<view class="text-xs font-medium text-muted">
*支付前需与服务人员协商一致如有异议请联系平台客服
</view>
<view class="mt-1 text-xs font-medium text-muted">
系统只保障在线支付的项目切勿线下支付
</view>
</view>
</view>
<view style="margin-top: 90rpx" class="footer flex-1 safe-area-inset-bottom">
<u-button
type="primary"
@click="confirm"
>
<text class="font-medium mr-2">立即支付</text>
<price
:content="orderAmount"
main-size="40rpx"
minor-size="24rpx"
font-weight="900"
color="#ffffff"
></price>
</u-button>
</view>
</view>
</u-popup>
</template>
<script lang="ts" setup>
import {computed, ref, watch} from 'vue'
import { useRouter } from 'uniapp-router-next'
import {apiOrderAppend} from "@/api/order";
const props = defineProps<{
show: boolean
title: string
goods: any
order_id: number
}>()
const emits = defineEmits<{
(event: 'update:show', show: boolean): void
}>()
const showPopup = computed({
get() {
return props.show
},
set(val) {
if (!val) {
goodsNum.value = 1
}
emits('update:show', val)
}
})
const router = useRouter()
const goodsNum = ref<number>(1)
const goodsPrice = ref<string>('')
const duration = ref<number>(0)
const orderAmount = ref<number>(0)
const getOrderInfo = async () => {
const data = await apiOrderAppend({
order_id: props.order_id,
goods: {
0: { id: props.goods.goods_id, goods_num: goodsNum.value }
},
action: 'settlement'
})
orderAmount.value = data.order_amount
goodsPrice.value = data.goods_price
duration.value = data.duration
}
const confirm = async () => {
const { id, type } = await apiOrderAppend({
order_id: props.order_id,
goods: {
0: { id: props.goods.goods_id, goods_num: goodsNum.value }
},
action: 'submit'
})
router.navigateTo({
url: '/bundle/pages/order_pay/order_pay',
query: {
order_id: id,
from: type,
order_amount: orderAmount.value
}
})
showPopup.value = false
}
watch(() => props.show, () => {
setTimeout(getOrderInfo, 100)
})
</script>

View File

@@ -0,0 +1,116 @@
<template>
<u-popup
v-model="showPopup"
mode="bottom"
:customStyle="{
'background': `none`
}"
:closeable="true"
:mask-close-able="false"
@close="showPopup = false"
>
<view
style="border-radius: 20rpx 20rpx 0 0;"
class="append-popup bg-white p-[30rpx] text-center"
>
<view class="py-2 font-medium text-2xl text-black">
{{ title }}
</view>
<view class="mt-3">
<view
class="flex text-base bg-[#f6f7f8]"
style="border-radius: 20rpx; padding: 20rpx 24rpx;"
>
<view class="flex-none text-base font-medium">
补差价金额
</view>
<view class="ml-2 flex-1 text-right">
<input type="number" v-model="orderAmount" placeholder="¥ 0.00">
</view>
</view>
<view
class="flex text-base bg-[#f6f7f8] mt-3"
style="border-radius: 20rpx; padding: 20rpx 24rpx;"
>
<view class="flex-none text-base font-medium">
备注
</view>
<view class="ml-2 flex-1 text-right">
<input type="text" v-model="orderDesc" placeholder="请输入补差价原因">
</view>
</view>
<view class="mt-4 text-left">
<view class="text-xs font-medium text-muted">
*支付前需与服务人员协商一致如有异议请联系平台客服
</view>
<view class="mt-1 text-xs font-medium text-muted">
系统只保障在线支付的项目切勿线下支付
</view>
</view>
</view>
<view style="margin-top: 90rpx" class="footer flex-1 safe-area-inset-bottom">
<u-button
type="primary"
@click="confirm"
>
立即支付
</u-button>
</view>
</view>
</u-popup>
</template>
<script lang="ts" setup>
import {computed, ref} from 'vue'
import {apiOrderGap} from "@/api/order";
import {useRouter} from "uniapp-router-next";
const props = defineProps<{
show: boolean
title: string
order_id: number
}>()
const emits = defineEmits<{
(event: 'update:show', show: boolean): void
}>()
const showPopup = computed({
get() {
return props.show
},
set(val) {
if (!val) {
orderAmount.value = null
orderDesc.value = ''
}
emits('update:show', val)
}
})
const router = useRouter()
const orderAmount = ref<number | null>(null)
const orderDesc = ref<string>('')
const confirm = async () => {
const { id , type } = await apiOrderGap({
order_id: props.order_id,
order_amount: orderAmount.value,
remark: orderDesc.value
})
router.navigateTo({
url: '/bundle/pages/order_pay/order_pay',
query: {
order_id: id,
from: type,
order_amount: orderAmount.value
}
})
showPopup.value = false
}
</script>

View File

@@ -0,0 +1,269 @@
<template>
<view class="flex w-full justify-end">
<!-- 取消订单 -->
<view
v-if="cancel_btn"
class="ml-2"
@click.stop="handleCommand('cancel')"
>
<u-button
:size="size"
type="default"
>
取消订单
</u-button>
</view>
<!-- 去支付 -->
<view
v-if="pay_btn"
class="ml-2"
@click.stop="handlePayment"
>
<u-button
:size="size"
type="primary"
>
立即支付
</u-button>
</view>
<!-- 我要加时 -->
<view
v-if="append_btn"
class="ml-2"
@click.stop="handleCommand('append')"
>
<u-button
:size="size"
type="default"
>
我要加时
</u-button>
</view>
<!-- 补差价 -->
<view
v-if="gap_btn"
class="ml-2"
@click.stop="handleCommand('gap')"
>
<u-button
:size="size"
type="primary"
>
补差价
</u-button>
</view>
<!-- 去评价 -->
<view
v-if="comment_btn"
class="ml-2"
@click.stop="router.navigateTo('/bundle/pages/evaluate_submit/index?order_goods_id=' + props.order_goods_id)"
>
<u-button
:size="size"
type="primary"
:plain="true"
>
去评价
</u-button>
</view>
<!-- 查看评价 -->
<view
v-if="look_comment_btn"
class="ml-2"
@click.stop="router.navigateTo('/bundle/pages/evaluate_detail/index?id='+props.order_goods_id)"
>
<u-button
:size="size"
type="default"
>
查看评价
</u-button>
</view>
<!-- 核销订单 -->
<!-- <template v-if="verification">-->
<!-- <view class="flex justify-around">-->
<!-- <button-->
<!-- class="Btn flex-1 mr-2 bg-white text-sm text-black leading-[70rpx] h-[70rpx] rounded-full"-->
<!-- @click.stop="showInputCode = true"-->
<!-- >-->
<!-- 手动核销码-->
<!-- </button>-->
<!-- <button-->
<!-- class="flex-1 bg-primary text-sm text-white leading-[70rpx] h-[70rpx] rounded-full"-->
<!-- @click.stop="handleScanQRCode"-->
<!-- >-->
<!-- 扫码核销-->
<!-- </button>-->
<!-- </view>-->
<!-- &lt;!&ndash; 手动输入核销码MODAL &ndash;&gt;-->
<!-- <u-modal-->
<!-- ref="uModalInput"-->
<!-- v-model="showInputCode"-->
<!-- show-cancel-button-->
<!-- confirmColor="#F36161"-->
<!-- confirm-text="确定"-->
<!-- @confirm="inputOrderCode"-->
<!-- @cancel="handleCancel"-->
<!-- title="手动核销"-->
<!-- >-->
<!-- <view class="slot-content justify-center" style="padding: 60rpx 40rpx">-->
<!-- <u-input-->
<!-- type="number"-->
<!-- v-model="code"-->
<!-- :border="true"-->
<!-- placeholder="请输入核销码"-->
<!-- style="width: 100%"-->
<!-- />-->
<!-- </view>-->
<!-- </u-modal>-->
<!-- </template>-->
</view>
<!-- <u-popup v-model="showConfirmPop" mode="center" height="500" border-radius="14">-->
<!-- <view class="text-center w-[560rpx] pt-[40rpx]">-->
<!-- <view class="w-[160rpx] h-[160rpx] px-[200rpx]">-->
<!-- <u-image width="160" height="160" :src="goods_image"/>-->
<!-- </view>-->
<!-- <view class="mt-[20rpx] truncate">{{ goods_name }}</view>-->
<!-- <view class="mt-[10rpx] mb-[40rpx] text-[#f36161]">完成服务请点击确认</view>-->
<!-- <button-->
<!-- class="flex-1 bg-primary text-sm text-white leading-[70rpx] h-[70rpx] rounded-full mx-[40rpx]"-->
<!-- @click.stop="confirmVerification"-->
<!-- >-->
<!-- 确认核销-->
<!-- </button>-->
<!-- </view>-->
<!-- </u-popup>-->
</template>
<script lang="ts" setup>
import {useRouter} from "uniapp-router-next";
const emits = defineEmits<{
(event: 'command', value: any): void
}>()
const props = defineProps<{
order_id?: string | number // 订单ID
cancel_btn: number // 取消订单
comment_btn: number // 评价
gap_btn: number // 补差价
append_btn: number // 加时
pay_btn?: number // 支付
type?: number | null // 支付方式
size: 'default' | 'medium' | 'mini'
look_comment_btn?: number // 查看评价
order_goods_id?: string | number // 订单商品ID
}>()
const router = useRouter()
// const code = ref<number | string>('') // 输入核销码
// const showInputCode = ref<boolean>(false) // 显示(输入核销码):是 | 否
// 取消订单
const handleCommand = async (command: string): Promise<void> => {
try {
emits('command', {
order_id: props.order_id,
command: command
})
} catch (error) {
console.log('错误信息:', error, props.order_id)
}
}
// 删除订单
// const handleOrderDel = async (id: number | string): Promise<void> => {
// const modelRes = await uni.showModal({
// title: '温馨提示',
// content: '确认删除该订单吗?'
// })
// if (modelRes.cancel) return
//
// try {
// await apiOrderDel({id: props.order_id})
// emits('refresh')
// // uni.navigateBack()
// } catch (error) {
// console.log('错误信息:', error, props.order_id)
// }
//
// await emits('refresh')
// }
// 支付
const handlePayment = () => {
uni.navigateTo({
url: `/bundle/pages/order_pay/order_pay?order_id=${props.order_id}&from=order`
})
}
// 确认服务
// const handleConfirmService = async (id: number | string): Promise<void> => {
// const modelRes = await uni.showModal({
// title: '温馨提示',
// content: '确认服务该订单吗?'
// })
// if (modelRes.cancel) return
// await apiStaffOrderConfirmService({id: id})
// uni.$u.toast('操作成功')
// emits('refresh')
// }
// 扫码核销
// const handleScanQRCode = async () => {
// // #ifdef H5
// if (!isWeixinClient()) return toast('h5暂不支持扫码')
// showInputCode.value = true
// // #endif
//
// // #ifndef H5
// try {
// const data: any = await uni.scanCode({})
// code.value = data.result
// showConfirmPop.value = true
// } catch (error) {
// uni.$u.toast(error)
// }
// // #endif
// }
// 确认核销
// const showConfirmPop = ref(false)
// const confirmVerification = async () => {
// try {
// await apiStaffOrderVerification({verification_code: code.value})
// code.value = ''
// uni.$u.toast('订单核销成功')
// showConfirmPop.value = false
// emits('refresh')
// } catch (error) {
// uni.$u.toast(error)
// }
// }
// 手动输入核销码
// const inputOrderCode = async (): Promise<void> => {
// if (code.value === '') return toast('请输入核销码')
// await apiStaffOrderVerification({verification_code: code.value})
// uni.$u.toast('订单核销成功')
// code.value = ''
// emits('refresh')
// }
// 取消核销码弹框
// const handleCancel = () => {
// code.value = ''
// emits('refresh')
// }
</script>

View File

@@ -0,0 +1,69 @@
<template>
<view
class="page-status"
v-if="status !== PageStatusEnum['NORMAL']"
:class="{ 'page-status--fixed': fixed }"
>
<!-- Loading -->
<template v-if="status === PageStatusEnum['LOADING']">
<slot name="loading">
<u-loading :size="60" mode="flower"/>
</slot>
</template>
<!-- Error -->
<template v-if="status === PageStatusEnum['ERROR']">
<slot name="error"></slot>
</template>
<!-- Empty -->
<template v-if="status === PageStatusEnum['EMPTY']">
<slot name="empty"></slot>
</template>
</view>
<template v-else>
<slot></slot>
</template>
</template>
<script lang="ts">
export default {
options: {
virtualHost: true
}
}
</script>
<script lang="ts" setup>
import {PageStatusEnum} from '@/enums/appEnums'
const props = defineProps({
status: {
type: String,
default: PageStatusEnum['LOADING']
},
fixed: {
type: Boolean,
default: true
}
})
</script>
<style lang="scss" scoped>
.page-status {
height: 100%;
width: 100%;
min-height: 100%;
padding: 0;
flex: 1;
display: flex;
justify-content: center;
align-items: center;
background-color: #ffffff;
&--fixed {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 900;
}
}
</style>

View File

@@ -0,0 +1,55 @@
<template>
<!-- 结果查询 -->
<modal-popup
v-model:show="showCheckPay"
title="支付宝支付"
:closeable="true"
@close="emits('check')"
>
<template #content>
<view
class="p-20"
style="width: 100%;padding: 15rpx 15rpx;margin: 50rpx 0;"
>
请复制链接,粘贴至浏览器并支付
</view>
</template>
<template #footer>
<view class="flex-1">
<u-button
type="primary"
:custom-style="{
'flex': 1
}"
@click="copy(alipayLink)"
>
复制链接
</u-button>
</view>
</template>
</modal-popup>
</template>
<script lang="ts" setup>
import {computed} from "vue";
import {useCopy} from "@/hooks/useCopy";
const emits = defineEmits<{
(e: 'update:show', value: boolean): void
(e: 'check'): void
}>()
const props = defineProps<{
show: boolean
alipayLink: string
}>()
const {copy} = useCopy()
const showCheckPay = computed({
get() {
return props.show
},
set(value) {
emits('update:show', value)
}
})
</script>

Some files were not shown because too many files have changed in this diff Show More