【小程序】 新增# 项目初始化

main
kaeery 2025-01-20 17:47:46 +08:00
commit 549435671a
397 changed files with 54571 additions and 0 deletions
src
bundle
pages
account_detail
appoint_time
category_goods_coupon
category_goods_list
collection_list
contact_service
coupon_order
dist_order
distributor_binding_user
distributor_order
evaluate_goods
evaluate_submit
master_worker_detail
master_worker_list
my_coupon
my_distributor
receive_coupon
recharge_record
search
service_explain
service_order_detail
share_qrcode
user_address
user_address_edit
user_recharge
user_wallet
user_withdraw
withdraw_distributor_record
withdraw_money
withdraw_record
withdrawal_details

39
.eslintrc.js 100644
View File

@ -0,0 +1,39 @@
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
root: true,
ignorePatterns: ['src/uni_modules/'],
extends: [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-typescript/recommended',
'@vue/eslint-config-prettier'
],
rules: {
'prettier/prettier': [
'warn',
{
semi: false,
singleQuote: true,
printWidth: 100,
proseWrap: 'preserve',
bracketSameLine: false,
endOfLine: 'lf',
tabWidth: 4,
useTabs: false,
trailingComma: 'none'
}
],
'vue/multi-word-component-names': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'no-undef': 'off',
'vue/prefer-import-from-vue': 'off',
'no-prototype-builtins': 'off',
'prefer-spread': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off'
},
globals: {}
}

34
.gitignore vendored 100644
View File

@ -0,0 +1,34 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
dist
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.hbuilderx
# .env
.env.development
.env.production
yarn.lock
package-lock.json

3
.vscode/extensions.json vendored 100644
View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
}

26
.vscode/settings.json vendored 100644
View File

@ -0,0 +1,26 @@
{
"editor.detectIndentation": false,
"editor.tabSize": 4,
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"css.validate": false,
"less.validate": false,
"scss.validate": false,
"prettier.printWidth": 100, //
"prettier.tabWidth": 4, //
"prettier.useTabs": false, // 使tab使
"prettier.semi": false, //
"prettier.singleQuote": true, // 使
"prettier.proseWrap": "preserve", // 使GitHub commentmarkdown
"prettier.arrowParens": "avoid", // (x) => {} avoid
"prettier.bracketSpacing": true, // "{ foo: bar }"
"prettier.endOfLine": "auto", // \n \r \n\r auto
"prettier.htmlWhitespaceSensitivity": "ignore",
"prettier.ignorePath": ".prettierignore", // 使prettier.prettierignore
"prettier.jsxSingleQuote": false, // jsx使
"prettier.requireConfig": false, // Require a 'prettierconfig' to format prettier
"prettier.trailingComma": "none" //
}

20
index.html 100644
View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script>
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
CSS.supports('top: constant(a)'))
document.write(
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
</script>
<title></title>
<!--preload-links-->
<!--app-context-->
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

82
package.json 100644
View File

@ -0,0 +1,82 @@
{
"name": "uni-preset-vue",
"version": "0.0.0",
"scripts": {
"dev:app": "uni -p app",
"dev:custom": "uni -p",
"dev:h5": "uni",
"dev:h5:ssr": "uni --ssr",
"dev:mp-alipay": "uni -p mp-alipay",
"dev:mp-baidu": "uni -p mp-baidu",
"dev:mp-kuaishou": "uni -p mp-kuaishou",
"dev:mp-lark": "uni -p mp-lark",
"dev:mp-qq": "uni -p mp-qq",
"dev:mp-toutiao": "uni -p mp-toutiao",
"dev:mp-weixin": "uni -p mp-weixin",
"dev:quickapp-webview": "uni -p quickapp-webview",
"dev:quickapp-webview-huawei": "uni -p quickapp-webview-huawei",
"dev:quickapp-webview-union": "uni -p quickapp-webview-union",
"build:app": "uni build -p app",
"build:custom": "uni build -p",
"build:h5": "node scripts/build.h5.mjs",
"build:h5:ssr": "uni build --ssr",
"build:mp-alipay": "uni build -p mp-alipay",
"build:mp-baidu": "uni build -p mp-baidu",
"build:mp-kuaishou": "uni build -p mp-kuaishou",
"build:mp-lark": "uni build -p mp-lark",
"build:mp-qq": "uni build -p mp-qq",
"build:mp-toutiao": "uni build -p mp-toutiao",
"build:mp-weixin": "uni build -p mp-weixin",
"build:quickapp-webview": "uni build -p quickapp-webview",
"build:quickapp-webview-huawei": "uni build -p quickapp-webview-huawei",
"build:quickapp-webview-union": "uni build -p quickapp-webview-union",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
},
"dependencies": {
"@dcloudio/uni-app": "^3.0.0-alpha-3050520220824001",
"@dcloudio/uni-app-plus": "^3.0.0-alpha-3050520220824001",
"@dcloudio/uni-components": "^3.0.0-alpha-3050520220824001",
"@dcloudio/uni-h5": "^3.0.0-alpha-3050520220824001",
"@dcloudio/uni-mp-alipay": "^3.0.0-alpha-3050520220824001",
"@dcloudio/uni-mp-baidu": "^3.0.0-alpha-3050520220824001",
"@dcloudio/uni-mp-kuaishou": "^3.0.0-alpha-3050520220824001",
"@dcloudio/uni-mp-lark": "^3.0.0-alpha-3050520220824001",
"@dcloudio/uni-mp-qq": "^3.0.0-alpha-3050520220824001",
"@dcloudio/uni-mp-toutiao": "^3.0.0-alpha-3050520220824001",
"@dcloudio/uni-mp-weixin": "^3.0.0-alpha-3050520220824001",
"@dcloudio/uni-quickapp-webview": "^3.0.0-alpha-3050520220824001",
"enumtor": "^1.0.5",
"lodash-es": "^4.17.21",
"pinia": "^2.0.20",
"vconsole": "^3.14.6",
"vue": "^3.2.37",
"vue-i18n": "^9.2.2",
"weixin-js-sdk": "^1.6.0",
"z-paging": "^2.3.8"
},
"devDependencies": {
"@dcloudio/types": "^3.0.13",
"@dcloudio/uni-automator": "^3.0.0-alpha-3050520220824001",
"@dcloudio/uni-cli-shared": "^3.0.0-alpha-3050520220824001",
"@dcloudio/uni-stacktracey": "^3.0.0-alpha-3050520220824001",
"@dcloudio/vite-plugin-uni": "^3.0.0-alpha-3050520220824001",
"@rushstack/eslint-patch": "^1.1.4",
"@types/lodash-es": "^4.17.6",
"@types/node": "^18.7.16",
"@vue/eslint-config-prettier": "^7.0.0",
"@vue/eslint-config-typescript": "^11.0.0",
"autoprefixer": "^10.4.8",
"eslint": "^8.22.0",
"eslint-plugin-vue": "^9.4.0",
"execa": "^6.1.0",
"fs-extra": "^10.1.0",
"postcss": "^8.4.16",
"postcss-rem-to-responsive-pixel": "^5.1.3",
"prettier": "^2.7.1",
"sass": "^1.54.5",
"tailwindcss": "^3.1.8",
"typescript": "^4.7.4",
"vite": "^2.9.14",
"weapp-tailwindcss-webpack-plugin": "^1.7.0"
}
}

View File

@ -0,0 +1,29 @@
{
"compileType": "miniprogram",
"setting": {
"coverView": true,
"es6": true,
"postcss": true,
"minified": true,
"enhance": true,
"showShadowRootInWxmlPanel": true,
"packNpmRelationList": [],
"babelSetting": {
"ignore": [],
"disablePlugins": [],
"outputPath": ""
},
"ignoreUploadUnusedFiles": true
},
"condition": {},
"editorSetting": {
"tabIndent": "insertSpaces",
"tabSize": 2
},
"libVersion": "3.0.0",
"packOptions": {
"ignore": [],
"include": []
},
"appid": "wxf712ba4cbc2df1e9"
}

View File

@ -0,0 +1,7 @@
{
"description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html",
"projectname": "housekeeping-uniapp",
"setting": {
"compileHotReLoad": true
}
}

View File

@ -0,0 +1,37 @@
import { execaCommand } from 'execa'
import path from 'path'
import fsExtra from 'fs-extra'
const { existsSync, remove, copy } = fsExtra
const cwd = process.cwd()
//打包发布路径,谨慎改动
const releaseRelativePath = '../public/mobile'
const distPath = path.resolve(cwd, 'dist/build/h5')
const releasePath = path.resolve(cwd, releaseRelativePath)
async function build() {
await execaCommand('uni build', { stdio: 'inherit', encoding: 'utf-8', cwd })
if (existsSync(releasePath)) {
await remove(releasePath)
}
console.log(`文件正在复制 ==> ${releaseRelativePath}`)
try {
await copyFile(distPath, releasePath)
} catch (error) {
console.log(`\n ${error}`)
}
console.log(`文件已复制 ==> ${releaseRelativePath}`)
}
function copyFile(sourceDir, targetDir) {
return new Promise((resolve, reject) => {
copy(sourceDir, targetDir, (err) => {
if (err) {
reject(err)
} else {
resolve()
}
})
})
}
build()

32
src/App.vue 100644
View File

@ -0,0 +1,32 @@
<script setup lang="ts">
import { onLaunch } from '@dcloudio/uni-app'
import { useAppStore } from './stores/app'
import { useUserStore } from './stores/user'
import { apiIndexVisit } from '@/api/app'
import { getClient } from '@/utils/client'
const appStore = useAppStore()
const { getUser } = useUserStore()
onLaunch(async () => {
await appStore.getConfig()
appStore.getLocationFunc()
appStore.getSystemInfoFn()
// #ifdef H5
const { status, close, url } = appStore.getH5Config
if (status == 0) {
if (close == 1) return (location.href = url)
uni.reLaunch({ url: '/pages/empty/empty' })
}
// #endif
// #ifdef MP-WEIXIN
await appStore.checkForUpdate()
// #endif
await getUser()
await apiIndexVisit({
terminal: getClient()
})
})
</script>
<style lang="scss">
//
</style>

26
src/api/account.ts 100644
View File

@ -0,0 +1,26 @@
import { client } from '@/utils/client'
import request from '@/utils/request'
// 登录
export function login(data: Record<string, any>) {
return request.post({ url: '/login/check', data: { ...data, client } })
}
//注册
export function register(data: Record<string, any>) {
return request.post({ url: '/login/register', data: { ...data, client } })
}
//忘记密码
export function forgotPassword(data: Record<string, any>) {
return request.post({ url: '/login/forgotPassword', data })
}
//向微信请求code的链接
export function getWxCodeUrl() {
return request.get({ url: '/login/codeUrl', data: { url: location.href } })
}
export function OALogin(data: Record<string, any>) {
return request.get({ url: '/login/oaLogin', data })
}

99
src/api/app.ts 100644
View File

@ -0,0 +1,99 @@
import request from '@/utils/request'
// #ifdef H5
import { getSignLink } from '@/hooks/wechat'
// #endif
//发送短信
export function smsSend(data: any) {
return request.post({ url: '/sms/send', data: data })
}
// 获取公共配置
export function getConfig() {
return request.get({ url: '/config' })
}
// 访问量
export const apiIndexVisit = (params: any) => request.get({ url: '/index/visit', data: params })
// 获取政策协议
export function getPolicy(data: any) {
return request.get({ url: '/policy', data: data })
}
// 获取客服信息
export function apiContactService(data: any) {
return request.get({ url: '/decorate/pages/service', data: data }, { isAuth: true })
}
// 公众号登录
export const apiOALogin = (params: any) => request.post({ url: '/login/oaLogin', data: params })
// 公众号-获取授权url
export const apiCodeUrlGet = () =>
request.get({
url: '/login/codeUrl',
data: {
url: location.href,
headers: { 1: 1 }
}
})
// 获取城市信息-字母分组
export const apiCityInfo = (params: any) => request.get({ url: '/region/city', data: params })
// 微信sdk配置
export const apiJsConfig = (data: any) =>
request.get({ url: '/wechat/jsConfig', data: { url: getSignLink() } })
/**
* @param { Object } params { address: 广xxx }
* @return { Promise }
* @description
*/
export const getGeocoder = (params: any) =>
request.get({ url: '/region/keyWordAddress', data: params })
/**
* @param { Object } params { location: xxx,xxx }
* @return { Promise }
* @description
*/
export const getGeocoderCoordinate = (params: any) =>
request.post({ url: '/region/genCoder', data: params })
// 支付方式
export const apiPayPayWay = (params: any) =>
request.get({ url: '/order/pay/way/list', data: params })
// 预支付
export const apiPayPrepay = (params: any) => request.post({ url: '/pay/wx/prePay', data: params })
// h5预支付
export const apiH5PayPrepay = (params: any) => request.post({ url: '/pay/h5/prePay', data: params })
// DY 预支付
export const apiBytePayPrepay = (params: any) =>
request.post({ url: '/pay/byte/createOrder', data: params })
// DY 更新订单状态
export const apiByteQueryOrder = (params: any) =>
request.post({ url: '/pay/byte/queryOrder', data: params })
// 充值支付接口
export const apiRechargeOrder = (params: any) =>
request.post({ url: '/pay/wx/prePay', data: params })
// 查询订单
export const apiQueryOrder = (params: any) =>
request.get({ url: '/order/pay/status', data: params })
// 查询充值记录
export const apiQueryRechargeList = (params: any) =>
request.get({ url: '/recharge/record', data: params })
// 查询充值状态
export const apiQueryRecharge = (params: any) =>
request.get({ url: '/recharge/payStatus', data: params })
// 绑定分销员id
export const apiBindingDistId = (params: any) =>
request.post({ url: '/user/bindDistributor', data: params })

28
src/api/coupon.ts 100644
View File

@ -0,0 +1,28 @@
import request from '@/utils/request'
// 获取我的优惠券
export const apiMyCouponList = (params) => request.get({ url: '/coupon/myCoupon', data: params })
// 领券中心
export const apiCouponCenterList = (params) => request.get({ url: '/coupon/list', data: params })
// 优惠券详情
export const apiCouponDetail = (params) => request.get({ url: '/coupon/detail', data: params })
// 领取优惠券
export const apiCouponManualGet = (params) => request.post({ url: '/coupon/getCoupon', data: params })
// 品类券对应商品
export const apiCouponCategoryGoods = (params) => request.get({ url: '/coupon/deductionGoodsCategory', data: params })
// 商品券对应商品
export const apiCouponGoods = (params) => request.get({ url: '/coupon/deductionGoods', data: params })
// 下单获取最大优惠
export const apiCouponMaxMoney = (params) => request.get({ url: '/coupon/maxDiscounts', data: params })
// 可用优惠券+不可用优惠券
export const apiCouponByUse = (params) => request.get({ url: '/coupon/selectCoupon', data: params })
// 可用优惠券+不可用优惠券数量
export const apiCouponCount = (params) => request.get({ url: '/coupon/selectCouponCount', data: params })

View File

@ -0,0 +1,34 @@
import request from '@/utils/request'
/**获取分销商数据 */
export function getDistributorCenter(params) {
return request.get({ url: '/distributor/myDistributor', data: params })
}
/**获取月份账单 */
export function getDistributorOrder(params) {
return request.get({ url: '/distributor/distributorOrder', data: params })
}
/**获取月份账单详情 */
export function getDistributorOrderDetail(params) {
return request.get({ url: '/distributor/distributorOrderDetail', data: params })
}
/**获取提现记录 */
export function getDistributorWithDrawList(params) {
return request.get({ url: '/distributor/withdrawalRecord', data: params })
}
/**分享太阳码 */
export function getDistributorQrcode(params) {
return request.get({ url: '/distributor/distributorQrCode', data: params })
}
/**获取分销用户 */
export function getDistributorBindingUser(params) {
return request.get({ url: '/distributor/getDistributorUser', data: params })
}
/**获取分销订单-月份总金额 */
export function getDistributorOrderByMonth(params) {
return request.get({ url: '/distributor/distributorOrderByMonth', data: params })
}
/**获取分销用户-月份分销总用户 */
export function getDistributorUserByMonth(params) {
return request.get({ url: '/distributor/distributorUserByMonth', data: params })
}

55
src/api/goods.ts 100644
View File

@ -0,0 +1,55 @@
/*
* @Author: micky 1254597151@qq.com
* @Date: 2023-08-14 15:38:40
* @LastEditors: micky 1254597151@qq.com
* @LastEditTime: 2023-12-20 20:08:02
* @FilePath: \housekeeping-uniapp\src\api\goods.ts
* @Description: ,`customMade`, koroFileHeader : https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
import request from '@/utils/request'
// 获取商品详情
export const apiGoodsDetail = params => request.get({ url: '/goods/detail', data: params })
// 收藏列表
export const apiCollectLists = () => request.get({ url: '/collect/list' })
// 商品收藏
export const apiGoodsCollection = params => request.post({ url: '/collect/add', data: params })
// 取消收藏
export const apiGoodsCollectionCancel = params => request.get({ url: '/collect/del', data: params })
// 获取预约时间列表
export const apiAppointTime = (params = {}) => request.get({ url: '/time/getTime', data: params })
// 添加评价
export const apiAddEvaluateGoods = (params = '') =>
request.post({ url: '/comment/add', data: params })
// 获取商品评价列表
export const apiEvaluateGoodsLists = (params: any) =>
request.post({ url: '/comment/list', data: params })
// 待评价列表
export const apiWaitEvaluateGoodsLists = (params: any) =>
request.get({ url: '/order/wait/comment/list', data: params }, {})
// 获取商品评价统计数据-已评价、待评价
export const apiEvaluateStatis = (params: any) =>
request.get({ url: '/comment/statistics/user', data: params })
// 获取商品评价统计数据-好评、差评...
export const apiEvaluateGoodsStatis = (params: any) =>
request.get({ url: '/comment/statistics/goods', data: params })
// 退单规则
export const apiRuleList = (params: any) =>
request.get({ url: '/goods/getCancelOrderGoodsConfig', data: params })
// 获取活动专区详情
export const apiActivityDetail = (params: any) =>
request.get({ url: '/activity/detail', data: params })
// 获取活动专区列表
export const apiActivityList = (params: any) => request.get({ url: '/activity/list', data: params })

56
src/api/order.ts 100644
View File

@ -0,0 +1,56 @@
import request from '@/utils/request'
// 订单初始化
export const apiInitOrder = (params: any) => request.get({ url: '/order/init', data: params })
// 订单下单
export const apiPlaceOrder = (params: any) => request.post({ url: '/order/add', data: params })
// 订单列表
export const apiOrderLists = (params: any) =>
request.get({ url: '/order/list', data: params }, { ignoreCancel: true })
// 订单详情
export const apiOrderDetail = (params: any) => request.get({ url: '/order/detail', data: params })
// 取消订单
export const apiOrderCancel = (params: any) => request.post({ url: '/order/cancel', data: params })
// 删除订单
export const apiOrderDel = (params: any) => request.get({ url: '/order/del', data: params })
// 关闭订单(取消支付)
export const apiOrderClose = (params: any) =>
request.get({ url: '/pay/wx/closeOrder', data: params })
/** 师傅订单服务 Start **/
// 师傅服务列表
export const apiStaffOrderLists = (params: any) =>
request.get({ url: '/order/staff/list', data: params }, { ignoreCancel: true })
// 师傅服务详情
export const apiStaffOrderDetail = (params: any) =>
request.get({ url: '/order/detail', data: params })
// 确认服务
export const apiStaffOrderConfirmService = (params: any) =>
request.get({ url: '/order/staff/confirm', data: params })
// 核销订单
export const apiStaffOrderVerification = (params: any) =>
request.get({ url: '/order/staff/verification', data: params })
// 用户核销订单
export const apiUserOrderVerification = (params: any) =>
request.get({ url: '/order/user/verification', data: params })
/** 师傅订单服务 Start **/
// DY 取消更新状态
export const apiByteQueryRefund = (params: any) =>
request.post({ url: '/pay/byte/queryRefund', data: params })
// 取消订单扣分规则
export const apiCancelOrderRule = (params: any) =>
request.get({ url: '/order/getCancelOrderConfigByOrder', data: params })

25
src/api/shop.ts 100644
View File

@ -0,0 +1,25 @@
import request from '@/utils/request'
//首页数据
export function getIndex(data: any) {
return request.get({ url: '/index/index', data })
}
// 装修页面
export function getDecorate(data: any) {
return request.get({ url: '/decorate', data })
}
// 热门搜索
export function getHotSearch() {
return request.get({ url: '/hotSearch' })
}
/**
* @description
* @param { string } keyword
* @return { Promise }
*/
export function getSearch(data: { keyword: string; pageNo: number; pageSize: number }) {
return request.get({ url: '/search', data })
}

77
src/api/store.ts 100644
View File

@ -0,0 +1,77 @@
/*
* @Author: micky 1254597151@qq.com
* @Date: 2023-08-14 15:38:40
* @LastEditors: micky 1254597151@qq.com
* @LastEditTime: 2023-12-25 20:10:21
* @FilePath: \housekeeping-uniapp\src\api\store.ts
* @Description: ,`customMade`, koroFileHeader : https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
import request from '@/utils/request'
/**
* @param { Object } params
* @return { Promise }
* @description
*/
export const apiAdLists = (params: any) => request.get({ url: '/ad/lists', data: params })
// 服务分类-分页列表
export const apiCategoryLists = () => request.get({ url: '/category/list' })
// 商品服务分类-父级
export const apiGoodsCategoryLists = (params: any) =>
request.get({ url: '/category/commonList', data: params })
/**
* @param { Object } params
* @return { Promise }
* @description
*/
export const apiGoodsLists = (params: any) =>
request.get({ url: '/goods/list', data: params }, { ignoreCancel: true })
/**
* @param { Object } params
* @return { Promise }
* @description
*/
export const apiStaffLists = (params: any) => request.post({ url: '/staff/list', data: params })
/**
* @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 = () => request.get({ url: '/region/char/city' })
/**
* @description
* @return { Promise }
*/
export function getHotSearch() {
return request.get({ url: '/hotSearch' })
}
/**
* @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 apiRecommendService = (params: any) =>
request.get({ url: '/goods/recommendationList', data: params })

54
src/api/user.ts 100644
View File

@ -0,0 +1,54 @@
import request from '@/utils/request'
export function getUserCenter(header?: any) {
return request.get({ url: '/user/center', header })
}
// 个人信息
export function getUserInfo() {
return request.get({ url: '/user/info' }, { isAuth: true })
}
// 个人编辑
export function userEdit(data: any) {
return request.post({ url: '/user/edit', data }, { isAuth: true })
}
// 绑定手机
export function userBindMobile(data: any, header?: any) {
return request.post({ url: '/user/bindMobile', data, header }, { isAuth: true })
}
// 微信电话
export function userMnpMobile(data: any) {
return request.post({ url: '/user/mnpMobile', data }, { isAuth: true })
}
// 更改密码
export function userChangePwd(data: any) {
return request.post({ url: '/user/changePwd', data }, { isAuth: true })
}
// 地址列表
export const apiAddressLists = () => request.get({ url: '/address/list' })
// 获取地址详情
export const apiAddressDetail = (params: any) =>
request.get({ url: '/address/detail', data: params })
// 编辑地址
export const apiAddressEdit = (params: any) => request.post({ url: '/address/edit', data: params })
// 新增地址
export const apiAddressAdd = (params: any) => request.post({ url: '/address/add', data: params })
// 删除地址
export const apiAddressDel = (params: any) => request.get({ url: '/address/del', data: params })
// 提交订单
export const apiEvaluateAdd = (params: any) => request.post({ url: '/comment/add', data: params })
//更新微信小程序头像昵称
export function updateUser(data: Record<string, any>, header: any) {
return request.post({ url: '/user/updateUser', data, header })
}

48
src/api/wallet.ts 100644
View File

@ -0,0 +1,48 @@
/*
* @Author: micky 1254597151@qq.com
* @Date: 2023-08-14 15:38:40
* @LastEditors: micky 1254597151@qq.com
* @LastEditTime: 2024-01-28 16:20:42
* @FilePath: \housekeeping-uniapp\src\api\wallet.ts
* @Description: ,`customMade`, koroFileHeader : https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
import request from '@/utils/request'
// 钱包信息
export const apiUserWallet = (params: any) => request.get({ url: '/recharge/config', data: params })
// 账户明细列表
export const apiAccountLogLists = (params: any) =>
request.get({ url: '/account/log/list', data: params }, { ignoreCancel: true })
// 充值
export const apiRecharge = (params: any) =>
request.post({ url: '/recharge/placeOrder', data: params })
// 充值记录列表
export const apiRechargeLogLists = (params: any) =>
request.get({ url: '/recharge/record', 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 })
// 提现申请-不需要审核
export const apiWithdrawCommission = (params: any) =>
request.post({ url: '/distributor/withdrawCommission', data: params })
// 提现申请-新-需要审核
export const applyForWithdraw = (params: any) =>
request.post({ url: '/distributor/applyForAdd', data: params })

View File

@ -0,0 +1,55 @@
<template>
<view class="main">
<tabs
:current="current"
height="80"
bar-width="60"
:auth="true"
bgColor="#fff"
activeColor="#F36161"
inactiveColor="#666"
:itemWidth="150"
:is-scroll="false"
@change="onChange"
>
<tab v-for="(item, i) in tabList" :key="i" :name="item.name" />
</tabs>
<view class="list pt-[20rpx]">
<list :status="status" :index="current" />
</view>
</view>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
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: ''
},
{
name: '收入',
status: 1
},
{
name: '支出',
status: 0
}
])
const current = ref<number>(0)
const status = ref<number | string>()
const onChange = (index: number) => {
current.value = index
status.value = tabList.value[index].status
}
</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">
<view class="flex">
<view class="flex-1 balance-details-item-text">{{
item?.changeTypeDesc
}}</view>
<view
class="balance-details-item-amount"
:class="{ 'text-error': item.action === 0 }"
>
{{ item.actionDesc + item.changeAmount }}
</view>
</view>
<view class="balance-details-item-time">{{ item.remark }}</view>
<view class="balance-details-item-time">{{ item.createTime }}</view>
</view>
</view>
</view>
</z-paging>
</template>
<script lang="ts" setup>
import { reactive, watch, shallowRef } from 'vue'
import { apiAccountLogLists } from '@/api/wallet'
import { onLoad } from '@dcloudio/uni-app'
const props = withDefaults(
defineProps<{
status: string | number
index: number
}>(),
{
status: ''
}
)
watch(
() => props.index,
async () => {
paging.value.reload()
},
{ immediate: true }
)
const balance = reactive({
lists: [] as any,
change_type: 1
})
// Ref
const paging = shallowRef()
const queryList = async (pageNo = 1, pageSize = 10) => {
try {
const { lists } = await apiAccountLogLists({
action: props.status,
pageNo,
pageSize
})
paging.value.complete(lists)
} catch (e) {
paging.value.complete(false)
}
}
onLoad((options: any) => {
balance.change_type = options.changeObject
if (balance.change_type == 2) {
uni.setNavigationBarTitle({
title: '佣金明细'
})
}
})
</script>
<style lang="scss" scoped>
.balance-details {
.balance-details-item {
background-color: #fff;
padding: 20rpx 30rpx;
border-bottom: 1rpx solid #ebebeb;
.balance-details-item-text {
font-size: 30rpx;
font-weight: 400;
}
.balance-details-item-amount-add {
font-size: 34rpx;
font-weight: 400;
color: #ff0017;
}
.balance-details-item-amount {
font-size: 34rpx;
font-weight: 400;
}
.balance-details-item-time {
font-size: 24rpx;
font-weight: 400;
color: #999;
margin-top: 10rpx;
}
}
}
</style>

View File

@ -0,0 +1,266 @@
<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.startTime }} - {{ item2.endTime }}
</view>
</block>
</view>
<!-- Main End -->
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { apiAppointTime } from '@/api/goods'
import { onLoad, onShow } from '@dcloudio/uni-app'
interface DateObj {
year: number
week: string | undefined
date: string
}
interface TimeSlotObj {
startTime: string
endTime: 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 uni.$u.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.startTime,
end_time: timeSlotItem.endTime
}
if (goodsForm.value.id != '') {
const params = {
goodsData: goodsForm.value,
appointData: appointTime
}
goPage(`/pages/order_buy/index?type=1&params=${JSON.stringify(params)}`)
return
}
uni.$emit('appointTime', appointTime)
uni.navigateBack()
}
/**
* @return { Promise } void
* @description 请求获取预约时间信息
*/
const getAppointTime = async (id: number): Promise<void> => {
//
const res = await apiAppointTime({ goodsId: id })
console.log('data', res)
timeSlot.value = handleTimeSlot(res.orderTimeListVoList)
date.value = handleAppointDay(res.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) // 1030 --> 1030
timeSlot.forEach(item => {
const end = item.endTime.replace(':', '')
//
item.disabled = Number(startTime) + 300 >= Number(end)
// console.log("startTime: " + startTime);
// console.log("end: " + end);
})
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 => {
console.log('options', options)
if (options.params) {
const goodsData = JSON.parse(options.params)
goodsForm.value = goodsData
}
if (options.goodsId) getAppointTime(options.goodsId)
})
</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: 105rpx;
text-align: center;
display: inline-block;
color: #222222;
margin: 0 20rpx;
font-size: 32rpx;
}
.active {
color: $blue5;
border-bottom: 4px solid $blue5;
padding-bottom: 16rpx;
}
}
.time-box {
padding: 24rpx;
display: grid;
grid-template-columns: repeat(2, 48%);
grid-column-gap: 4%;
grid-row-gap: 20rpx;
.time-item {
// width: 220rpx;
height: 90rpx;
line-height: 90rpx;
text-align: center;
display: inline-block;
background-color: #ffffff;
// margin: 0 20rpx 20rpx 0;
border-radius: 10rpx;
font-size: 32rpx;
}
.time-item:nth-child(3n) {
margin-right: 0;
}
.select {
background-color: rgba(255, 244, 244, 1);
color: $blue5;
// 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,98 @@
<template>
<view class="goods-item" @click="handleGoodItem(attributes!)">
<view class="info-img">
<!-- <u-image :src="attributes.image" class="goods-image" width="300" height="300" border-radius="15"></u-image> -->
<image :src="attributes!.image" class="goods-image"></image>
</view>
<view class="info-column">
<view style="text-align: left">
<view class="goods-title overflow-ellipsis-line ellipsis-line1">
{{ attributes!.name }}
</view>
</view>
<view class="price-wrap mt-[20rpx]">
¥
<span class="price">{{ attributes!.price }}</span>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { PropType } from 'vue'
interface GoodProp {
id: Number
name: string
remarks: string
category: string
unit: string
image: string
price: number
scribingPrice: number
status: number
statusDesc: string
}
const props = defineProps({
attributes: Object as PropType<GoodProp>
})
const emit = defineEmits(['handleGoodItem'])
const handleGoodItem = (row: GoodProp) => {
// emit('handleGoodItem', row.id)
uni.navigateTo({
url: `/pages/goods/index?id=${row.id}`
})
}
</script>
<style lang="scss" scoped>
.goods-item {
// display: inline-block; //便
// padding: 20rpx;
border-radius: 10rpx;
background-color: #fff;
padding: 20rpx;
// background-color: pink;
// height: 200px;
// width: 47%; //goods item
.info-img {
width: 100%;
height: 300rpx;
border-radius: 12rpx;
overflow: hidden;
.goods-image {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.info-column {
text-align: left;
padding-top: 20rpx;
padding-left: 5rpx;
.goods-title {
font-size: 0.95rem;
margin-bottom: 10rpx;
}
.goods-scribingPrice {
font-size: 0.775rem;
color: #808695;
text-decoration: line-through;
}
.price-wrap {
color: red;
.price {
font-size: 1.1rem; //¥
}
}
}
}
</style>

View File

@ -0,0 +1,62 @@
<template>
<view class="goods__container">
<z-paging ref="paging" v-model="goodsList" @query="transformQueryList" :fixed="false" height="100%"
auto-show-back-to-top>
<view class="goods">
<block v-for="good in goodsList" :key="good.id">
<goodItem :attributes="good" />
</block>
</view>
</z-paging>
</view>
</template>
<script setup lang='ts'>
import { ref, unref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { useZPaging, useCoupon } from '@/hooks/useCoupon'
import goodItem from './components/goodItem.vue'
import { apiCouponGoods } from '@/api/coupon'
interface GoodProp {
id: number
name: string
remarks: string
category: string
unit: string
image: string
price: number
scribingPrice: number
status: number
statusDesc: string
}
const goodsList = ref<GoodProp[]>([])
const { couponId, setCouponId } = useCoupon()
const { paging, queryList } = useZPaging({}, apiCouponGoods)
onLoad((options) => {
const { id } = options as any
setCouponId(id)
})
function transformQueryList(page_no: number, page_size: number) {
const config = {
couponId: unref(couponId),
}
return queryList(page_no, page_size, config)
}
</script>
<style lang="scss" scoped>
.goods__container {
height: 100vh;
.goods {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
padding: 20rpx;
}
}
</style>

View File

@ -0,0 +1,151 @@
<template>
<tabs
class="tabs"
:current="current"
v-bind="tabStyle"
@change="handleChange"
:swipeable="false"
>
<tab v-for="(item, i) in categoryLists" :key="i" :name="item.name"></tab>
<view class="core">
<z-paging
class="z-paging"
ref="paging"
v-model="categoryLists"
@query="transformQueryList"
:fixed="false"
height="100%"
auto-show-back-to-top
@scroll="handleScroll"
>
<view class="cate-container mt-[20rpx]">
<view
v-for="(category, i) in categoryLists"
:key="i"
:id="'category-item' + i"
class="category-item"
>
<view class="text-center mt-[40rpx] mb-[20rpx] text-content text-sm">
{{ category.name }}
</view>
<view class="goods">
<block v-for="(item, idx) in category.sonGoodsList" :key="item.id">
<GoodItem :attributes="item" />
</block>
</view>
</view>
</view>
</z-paging>
</view>
</tabs>
</template>
<script setup lang="ts">
import { ref, onMounted, getCurrentInstance, unref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import GoodItem from './components/goodItem.vue'
import { useCoupon, useZPaging } from '@/hooks/useCoupon'
import { apiCouponCategoryGoods } from '@/api/coupon'
let TAB_HEIGHT = 50
const instance = getCurrentInstance() //
const distances = ref<number[]>([])
const categoryLists = ref<any[]>([])
const current = ref(0)
const isTabChange = ref(false)
const tabStyle = computed(() => {
return {
height: 100,
barWidth: 60,
fontSize: 32,
barStyle: { bottom: 0 },
activeColor: '#1296DB'
}
})
const { couponId, setCouponId } = useCoupon()
const { paging, queryList } = useZPaging({}, apiCouponCategoryGoods)
onMounted(async () => {
setTabHeight()
setDisatnces()
})
onLoad(options => {
const { id } = options as any
setCouponId(id)
})
// (start)
const handleScroll = event => {
if (unref(isTabChange)) return
const { scrollTop } = event.detail
const index = distances.value.findIndex(distance => distance - TAB_HEIGHT - scrollTop > 0)
current.value = index > 0 ? index - 1 : distances.value.length - 1
}
const handleChange = async (index: number) => {
// current.value = index
isTabChange.value = true
const id = `#category-item${index}`
uni.createSelectorQuery()
.select(id)
.boundingClientRect(data => {
uni.createSelectorQuery()
.select('.cate-container')
.boundingClientRect(res => {
const scrollTop = data.top - res.top
paging.value.scrollToY(scrollTop)
isTabChange.value = false
})
.exec()
})
.exec()
}
function setDisatnces() {
setTimeout(() => {
const query = uni.createSelectorQuery().in(instance)
query
.selectAll('.category-item')
.boundingClientRect(data => {
distances.value = data.map(v => v.top)
})
.exec()
}, 200)
}
function setTabHeight() {
const query = uni.createSelectorQuery().in(instance)
query
.select('.tabs >>> #cu-tab')
.boundingClientRect(res => {
TAB_HEIGHT = (res.height / (uni.upx2px(100) / 100)) as number
})
.exec()
}
// (end)
function transformQueryList(page_no: number, page_size: number) {
const config = {
couponId: unref(couponId)
}
queryList(page_no, page_size, config)
return paging.value.complete()
}
</script>
<style lang="scss" scoped>
.goods {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
padding: 0 20rpx;
}
.core {
height: calc(100vh - 100rpx);
}
</style>

View File

@ -0,0 +1,156 @@
<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.id }"
@click="changeTabs(menuData)"
>
<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]">{{ ellipsisText(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]">
<GoodsDesc :lists="goodsData"></GoodsDesc>
</view>
</z-paging>
</view>
</view>
</template>
<script lang="ts" setup>
import { computed, ref, shallowRef } from 'vue'
import { onLoad, onShareAppMessage } from '@dcloudio/uni-app'
import { apiGoodsCategoryLists, apiCategoryLists, apiGoodsLists } from '@/api/store'
// import GoodsCard from '@/components/goods-card/index.vue'
import GoodsDesc from '@/components/goods-desc/index.vue'
const categoryId = ref<number>()
const menuData = ref<any>({})
const goodsData = ref<any>([])
const paging = shallowRef()
//
const idx = ref(0)
const ellipsisText = computed(() => name => name?.length > 4 ? `${name.slice(0, 4)}...` : name)
onLoad((options: any) => {
console.log('options:', options)
categoryId.value = options?.id * 1 || 0
getCategoryList()
})
//
const changeTabs = (event: any) => {
console.log('event:', event)
// 2
categoryId.value = event.id === categoryId.value ? menuData.value.id : event.id
paging.value.reload()
}
//
const getCategoryList = async (): Promise<void> => {
try {
const res = await apiGoodsCategoryLists({ pid: categoryId.value })
uni.setNavigationBarTitle({
title: res[0].name
})
menuData.value = res[0]
if (menuData.value.sons?.length) {
idx.value = menuData.value.sons.findIndex((item: any) => item.id === categoryId.value)
}
} catch (err) {
console.log(err)
}
}
//
const queryList = async (pageNo: number, pageSize: number) => {
try {
const { lists } = await apiGoodsLists({
pageNo: pageNo,
pageSize: pageSize,
categoryId: categoryId.value
})
paging.value.complete(lists)
} catch (e) {
console.log('报错=>', e)
//TODO handle the exception
paging.value.complete(false)
}
}
onShareAppMessage(() => {
return {
title: `粤好生活`,
imageUrl: ''
}
})
</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,456 @@
<template>
<!-- 搜索 -->
<view class="search flex m-[24rpx]">
<!-- 左侧图标 -->
<u-icon name="search" size="34" color="#888888"></u-icon>
<!-- 输入 -->
<input type="text" class="search--input" @input="handleSearch" placeholder="请输入城市名称" @focus="showClear = true"
@blur="showClear = false" v-model="keyword" />
<!-- 清空图标 -->
<image src="../../../static/images/icon_clear.png" class="clear" v-show="showClear" @click="handleReSearch"></image>
</view>
<!-- 搜索结果 -->
<view class="city-container" v-show="searchStatus">
<view class="letter-head">搜索结果</view>
<view class="bg-white city-box-head rounded-lg">
<block v-for="(searchItem, searchIndex) in searchResult" :key="searchIndex">
<view class="text-base city-item" @click="chooseCity(searchItem)">
{{ searchItem.name }}
</view>
</block>
</view>
</view>
<!-- 定位城市 -->
<view v-show="!searchStatus">
<view class="city-container">
<!-- 当前定位城市 -->
<view class="letter-head">当前定位城市</view>
<view class="city-box-head flex justify-between bg-white p-[24rpx]">
<view class="flex text-base normal" @click="goBack">
<image src="../../../static/images/icon_city_address.png"></image>
<text class="ml-[18rpx]">{{ cityInfo.name }}</text>
</view>
<view class="flex text-base text-blue" @click="reChooseLocation"></view>
</view>
<!-- 热门城市列表 -->
<view>
<view class="letter-head">热门城市</view>
<view class="city-box-head">
<block v-for="(hotItem, hotIndex) in hotList" :key="hotIndex">
<view class="text-base city-item" @click="chooseCity(hotItem)">
{{ hotItem.name }}
</view>
</block>
</view>
</view>
<!-- 城市列表 -->
<view v-for="(cityItem, cityIndex) in cityList" :key="cityIndex" class="anchor">
<view class="letter">{{ cityItem.firstChar }}</view>
<view class="bg-white city-box">
<block v-for="(cityItem2, cityIndex2) in cityItem.list" :key="cityIndex2">
<view class="text-base truncate city-item" @click="chooseCity(cityItem2)">
{{ cityItem2.name }}
</view>
</block>
</view>
</view>
</view>
<!-- 侧边导航条 -->
<view class="bar__sidebar" @touchstart.stop.prevent="onTouchMove" @touchmove.stop.prevent="onTouchMove"
@touchend.stop.prevent="onTouchStop" @touchcancel.stop.prevent="onTouchStop">
<view v-for="(barItem, barIndex) in labelList" :key="barIndex" class="bar__index"
:class="'title ' + (barIndex == touchmoveIndex ? 'active' : '')">
{{ barItem.firstChar }}
</view>
</view>
<!-- 侧边弹窗 -->
<view class="list-alert" v-if="touchmove && labelList[touchmoveIndex + '']">
<text>{{ labelList[touchmoveIndex]?.firstChar }}</text>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref, computed, unref, nextTick } from 'vue'
import { onLoad, onPageScroll } from '@dcloudio/uni-app'
import { apiRegionCity } from '@/api/store'
import { useAppStore } from '@/stores/app'
import { getGeocoderCoordinate } from '@/api/app'
const appStore = useAppStore()
//
const searchStatus = ref<boolean>(false)
//
const keyword = ref<string | number>('')
//
const showClear = ref<boolean>(false)
//
const searchResult = ref<any>([])
//
const cityList = ref<any>([])
//
const labelList = ref<any>([])
//
const hotList = [
{
name: '北京市',
gcj02_lat: '39.929986',
gcj02_lng: '116.395645',
id: 110100
},
{
name: '上海市',
gcj02_lat: '31.249162',
gcj02_lng: '121.487899',
id: 310100
},
{
name: '广州市',
gcj02_lat: '23.120049',
gcj02_lng: '113.30765',
id: 440100
},
{
name: '深圳市',
gcj02_lat: '22.546054',
gcj02_lng: '114.025974',
id: 440300
},
{
name: '重庆市',
gcj02_lat: '29.544606',
gcj02_lng: '106.530635',
id: 110100
},
{
name: '成都市',
gcj02_lat: '30.679943',
gcj02_lng: '104.067923',
id: 510100
},
{
name: '杭州市',
gcj02_lat: '30.259244',
gcj02_lng: '120.219375',
id: 110100
},
{
name: '苏州市',
gcj02_lat: '31.317987',
gcj02_lng: '120.619907',
id: 320500
},
{
name: '武汉市',
gcj02_lat: '30.581084',
gcj02_lng: '114.3162',
id: 420100
},
{
name: '沈阳市',
gcj02_lat: '41.808645',
gcj02_lng: '123.432791',
id: 210100
}
]
//
const touchmove = ref(false)
const touchmoveIndex = ref(0)
const anchor = ref<any>([])
const sidebar = ref<any>({})
const cityInfo = computed(() => appStore.cityInfo)
onLoad(() => {
uni.getSetting({
success: (res) => {
if (!res.authSetting['scope.userLocation']) {
uni.authorize({
scope: 'scope.userLocation',
success: () => {
// uni.getLocation({
// // #ifndef APP
// type: 'gcj02',
// // #endif
// async success(res) {
// const res1 = await getGeocoderCoordinate({
// lat: res.latitude,
// lng: res.longitude
// })
// const obj = {
// name: res1.amapGenCoderResponse.amapAddressComponent.city, //
// city_id: res1.cityId,
// latitude: res.latitude,
// longitude: res.longitude
// }
// console.log("", obj)
// appStore.setCityInfo(obj)
// }
// })
appStore.getLocation()
},
fail: () => {
appStore.getAuthorizeFunc()
}
})
}
},
fail: (res) => {
console.log(res);
}
})
initCityData()
})
onPageScroll(({ scrollTop }) => {
const index = anchor.value.findIndex((item: number | string) => {
return item >= scrollTop
})
const isLessTop = index !== -1
if (isLessTop && !unref(touchmove)) touchmoveIndex.value = index
})
//
const handleSearch = (event: any) => {
// keyword.value = event?.detail?.value.trim()
if (!keyword.value) {
searchStatus.value = false
return
}
searchStatus.value = true
searchResult.value = []
for (const key in cityList.value) {
const len = cityList.value[key].list.length
const item = cityList.value[key].list
for (let i = 0; i < len; i++) {
const reg = new RegExp(keyword.value)
if (reg.test(item[i].name)) {
searchResult.value.push(item[i])
}
}
}
}
//
const handleReSearch = () => {
keyword.value = ''
showClear.value = false
searchStatus.value = false
searchResult.value = []
}
//
const reChooseLocation = () => {
uni.chooseLocation({
success(res: any) {
appStore.initCityFunc(res.latitude, res.longitude)
},
fail(err) {
console.log(err)
}
})
}
//
const onTouchMove = (event: any): void => {
const y = parseInt(event.changedTouches[0].clientY)
const len = unref(labelList).length
const itemHeight = Number(unref(sidebar).height / len)
let index = Math.floor((y - unref(sidebar).top) / itemHeight)
if (index < 0) {
index = 0
} else if (index > len - 1) {
index = len - 1
}
if (unref(touchmoveIndex) != index) {
touchmove.value = true
touchmoveIndex.value = index
uni.pageScrollTo({
duration: 0,
scrollTop: unref(anchor)[index - 1] ? unref(anchor)[index - 1] : 0
})
}
}
const onTouchStop = () => {
touchmove.value = false
}
//
const chooseCity = (param: any) => {
const obj = {
name: param.name,
city_id: param.id,
latitude: param.db09_lat,
longitude: param.db09_lng
}
appStore.setCityInfo(obj)
uni.navigateBack()
}
//
const initCityData = async (): Promise<void> => {
const data = await apiRegionCity()
cityList.value = data
labelList.value = data
setRect()
}
//
const setRect = async () => {
await nextTick()
const title = uni.createSelectorQuery().selectAll('.bar__sidebar')
title
.boundingClientRect((res: any) => {
sidebar.value = {
height: res[0].height,
top: res[0].top
}
})
.exec()
const letter = uni.createSelectorQuery().selectAll('.anchor')
letter
.boundingClientRect((res: any) => {
res.top = Number(res.bottom)
anchor.value = res.map((item: { bottom: number }) => Number(item.bottom))
})
.exec()
}
/**
* @return { void }
* @description 返回上一页
*/
const goBack = () => {
uni.navigateBack()
}
</script>
<style lang="scss">
//
.search {
height: 70rpx;
padding: 15rpx 30rpx;
border-radius: 10rpx;
background-color: white;
//
&--input {
width: 84%;
padding-left: 20rpx;
}
//
.clear {
width: 34rpx;
height: 34rpx;
padding-left: 20rpx;
}
}
.city-container {
padding: 0 60rpx 0 24rpx;
//
.letter,
.letter-head {
height: 70rpx;
line-height: 50rpx;
padding: 10rpx 4rpx;
margin-top: 10rpx;
color: #888888;
font-size: 26rpx;
}
//
.city-box,
.city-box-head {
border-radius: 4rpx;
.city-item {
padding: 0 24rpx;
width: 210rpx;
height: 88rpx;
display: inline-block;
margin-right: 18rpx;
line-height: 88rpx;
text-align: center;
border-radius: 4rpx;
background-color: #ffffff;
// line-1
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.city-item:nth-child(3n) {
margin-right: 0;
}
.city-item:hover {
color: white;
background: rgba(#1296db, 0.5);
}
image {
width: 32rpx;
height: 32rpx;
}
}
}
.bar__sidebar {
position: fixed;
top: 50%;
right: 8rpx;
display: flex;
flex-direction: column;
text-align: center;
transform: translateY(-50%);
user-select: none;
z-index: 99;
.active {
border-radius: 50%;
color: white;
background-color: #1296db;
}
}
.bar__index {
font-weight: 500;
padding: 8rpx 8rpx;
font-size: 22rpx;
line-height: 1;
}
.list-alert {
position: fixed;
width: 120rpx;
height: 120rpx;
right: 90rpx;
top: 50%;
margin-top: -60rpx;
border-radius: 24rpx;
font-size: 50rpx;
color: #fff;
background-color: rgba(0, 0, 0, 0.65);
display: flex;
justify-content: center;
align-items: center;
padding: 0;
z-index: 9999999;
text {
line-height: 50rpx;
}
}
</style>

View File

@ -0,0 +1,110 @@
<!--
* @Author: 王翠碧 1254597151@qq.com
* @Date: 2023-05-22 22:41:29
* @LastEditors: 王翠碧 1254597151@qq.com
* @LastEditTime: 2023-05-26 23:12:10
* @FilePath: \uniapp\src\bundle\pages\collection_list\index.vue
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
-->
<template>
<!-- Main Start -->
<uni-transition
mode-class="zoom-in"
needLayout="true"
:show="collectData.length"
:duration="500"
>
<u-swipe-action>
<u-swipe-action-item
v-for="item in collectData"
:key="item.id"
:options="{ text: '取消收藏' }"
>
<!-- <block> -->
<view class="collect flex justify-between" @click="toGoodsDetail(item.goodsId)">
<view class="flex">
<u-image :src="item.image" width="140" height="140"></u-image>
<view class="ml-[20rpx]">
<view class="normal font-medium text-xl">{{ item.name }}</view>
<view class="mt-[24rpx]">
<price :price="item.price" :desc="item.unit"></price>
</view>
</view>
</view>
<view class="pt-[30rpx]">
<u-button
size="mini"
:plain="true"
type="primary"
shape="circle"
@click="toGoodsDetail(item.goodsId)"
>
去下单
</u-button>
</view>
</view>
<!-- </block> -->
</u-swipe-action-item>
</u-swipe-action>
</uni-transition>
<!-- Main End -->
<!-- empty Start -->
<view class="empty" v-show="!collectData.length">
<u-empty
text="暂无收藏数据~"
:src="'/static/images/empty/collection.png'"
:icon-size="300"
color="#888888"
></u-empty>
</view>
<!-- empty Start -->
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { apiCollectLists } from '@/api/goods'
import Price from '@/components/price/index.vue'
/** Data Start **/
const collectData = ref<any>([])
/** Data End **/
/** Methods Start **/
//
const initCollect = async (): Promise<void> => {
const { lists } = await apiCollectLists()
collectData.value = lists
}
//
const toGoodsDetail = (id: number | null) => {
console.log('id', id)
uni.navigateTo({
url: `/pages/goods/index?id=${id}`
})
}
/** Methods End **/
/** Life Cycle Start **/
onShow(() => {
initCollect()
})
/** Life Cycle End **/
</script>
<style lang="scss">
.collect {
border-radius: 14rpx;
background-color: white;
margin: 20rpx 20rpx 0 20rpx;
padding: 30rpx;
image {
border-radius: 16rpx !important;
}
}
.empty {
padding-top: 300rpx;
}
</style>

View File

@ -0,0 +1,121 @@
<template>
<uni-transition
mode-class="zoom-in"
needLayout="true"
:show="serviceData.qrcode.length"
:duration="500"
>
<view class="box">
<view class="flex justify-center">
<u-image :src="getImageUrl(serviceData.qrcode)" width="300" height="300"></u-image>
</view>
<view class="text-center mt-[20rpx] normal text-base">
{{ serviceData.title }}
</view>
<view class="text-center mt-[20rpx] text-muted text-sm" v-if="serviceData.time">
服务时间{{ serviceData.time }}
</view>
<view class="text-center mt-[20rpx] text-muted text-sm" v-if="serviceData.mobile">
服务电话{{ serviceData.mobile }}
</view>
<view class="mt-[40rpx]">
<!-- #ifdef H5 -->
<button
class="custom-button-bgColor text-lg text-white rounded-full"
@click="toast('长按二维码保存')"
>
长按二维码保存
</button>
<!-- #endif -->
<!-- #ifndef H5 -->
<button
class="custom-button-bgColor text-lg text-white leading-[80rpx] h-[80rpx] rounded-full"
@click="saveImageQr"
>
保存二维码
</button>
<!-- #endif -->
</view>
</view>
</uni-transition>
</template>
<script lang="ts" setup>
//
import { ref } from 'vue'
import { toast } from '@/utils/util'
import { apiContactService } from '@/api/app'
import { useAppStore } from '@/stores/app'
const { getImageUrl } = useAppStore()
/** Interface Start **/
interface serviceDataObj {
qrcode: string //
title: string //
mobile: string //
time: string //
}
/** Interface End **/
/** Data Start **/
const serviceData = ref<serviceDataObj>({
qrcode: '',
title: '',
mobile: '',
time: ''
})
/** Data End **/
/** Methods Start **/
//
const getContactService = async (): Promise<void> => {
const data = await apiContactService('')
const res = JSON.parse(data.pageData)
serviceData.value = res[0].content
}
//
const saveImageQr = async (): Promise<void> => {
try {
const res = await uni.getImageInfo({ src: getImageUrl(serviceData.value.qrcode) })
try {
await uni.saveImageToPhotosAlbum({ filePath: res.path })
uni.$u.toast('保存成功')
} catch (e) {
const modelRes = await uni.showModal({
title: '图片保存失败',
content: '请确认是否已开启授权'
})
if (modelRes.confirm) uni.openSetting()
}
} catch (err) {
uni.$u.toast('请在小程序后台配置downloadFile')
}
}
/** Methods End **/
/** Life Cycle Start **/
getContactService()
/** Life Cycle End **/
</script>
<style lang="scss">
page {
padding: 24rpx;
box-sizing: border-box;
background-color: #ffffff;
}
.box {
padding: 64rpx 120rpx;
background: linear-gradient(
to bottom,
rgba(#f36161, 0.1),
rgba(#f36161, 0) 164rpx,
transparent 0
);
}
</style>

View File

@ -0,0 +1,275 @@
<template>
<view class="coupon">
<view class="coupon__detail">
<view class="coupon__detail__header">
<view class="left">
<view class="title">{{ couponDetail.name }}</view>
<view class="date">{{ couponDetail.useTimeEnd }}到期</view>
</view>
<view class="right">
<view class="money">
<text></text>
<text class="strong">{{ couponDetail.money }}</text>
</view>
<text class="condition">{{ condition(couponDetail) }}</text>
</view>
</view>
<view class="coupon__detail__rule">
<text class="sub_title">使用规则</text>
<view class="content">
<view class="flex">
<text class="label">优惠券类型</text>
<text class="flex-1">{{ parseUseGoodsType(couponDetail) }}</text>
</view>
<view class="flex">
<text class="label">领取说明</text>
<text>每人限领{{ couponDetail.getNum }}</text>
</view>
<view class="flex">
<text class="label">有效时间</text>
<text>{{ parseTime(couponDetail) }}</text>
</view>
<view class="flex" v-show="couponDetail.useGoodsType !== 1">
<text class="label">使用限制</text>
<text>{{ parseLimitContent(couponDetail) }}</text>
</view>
</view>
</view>
</view>
<view class="coupon__btn">
<u-button class="manual" type="error" shape="circle" @click="handleManualCoupon"
:disabled="isDisabled">立即领取</u-button>
</view>
</view>
</template>
<script setup lang='ts'>
import { apiCouponDetail } from '@/api/coupon';
import { onBackPress, onLoad, onUnload } from '@dcloudio/uni-app';
import { computed, reactive, ref, unref, toRaw } from 'vue';
interface CouponProp {
name: string
money: number
getType: number //
channelType: number ///
useGoodsType: number,//
conditionType: number,//使
sendTimeStart: number, //
sendTimeEnd: number,//
useTimeStart: string,//使
useTimeEnd: string,//使
sendTotal: number,
sendTotalType: number//
getNum: number, //
status: number,//
conditionMoney: number | string,//
distributors: any[]//
goodsCategoryDetailVos: any[],//
goodsDetailVos: any[]//
isToggle?: boolean //
goods: any[] //
goodsCategories: any[] //
}
const couponId = ref()
const couponDetail = reactive<Partial<CouponProp>>({})
const count = ref(3) //x
const num = ref(0)
const isDisabled = computed(() => couponDetail.getNum === 0)
const condition = computed(() => {
return (row: Partial<CouponProp>) => {
if (!Object.keys(row).length) return
const { conditionType, conditionMoney } = row
return conditionType === 1 ? '立减券' : `${conditionMoney}可用`
}
})
const parseTime = computed(() => {
return (row: Partial<CouponProp>) => {
if (!Object.keys(row).length) return
const { useTimeStart, useTimeEnd } = row as CouponProp
const [useTimeStartDate, useTimeStartHS] = useTimeStart.split(' ')
const [useTimeEndDate, useTimeEndHS] = useTimeEnd.split(' ')
const startText = useTimeStartHS.split(':').slice(0, -1).join(':')
const endText = useTimeEndHS.split(':').slice(0, -1).join(':')
return `${useTimeStartDate} ${startText}${useTimeEndDate} ${endText}`
}
})
const parseLimitContent = computed(() => {
return (row: Partial<CouponProp>) => {
if (!Object.keys(row).length) return
const { useGoodsType, goodsCategories, goods } = toRaw(couponDetail) as CouponProp
if (useGoodsType === 1) return
const categoryNames = useGoodsType === 2
? transformGoods(goodsCategories, 'name')
: transformGoods(goods, 'categoryName')
return `仅可购买${categoryNames}商品`
}
})
const useGoodsTypeMap: Record<number, string> = {
1: '通用券',
2: '品类券',
3: '商品券'
}
const parseUseGoodsType = computed(() => {
return (row: Partial<CouponProp>) => {
const { useGoodsType } = row
return useGoodsTypeMap[useGoodsType!]
}
})
function transformGoods(goods: any[], field: string, separator = '、') {
return goods.map(category => category[field]).join(separator)
}
onLoad((options) => {
const { id } = options
couponId.value = id
fetchCouponDetail()
})
/**获取优惠券详情 */
const fetchCouponDetail = async () => {
try {
const res = await apiCouponDetail({ couponId: unref(couponId) })
for (const key in res) {
couponDetail[key] = res[key]
}
} catch (error) { }
}
/**
*
* 1根据用户可领取张数
* 2领取后重新刷新详情页
*/
const handleManualCoupon = () => {
couponDetail.getNum = couponDetail.getNum - 1
uni.$u.toast(`优惠券已放到卡包中${couponDetail.getNum}`)
/**
* 1有多张优惠券
* 2只有一张优惠券
*/
// uni.navigateBack({
// delta: 1,
// success: () => {
// const pages = getCurrentPages()
// const prevPage = pages[pages.length - 1]
// prevPage.onLoad?.()
// }
// })
}
onUnload(() => {
uni.$emit('refresh', {})
})
</script>
<style lang="scss" scoped>
.coupon {
display: flex;
flex-direction: column;
height: 100vh;
.coupon__detail {
height: calc(100% - 60px);
overflow: auto;
&__header {
display: flex;
background-color: #fff;
margin: 20rpx;
border-radius: 12rpx;
.left {
flex: 1;
padding: 20rpx;
line-height: 1.8;
.title {
font-size: 36rpx;
}
.date {
color: #777;
font-size: 24rpx;
}
}
.right {
display: flex;
flex-direction: column;
justify-content: center;
padding: 0 20rpx;
height: 148rpx;
border-left: 1px dashed #F1F2F2;
.money {
color: #fc5531;
.strong {
font-size: 52rpx;
font-weight: 700;
}
}
.condition {
color: #777;
font-size: 28rpx;
}
}
}
&__rule {
background-color: #fff;
margin: 20rpx;
padding: 30rpx;
border-radius: 12rpx;
.sub_title {
font-size: 44rpx;
}
.content {
margin-top: 20rpx;
line-height: 60rpx;
font-size: 13px;
.label {
display: flex;
align-items: center;
width: 180rpx;
&::before {
content: '';
display: inline-block;
width: 8rpx;
height: 8rpx;
border-radius: 4rpx;
margin-right: 12rpx;
background-color: #000;
}
}
}
}
}
.coupon__btn {
display: flex;
align-items: center;
justify-content: center;
height: 120rpx;
padding: 0 40rpx;
background-color: #fff;
.manual {
height: 80rpx;
width: 100%;
text-align: center;
line-height: 80rpx;
color: #fff;
border-radius: 999px;
background-color: #fc5531;
}
}
}
</style>

View File

@ -0,0 +1,72 @@
<template>
<view class="coupon__center">
<z-paging
auto-show-back-to-top
ref="paging"
v-model="couponList"
@query="queryList"
:fixed="false"
height="100%"
>
<block v-for="item in couponList" :key="item.id">
<coupon-card :item="item">
<template #manual>
<text class="btn__link" @click="handleManualCoupon(item)"></text>
</template>
<template #count>
<text class="count" v-if="item.alreadyGetCouponCount > 0">
已领取{{ item.alreadyGetCouponCount }}
</text>
</template>
</coupon-card>
</block>
</z-paging>
</view>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { useZPaging } from '@/hooks/useCoupon'
import { apiCouponCenterList, apiCouponManualGet } from '@/api/coupon'
import cache from '@/utils/cache'
import { storeToRefs } from 'pinia'
import { DIST_ID } from '@/enums/cacheEnums'
const couponList = ref<any>([])
const config = {
initParams: {
distributorId: cache.get(DIST_ID) ?? '' //
},
requestApi: apiCouponCenterList
}
const { queryList, paging } = useZPaging(config.initParams, config.requestApi)
/**
* 1领取张数只有1张提示+刷新列表
* 2领取张数有多张提示 + 文本xx+ 刷新列表
* 3领取后刷新列表
*/
const handleManualCoupon = async (row: any) => {
const { id } = row
try {
await apiCouponManualGet({ id })
uni.$u.toast(`优惠券已放到卡包中`)
paging.value.reload()
} catch (error) {}
}
</script>
<style lang="scss" scoped>
.coupon__center {
height: 100vh;
.btn__link {
background-color: $white;
color: $blue1;
padding: 8rpx 16rpx;
font-size: 20rpx;
border-radius: 999px;
margin-top: 16rpx;
}
}
</style>

View File

@ -0,0 +1,234 @@
<template>
<view class="coupon__order">
<view class="coupon__order__header">
<view class="popup__header__tabs">
<tabs
:current="setTabIndex"
:swipeable="false"
v-bind="tabStyle"
@change="handleChangeTab"
>
<tab
v-for="(item, i) in couponTabs"
:key="i"
:name="`${item.name}(${item.count})`"
></tab>
</tabs>
</view>
</view>
<view class="coupon__order__content">
<z-paging
auto-show-back-to-top
ref="paging"
v-model="couponList"
@query="transformQueryList"
:fixed="false"
height="100%"
:to-bottom-loading-more-enabled="false"
:loading-more-enabled="false"
>
<block v-for="(item, index) in couponList" :key="`${index}-${item.id}`">
<coupon-card :item="item" :activeTabName="activeTabName">
<template v-slot:selection>
<u-checkbox-group
v-if="tabStatus === CouponStatusEnum.AVALIABLE"
class="selection"
>
<u-checkbox
v-model="item.defaultSelect"
:name="item.id"
@change="detail => handleSelect(detail, item)"
shape="circle"
active-color="#2979FF"
></u-checkbox>
</u-checkbox-group>
</template>
<template #count>
<text class="count" v-if="item.alreadyGetCouponCount >= 2">
{{ parseText }}使用{{ item.alreadyGetCouponCount }}
</text>
</template>
<template #expiringSoon>
<div class="expiring-soon" v-if="showExpiringSoon(item!)"></div>
</template>
</coupon-card>
</block>
</z-paging>
</view>
<view class="coupon__order__btn" v-if="isVisibleConfirmBtn">
<u-button type="primary" shape="circle" @click="handleSelectCoupon"></u-button>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref, computed, unref, watch, reactive } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { useTabs, useZPaging } from '@/hooks/useCoupon'
import { apiCouponByUse, apiCouponCount } from '@/api/coupon'
import { Recordable } from '@/config/interface'
import { CouponCountMap, showExpiringSoonDays } from '@/enums/appEnums'
import { CouponStatusEnum } from '@/enums/appEnums'
const goodsId = ref()
const goodsCategoryId = ref()
const couponList = ref<any[]>([])
const couponTabs = ref<any[]>([])
const availableCount = ref(0) //
let selectedCouponInfo = reactive<Recordable<string>>({})
const { tabStatus, setTabIndex, handleChangeTab } = useTabs()
const { paging, queryList, refresh } = useZPaging({}, apiCouponByUse)
const isVisibleConfirmBtn = computed(
() => unref(availableCount) > 0 && unref(tabStatus) === CouponStatusEnum.AVALIABLE
)
const tabStyle = computed(() => {
return {
height: 100,
barWidth: 90,
fontSize: 32,
barStyle: { bottom: 0 },
activeColor: '#1296DB'
}
})
const parseText = computed(() => (unref(tabStatus) === CouponStatusEnum.AVALIABLE ? '可' : '不可'))
const activeTabName = computed(() => couponTabs.value[setTabIndex.value]?.name)
const showExpiringSoon = computed(() => {
return (row: any) => {
const { useTimeEnd, useStatus } = row
const currentTime = new Date().getTime()
const endTime = Date.parse(useTimeEnd)
const daysDifference = Math.abs(endTime - currentTime) / (60 * 60 * 24 * 1000)
return daysDifference <= showExpiringSoonDays && activeTabName.value == '可用优惠券'
}
})
watch(
() => unref(tabStatus),
() => {
const params = { usableStatus: unref(tabStatus) }
refresh(params)
}
)
/**选择优惠券 */
const handleSelect = (detail, item) => {
const { name: id, value } = detail
const selectedItem = unref(couponList).find(coupon => coupon.id === id)
if (!selectedItem) return
unref(couponList).forEach(coupon => {
coupon.defaultSelect = false
})
selectedItem.defaultSelect = !selectedCouponInfo.defaultSelect
value ? saveSelectItem(item) : (selectedCouponInfo = {})
}
/**确定 */
const handleSelectCoupon = async () => {
if (Object.keys(selectedCouponInfo).length > 0) {
paging.value.reload()
} else {
selectedCouponInfo = unref(couponList).find(i => i.defaultSelect) ?? []
}
uni.navigateBack()
uni.$emit('selectCoupon', {
selectedCouponInfo,
availableCount: unref(availableCount)
})
}
/**获取优惠券数量 */
const getCouponCount = async () => {
const params = {
goodsId: unref(goodsId),
goodsCategoryId: unref(goodsCategoryId)
}
try {
const res = await apiCouponCount(params)
availableCount.value = res.availableCount
couponTabs.value = Object.keys(res).map(key => {
return {
name: CouponCountMap[key],
count: res[key]
}
})
} catch (error) {}
}
onLoad(async options => {
await setPayload(options)
getCouponCount()
})
/**保存传递过来的参数 */
function setPayload(options: any) {
const { goodsId: goodId, goodsCategoryId: categoryId, selectCoupon } = options
const couponInfo = JSON.parse(selectCoupon)
goodsId.value = goodId
goodsCategoryId.value = categoryId
if (!Object.keys(couponInfo).length) return
saveSelectItem(couponInfo)
}
/**保存选中项内容 */
function saveSelectItem(item: any) {
for (const key in item) {
selectedCouponInfo[key] = item[key]
}
}
function transformQueryList(page_no: number, page_size: number) {
const config = {
initParams: {
goodsId: unref(goodsId),
goodsCategoryId: unref(goodsCategoryId),
usableStatus: unref(tabStatus),
selectCouponId: selectedCouponInfo.id ?? ''
}
}
return queryList(page_no, page_size, config.initParams)
}
</script>
<style lang="scss" scoped>
.coupon__order {
display: flex;
flex-direction: column;
height: 100vh;
&__header {
}
&__content {
height: calc(100% - 100rpx);
overflow: auto;
.selection {
position: absolute;
right: -20rpx;
top: 30rpx;
:deep(.u-checkbox__icon-wrap) {
background-color: $white;
}
}
}
&__btn {
height: 120rpx;
background-color: $white;
padding: 20rpx 30rpx;
}
}
.expiring-soon {
position: absolute;
background: #d83d3b;
width: 162rpx;
height: 32rpx;
rotate: 26deg;
top: -4px;
right: -11px;
font-size: 19rpx;
line-height: 32rpx;
padding-left: 53rpx;
}
</style>

View File

@ -0,0 +1,121 @@
<template>
<view class="card" v-if="isEmpty">
<view class="cell" v-for="cell in orderFields" :key="cell.label">
<text class="width-200">{{ cell.label }}</text>
<text class="flex-1 overflow-ellipsis-line">{{ setUnit(cell.value, cell.unit) }}</text>
</view>
</view>
<view class="loading" v-else-if="!isLoading">
<u-loading></u-loading>
<view>加载中...</view>
</view>
<view class="empty" v-else>
<u-empty text="" mode="data"></u-empty>
</view>
</template>
<script setup lang="ts">
import { computed, reactive, ref, unref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { useLockFn } from '@/hooks/useLockFn'
import { getDistributorOrderDetail } from '@/api/distributor_center'
import { setUnit } from '@/utils/util'
interface OrderProps {
[key: string]: string | number
id: number
commission: number
orderNo: number
goodsCategoryName: string
goodsName: string
goodsNum: number
orderAmount: number
finishTime: string
commissionRate: number
appointTimeStart: string
appointTimeEnd: string
}
const fieldsMap: Record<string, string | string[]> = {
orderNo: '订单编号',
goodsCategoryName: '服务类别',
goodsName: '服务名称',
goodsNum: ['商品数量', '个'],
orderAmount: ['实付金额', '元'],
address: '上门地址',
appointTime: '预约时间',
finishTime: '完成时间'
// commissionRate: ['', '%'],
// commission: ['', '']
}
const orderFields = ref<any[]>([])
const orderDetail = reactive<Partial<OrderProps>>({})
const isEmpty = computed(() => unref(orderFields).length > 0)
const isLoading = computed(() => Object.keys(orderDetail).length > 0)
onLoad(options => {
const { id } = options
initOrderDetail(id)
})
/**订单详情 */
const { lockFn: initOrderDetail } = useLockFn(async id => {
const data = await getDistributorOrderDetail({ id })
Object.keys(data).length > 0 && setDetailData(data)
})
/**保存信息 */
function setDetailData(data: any) {
for (const key in data) {
orderDetail[key] = data[key]
}
orderDetail.appointTime = `${orderDetail.appointTimeStart?.replace(
/:\d{2}$/,
''
)}-${orderDetail.appointTimeEnd?.split(' ')[1].replace(/:\d{2}$/, '')}`
//
if (orderDetail.orderStatus !== 3 && orderDetail.orderStatus !== 4) {
delete fieldsMap.finishTime
}
orderFields.value = Object.keys(fieldsMap).map(field => {
const text = fieldsMap[field]
const [label, unit] = Array.isArray(text) ? text : [text, '']
return {
label: label,
value: orderDetail[field],
unit
}
})
}
</script>
<style lang="scss" scoped>
.card {
background-color: $white;
margin: 20rpx 40rpx 0;
padding: 30rpx;
border-radius: 16rpx;
.cell {
display: flex;
line-height: 1.8;
.width-200 {
width: 220rpx;
height: 70rpx;
color: #827e7e;
}
}
}
.empty {
@include center;
min-height: 100vh;
}
.loading {
@include center;
flex-direction: column;
min-height: 100vh;
}
</style>

View File

@ -0,0 +1,91 @@
<template>
<view class="card">
<view class="card__top top_style title-active">
<text>{{ item?.currentDate }}</text>
<text>
共计
<text class="text-3xl text-[#1296DB]">{{ item?.userVoList.length }}</text>
</text>
</view>
<view class="card__content">
<view class="header">
<text>用户账号</text>
<text>绑定到期时间</text>
</view>
<view class="table">
<view class="core h-8" v-for="user in item?.userVoList" :key="user.id">
<text>{{ user.userNo }}</text>
<text>{{ user.unBindTime }}</text>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { PropType } from 'vue'
interface ChildrenProps {
id: number
userNo: string
username: string
unBindTime: string
}
interface UserProps {
id: number
currentDate: string
userVoList: ChildrenProps[]
}
const props = defineProps({
item: Object as PropType<UserProps>
})
</script>
<style lang="scss" scoped>
.card {
background-color: $white;
margin: 20rpx 40rpx 0;
padding: 20rpx 30rpx;
border-radius: 16rpx;
@include flex-column;
&__content {
flex: 1;
margin-top: 20rpx;
.header {
@include flex;
> text {
flex: 1;
}
}
.table {
@include flex-column;
line-height: 1.8;
margin-top: 15rpx;
.core {
@include flex;
> text {
flex: 1;
}
}
}
.total {
margin-top: 40rpx;
}
}
.top_style {
@apply border-0 border-b border-solid border-[#f7f4f4] h-10 flex items-center justify-between;
}
.title-active::before {
left: -30rpx;
background-color: #1296db;
}
}
</style>

View File

@ -0,0 +1,127 @@
<template>
<u-navbar title="" class="u-navbar">
<view class="slot-wrap" @click="handleToggle">
<!-- {{ currentDate }}份分销用户 -->
<text class="m-[5px]">店铺客户</text>
<u-icon name="arrow-down"></u-icon>
<u-picker
mode="time"
v-model="show"
:default-time="`${convertChineseMonthToDate(currentDate)}-${day}`"
:params="params"
@confirm="handleConfirm"
></u-picker>
</view>
</u-navbar>
<div class="total-money bg-[#FDF6EC] text-[#ef2c2c] px-4 text-2xl py-2">
<span>{{ `${currentDate}总新增用户:` }}</span>
<span class="font-bold">{{ `${monthTotalPeople}` }}</span>
</div>
<view class="list" :style="`height:calc(100vh - ${navbarHeight}px)`">
<z-paging
auto-show-back-to-top
ref="paging"
v-model="dataList"
@query="queryList"
:fixed="false"
height="100%"
>
<block v-for="(item, index) in dataList" :key="`${index} + 'unique'`">
<userCard :item="item" />
</block>
</z-paging>
</view>
</template>
<script setup lang="ts">
import { onMounted, ref, unref } from 'vue'
import { storeToRefs } from 'pinia'
import userCard from './components/userCard.vue'
import { useUserStore } from '@/stores/user'
import { useZPaging } from '@/hooks/useCoupon'
import { useToggle } from '@/hooks/useLockFn'
import { getDistributorBindingUser, getDistributorUserByMonth } from '@/api/distributor_center'
import { convertChineseMonthToDate } from '@/utils/util'
const date = `${new Date().getFullYear()}${new Date().getMonth() + 1}`
const today = new Date().getDate()
const day = today < 10 ? String(today).padStart(2, '0') : today
const params = {
year: true,
month: true,
day: false,
hour: false,
minute: false,
second: false
}
const dataList = ref<any>([])
const currentDate = ref(date)
const monthTotalPeople = ref(0)
const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore)
const { show, handleToggle } = useToggle()
const config = {
initParams: {
distributorId: unref(userInfo).distributorId,
date: convertChineseMonthToDate(unref(currentDate))
},
requestApi: getDistributorBindingUser
}
const { paging, queryList, refresh } = useZPaging(config.initParams, config.requestApi)
/**选择日期 */
const handleConfirm = res => {
const { year, month } = res
currentDate.value = `${year}${month}`
refresh({ date: convertChineseMonthToDate(unref(currentDate)) })
getUserByMonth()
}
/**每月获取总分销用户 */
const getUserByMonth = async () => {
try {
const params = {
distributorId: unref(userInfo).distributorId,
date: convertChineseMonthToDate(unref(currentDate))
}
const data = await getDistributorUserByMonth(params)
monthTotalPeople.value = Number(data) ?? 0
} catch (error) {}
}
onMounted(getUserByMonth)
let navbarHeight = ref(95)
onMounted(() => {
uni.createSelectorQuery()
.select('.u-navbar')
.boundingClientRect(data => {
navbarHeight.value = (data?.height || 95) as number
})
.exec()
})
</script>
<style scoped lang="scss">
.slot-wrap {
display: flex;
justify-content: center;
align-items: center;
flex: 1;
margin-left: 110rpx;
height: 88rpx;
}
.total-money {
position: fixed;
left: 0;
height: 86rpx;
width: 100%;
}
.list {
// height: calc(100vh - 88rpx);
padding-top: 86rpx;
}
</style>

View File

@ -0,0 +1,108 @@
<template>
<view class="card">
<view class="card__top top_style title-active">
<text>{{ item?.currentDate?.split(' ')[0] }}</text>
<!-- <text class="">
共计
<text class="text-3xl text-[#1296DB]">{{ item.dayTotalMoney }}</text>
</text> -->
</view>
<view class="card__content">
<view
class="core"
v-for="(child, index) in item?.distributorOrderListVo"
:key="index"
@click="handleNavigate(child)"
>
<view class="title flex-1">
<text class="overflow-ellipsis-line mb-2">{{ child.orderNo }}</text>
<text class="overflow-ellipsis-line">{{ child.goodsName }}</text>
</view>
<view class="money">
<text>实付金额</text>
<text class="totalPrice">{{ joinSuffix(child.orderAmount) }}</text>
</view>
<!-- <view class="money">
<text>分销抽佣</text>
<text class="totalPrice">{{ joinSuffix(child.commission) }}</text>
</view> -->
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { PropType } from 'vue'
import { joinSuffix } from '@/utils/util'
interface ChildrenProps {
createTime: string
distributorId: number
goodsName: string
orderAmount: number
orderId: number
orderNo: number
}
interface CardProps {
currentDate: string
distributorOrderListVo: ChildrenProps[]
money: number
dayTotalMoney: number
}
const props = defineProps({
item: Object as PropType<CardProps>
})
const emit = defineEmits(['navigate'])
/**跳转地址 */
const handleNavigate = (item: ChildrenProps) => {
emit('navigate', item)
}
</script>
<style lang="scss" scoped>
.card {
background-color: $white;
margin: 20rpx 40rpx 0;
padding: 0rpx 30rpx 20rpx;
border-radius: 16rpx;
@include flex-column;
&__content {
flex: 1;
margin-top: 20rpx;
.core {
@include flex-justify;
margin-top: 20rpx;
padding-bottom: 20rpx;
border-bottom: 1px solid #f7f4f4;
.totalPrice {
margin-top: 20rpx;
}
&:last-child {
border-bottom: none;
}
.title {
@include flex-column;
}
.money {
@include flex-column;
color: #1296db;
align-items: flex-end;
}
}
}
.top_style {
@apply border-0 border-b border-solid border-[#f7f4f4] h-10 flex items-center justify-between;
}
.title-active::before {
left: -30rpx;
background-color: #1296db;
}
}
</style>

View File

@ -0,0 +1,134 @@
<template>
<u-navbar title="" class="u-navbar">
<view class="slot-wrap" @click="handleToggle">
<!-- {{ currentDate }}份分销账单 -->
<text class="m-[5px]">店铺订单</text>
<u-icon name="arrow-down"></u-icon>
<u-picker
mode="time"
v-model="show"
:default-time="`${convertChineseMonthToDate(currentDate)}-${day}`"
:params="params"
@confirm="handleConfirm"
></u-picker>
</view>
</u-navbar>
<!-- <div
class="total-money bg-[#FDF6EC] text-[#ef2c2c] px-4 text-2xl py-2"
:style="{ top: navbarHeight + 'px' }"
>
<span>{{ `${currentDate}总分销金额:` }}</span>
<span class="font-bold">{{ `${monthTotalMoney}` }}</span>
</div> -->
<view class="list">
<z-paging
auto-show-back-to-top
ref="paging"
v-model="dataList"
@query="queryList"
:fixed="false"
height="100%"
>
<block v-for="(item, index) in dataList" :key="`${index} + 'unique'`">
<orderCard :item="item" @navigate="handleNavigate" />
</block>
</z-paging>
</view>
</template>
<script setup lang="ts">
import { ref, unref, onMounted } from 'vue'
import { storeToRefs } from 'pinia'
import orderCard from './components/orderCard.vue'
import { useUserStore } from '@/stores/user'
import { useZPaging } from '@/hooks/useCoupon'
import { useToggle } from '@/hooks/useLockFn'
import { getDistributorOrder, getDistributorOrderByMonth } from '@/api/distributor_center'
import { convertChineseMonthToDate } from '@/utils/util'
import { router } from '@/utils/util'
const date = `${new Date().getFullYear()}${new Date().getMonth() + 1}`
const today = new Date().getDate()
const day = today < 10 ? String(today).padStart(2, '0') : today
const params = {
year: true,
month: true,
day: false,
hour: false,
minute: false,
second: false
}
const dataList = ref<any>([])
const currentDate = ref(date)
const monthTotalMoney = ref(0)
const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore)
const { show, handleToggle } = useToggle()
const config = {
initParams: {
distributorId: unref(userInfo).distributorId,
date: convertChineseMonthToDate(unref(currentDate))
},
requestApi: getDistributorOrder
}
const { paging, queryList, refresh } = useZPaging(config.initParams, config.requestApi)
/**选择日期 */
const handleConfirm = res => {
const { year, month } = res
currentDate.value = `${year}${month}`
refresh({ date: convertChineseMonthToDate(unref(currentDate)) })
getOrderByMonth()
}
/**跳转地址 */
const handleNavigate = (item: any) => {
const { orderId } = item
router(`/bundle/pages/dist_order/index?id=${orderId}`)
}
const getOrderByMonth = () => {
const params = {
distributorId: unref(userInfo).distributorId,
date: convertChineseMonthToDate(unref(currentDate))
}
getDistributorOrderByMonth(params).then(res => {
monthTotalMoney.value = res || 0
})
}
getOrderByMonth()
let navbarHeight = ref(95)
onMounted(() => {
uni.createSelectorQuery()
.select('.u-navbar')
.boundingClientRect(data => {
navbarHeight.value = (data?.height || 95) as number
})
.exec()
})
</script>
<style scoped lang="scss">
.slot-wrap {
display: flex;
justify-content: center;
align-items: center;
flex: 1;
margin-left: 110rpx;
height: 88rpx;
}
.total-money {
position: fixed;
left: 0;
height: 86rpx;
width: 100%;
}
.list {
height: calc(100vh - 95px);
// padding-top: 86rpx;
}
</style>

View File

@ -0,0 +1,127 @@
<template>
<view class="card">
<view class="card--header flex justify-between col-start">
<view class="flex">
<u-image
:src="user?.avatar || defaultAvatar"
width="80"
height="80"
border-radius="50%"
></u-image>
<view class="ml-[20rpx]">
<view class="text-base normal font-medium">
{{ user?.nickname || FieldType.defalutNickname }}
</view>
<view class="text-muted text-xs mt-[10rpx]">{{ createTime }}</view>
</view>
</view>
<view class="flex">
<u-rate
:count="5"
v-model="serviceComment"
size="32"
inactive-icon="star-fill"
activeColor="#FBC02D"
disabled
></u-rate>
<!-- <view class="ml-[20rpx] lighter text-xs">
<text v-if="serviceComment == 5"></text>
<text v-if="serviceComment == 4"></text>
<text v-if="serviceComment == 3"></text>
<text v-if="serviceComment == 2"></text>
<text v-if="serviceComment == 1"></text>
</view> -->
</view>
</view>
<view class="card--main">
<view class="content truncate">
{{ comment }}
</view>
<view class="flex flex-wrap">
<block v-for="(item3, index) in commentUrlList" :key="index">
<view
class="mt-[10rpx]"
:class="{ 'mr-[10rpx]': (index + 1) % 3 != 0 }"
@click.stop="previewImage(index)"
>
<u-image
:src="item3"
width="210"
height="210"
border-radius="15rpx"
></u-image>
</view>
</block>
</view>
<view class="reply normal text-base mt-[20rpx] font-medium" v-if="reply">
商家回复: {{ reply }}
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import defaultAvatar from '@/static/images/user/default_avatar.png'
import { FieldType } from '@/enums/appEnums'
const props = withDefaults(
defineProps<{
goodsId: string | number
comment: string | null
commentUrlList: string | null
reply: string | null
createTime: string | null
serviceComment: string | number | any
user: any
}>(),
{
goodsId: '',
comment: '',
commentUrlList: '',
reply: '',
createTime: '',
serviceComment: '',
user: {
avatar: '',
nickname: ''
}
}
)
//
const previewImage = (current: number) => {
uni.previewImage({
current,
urls: props.commentUrlList
})
}
</script>
<style lang="scss" scoped>
.card {
border-radius: 14rpx;
background-color: white;
margin: 0 20rpx 0 20rpx;
padding: 30rpx;
&--header {
width: 100%;
}
&--main {
overflow-x: hidden;
overflow-y: hidden;
.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"
:commentUrlList="item.commentUrlList"
:reply="item.reply"
:createTime="item.createTime"
:goodsId="item.goodsId"
:serviceComment="item.serviceComment"
:user="item.userInfoVo"
></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<{
goodsId: number | string
i: number
index: number
cid: number
}>(),
{
goodsId: 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 = 100) => {
try {
const { lists } = await apiEvaluateGoodsLists({
goodsId: +props.goodsId,
commentLevel: props.i,
pageNo,
pageSize: 100
})
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,107 @@
<!--
* @Author: 王翠碧 1254597151@qq.com
* @Date: 2023-05-22 22:41:29
* @LastEditors: 王翠碧 1254597151@qq.com
* @LastEditTime: 2023-05-27 14:44:46
* @FilePath: \uniapp\src\bundle\pages\evaluate_goods\index.vue
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
-->
<template>
<view class="container" v-if="tabList.length">
<tabs
:current="current"
@change="handleChange"
height="80"
bar-width="60"
:barStyle="{ bottom: '0' }"
:auth="true"
v-bind="tabsColor"
>
<tab v-for="(item, i) in tabList" :key="i" :name="`${item.name}(${item?.count})`">
<view class="List pt-[20rpx]" v-if="isLogin">
<List :cid="item.id" :i="i" :index="current" :goodsId="goodsId"></List>
</view>
</tab>
</tabs>
</view>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { apiEvaluateGoodsLists } from '@/api/goods'
import List from './components/list.vue'
import { useUserStore } from '@/stores/user'
import { apiEvaluateGoodsStatis } from '@/api/goods'
import { useTabs } from '@/hooks/useLockFn'
const { tabsColor } = useTabs()
const goodsId = ref<number>(8)
const tabList = ref<any>([
{
name: '全部',
count: 0
},
{
name: '好评',
count: 0
},
{
name: '中评',
count: 2
},
{
name: '差评',
count: 0
},
{
name: '晒图',
count: 0
}
])
const current = ref<number>(0)
//
const userStore = useUserStore()
const isLogin = computed(() => userStore.token)
const handleChange = (index: number) => {
current.value = Number(index)
}
//
const getEvaluateStatis = async () => {
const res = await apiEvaluateGoodsStatis({ goodsId: goodsId.value })
tabList.value[0].count = res.totalCount
tabList.value[1].count = res.goodCount
tabList.value[2].count = res.generalCount
tabList.value[3].count = res.badCount
tabList.value[4].count = res.pictureCount
}
onLoad((options: any) => {
goodsId.value = options.id || 0
getEvaluateStatis()
})
</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,144 @@
<template>
<!-- @click.stop="toGoodsDetail(goods_id)" -->
<view class="card">
<!-- Header Start -->
<view class="flex justify-between card--header">
<view class="text-xs text-muted">{{ create_time }}</view>
<view class="flex items-center">
<u-rate
:count="5"
v-model="service_comment"
size="28"
inactive-icon="star-fill"
activeColor="#FBC02D"
disabled
></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>
<!-- Main Start -->
<view class="card--main">
<view class="content truncate">
{{ 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) % 3 != 0 }"
@click.stop="previewImage(index)"
>
<u-image :src="item3" width="210" height="210"></u-image>
</view>
</block>
</view>
<view class="flex mt-[20rpx] goods">
<u-image :src="goods_image" width="140" height="140"></u-image>
<view class="ml-[20rpx]">
<view class="text-xl font-medium normal">{{ goods_name }}</view>
<view class="mt-[24rpx]">
<price :price="goods_price" :desc="unit_name"></price>
</view>
</view>
</view>
<!-- <view class="reply normal text-base mt-[20rpx] font-medium truncate" v-if="reply">
商家回复: {{ reply }}
</view> -->
<view class="reply normal text-base mt-[20rpx] font-medium" v-if="reply">
<u-read-more show-height="300" toggle text-indent="0">
<rich-text :nodes="joinReply(reply)"></rich-text>
</u-read-more>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import Price from '@/components/price/index.vue'
import { computed } from 'vue'
/** Props Start **/
const props = withDefaults(
defineProps<{
goods_id: string | number
comment: string | null
goods_comment_image: string | null | string[]
reply: string | null
create_time: string | null
service_comment: any
goods_image: string
goods_name: string
goods_price: string | number
unit_name: string
}>(),
{
goods_id: '',
comment: '',
goods_comment_image: '',
reply: '',
create_time: '',
service_comment: '',
goods_image: '',
goods_name: '',
goods_price: '',
unit_name: ''
}
)
/** Props End **/
const joinReply = computed(() => (reply: string | null) => `商家回复: ${reply}`)
/** Methods Start **/
//
const toGoodsDetail = (id: number | string) => {
uni.redirectTo({
url: `/pages/goods/index?id=${id}`
})
}
//
const previewImage = (current: number) => {
uni.previewImage({
current,
urls: props.goods_comment_image
})
}
/** Methods End **/
</script>
<style lang="scss" scoped>
.card {
border-radius: 14rpx;
background-color: white;
margin: 0 20rpx 0 20rpx;
padding: 30rpx;
&--header {
width: 100%;
}
&--main {
overflow-x: hidden;
.content {
padding: 20rpx 0;
font-size: 28rpx;
color: #222222;
}
.goods {
padding-top: 20rpx;
border-top: 1px solid #f6f6f6;
}
.reply {
word-break: break-all;
padding: 24rpx 20rpx;
background-color: #f6f6f6;
border-radius: 10rpx;
}
}
}
</style>

View File

@ -0,0 +1,83 @@
<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">
<already
:comment="item2.comment"
:goods_comment_image="item2.commentUrlList"
:reply="item2.reply"
:create_time="item2.createTime"
:goods_id="item2.goodsId"
:service_comment="item2.serviceComment"
:goods_image="item2.goodsImageUrl"
:goods_name="item2.goodsName"
:goods_price="item2.price"
:unit_name="item2.unitName"
></already>
</block>
</z-paging>
</template>
<script lang="ts" setup>
import { ref, watch, nextTick, shallowRef, unref } from 'vue'
import Already from './already.vue'
import { apiEvaluateGoodsLists } from '@/api/goods'
import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia'
const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore)
const props = withDefaults(
defineProps<{
type: number
count: number
i: number
index: number
}>(),
{
type: 0,
count: 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 apiEvaluateGoodsLists({
userId: userInfo.value.id,
pageNo,
pageSize
})
paging.value.complete(lists)
console.log('lists', lists)
} catch (e) {
console.log('报错=>', e)
//TODO handle the exception
paging.value.complete(false)
}
}
</script>

View File

@ -0,0 +1,80 @@
<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">
<wait
:name="item2.goodsName"
:image="item2.goodsImage"
:unit_name="item2.unitName"
:price="item2.goodsPrice"
:goodsId="item2.id"
:finishTime="item2.finishTime"
></wait>
</block>
</z-paging>
</template>
<script lang="ts" setup>
import { ref, watch, nextTick, shallowRef, unref } from 'vue'
import Wait from './wait.vue'
import { apiWaitEvaluateGoodsLists } from '@/api/goods'
import { onShow } from '@dcloudio/uni-app'
const props = withDefaults(
defineProps<{
type: number
count: number
i: number
index: number
}>(),
{
type: 0,
count: 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 = 1, pageSize = 10) => {
try {
const { lists } = await apiWaitEvaluateGoodsLists({
pageNo,
pageSize
})
paging.value.complete(lists)
} catch (e) {
console.log('报错=>', e)
//TODO handle the exception
paging.value.complete(false)
}
}
onShow(() => {
// paging.value?.reload()
})
</script>
<style scoped></style>

View File

@ -0,0 +1,82 @@
<!--
* @Author: micky 1254597151@qq.com
* @Date: 2023-08-14 15:38:40
* @LastEditors: micky 1254597151@qq.com
* @LastEditTime: 2023-11-03 11:36:00
* @FilePath: \housekeeping-uniapp\src\bundle\pages\evaluate_list\components\wait.vue
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
-->
<template>
<view class="card flex flex-col justify-between" @click="goPage">
<view class="mb-3">
<text class="font-medium">完成时间</text>
<text>{{ formatDate(finishTime) }}</text>
</view>
<view class="flex justify-between items-center">
<view class="flex flex-1 mr-1">
<u-image :src="image" width="140" height="140" border-radius="16"></u-image>
<view class="ml-[20rpx]">
<view class="normal font-medium text-xl overflow-ellipsis-line">
{{ name }}
</view>
<view class="mt-[24rpx]">
<price :price="price" :desc="unit_name"></price>
</view>
</view>
</view>
<view>
<button
class="evaluate bg-white text-sm text-muted leading-[60rpx] h-[60rpx] rounded-full"
@click="goPage"
>
去评价
</button>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import Price from '@/components/price/index.vue'
import { formatDate } from '@/utils/util'
const props = withDefaults(
defineProps<{
name: string | null
image: string | null
unit_name: any
price: any
goodsId: string | number
finishTime: string | number
}>(),
{
name: '',
image: '',
unit_name: '',
price: '',
goodsId: '',
finishTime: ''
}
)
const goPage = () => {
uni.redirectTo({
url: `/bundle/pages/evaluate_submit/index?id=${props.goodsId}`
})
}
</script>
<style lang="scss" scoped>
.card {
border-radius: 14rpx;
background-color: white;
margin: 0 20rpx 0 20rpx;
padding: 30rpx;
}
.evaluate {
border: 1px solid $blue5;
color: $blue5;
background-color: rgba(255, 255, 255, 1);
text-align: center;
}
</style>

View File

@ -0,0 +1,106 @@
<template>
<view class="container">
<!-- :auth="true" 是表示需要权限登录的 -->
<tabs
:current="current"
@change="handleChange"
height="86"
:font-size="32"
bar-width="60"
:barStyle="{ bottom: '0' }"
:auth="true"
v-bind="tabsColor"
>
<tab v-for="(item, i) in tabList" :key="i" :name="`${item.name}(${item.count})`">
<view class="List pt-[20rpx]" v-if="isLogin">
<List
v-if="i === 0"
:type="item.type"
:count="item.count"
:i="i"
:index="current"
/>
<alreadyList
v-if="i === 1"
:type="item.type"
:count="item.count"
:i="i"
:index="current"
/>
</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 alreadyList from './components/alreadyList.vue'
import tab from '@/components/tab/tab.vue'
import tabs from '@/components/tabs/tabs.vue'
import { useUserStore } from '@/stores/user'
import { apiEvaluateStatis } from '@/api/goods'
import { onShow } from '@dcloudio/uni-app'
import { useTabs } from '@/hooks/useLockFn'
const { tabsColor } = useTabs()
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
getEvaluateStatis()
})
//
const getEvaluateStatis = async () => {
const res = await apiEvaluateStatis('')
tabList.value[0].count = res.waitCommentCount
tabList.value[1].count = res.commentCount
}
// getEvaluateStatis()
onShow(() => {
// getEvaluateStatis()
})
</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,164 @@
<template>
<!-- Header Start -->
<view class="card flex">
<u-image :src="evaluateInfo?.orderGoods?.imageUrl" width="140" height="140"></u-image>
<view class="ml-[20rpx]">
<view class="normal font-medium text-xl overflow-ellipsis-line">
{{ evaluateInfo?.orderGoods?.goodsName }}
</view>
<view class="mt-[24rpx]">
<price
:price="evaluateInfo?.goodsPrice"
:desc="evaluateInfo?.orderGoods?.unitName"
></price>
</view>
</view>
</view>
<!-- Header End -->
<!-- Main Start -->
<view class="card mt-[20rpx]">
<view class="flex">
<text class="normal font-medium text-base mr-[20rpx]">服务评分</text>
<u-rate
:count="5"
v-model="formData.serviceComment"
:min-count="1"
inactive-icon="star-fill"
activeColor="#FBC02D"
size="34"
></u-rate>
<view class="ml-[20rpx] lighter text-xs">
<text v-if="formData.serviceComment === 5"></text>
<text v-if="formData.serviceComment === 4"></text>
<text v-if="formData.serviceComment === 3"></text>
<text v-if="formData.serviceComment === 2"></text>
<text v-if="formData.serviceComment === 1"></text>
</view>
</view>
<view class="content mt-[30rpx]">
<u-input
v-model="formData.comment"
type="textarea"
placeholder="请输入评价内容"
height="200"
maxlength="250"
/>
</view>
<view class="mt-[30rpx]">
<Uploader v-model="formData.commentImages" :maxUpload="6" image-fit="aspectFill" />
</view>
</view>
<view class="footer mt-[30rpx]">
<button
class="bg-primary text-lg text-white leading-[80rpx] h-[80rpx] rounded-full"
@click="onSubmit"
>
提交
</button>
</view>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { apiEvaluateAdd } from '@/api/user'
import Price from '@/components/price/index.vue'
import Uploader from '@/components/uploader/index.vue'
import { apiOrderDetail } from '@/api/order'
import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia'
const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore)
interface FormDataObj {
orderId: number
serviceComment: number | null
comment: string
commentImages: Array<string | null>
}
const evaluateInfo = ref<any>({})
const formData = ref<FormDataObj>({
orderId: 0,
serviceComment: 0,
comment: '',
commentImages: []
})
const onSubmit = async (): Promise<void> => {
try {
let commentLevel = 0 // 1 2 3
switch (formData.value.serviceComment) {
case 1:
commentLevel = 3
break
case 2:
commentLevel = 3
break
case 3:
commentLevel = 2
break
case 4:
commentLevel = 1
break
case 5:
commentLevel = 1
break
}
// return
if (commentLevel === 0) return uni.$u.toast('请选择服务评分')
if (commentLevel == 3 && formData.value.comment.length == 0)
return uni.$u.toast('请输入10个字以上的评价内容')
await apiEvaluateAdd({
...formData.value,
commentLevel,
goodsId: evaluateInfo.value?.orderGoods?.goodsId,
userId: userInfo.value.id
})
uni.$u.toast('操作成功')
uni.redirectTo({ url: '/bundle/pages/evaluate_list/index?type=1' })
} catch (error) {
console.log('提交评价: ', error)
}
}
const initEvaluateGoodsInfo = async (): Promise<void> => {
try {
const res = await apiOrderDetail({
id: formData.value.orderId
})
evaluateInfo.value = res
} catch (error) {
console.log('获取评价: ', error)
}
}
onLoad(options => {
formData.value.orderId = options.id || 0
initEvaluateGoodsInfo()
})
</script>
<style lang="scss">
.card {
border-radius: 14rpx;
background-color: #ffffff;
margin: 20rpx 20rpx 0 20rpx;
padding: 30rpx;
.content {
padding: 0 24rpx;
border-radius: 14rpx;
background-color: #f6f6f6;
}
}
.footer {
padding: 20rpx 30rpx;
}
</style>

View File

@ -0,0 +1,80 @@
<template>
<view class="master-detail">
<z-paging
auto-show-back-to-top
:auto="true"
ref="paging"
v-model="dataList"
@query="queryList"
height="100%"
>
<view class="header">
<view class="flex items-center h-[200rpx] bg-primary px-[24rpx]">
<u-image
class="mx-[15rpx]"
:src="masterData.userVo?.avatar"
width="100"
height="100"
border-radius="50%"
>
</u-image>
<text class="text-2xl text-white font-medium ml-[20rpx]">{{
masterData.name
}}</text>
</view>
<view class="bg-white px-[24rpx] py-[30rpx]">
<view class="text-xl font-medium pb-[20rpx]">TA的信息</view>
<view class="text-sm pb-[20rpx]"
>所在地区{{ masterData.city }} - {{ masterData.district }}</view
>
<view class="text-sm pb-[20rpx]">加入时间{{ masterData.createTime }}</view>
</view>
</view>
<view class="p-[24rpx] bg-white mt-[20rpx]">
<view class="text-xl font-medium pb-[20rpx] normal"
>TA的服务{{ masterData?.goodsList?.length || 0 }}
</view>
<view class="">
<goods-card :goodsList="masterData.goodsList"></goods-card>
</view>
</view>
</z-paging>
</view>
</template>
<script lang="ts" setup>
import { ref, unref, shallowRef, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { apiStaffDetail } from '@/api/store'
import GoodsCard from '@/components/goods-card/index.vue'
// id
const masterId = ref<number | string>('')
//
const dataList = ref<any>([])
// Ref
const paging = shallowRef<any>(null)
const masterData = computed(() => unref(dataList)[0] || { goods: [] })
onLoad((options) => {
masterId.value = options?.id || ''
})
const queryList = async () => {
try {
const data = await apiStaffDetail({
id: masterId.value
})
paging.value.complete([data])
} catch (e) {
console.log('报错=>', e)
//TODO handle the exception
paging.value.complete(false)
}
}
</script>
<style lang="scss"></style>

View File

@ -0,0 +1,148 @@
<template>
<view class="container">
<view class="px-[24rpx] py-[14rpx] bg-white">
<u-search
v-model="keyword"
placeholder="请输入关键词搜索"
height="72"
@search="search"
@custom="search"
@clear="queryList"
></u-search>
</view>
<view class="main bg-white mt-[20rpx]">
<z-paging
auto-show-back-to-top
:auto="true"
ref="paging"
v-model="dataList"
@query="queryList"
:fixed="false"
height="100%"
>
<!-- 订单卡片 -->
<navigator
v-for="item in dataList"
:key="item.id"
:url="`/bundle/pages/master_worker_detail/index?id=${item.id}`"
>
<view class="flex master_worker_item col-start">
<u-image
:src="item.avatarUrl"
width="100"
height="100"
border-radius="50%"
></u-image>
<view class="ml-[20rpx]">
<view class="text-lg font-medium normal">{{ item.name }}</view>
<view class="lighter text-sm mt-[10rpx]"
>所在地区{{ item.city }} - {{ item.district }}</view
>
<view class="lighter text-sm mt-[10rpx] truncate w-[580rpx]"
>服务项目{{ item.goodsList }}</view
>
</view>
</view>
</navigator>
</z-paging>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref, shallowRef } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { apiStaffLists } from '@/api/store'
//
const searchStatus = ref<boolean>(false)
//
const keyword = ref<string | number>('')
//
const dataList = ref<any>([])
// Ref
const paging = shallowRef<any>(null)
//
const search = (flag: boolean) => {
if (flag) {
if (keyword.value !== '') searchStatus.value = true
} else {
keyword.value = ''
searchStatus.value = false
}
paging.value?.reload()
}
const queryList = async (pageNo: number, pageSize: number) => {
try {
const { lists } = await apiStaffLists({
pageNo,
pageSize,
staffInfo: keyword.value
})
paging.value.complete(lists)
} catch (e) {
console.log('报错=>', e)
//TODO handle the exception
paging.value.complete(false)
}
}
//
const toDetail = (id: number | string) => {
uni.navigateTo({
url: `/bundle/pages/master_worker_detail/index?id=${id}`
})
}
</script>
<style lang="scss">
.container {
display: flex;
height: 100vh;
overflow: hidden;
flex-direction: column;
//
.search-box {
width: 100%;
height: 100rpx;
padding: 15rpx 24rpx;
.search {
width: 620rpx;
height: 100%;
padding: 15rpx 30rpx;
border-radius: 10rpx;
background-color: #f6f6f6;
//
&--input {
width: 84%;
padding-left: 20rpx;
}
//
.clear {
width: 34rpx;
height: 34rpx;
padding-left: 20rpx;
}
}
}
.main {
flex: 1;
min-height: 0;
overflow: scroll;
//
.master_worker_item {
padding: 30rpx 24rpx 28rpx 24rpx;
border-bottom: 1px solid #f2f2f2;
}
}
}
</style>

View File

@ -0,0 +1,136 @@
<template>
<z-paging
auto-show-back-to-top
ref="paging"
v-model="dataList"
@query="queryList"
:fixed="false"
height="100%"
>
<block v-for="item in dataList" :key="item.id">
<coupon-card :item="item">
<template #use="{ row }">
<view class="btn__link" v-if="condition">
<text @click.stop="handleNavigate(row)">立即使用</text>
</view>
</template>
<template v-slot:status>
<view class="btn__link" v-if="!condition">
<text>{{ parseText }}</text>
</view>
</template>
<template #count>
<text class="count" v-if="item.getNum > 1">
已领取{{ item.alreadyGetCouponCount }}
</text>
</template>
<template #expiringSoon>
<div class="expiring-soon" v-if="showExpiringSoon(item!)"></div>
</template>
</coupon-card>
</block>
</z-paging>
</template>
<script lang="ts" setup>
import { ref, inject, computed, unref, watch } from 'vue'
import { useZPaging } from '@/hooks/useCoupon'
import { COUPON_INJECTION_KEY } from '@/config/symbol'
import { apiMyCouponList } from '@/api/coupon'
import {
CouponEnum,
CouponStatusMap,
NavigateMap,
UseGoodsTypeEnum,
CouponStatusEnum,
showExpiringSoonDays
} from '@/enums/appEnums'
import { router } from '@/utils/util'
const parent = inject(COUPON_INJECTION_KEY)
const dataList = ref<any>([])
const tabStatus = computed(() => parent?.tabStatus.value) as any
const condition = computed(() => unref(tabStatus) === CouponEnum.NO_APPLY)
const parseText = computed(() => CouponStatusMap[unref(tabStatus)])
const showExpiringSoon = computed(() => {
return (row: any) => {
const { useTimeEnd, useStatus } = row
const currentTime = new Date().getTime()
const endTime = Date.parse(useTimeEnd)
const daysDifference = Math.abs(endTime - currentTime) / (60 * 60 * 24 * 1000)
return daysDifference <= showExpiringSoonDays && useStatus == CouponStatusEnum.AVALIABLE
}
})
/**优惠券列表参数 */
const config = {
initParams: {
useStatus: unref(tabStatus)
},
requestApi: apiMyCouponList
}
const { paging, queryList, refresh } = useZPaging(config.initParams, config.requestApi!)
/**
* 1通用券跳转到首页
* 2品类券对应品类页面
* 3商品券对应商品页面
*/
const handleNavigate = (row: any) => {
const { useGoodsType } = row
const url = NavigateMap[useGoodsType]
router(joinUrl(url, row))
}
watch(
() => unref(tabStatus),
() => {
const params = { useStatus: unref(tabStatus) }
refresh({ ...params })
}
)
function joinUrl(url: string, row: any) {
const { useGoodsType, id } = row
let params = ''
params = useGoodsType === UseGoodsTypeEnum.ALL ? '' : `?id=${id}`
return url + params
}
</script>
<style lang="scss" scoped>
.btn__link {
background-color: $white;
color: $blue1;
padding: 6rpx 16rpx;
font-size: 20rpx;
border-radius: 999px;
margin-top: 16rpx;
}
.status {
position: absolute;
top: 6rpx;
right: -78rpx;
padding: 10rpx 80rpx;
background-color: $gray1;
color: $white;
transform: rotate(45deg);
font-size: 24rpx;
}
.expiring-soon {
position: absolute;
background: #d83d3b;
width: 162rpx;
height: 32rpx;
rotate: 26deg;
top: -4px;
right: -11px;
font-size: 19rpx;
line-height: 32rpx;
padding-left: 53rpx;
}
</style>

View File

@ -0,0 +1,73 @@
<template>
<tabs
:current="setTabIndex"
v-bind="tabStyle"
:is-scroll="false"
:auth="true"
@change="handleChangeTab"
>
<tab v-for="(item, i) in optionMap.couponStatus" :key="i" :name="item.name" />
<view class="List">
<list />
</view>
<view class="header-box">
<button class="header-btn" @click.stop="handleNavigateCouponCenter">
<u-icon name="coupon" size="36" color="text-content" class="mr-2"></u-icon>
获取更多好券 >
</button>
</view>
</tabs>
</template>
<script lang="ts" setup>
import { provide, computed } from 'vue'
import list from './components/list.vue'
import { useTabs } from '@/hooks/useCoupon'
import { COUPON_INJECTION_KEY } from '@/config/symbol'
import { optionMap } from '@/config'
import { router } from '@/utils/util'
const { tabStatus, setTabIndex, handleChangeTab } = useTabs()
const tabStyle = computed(() => {
return {
height: 80,
barWidth: 60,
bgColor: '#06a9ff',
inactiveColor: '#fff',
activeColor: '#fff'
}
})
/**点击获取更多好券跳转 */
const handleNavigateCouponCenter = () => {
router('/bundle/pages/coupon/index')
}
provide(COUPON_INJECTION_KEY, {
tabStatus
})
</script>
<style lang="scss" scoped>
.List {
position: relative;
height: calc(100vh - 80rpx - env(safe-area-inset-bottom));
padding-bottom: 15rpx;
padding-top: 100rpx;
}
.header-box {
position: absolute;
z-index: 999;
top: 80rpx;
left: 0;
right: 0;
background: $white;
padding: 20rpx 20rpx 0;
.header-btn {
@apply bg-[#FAE8E7] text-lg text-[#D83D3B] leading-[82rpx] h-[82rpx] rounded-xl font-bold flex justify-center;
}
}
</style>

View File

@ -0,0 +1,295 @@
<template>
<view class="wrapper">
<view @click="handleToggle">
<u-notice-bar
mode="horizontal"
:list="noticeList"
:duration="1500"
v-bind="setNoticeBarBgColor"
></u-notice-bar>
</view>
<view class="card__money">
<view class="total__money">
<text>已提现收益</text>
<text class="total">{{ parseMoney('alreadyWithdraw') }}</text>
</view>
<view class="btn" @click="handleNavigateWithDraw"></view>
<view class="part__money">
<view class="freeze">
<text>待结算收益</text>
<text class="price">{{ parseMoney('toBeSettledMoney') }}</text>
</view>
<view class="withdraw">
<text>可提现收益</text>
<text class="price">{{ parseMoney('canWithdrawCommission') }}</text>
</view>
</view>
</view>
<!-- <view class="card__usercount">
<view>
<text class="title">总分销用户</text>
<text>{{ distributorInfo.totalDistributorUser }}</text>
</view>
<view>
<text class="title">今日分销用户</text>
<text>{{ distributorInfo.toDayDistributorUser }}</text>
</view>
</view> -->
<view class="card__cell">
<u-cell-group>
<u-cell-item
class="cell-item"
v-for="cell in cellGroup"
:key="cell.title"
:title="cell.title"
@click="cell.event(cell.field)"
></u-cell-item>
</u-cell-group>
</view>
<view class="share-btn">
<button class="share" open-type="share">分享链接</button>
</view>
</view>
<u-modal v-model="show">
<view class="slot-content">
<view>
在有效期内新用户或公池中的用户扫店铺码或通过店铺链接进入小程序可以和店铺信息相互绑定
</view>
<view>
有效期截止之后新用户或公池中的用户扫店铺码或通过店铺链接进入小程序不再和店铺信息绑定
</view>
</view>
</u-modal>
</template>
<script setup lang="ts">
import { computed, onMounted, reactive, ref, unref, onActivated } from 'vue'
import { onShareAppMessage, onShow } from '@dcloudio/uni-app'
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/stores/user'
import { useLockFn, useToggle } from '@/hooks/useLockFn'
import { getDistributorCenter } from '@/api/distributor_center'
import { formatMoney, navigateTo, router } from '@/utils/util'
import { Recordable } from '@/config/interface'
interface DistributorProps {
[key: string]: number | string | undefined
alreadyWithdraw: number
canWithdrawCommission: number
toBeSettledMoney: number
toDayDistributorUser: number
totalDistributorUser: number
distributorEndTime: string
}
const cellGroup = [
{ title: '店铺订单', field: 'order', event: handleNavigate },
{ title: '店铺客户', field: 'user', event: handleNavigate },
{ title: '收益记录', field: 'record', event: handleNavigate },
{ title: '店铺码', field: 'share', event: handleNavigate }
]
const navigateUrls: Recordable<string> = {
order: '/bundle/pages/distributor_order/index',
user: '/bundle/pages/distributor_binding_user/index',
withdraw: '/bundle/pages/withdraw_money/index',
record: '/bundle/pages/withdraw_distributor_record/index',
share: '/bundle/pages/share_qrcode/index'
}
const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore)
const { show, handleToggle } = useToggle()
const distributorInfo = reactive<Partial<DistributorProps>>({})
const compareDistributorTime = computed(
() =>
Date.now() / 1000 >=
new Date(`${distributorInfo.distributorEndTime?.replace(/\-/g, '/')}`).getTime() / 1000
)
const noticeList = computed(() =>
unref(compareDistributorTime)
? ['店铺码已过期,如需再次成为店长,请联系客服!!!']
: [`店铺码有效期截至时间:${distributorInfo.distributorEndTime}`]
)
const setNoticeBarBgColor = computed(() => ({
bgColor: unref(compareDistributorTime) ? '#F56C6C' : '#fdf6ec',
color: unref(compareDistributorTime) ? '#fff' : '#f9ae3d'
}))
const parseMoney = computed(() => {
return (field: string) => {
return '¥' + formatMoney(distributorInfo[field])
}
})
/**去提现 */
const handleNavigateWithDraw = () => {
router(`/bundle/pages/withdraw_money/index`)
}
/**跳转地址 */
function handleNavigate(type: string) {
router(navigateUrls[type])
}
/**分享给好友 */
onShareAppMessage(() => {
return {
path: `/pages/index/index?scene=${unref(userInfo).distributorId}`,
imageUrl: '../../../static/images/index.jpg'
}
})
onShow(() => {
lockFn()
})
const { lockFn } = useLockFn(async () => {
const data = await getDistributorCenter({ distributorId: unref(userInfo).distributorId })
initDistributorInfo(data)
})
/**设置数据 */
function initDistributorInfo(data: DistributorProps) {
for (const key in data) {
distributorInfo[key] = data[key]
}
}
</script>
<style lang="scss" scoped>
.wrapper {
position: relative;
}
.card__money {
padding: 40rpx;
background-image: linear-gradient(to right, $blue3 0%, $blue4 100%);
color: $white;
line-height: 1.6;
border-radius: 20rpx;
margin: 20rpx;
position: relative;
.total__money {
@include flex-column;
position: relative;
.total {
font-size: 46rpx;
font-weight: 600;
}
.withdraw {
display: none;
position: absolute;
right: -40rpx;
top: 0;
background-color: pink;
color: #2f63fe;
padding: 12rpx 30rpx;
border-top-left-radius: 999px;
border-bottom-left-radius: 999px;
font-size: 26rpx;
}
}
.part__money {
@include flex;
margin-top: 40rpx;
> view {
flex: 1;
@include flex-column;
.price {
font-size: 40rpx;
}
}
}
}
.card__usercount {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
margin: 20rpx;
> view {
@include flex-center;
padding: 30rpx;
background-color: $white;
border-radius: 12rpx;
line-height: 1.8;
.title {
font-weight: 600;
}
}
}
.card__cell {
margin: 20rpx;
border-radius: 16rpx;
overflow: hidden;
:deep(.cell-item) {
.u-border-bottom {
padding: 40rpx 32rpx !important;
.u-cell_title {
font-size: 32rpx !important;
}
}
}
}
.share-btn {
position: fixed;
left: 0;
right: 0;
bottom: calc(60rpx + env(safe-area-inset-bottom));
padding: 0 30rpx;
.share {
width: 100%;
height: 100rpx;
line-height: 100rpx;
background-color: $blue1;
color: $white;
border-radius: 999px;
}
}
.share {
// position: absolute;
// right: 0;
// bottom: -140rpx;
// background-color: $blue1;
// color: $white;
// border-top-left-radius: 999px;
// border-bottom-left-radius: 999px;
// font-size: 32rpx;
}
.slot-content {
padding: 0 40rpx;
line-height: 1.8;
> view {
margin: 30rpx 0;
color: $red;
font-size: 28rpx;
}
}
.btn {
position: absolute;
top: 40rpx;
right: 0;
padding: 12rpx 40rpx;
background-color: #457ded;
border-radius: 999px 0 0 999px;
border: 1px solid #fff;
border-right: none;
box-sizing: border-box;
font-size: 32rpx;
}
</style>

View File

@ -0,0 +1,86 @@
<template>
<view>
<u-navbar
:background="{
background: `url(${getImageUrl(img)}) no-repeat center center`,
backgroundSize: 'cover'
}"
height="210"
class="navbar"
back-icon-color="#fff"
/>
<view class="List">
<z-paging
auto-show-back-to-top
ref="paging"
v-model="couponLists"
@query="queryList"
:fixed="false"
height="100%"
>
<view class="px-[30rpx]" v-for="item in couponLists" :key="item.id">
<coupon-card :item="item" />
</view>
</z-paging>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref, shallowRef } from 'vue'
import { getDecorate } from '@/api/shop'
import { useAppStore } from '@/stores/app'
// import { apiCouponList } from '@/api/coupon'
const { getImageUrl } = useAppStore()
//
const img = ref('')
const getData = async () => {
const data = await getDecorate({ id: 6 })
const obj = JSON.parse(data.data)[0].content
switch (obj.enabled) {
case 1:
img.value = obj.defaultImg
break
case 2:
img.value = obj.customImg
break
}
}
getData()
//
const paging = shallowRef<any>(null)
const couponLists = ref<any>([])
const queryList = async (page_no = 1, page_size = 10) => {
try {
const { lists } = await apiCouponList({
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>
.navbar {
background-color: #fff;
position: relative;
:deep(.u-back-wrap) {
position: absolute;
top: 0;
left: 0;
z-index: 1000;
}
}
.List {
height: calc(100vh - 420rpx - env(safe-area-inset-bottom));
padding: 30rpx 0;
}
</style>

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.createTime }}</view>
</view>
<view class="right"> +{{ item.orderAmount }} </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,75 @@
<template>
<view class="bg-white suggest">
<!-- 热门搜索 -->
<view class="hot" v-if="hot_search.length">
<view class="font-medium pl-[24rpx] pt-[26rpx] pb-[6rpx] text-lg">热门搜索</view>
<view class="w-full px-[24rpx]">
<block v-for="(hotItem, index) in hot_search" :key="index">
<view class="max-w-full truncate keyword" @click="handleHistoreSearch(hotItem)"
>{{ hotItem }}
</view>
</block>
</view>
</view>
<!-- <view class="mx-[24rpx] my-[40rpx]" v-if="hot_search.length && his_search.length"></view> -->
<!-- 历史搜索 -->
<view class="history" v-if="his_search.length">
<view class="flex justify-between px-[24rpx] 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 px-[24rpx]">
<block v-for="(hisItem, index) in his_search" :key="index">
<view class="truncate keyword" @click="handleHistoreSearch(hisItem)">{{
hisItem
}}</view>
</block>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref, reactive, nextTick, onMounted } from 'vue'
const emit = defineEmits<{
(event: 'search', value: string): void
(event: 'clear', value: void): void
}>()
const props = withDefaults(
defineProps<{
hot_search?: any[]
his_search?: string[]
}>(),
{
hot_search: [],
his_search: []
}
)
const handleHistoreSearch = (text: string) => {
emit('search', text)
}
onMounted(() => {
console.log('props', props.hot_search)
})
</script>
<style lang="scss" scoped>
.suggest {
height: 100%;
.keyword {
display: inline-block;
margin: 24rpx 16rpx 0 0;
padding: 8rpx 24rpx;
border-radius: 26rpx;
background-color: #f4f4f4;
}
}
</style>

View File

@ -0,0 +1,141 @@
<template>
<view class="search">
<!-- 搜索框 -->
<view class="px-[24rpx] py-[14rpx] bg-white">
<u-search
v-model="keyword"
placeholder="请输入关键词搜索"
height="72"
@search="handleSearch"
@custom="handleSearch"
@clear="search.searching = false"
/>
</view>
<!-- 搜索 -->
<view class="search-content">
<!-- -->
<suggest
v-show="!search.searching"
@search="handleSearch"
@clear="handleClear"
:hot_search="search.hot_search"
:his_search="search.his_search"
/>
<!-- -->
<view class="search-content-s pt-[20rpx]" v-show="search.searching">
<z-paging
ref="paging"
v-model="search.result"
@query="queryList"
:fixed="false"
height="100%"
>
<view class="bg-white px-[24rpx]">
<goods-card :goodsList="search.result" type="list" />
</view>
</z-paging>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref, reactive, shallowRef, watch, nextTick } from 'vue'
import Suggest from './component/suggest.vue'
import { HISTORY } from '@/enums/cacheEnums'
import { getHotSearch, apiGoodsLists } from '@/api/store'
import cache from '@/utils/cache'
import GoodsCard from '@/components/goods-card/index.vue'
interface Search {
hot_search: string[]
his_search: string[]
result: any
searching: boolean
}
const search = reactive<Search>({
hot_search: [],
his_search: [],
result: [],
searching: false
})
const keyword = ref<string>('')
const paging = shallowRef()
watch(
() => keyword.value,
val => {
nextTick(() => {
if (!val) {
search.searching = false
}
})
}
)
const handleSearch = (value: string) => {
keyword.value = value
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 getHotSearchFunc = async () => {
try {
const data = await getHotSearch()
search.hot_search = data
} 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 (pageNo: number, pageSize: number) => {
try {
const { lists } = await apiGoodsLists({
name: keyword.value,
pageNo,
pageSize
})
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 {
&-content {
height: calc(100vh - 46px - env(safe-area-inset-bottom));
&-s {
height: 100%;
}
}
}
</style>

View File

@ -0,0 +1,60 @@
<!--
* @Author: micky 1254597151@qq.com
* @Date: 2023-08-23 17:14:02
* @LastEditors: micky 1254597151@qq.com
* @LastEditTime: 2023-09-21 21:04:33
* @FilePath: \housekeeping-uniapp\src\bundle\pages\service_explain\index.vue
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
-->
<template>
<view class="main">
<view class="my-3 text-3xl font-bold flex justify-center">
<view class="title">粤好生活服务说明</view>
</view>
<u-image
src="@/static/images/service/1.jpg"
width="100%"
height="560"
class="image"
mode="scaleToFill"
></u-image>
<u-image
src="@/static/images/service/2.jpg"
width="100%"
height="560"
class="image"
mode="scaleToFill"
></u-image>
</view>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
</script>
<style lang="scss" scoped>
.main {
height: 100vh;
display: flex;
flex-direction: column;
background: #fefefe;
.image {
flex: 1;
}
}
.title {
background-image: linear-gradient(to right, #1b4655, #675fe9);
-webkit-background-clip: text;
color: transparent;
position: relative;
&::after {
content: '';
width: 100%;
height: 3px;
position: absolute;
background: $u-type-primary;
bottom: -3px;
left: 0;
}
}
</style>

View File

@ -0,0 +1,146 @@
<template>
<view class="card" @click.stop="goPage">
<view class="card--header flex justify-between">
<view class="order-sn">订单编号{{ orderInfo.sn }}</view>
<view class="status">{{ getStatueName(orderInfo) }}</view>
</view>
<view class="card--main flex">
<u-image
:src="orderInfo.orderGoodsDetailVo.imageUrl"
width="160"
height="160"
border-radius="16"
></u-image>
<view class="ml-[20rpx] service-text">
<view class="service-text--name truncate">
{{ orderInfo.orderGoodsDetailVo.goodsName }}
</view>
<view class="appointTitle mt-[16rpx]">
预约: {{ orderInfo.appointTime }} {{ orderInfo.weekDay }}
{{ orderInfo.appointTimeStart }} - {{ orderInfo.appointTimeEnd }}
</view>
<view class="appointTitle mt-[16rpx]"></view>
<view class="mt-[16rpx]">
实付金额:
<text class="text-[#f36161] text-base pl-1.5">
¥{{ orderInfo.orderAmount }}
</text>
</view>
</view>
</view>
<view class="card--footer flex justify-between">
<view class="text-primary">
<template v-if="orderInfo.cancelTime">
<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, watchEffect } from 'vue'
import { apiOrderClose } from '@/api/order'
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
const time = (endTimestamp - startTimestamp) * 1000
timeStamp.value = time
if (timeStamp.value === 0) orderClose()
})
//
const orderClose = async () => {
await apiOrderClose({
outTradeNo: props.orderInfo.id
})
}
const goPage = () => {
uni.navigateTo({
url: `/bundle/pages/service_order_detail/index?id=${props.orderInfo.id}`
})
}
const getStatueName = orderInfo => {
// 1
if (orderInfo.orderStatus == 1 && orderInfo.isDispatch == 1) {
return '已接单'
}
return orderInfo.orderStatusName
}
</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,101 @@
<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"
:mobile="item?.mobile"
:staffId="item.staffId"
:orderStatus="item.orderStatus"
:cancel="item.cancelBtn"
:evaluate="item.commentBtn"
:contact="item.contactBtn"
:pay="item.pay_btn"
:confirmService="item.serviceBtn"
:verification="item.verificationBtn"
:goodsImage="item.orderGoodsDetailVo.imageUrl"
:goodsName="item.orderGoodsDetailVo.goodsName"
:type="2"
:appointTime="item.appointTime"
:appointTimeStartStr="item.appointTimeStart"
:isDispatch="item.isDispatch"
@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'
const props = withDefaults(
defineProps<{
orderStatus: number | string
i: number
index: number
refresh: boolean
}>(),
{
orderStatus: '',
refresh: false
}
)
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 }
)
watch(
() => props.refresh,
val => {
console.log(val, 'val')
val && paging.value?.reload()
}
)
const queryList = async (pageNo = 1, pageSize = 10) => {
try {
const { lists } = await apiStaffOrderLists({
orderStatus: props.orderStatus,
pageNo,
pageSize
})
if (paging.value) {
paging.value.complete(lists)
} else {
queryList()
}
} catch (e) {
console.log('报错=>', e)
//TODO handle the exception
paging.value.complete(false)
}
}
</script>
<style scoped></style>

View File

@ -0,0 +1,111 @@
<!--
* @Author: 王翠碧 1254597151@qq.com
* @Date: 2023-05-22 22:41:29
* @LastEditors: micky 1254597151@qq.com
* @LastEditTime: 2023-09-07 19:09:50
* @FilePath: \uniapp\src\bundle\pages\service_order\index.vue
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
-->
<template>
<view class="order-list">
<!-- :auth="true" 是表示需要权限登录的 -->
<tabs
:current="current"
@change="handleChange"
height="80"
bar-width="60"
:font-size="32"
:barStyle="{ bottom: '0' }"
:auth="true"
activeColor="#1296DB"
>
<tab v-for="(item, i) in tabList" :key="i" :name="item.name">
<view class="orderList pt-[20rpx]" v-if="isLogin">
<orderList
:orderStatus="item.orderStatus"
:i="i"
:index="current"
:refresh="item.refresh"
></orderList>
</view>
</tab>
</tabs>
<tabbar />
</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: '全部',
orderStatus: '',
refresh: false
},
{
name: '预约中',
orderStatus: 1,
refresh: false
},
{
name: '服务中',
orderStatus: 2,
refresh: false
},
{
name: '已完成',
orderStatus: 3,
refresh: false
},
{
name: '已取消',
orderStatus: 4,
refresh: false
}
])
const current = ref<number>(0)
const handleChange = (index: number) => {
current.value = Number(index)
tabList.value[index].refresh = true
setTimeout(() => {
tabList.value[index].refresh = false
}, 200)
}
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,261 @@
<template>
<uni-transition mode-class="zoom-in" needLayout="true" :show="orderData" :duration="500">
<view class="order_detail">
<!-- Main Start -->
<view class="flex pb-[30rpx]">
<image
v-if="orderData.orderStatus === 0"
class="header-image"
src="/static/images/icon_pay.png"
/>
<image
v-if="orderData.orderStatus === 1 || orderData.orderStatus === 2"
class="header-image"
src="/static/images/icon_wait.png"
/>
<image
v-if="orderData.orderStatus === 3"
class="header-image"
src="/static/images/icon_success.png"
/>
<image
v-if="orderData.orderStatus === 4"
class="header-image"
src="@/static/images/icon_close.png"
/>
<text class="statusDec ml-[15rpx]">{{ orderData.orderStatusName }}</text>
</view>
<!-- 地址卡片 -->
<view class="card">
<view class="card--header" @click="openLocation">
<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.orderGoods?.imageUrl"
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.orderGoods?.goodsName }}
</view>
<text>x{{ orderData.orderGoods?.goodsNum }}</text>
</view>
<view class="mt-[24rpx]">
<price
:price="orderData.orderGoods?.goodsPrice"
:desc="orderData.orderGoods?.unitName"
/>
</view>
</view>
</view>
</view>
<!-- 上门时间 -->
<view class="card normal text-base flex justify-between">
<view>上门时间</view>
<view>
{{ orderData.appointTime }} {{ orderData.weekDay }}
{{ orderData.appointTimeStartStr }}-{{ orderData.appointTimeEndStr }}
</view>
</view>
<!-- 服务金额 -->
<view class="card normal text-base">
<view class="flex justify-between">
<view>服务金额</view>
<view>¥{{ orderData.orderAmount }}</view>
</view>
<view class="mt-[30rpx] flex justify-between" v-if="isUseCoupon">
<view>抵扣金额</view>
<view class="text-primary">¥{{ orderData.deductionMoney }}</view>
</view>
<view class="mt-[30rpx] flex justify-between">
<view>实付金额</view>
<view class="text-primary">¥{{ orderData.orderAmount }}</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.userRemark }}</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.payWayName }}</view>
</view>
<view class="mt-[30rpx] flex justify-between">
<view>下单时间</view>
<view>{{ orderData.createTime }}</view>
</view>
<view class="mt-[30rpx] flex justify-between" v-if="orderData.orderStatus === 3">
<view>完成时间</view>
<view>{{ orderData.finishTime }}</view>
</view>
<view class="mt-[30rpx] flex justify-between" v-if="orderData.orderStatus === 4">
<view>关闭时间</view>
<view>{{ orderData.cancelTime }}</view>
</view>
</view>
<!-- Main End -->
</view>
</uni-transition>
<!-- Footer Start -->
<view class="footer flex justify-end" v-if="orderData.serviceBtn || orderData.verificationBtn">
<order-footer
:orderId="orderData.id"
:confirmService="orderData.serviceBtn"
:verification="orderData.verificationBtn"
:goodsName="orderData.orderGoods.goodsName"
:goodsImage="orderData.orderGoods.imageUrl"
:mobile="orderData.mobile"
:appointTime="orderData.appointTime"
:appointTimeStartStr="orderData.appointTimeStartStr"
@refresh="initOrderDetail"
/>
</view>
<!-- Footer End -->
</template>
<script lang="ts" setup>
import { ref, computed, unref } from 'vue'
import { onLoad, onShow, onUnload } from '@dcloudio/uni-app'
import { apiStaffOrderDetail } from '@/api/order'
import Price from '@/components/price/index.vue'
import OrderFooter from '@/components/order-footer/index.vue'
/** Data Start **/
const orderData = ref<any>({
order_goods: [],
order_amount: '',
total_amount: '',
total_goods_price: ''
})
const orderId = ref<number | string>('')
/** Data End **/
/** Methods Start **/
//
const initOrderDetail = async (): Promise<void> => {
orderData.value = await apiStaffOrderDetail({ id: orderId.value })
}
/** Methods End **/
const isUseCoupon = computed(() => unref(orderData.value.couponDetailVo))
/** Life Cycle Start **/
onLoad(options => {
// ID
orderId.value = parseInt(options?.id)
//
initOrderDetail()
})
/** Life Cycle End **/
//
const openLocation = () => {
// latitude: 23.18139
// longitude: 113.48067
console.log(
'orderData.latitude:',
orderData.value.latitude,
'orderData.longitude:',
orderData.value.longitude
)
uni.openLocation({
latitude: parseFloat(orderData.value.latitude),
longitude: parseFloat(orderData.value.longitude),
success: function () {
console.log('openLocation:success')
}
})
}
</script>
<style lang="scss">
.order_detail {
height: 100%;
padding: 30rpx 24rpx;
padding-bottom: 140rpx;
background: linear-gradient(to bottom, #1296db 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,216 @@
<template>
<view class="qrcode">
<view class="core">
<view class="code">
<image class="image" :src="src" :show-menu-by-longpress="true" v-if="src" />
<u-loading :size="60" mode="flower" v-else />
</view>
<view>长按识别二维码</view>
</view>
<view class="operation">
<view>
<button class="part bg-white" open-type="share">
<text class="icon share"></text>
<text class="label">分享</text>
</button>
</view>
<view>
<button class="part-photo bg-white" :disabled="isDisabled" @click="saveToAlbum">
<text class="icon download"></text>
<text class="label">保存到相册</text>
</button>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { computed, onMounted, ref, unref } from 'vue'
import { onShareAppMessage } from '@dcloudio/uni-app'
import { storeToRefs } from 'pinia'
import { useLockFn } from '@/hooks/useLockFn'
import { useUserStore } from '@/stores/user'
import { getDistributorQrcode } from '@/api/distributor_center'
const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore)
const src = ref('')
const isDisabled = computed(() => !unref(src))
const { lockFn: fetchShareQrcode } = useLockFn(async () => {
const base64Src = await getDistributorQrcode({ distributorId: unref(userInfo).distributorId })
src.value = base64Src
})
onMounted(() => fetchShareQrcode())
onShareAppMessage(() => {
return {
title: '粤好生活',
path: `/pages/index/index?scene=${unref(userInfo).distributorId}`,
imageUrl: '../../../static/images/index.jpg'
}
})
const saveToAlbum = () => {
uni.getSetting({
success: res => {
let authKey = ''
// #ifdef MP-WEIXIN
authKey = 'scope.writePhotosAlbum'
// #endif
// #ifdef MP-TOUTIAO
authKey = 'scope.album'
// #endif
//
if (!res.authSetting[authKey]) {
uni.authorize({
scope: authKey,
success: async () => {
saveImg()
},
//
fail: () => {
uni.showModal({
title: '您已拒绝获取相册权限',
content: '是否进入权限管理,调整授权',
success: res => {
if (res.confirm) {
uni.openSetting({
success: res => {
res.authSetting[authKey]
? saveImg()
: uni.$u.toast('已取消')
}
})
} else if (res.cancel) {
uni.$u.toast('已取消')
}
}
})
}
})
} else {
//
saveImg()
}
}
})
}
/**保存图片 */
async function saveImg() {
try {
const filePath = (await base64ToSave()) as string
uni.saveImageToPhotosAlbum({
filePath,
success: () => {
uni.$u.toast('图片保存成功')
removeSave()
},
fail: res => {
console.log(res)
uni.$u.toast('图片保存失败')
}
})
} catch (error) {}
}
/**base64转图片 */
function base64ToSave() {
const fsm = uni.getFileSystemManager()
return new Promise((resolve, reject) => {
const [, format, bodyData] = /data:image\/(\w+);base64,(.*)/.exec(unref(src)) || []
if (!format) {
reject(new Error('图片格式错误'))
}
const filePath = wx.env.USER_DATA_PATH + '/img.jpg'
const buffer = uni.base64ToArrayBuffer(bodyData)
fsm.writeFile({
filePath,
data: buffer,
encoding: 'base64',
success: () => {
resolve(filePath)
},
fail: () => {
reject(new Error('图片写入失败'))
}
})
})
}
/**删除文件 */
function removeSave(format = 'jpeg') {
return new Promise(resolve => {
const fsm = uni.getFileSystemManager()
const filePath = wx.env.USER_DATA_PATH + '/img.jpg'
fsm.unlink({
filePath,
success: () => {
console.log('删除成功')
}
})
})
}
</script>
<style lang="scss" scoped>
.qrcode {
height: 100vh;
background: #fff;
.core {
@include center;
flex-direction: column;
padding: 40rpx 0;
background-color: $white;
.code {
@include center;
width: 500rpx;
height: 500rpx;
margin-bottom: 40rpx;
.image {
width: 100%;
height: 100%;
}
}
}
.operation {
@include center;
> view {
flex: 1;
@include flex-center;
margin-top: 40rpx;
.icon {
width: 80rpx;
height: 80rpx;
background-repeat: no-repeat;
background-size: 100%;
}
.label {
font-size: 28rpx;
}
.part {
@include flex-column;
.share {
background-image: url('~@/static/images/share-friend.png');
}
}
.part-photo {
@include flex-center;
.download {
background-image: url('~@/static/images/download.png');
}
}
}
}
}
</style>

View File

@ -0,0 +1,240 @@
<template>
<!-- 收货地址空状态 -->
<!-- 收货地址的列表数据 -->
<view class="address-list">
<view v-if="!type" class="text-error fixed top-0 left-0 pl-[20rpx] pt-[20rpx]">
点击地址将该地址设为默认地址
</view>
<z-paging
auto-show-back-to-top
ref="paging"
v-model="addressList"
@query="queryList"
:fixed="false"
use-page-scroll
height="100%"
>
<view :style="{ 'padding-top': !type ? '62rpx' : '0' }">
<block v-for="(item, index) in addressList" :key="item.id">
<view
class="address-list--item flex justify-between mt-[20rpx]"
:data-id="item.id"
@click.stop="onSelect(item)"
>
<view class="flex-1">
<view>
<text class="text-xl font-medium">
{{ item.contact }} {{ item.mobile }}
</text>
<text class="default" v-if="item.isDefault"></text>
</view>
<view class="mt-[20rpx] sm text-muted">
{{ item.province }}
{{ item.city }}
{{ item.district }}
{{ item.address }}
</view>
</view>
<view class="setting" @click.stop="goEditAddress(item.id)">编辑</view>
</view>
</block>
</view>
</z-paging>
</view>
<view class="fixed flex items-center justify-between bg-white footer">
<!-- #ifdef H5 || MP-WEIXIN -->
<!-- <view v-if="isWeixin" class="mr-[20rpx]">
<button
class="bg-color text-lg text-black border-none rounded-full flex items-center justify-center leading-[80rpx] h-[80rpx]"
@click="getWxAddressFun"
>
<image
class="icon-md mr-[10rpx]"
:src="'../../static/images/icon_wechat.png'"
></image>
微信导入
</button>
</view> -->
<!-- #endif -->
<view>
<button
class="text-lg text-white rounded-full leading-[80rpx] h-[80rpx] custom-button-bgColor"
@click="goEditAddress('')"
>
添加地址
</button>
</view>
</view>
</template>
<script lang="ts" setup>
import { shallowRef, ref, watch } from 'vue'
import { onLoad, onShow, onUnload } from '@dcloudio/uni-app'
import { getGeocoder } from '@/api/app'
import { apiAddressLists, apiAddressEdit } from '@/api/user'
import { wxOaAddress } from '@/hooks/wechat'
import { isWeixinClient } from '@/utils/client'
import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia'
const paging = shallowRef<any>(null)
const type = ref(false)
const addressList = ref<any>([]) //
const isWeixin = ref(true) //
const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore)
//
const queryList = async (pageNo, pageSize) => {
try {
const { lists } = await apiAddressLists({ pageNo, pageSize })
paging.value.setLocalPaging(lists)
} catch (e) {
console.log('报错=>', e)
//TODO handle the exception
paging.value.setLocalPaging(false)
}
}
//
const onSelect = async (data: any) => {
try {
if (type.value) {
uni.$emit('address', data)
uni.navigateBack()
} else {
await apiAddressEdit({
...data,
isDefault: 1,
userId: userInfo.value.id
})
uni.redirectTo({
url: '/bundle/pages/user_address/index'
})
}
} catch (error) {
console.log(error, '设置默认地址捕捉错误')
}
}
//
const goEditAddress = (params = '') => {
uni.navigateTo({
url: `/bundle/pages/user_address_edit/index?id=${params}`
})
}
//
const getWxAddressFun = async () => {
// #ifdef H5
try {
const res = await wxOaAddress()
getLocation(res.cityName + res.countyName + res.detailInfo, res)
} catch (error) {
console.log('捕捉微信公众号导入地址报错', error)
}
// #endif
// #ifdef MP-WEIXIN
const { errMsg } = await uni.authorize({ scope: 'scope.address' })
if (errMsg === 'authorize:ok') {
try {
const res = await uni.chooseAddress()
console.log(res)
getLocation(res.cityName + res.countyName + res.detailInfo, res)
} catch (error) {
console.log('导入微信地址地址', error)
}
}
// #endif
}
// d
const getLocation = async (address: string, address_info: any) => {
try {
const res = await getGeocoder({ keyWord: address })
const obj = {
latitude: res.lat,
longitude: res.lng,
name: address_info.userName,
mobile: address_info.telNumber
}
console.log('obj', obj)
uni.setStorageSync('wxAddress', JSON.stringify(obj))
goEditAddress()
} catch (error) {
console.log('逆解析地址', error)
}
}
onShow(() => {
paging.value?.reload()
})
onLoad((options: { type?: boolean }) => {
if (options.type) type.value = options.type
//#ifdef H5
isWeixin.value = isWeixinClient()
//#endif
})
onUnload(() => {
uni.$emit('changeAddress')
})
</script>
<style lang="scss">
page {
// overflow: hidden;
padding-bottom: calc(140rpx + env(safe-area-inset-bottom));
}
.icon-md {
width: 48rpx;
height: 48rpx;
vertical-align: top;
}
.address-list {
&--item {
border-radius: 14rpx;
background-color: #ffffff;
margin: 20rpx 20rpx 0 20rpx;
padding: 30rpx 30rpx 36rpx 30rpx;
.default {
padding: 0 4rpx;
margin-left: 20rpx;
color: #f36161;
font-size: 22rpx;
background: rgba(#f36161, 0.1);
}
.setting {
height: 90rpx;
width: 100rpx;
line-height: 90rpx;
text-align: right;
color: $blue5;
font-size: 26rpx;
}
}
}
.footer {
z-index: 999;
left: 0;
right: 0;
bottom: 0;
height: 118rpx;
line-height: 118rpx;
position: fixed;
padding: 0 30rpx;
box-sizing: content-box;
padding-bottom: env(safe-area-inset-bottom);
> view {
width: 100%;
}
}
</style>

View File

@ -0,0 +1,336 @@
<template>
<view class="m-[24rpx]">
<!-- Mian Start -->
<u-form :model="formData" ref="formRef" :error-type="['message', 'toast']">
<!-- 联系人 -->
<view class="card">
<u-form-item prop="contact">
<view class="flex w-full">
<view class="label">联系人</view>
<u-input
v-model="formData.contact"
placeholder="请输入联系人"
class="flex-1"
/>
</view>
</u-form-item>
</view>
<!-- 手机号 -->
<view class="card">
<u-form-item prop="mobile">
<view class="flex w-full">
<view class="label">手机号</view>
<u-input
v-model="formData.mobile"
placeholder="请输入手机号码"
class="flex-1"
/>
</view>
</u-form-item>
</view>
<!-- 省市区 -->
<view class="card">
<u-form-item prop="region">
<view class="flex w-full" @click="chooseLocation">
<view class="label">省市区</view>
<view class="flex-1">
<u-input
@click="chooseLocation"
v-model="formData.region"
placeholder="请选择"
:disabled="true"
/>
</view>
<u-icon name="arrow-right" size="22" color="#888888"></u-icon>
</view>
</u-form-item>
</view>
<!-- 门牌号 -->
<view class="card">
<u-form-item prop="address">
<view class="flex w-full col-start">
<view class="label">门牌号</view>
<view class="flex-1 pt-[6rpx]">
<u-input
v-model="formData.address"
type="textarea"
placeholder="请选择"
height="124"
/>
</view>
</view>
</u-form-item>
</view>
<!-- 是否默认地址 -->
<view class="is-default flex my-[20rpx] items-center">
<u-checkbox shape="circle" active-color="#2468f2" v-model="formData.isDefault">
设为默认
</u-checkbox>
</view>
</u-form>
<!-- Mian End -->
</view>
<!-- Footer Start -->
<view class="flex m-[20rpx] pt-[30rpx]">
<!-- #ifdef H5 || MP-WEIXIN -->
<view v-if="addressId" class="flex-1 mr-[20rpx]">
<button
class="bg-white text-lg text-info rounded-full leading-[80rpx] h-[80rpx]"
@click="delPupop = true"
>
删除
</button>
</view>
<!-- #endif -->
<view class="flex-1 mx-[24rpx]">
<button
class="custom-button-bgColor text-lg text-white rounded-full leading-[80rpx] h-[80rpx]"
@click="onSubmit"
>
保存
</button>
</view>
</view>
<!-- Footer End -->
<!-- 删除地址 弹窗 Start -->
<u-modal
id="delete-dialog"
v-model="delPupop"
:showCancelButton="true"
confirm-text="狠心删除"
title="温馨提示"
confirm-color="#1296DB"
@confirm="handleAddressDel"
@cancel="delPupop = false"
>
<view class="text-center p-[50rpx]">
<text>确认删除该地址吗</text>
</view>
</u-modal>
<!-- 删除地址 弹窗 End -->
</template>
<script lang="ts" setup>
import { onLoad, onReady, onUnload } from '@dcloudio/uni-app'
import { ref } from 'vue'
import { getGeocoderCoordinate, getGeocoder } from '@/api/app'
import { apiAddressDetail, apiAddressEdit, apiAddressAdd, apiAddressDel } from '@/api/user'
import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia'
type FORM = {
contact: string //
mobile: string | number //
province: string | number //
provinceId: string | number //
city: string | number //
cityId: string | number //
district: string | number //
districtId: string | number //
address: string //
isDefault: number | boolean //
region: string
longitude: number //
latitude: number //
}
const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore)
const formData = ref<FORM>({
contact: '',
mobile: '',
province: '',
provinceId: '',
city: '',
cityId: '',
district: '',
districtId: '',
address: '',
isDefault: true,
region: '',
longitude: 0,
latitude: 0
})
const formRef = ref()
const addressId = ref<string | number>('')
const delPupop = ref<boolean | null>(false)
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'] }],
address: [{ required: true, message: '请输入门牌号', trigger: ['change', 'blur'] }]
})
//
const booleanToNumber = () => {
return formData.value.isDefault ? 1 : 0
}
//
const getAddressDetail = async (): Promise<void> => {
formData.value = await apiAddressDetail({ id: addressId.value })
formData.value.region = `${formData.value.province} ${formData.value.city} ${formData.value.district}`
}
//
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,
userId: userInfo.value.id,
isDefault: booleanToNumber()
})
setTimeout(() => {
uni.navigateBack()
}, 300)
}
//
const handleAddressEdit = async (): Promise<void> => {
await apiAddressEdit({
...formData.value,
userId: userInfo.value.id,
isDefault: booleanToNumber()
})
setTimeout(() => {
uni.navigateBack()
}, 300)
}
//
const chooseLocation = () => {
uni.chooseLocation({
success(res) {
getLocation(res.name, res.latitude, res.longitude)
},
fail(error) {
console.log(error)
}
})
}
//
const getLocation = async (name: string, latitude: string | number, longitude: string | number) => {
try {
const res1 = await getGeocoderCoordinate({
lat: latitude,
lng: longitude
})
handleAddressInfo(name, res1, longitude, latitude)
} catch (error) {
console.log('地址逆解析', error)
}
}
//
const handleAddressInfo = (name: string, event: any, longitude: number, latitude: number) => {
let cityId = event.cityId
const adInfo = event.amapGenCoderResponse.amapAddressComponent
if (cityId == 110000 || cityId == 310000 || cityId == 210000 || cityId == 410000) {
cityId = cityId * 1
cityId += 100
}
formData.value.cityId = cityId + '' // id
formData.value.provinceId = event.provinceId + '' // id
formData.value.districtId = event.districtId + '' // id
formData.value.region = `${adInfo.province} ${adInfo.city} ${adInfo.district}` //
//
const idx = event.amapGenCoderResponse.address.indexOf(adInfo.street)
let streetName = event.amapGenCoderResponse.address.slice(idx)
if (name) {
streetName = name // name
}
formData.value.address = streetName
formData.value.longitude = longitude.toFixed(10) //
formData.value.latitude = latitude.toFixed(10) //
// console.log('', name, event)
}
//
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) {
uni.setNavigationBarTitle({
title: '编辑地址'
})
getAddressDetail()
} else {
uni.setNavigationBarTitle({
title: '添加地址'
})
let wxAddress = uni.getStorageSync('wxAddress')
console.log('wxAddress', wxAddress)
if (!wxAddress) return
wxAddress = JSON.parse(wxAddress)
formData.value.contact = wxAddress.name
formData.value.mobile = wxAddress.mobile
getLocation(wxAddress.latitude, wxAddress.longitude)
}
})
onReady(() => {
formRef.value?.setRules(rules.value)
})
onUnload(() => {
uni.removeStorageSync('wxAddress')
})
</script>
<style lang="scss">
.card {
// margin: 20rpx;
margin-bottom: 0;
padding: 0 24rpx;
// border-radius: 14rpx;
background-color: #ffffff;
.label {
color: #222222;
font-size: 28rpx;
margin-right: 30rpx;
line-height: 70rpx;
}
}
.is-default {
image {
width: 40rpx;
height: 40rpx;
}
}
</style>

View File

@ -0,0 +1,216 @@
<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.orderAmount"
/>
</view>
<view class="balance-recharge-tips mt-[20rpx]">
<view>当前可用余额为 ¥ {{ walletData.userMoney }}</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>
<payment
v-if="payState.showPay || payState.showCheck"
ref="paymentRef"
v-model:show="payState.showPay"
v-model:show-check="payState.showCheck"
:order-id="payState.orderId"
:order_amount="rechargeData?.orderAmount"
:from="payState.from"
:redirect="payState.redirect"
@success="handlePayResult"
@close="handlePayResult"
@fail="handlePayResult"
/>
</template>
<script lang="ts" setup>
import { ref, reactive, shallowRef } from 'vue'
import { onShow, onUnload, onLoad } from '@dcloudio/uni-app'
import { apiRecharge, apiUserWallet } from '@/api/wallet'
import { PaymentStatusEnum } from '@/utils/enum'
import { getClient } from '@/utils/client'
const walletData = ref({
userMoney: '' //
})
const paymentRef = shallowRef()
const rechargeData = ref({
orderAmount: '', //
terminal: 1,
payChannel: ''
})
const payState = reactive({
orderId: '',
from: '',
showPay: false,
showCheck: false,
redirect: '/pages/order_buy/index'
})
//
const rechargeSuccess = ref<boolean | null>(false)
//
const getWalletData = () => {
apiUserWallet('').then((res: any) => {
walletData.value.userMoney = res.userMoney
})
}
//
const handleRecharge = () => {
if (rechargeData.value.orderAmount === '') return uni.$u.toast('请输入充值金额')
const scene = getClient()
let payChannel = ''
switch (scene) {
case 1:
payChannel = 'mp_channel'
break
case 2:
payChannel = 'oa_channel'
break
case 3:
payChannel = 'h5_channel'
break
}
rechargeData.value.payChannel = payChannel
apiRecharge({ ...rechargeData.value })
.then((res: any) => {
payState.orderId = res.orderId
payState.from = 'recharge'
payState.showPay = true
})
.catch((err) => {
console.log('下单', err)
})
}
const handlePayResult = async () => {
payState.showPay = false
payState.showCheck = false
uni.redirectTo({
url: `/bundle/pages/user_wallet/user_wallet`
})
}
const changeRechargeComplete = () => {
rechargeSuccess.value = false
}
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()
})
</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,183 @@
<template>
<view class="user-wallet">
<view class="wrapper">
<view class="top">
<view class="text">总资产</view>
<view class="total">{{ walletData.userMoney || '0.00' }}</view>
<view class="flex">
<!-- <view class="flex-1">
<view class="text">可用余额</view>
<view class="num">{{ walletData.user_money }}</view>
</view> -->
<!-- <view class="flex-1">
<view class="text">可提现余额</view>
<view class="num">{{ walletData.user_earnings }}</view>
</view> -->
</view>
</view>
<view class="flex mt-[40rpx]">
<navigator
class="recharge"
hover-class="none"
url="/bundle/pages/user_recharge/user_recharge"
v-if="walletData?.openRecharge"
>
<!-- <view class="btn1" v-if="walletData.recharge_open"> </view> -->
<view class="btn1"> 充值 </view>
</navigator>
<!-- <navigator
class="withdraw"
hover-class="none"
url="/bundle/pages/user_withdraw/user_withdraw"
>
<view class="btn2"> 提现 </view>
</navigator> -->
</view>
</view>
<view class="menus grid grid-cols-3">
<navigator
class="menu"
hover-class="none"
:url="`/bundle/pages/account_detail/account_detail?changeObject=1`"
>
<image src="@/static/images/icon/balance.png" alt="" />
<view class="text">余额明细</view>
</navigator>
<!-- <navigator
class="menu"
hover-class="none"
:url="`/bundle/pages/account_detail/account_detail?changeObject=2`"
>
<image src="@/static/images/icon/brokerage.png" alt="" />
<view class="text">提现明细</view>
</navigator> -->
<navigator
class="menu"
hover-class="none"
:url="`/bundle/pages/recharge_record/recharge_record`"
>
<image src="@/static/images/icon/recharge.png" alt="" />
<view class="text">充值记录</view>
</navigator>
<!-- <navigator
class="menu"
hover-class="none"
:url="`/bundle/pages/withdraw_record/withdraw_record`"
>
<image src="@/static/images/icon/withdraw.png" alt="" />
<view class="text">提现记录</view>
</navigator> -->
</view>
</view>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { apiUserWallet } from '@/api/wallet'
const walletData = ref<any>({
userMoney: '' //
// user_earnings: '', //
// total_money: '', //
})
const getWalletData = () => {
apiUserWallet('').then((res: any) => {
walletData.value = res
})
}
onShow(() => {
getWalletData()
})
</script>
<style lang="scss" scoped>
.user-wallet {
.wrapper {
margin: 20rpx 15rpx;
width: 720rpx;
height: 484rpx;
border-radius: 14rpx;
background: #fff;
padding: 20rpx;
.top {
width: 680rpx;
height: 320rpx;
border-radius: 20rpx;
background: $u-type-primary;
padding: 30rpx;
.text {
font-weight: 400;
font-size: 24rpx;
text-align: center;
color: #e5fff5;
margin-top: 20rpx;
}
.total {
font-size: 68rpx;
font-weight: 600;
text-align: center;
color: #e5fff5;
}
.num {
font-weight: 600;
font-size: 38rpx;
text-align: center;
color: #fff;
}
}
.btn1,
.btn2 {
border-radius: 14rpx;
background: #f4f4f4;
width: 320rpx;
height: 84rpx;
display: flex;
align-items: center;
justify-content: center;
}
.btn2 {
margin-left: 40rpx;
background: $u-type-primary;
}
.recharge,
.withdraw {
font-weight: 400;
font-size: 30rpx;
line-height: 40rpx;
color: #333;
}
.withdraw {
color: #fff;
}
}
.menus {
margin: 20rpx 15rpx;
width: 720rpx;
height: 360rpx;
border-radius: 14rpx;
background: #fff;
padding: 10rpx;
.menu {
width: 220rpx;
height: 160rpx;
border-radius: 14rpx;
background: #fff;
image {
width: 54rpx;
height: 54rpx;
margin: 28rpx 78rpx 0;
}
.text {
text-align: center;
}
}
}
}
</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,113 @@
<!--
* @Author: micky 1254597151@qq.com
* @Date: 2023-09-27 14:54:44
* @LastEditors: micky 1254597151@qq.com
* @LastEditTime: 2023-11-21 14:58:25
* @FilePath: \housekeeping-uniapp\src\bundle\pages\withdraw_distributor_record\components\card.vue
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
-->
<template>
<view class="card">
<view class="card__top">
<text>提现到账</text>
<text :class="getStatusColor(item.status)">{{ WithdrawStatusMap[item?.status] }}</text>
</view>
<view class="card__content">
<view class="money w-full">
<view class="text-2xl">提现金额</view>
<view class="text-5xl font-bold my-1 ml-[-50rpx]">
<text class="mr-2"></text>
<text>{{ item.withdrawMoney }}</text>
</view>
</view>
<view v-for="(i, index) in fieldArr" :key="index" class="flex text-lg mb-2">
<text class="w-[180rpx]">{{ i.name }}</text>
<text>{{ item[i.prop] }}</text>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { PropType, computed, watch } from 'vue'
import { WithdrawStatusMap } from '@/enums/appEnums'
interface CardProps {
id: number
withdrawMoney: number
createTime: string
status: number
updateTime: string
withdrawTo: string
remark: string
}
const props = defineProps({
item: Object as PropType<CardProps>
})
const getStatusColor = computed(() => {
return function (val: number) {
return val === 0 ? 'text-warning' : val === 1 ? 'text-success' : 'text-error'
}
})
watch(
() => props.item,
() => {
;(props.item as CardProps).withdrawTo = '微信零钱'
if ((props.item as CardProps).status == 1) {
;(props.item as CardProps).remark = '您的佣金已提现到微信钱包'
} else if ((props.item as CardProps).status == 2) {
;(props.item as CardProps).remark = '提现失败,请联系客服'
} else {
;(props.item as CardProps).remark = ''
}
},
{
immediate: true
}
)
const fieldArr = [
{
name: '提现去向',
prop: 'withdrawTo'
},
{
name: '提现时间',
prop: 'createTime'
},
{
name: '到账时间',
prop: 'updateTime'
},
{
name: '备注',
prop: 'remark'
}
]
</script>
<style lang="scss" scoped>
.card {
background-color: $white;
border-radius: 12rpx;
margin: 30rpx;
&__top {
display: flex;
justify-content: space-between;
line-height: 72rpx;
padding: 0 24rpx;
font-size: 28rpx;
}
&__content {
display: flex;
flex-direction: column;
border-top: 1px solid #eee;
padding: 0 20rpx 20rpx;
.money {
padding: 30rpx;
text-align: center;
}
}
}
</style>

View File

@ -0,0 +1,130 @@
<!--
* @Author: micky 1254597151@qq.com
* @Date: 2023-09-27 14:54:44
* @LastEditors: micky 1254597151@qq.com
* @LastEditTime: 2024-01-04 17:17:21
* @FilePath: \housekeeping-uniapp\src\bundle\pages\withdraw_distributor_record\index.vue
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
-->
<template>
<u-navbar title="" class="u-navbar">
<view class="slot-wrap" @click="handleToggle">
<text class="m-[5px]">收益记录</text>
<u-icon name="arrow-down"></u-icon>
<u-picker
mode="time"
v-model="show"
:default-time="`${convertChineseMonthToDate(currentDate)}-${day}`"
:params="params"
@confirm="handleConfirm"
></u-picker>
</view>
</u-navbar>
<view class="record" :style="`height:calc(100vh - ${navbarHeight}px)`">
<z-paging
auto-show-back-to-top
ref="paging"
v-model="dataList"
@query="queryList"
:fixed="false"
height="100%"
>
<block v-for="(item, index) in dataList" :key="`${index} + 'unique'`">
<card :item="item" />
</block>
</z-paging>
</view>
</template>
<script setup lang="ts">
import { computed, ref, unref, onMounted } from 'vue'
import { apiWithdrawLists } from '@/api/wallet'
import card from './components/card.vue'
import { useZPaging } from '@/hooks/useCoupon'
import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia'
import { useToggle } from '@/hooks/useLockFn'
import { getDistributorWithDrawList } from '@/api/distributor_center'
import { convertChineseMonthToDate } from '@/utils/util'
const date = `${new Date().getFullYear()}${new Date().getMonth() + 1}`
const today = new Date().getDate()
const day = today < 10 ? String(today).padStart(2, '0') : today
const params = {
year: true,
month: true,
day: false,
hour: false,
minute: false,
second: false
}
const dataList = ref<any>([])
const currentDate = ref(date)
const monthTotalPeople = ref(0)
const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore)
const { show, handleToggle } = useToggle()
const config = {
initParams: {
distributorId: unref(userInfo).distributorId,
date: convertChineseMonthToDate(unref(currentDate))
},
requestApi: getDistributorWithDrawList
}
const { paging, queryList, refresh } = useZPaging(config.initParams, config.requestApi)
const getWithdrawRecord = async () => {
try {
const params = {
distributorId: unref(userInfo).distributorId,
date: convertChineseMonthToDate(unref(currentDate))
}
const data = await getDistributorWithDrawList(params)
monthTotalPeople.value = Number(data) ?? 0
} catch (error) {}
}
onMounted(getWithdrawRecord)
let navbarHeight = ref(95)
onMounted(() => {
uni.createSelectorQuery()
.select('.u-navbar')
.boundingClientRect(data => {
navbarHeight.value = (data?.height || 95) as number
})
.exec()
})
/**选择日期 */
const handleConfirm = res => {
const { year, month } = res
currentDate.value = `${year}${month}`
refresh({ date: convertChineseMonthToDate(unref(currentDate)) })
// getWithdrawRecord()
}
</script>
<style scoped lang="scss">
.slot-wrap {
display: flex;
justify-content: center;
align-items: center;
flex: 1;
margin-left: 110rpx;
height: 88rpx;
}
.total-money {
position: fixed;
left: 0;
height: 86rpx;
width: 100%;
}
.list {
// height: calc(100vh - 88rpx);
padding-top: 86rpx;
}
</style>

View File

@ -0,0 +1,227 @@
<template>
<view class="withdraw">
<view class="withdraw__total">
<text>可提现金额</text>
<text>
<text class="strong">
{{ allowWithDrawMoney }}
</text>
</text>
</view>
<view class="withdraw__money">
<view class="title">提现金额</view>
<view class="line">
<text class="unit"></text>
<input
:value="withdrawMoney"
class="input"
placeholder-class="placeholder"
placeholder-style="placeholder"
placeholder="请输入金额"
@input="handleWithDrawMoneyInput"
/>
<text class="all" @click="allWithdraw"></text>
</view>
<div v-if="notValidKey" class="text-error">{{ notValidMap[notValidKey] }}</div>
<view class="channel" @click="handleShowPicker">
<text>提现至</text>
<view class="value">
<text class="label">{{ channel.label }}</text>
<u-picker
mode="selector"
v-model="showPicker"
:range="channelOptions"
range-key="label"
:default-selector="[0]"
@confirm="handleConfirm"
></u-picker>
</view>
</view>
<view class="btn" @click="handleWithDraw">
<text>申请提现</text>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { toast } from '@/utils/util'
import { computed, ref, unref, onMounted } from 'vue'
import { applyForWithdraw } from '@/api/wallet'
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/stores/user'
import { useLockFn } from '@/hooks/useLockFn'
import { getDistributorCenter } from '@/api/distributor_center'
const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore)
const channelOptions = [{ value: 1, label: '微信' }]
const allowWithDrawMoney = ref('0')
const withdrawMoney = ref()
let channel = ref({
label: '微信',
value: 1
})
const showPicker = ref(false)
const handleShowPicker = () => {
showPicker.value = true
}
const notValidMap = {
formatError: '金额格式错误',
balance: '余额不足',
less: '金额不能小于0.1'
}
const notValidKey = ref('')
/**绑定提现金额 */
const handleWithDrawMoneyInput = (event: any) => {
const {
detail: { value }
} = event
withdrawMoney.value = value
validateAmount(value)
}
//
const allWithdraw = () => {
withdrawMoney.value = allowWithDrawMoney.value
validateAmount(withdrawMoney.value)
}
const validateAmount = (value: string) => {
if (!value) return true
const regex = /^(0|[1-9]\d{0,8})(\.\d{1,2})?$/
if (!regex.test(value)) notValidKey.value = 'formatError'
else if (withdrawMoney.value > allowWithDrawMoney.value) notValidKey.value = 'balance'
else if (withdrawMoney.value < 0.1) notValidKey.value = 'less'
else notValidKey.value = ''
}
/**选择提现方式 */
const handleConfirm = (list: any) => {
const index = list[0]
channel.value = {
label: channelOptions[index].label,
value: channelOptions[index].value
}
}
/**申请提现 */
const handleWithDraw = () => {
if (!withdrawMoney.value) return toast('请输入提现金额')
if (notValidKey.value) return
applyForWithdraw({
distributorId: unref(userInfo).distributorId,
withdrawMoney: withdrawMoney.value
// userName: ''
}).then(res => {
withdrawMoney.value = ''
toast('已发起提现申请')
lockFn()
})
}
onMounted(() => lockFn())
const { lockFn } = useLockFn(async () => {
const data = await getDistributorCenter({ distributorId: unref(userInfo).distributorId })
if (data?.canWithdrawCommission) {
allowWithDrawMoney.value = Number(data?.canWithdrawCommission).toFixed(2)
} else {
allowWithDrawMoney.value = '0.00'
}
})
</script>
<style lang="scss" scoped>
.withdraw {
height: 600rpx;
background-color: #2077fe;
&__total {
color: $white;
padding: 40rpx 30rpx 0;
display: flex;
flex-direction: column;
justify-content: flex-end;
line-height: 1.6;
.strong {
font-size: 60rpx;
font-weight: 600;
}
}
&__money {
background-color: #fff;
margin: 30rpx;
border-radius: 16rpx;
padding: 30rpx 30rpx 40rpx;
.unit {
font-size: 84rpx;
}
.line {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #f5f5f5;
margin-top: 20rpx;
.input {
flex: 1;
padding: 0 12rpx;
font-size: 60rpx;
height: 90rpx;
}
:deep(.placeholder) {
color: #777;
font-size: 32rpx;
}
.all {
color: #2077fe;
font-size: 28rpx;
}
}
.channel {
display: flex;
justify-content: space-between;
margin-top: 30rpx;
color: #919191;
.value {
position: relative;
.label {
margin-right: 20rpx;
}
&::after {
content: '';
position: absolute;
right: 0;
top: 50%;
width: 12rpx;
height: 12rpx;
border-top: 1px solid #919191;
border-right: 1px solid #919191;
transform: translateY(-50%) rotate(45deg);
}
}
}
.btn {
background-color: #2077fe;
height: 100rpx;
border-radius: 50rpx;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
margin-top: 80rpx;
font-size: 36rpx;
}
}
}
</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: 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: 5.8 KiB

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

View File

View File

@ -0,0 +1,105 @@
<template>
<button
class="avatar-upload p-0 m-0 rounded"
:style="styles"
hover-class="none"
open-type="chooseAvatar"
@click="chooseAvatar"
@chooseavatar="chooseAvatar"
>
<image class="w-full h-full" mode="heightFix" :src="modelValue" v-if="modelValue" />
<slot v-else>
<div
: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" />
添加图片
</div>
</slot>
</button>
</template>
<script lang="ts" setup>
import { addUnit } from '@/utils/util'
import { isBoolean } from 'lodash'
import { computed, CSSProperties, onUnmounted } from 'vue'
import { uploadFile } from '@/utils/util'
const props = defineProps({
modelValue: {
type: String
},
fileKey: {
type: String,
default: 'uri'
},
size: {
type: [String, Number],
default: 120
},
round: {
type: [Boolean, String, Number],
default: false
},
border: {
type: Boolean,
default: true
}
})
const emit = defineEmits<{
(event: 'update:modelValue', value: string): void
}>()
const styles = computed<CSSProperties>(() => {
const size = addUnit(props.size)
return {
width: size,
height: size,
borderRadius: '50%' // isBoolean(props.round) ? (props.round ? '50%' : '') : addUnit(props.round),
}
})
const chooseAvatar = (e: any) => {
// #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
const path = e.detail?.avatarUrl
if (path) {
uploadImageIng(path)
}
// #endif
}
const uploadImageIng = async (path: string) => {
uni.showLoading({
title: '正在上传中...'
})
try {
const res: any = await uploadFile(path)
uni.hideLoading()
emit('update:modelValue', res.path)
} catch (error) {
uni.hideLoading()
uni.$u.toast(error)
}
}
//
uni.$on('uAvatarCropper', 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,123 @@
<template>
<u-popup v-model="show" mode="bottom" border-radius="30" :closeable="true" @close="close">
<view class="box">
<view class="cheap">优惠</view>
<view
class="flex justify-center items-center text-[#A1A1A1] h-full"
v-if="userCoupon?.length == 0 && waituselists?.length == 0"
>
暂无可用优惠券</view
>
<z-paging
ref="paging"
v-model="state"
@query="upCallback"
:fixed="false"
height="100%"
v-else
>
<view v-if="userCoupon?.length !== 0"
>已领取优惠券{{ userCoupon?.length }}</view
>
<!-- <view class="flex mt-[20rpx]" v-for="(item, index) in 3" :key="index"> -->
<cheap-card v-for="item in userCoupon" :key="item.id" :item="item">
<template #btn>
<button class="use-btn" @click="toUse">使</button>
</template>
</cheap-card>
<view class="mt-[20rpx]" v-if="waituselists?.length !== 0"
>待领取优惠券{{ waituselists?.length }}</view
>
<cheap-card v-for="item in waituselists" :key="item.id" :item="item">
<template #btn>
<button class="take-btn" @click="toTake"></button>
</template>
</cheap-card>
</z-paging>
</view>
</u-popup>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
// import { apiUserCouponList } from '@/api/coupon'
const show = ref(false)
const state = ref({})
const upCallback = () => {
console.log(111)
}
const props = defineProps({
couponlists: {
type: Array<any>
},
good_id: {
type: Number
}
})
const open = () => {
show.value = true
}
const close = () => {
show.value = false
}
defineExpose({ open })
const waituselists = computed(() => {
return props.couponlists?.filter((item) => {
return item.btn == 1 || item.btn == 2
})
})
const userCoupon = ref([])
// const getCouponLists = async () => {
// const { lists } = await apiUserCouponList({ goods_id: props.good_id, status: 1 })
// userCoupon.value = lists
// }
// getCouponLists()
const toUse = () => {
uni.navigateTo({
url: '/pages/index/index'
})
close()
}
const toTake = () => {
uni.navigateTo({
url: '/bundle/pages/receive_coupon/receive_coupon'
})
close()
}
</script>
<style lang="scss" scope>
.box {
height: 70vh;
padding: 30rpx;
background-color: #f7f7f7;
padding-top: 100rpx;
padding-bottom: env(safe-area-inset-bottom);
.cheap {
color: rgba(0, 0, 0, 1);
font-weight: bold;
position: fixed;
left: 50%;
transform: translate(-50%);
top: 32rpx;
}
.use-btn {
border-radius: 40rpx;
background-color: rgba(255, 215, 0, 1);
width: 130rpx;
height: 56rpx;
font-size: 22rpx;
line-height: 56rpx;
margin: 0;
}
.take-btn {
border-radius: 40rpx;
background-color: rgba(255, 0, 0, 1);
color: rgba(255, 255, 255, 1);
height: 56rpx;
font-size: 22rpx;
line-height: 56rpx;
margin: 0;
}
}
</style>

View File

@ -0,0 +1,280 @@
<template>
<view class="card flex mt-[20rpx]" :class="{ 'gray': !isHidden }">
<view class="card__top">
<view class="card__left">
<text class="font-bold text-xl">{{ item.name }}</text>
<text>{{ couponDesc(item) }}</text>
<text class="useTime">使用有效期2023.08.25-2023.09.25</text>
<view class="detail" @click="handleToggle(item)">
<text>详细信息</text>
<text :class="['icon', { 'active': item.isToggle }]"></text>
</view>
<!-- 已使用已过期 -->
<!-- <view class="outime" v-if="!isHidden">
<text>{{ textMap[tabStatus!] }}</text>
</view> -->
<slot name="selection"></slot>
</view>
<view class="card__right">
<text><text class="money">20.00</text></text>
<text class="label">立减券</text>
<text v-if="isHidden" class="btn__link" @click="handleNavigate">使</text>
<slot name="manual"></slot>
</view>
</view>
<view class="card__bottom" v-show="item.isToggle">
<view>限品类仅可购买生鲜部分商品</view>
<view>券编号35394387276</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { COUPON_INJECTION_KEY } from '@/config/symbol'
import { computed, inject, ref, watchEffect } from 'vue'
// import { apiReceiveCoupon } from '@/api/coupon'
interface CouponProp {
name: string
money: number
getType: number //
channelType: number ///
useGoodsType: number,//
conditionType: number,//使
sendTimeStart: number, //
sendTimeEnd: number,//
useTimeStart: number,//使
useTimeEnd: number,//使
sendTotal: number,
sendTotalType: number//
getNum: number, //
status: number,//
conditionMoney: number | string,//
distributors: any[]//
goodsCategoryDetailVos: any[],//
goodsDetailVos: any[]//
isToggle?: boolean //
}
const props = withDefaults(
defineProps<{
item: any
status?: number | undefined | string // :1-使;2-使;3-
showBtn?: boolean //
}>(),
{
item: {},
status: 1,
showBtn: true
}
)
const textMap: Record<number, string> = {
2: '已使用',
3: '已过期'
}
const parent = inject(COUPON_INJECTION_KEY)
const tabStatus = computed(() => parent?.tabStatus.value)
const isHidden = computed(() => tabStatus.value === 1) /**status已使用不可见 */
/*优惠券类型+有门槛/优惠券类型+无门槛*/
function generateCouponText(row: any) {
const { useGoodsType, conditionType, conditionMoney, money } = row
const conditions = [
{ useGoodsType: 1, conditionType: 1 },
{ useGoodsType: 1, conditionType: 2 },
{ useGoodsType: 2, conditionType: 1 },
{ useGoodsType: 2, conditionType: 2 },
{ useGoodsType: 3, conditionType: 1 },
{ useGoodsType: 3, conditionType: 2 },
]
const useGoodsTypeMap: Record<number, string> = {
1: '所有',
2: '相应品类',
3: '相应商品'
}
const flag = conditions.some(condition => condition.useGoodsType === useGoodsType && condition.conditionType === conditionType)
if (!flag) return
const condition = conditionType === 1 ? `消费立减${money}` : `${money}${conditionMoney}`
return `${useGoodsTypeMap[useGoodsType]}服务${condition}`
}
const couponDesc = computed(() => (row: CouponProp) => generateCouponText(row))
/**切换显示详细信息 */
const handleToggle = (item: CouponProp) => {
item.isToggle = !item.isToggle
}
const timeStamp = ref<number | string | undefined>()
const StartTime = Date.now() / 1000
const EndTime = +props.item.invalid_time
watchEffect(() => {
timeStamp.value = Math.floor(EndTime - StartTime) * 1000
})
/**
* 1通用券跳转到首页
* 2品类券对应品类页面
* 3商品券对应商品页面
*/
const navigateMap: Record<number, string> = {
1: '/pages/index/index',
2: '/bundle/pages/category_goods_coupon/goods_category',
3: '/bundle/pages/category_goods_coupon/goods'
}
const handleNavigate = (e: Event) => {
e.stopPropagation()
const { useGoodsType } = props.item
const url = navigateMap[useGoodsType]
uni.navigateTo({ url: url })
}
</script>
<style lang="scss" scoped>
.card {
display: flex;
flex-direction: column;
box-sizing: border-box;
box-shadow: 0 8rpx 20rpx 0 #F3F3F4;
margin: 30rpx;
overflow: hidden;
&__top {
display: flex;
.card__left {
flex: 1;
display: flex;
flex-direction: column;
line-height: 1.8;
background-color: #fff;
padding: 20rpx;
border-radius: 12rpx;
position: relative;
.useTime {
font-size: 22rpx;
}
.detail {
color: #999;
font-size: 20rpx;
display: flex;
justify-content: space-between;
align-items: center;
.icon {
width: 20rpx;
height: 20rpx;
background-color: #999;
border-radius: 999px;
position: relative;
&::before {
content: '';
width: 6rpx;
height: 6rpx;
border-left: 1px solid #fff;
border-bottom: 1px solid #fff;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%) rotate(-45deg);
}
&.active {
&::before {
transform: translate(-50%, -50%) rotate(135deg);
}
}
}
}
.outime {
position: absolute;
top: 6rpx;
right: -78rpx;
padding: 10rpx 80rpx;
background-color: #777;
color: #fff;
transform: rotate(45deg);
font-size: 24rpx;
}
}
.card__right {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
background-image: linear-gradient(to left, #495aff 0%, #4481eb 100%);
color: #fff;
// background-image: linear-gradient(45deg, #FEF4F0 0%, #ffdde1 100%);
// color: #fc5531;
padding: 20rpx;
border-radius: 12rpx;
.money {
font-size: 40rpx;
}
&::before {
content: '';
position: absolute;
top: -10rpx;
left: -10rpx;
width: 20rpx;
height: 20rpx;
background-color: #F8F8F9;
border-radius: 100%;
}
&::after {
content: '';
position: absolute;
bottom: -10rpx;
left: -10rpx;
width: 20rpx;
height: 20rpx;
background-color: #F8F8F9;
border-radius: 100%;
z-index: 1;
}
.label {
font-size: 24rpx;
}
.btn__link {
background-color: #fff;
color: #485FFC;
padding: 8rpx 16rpx;
font-size: 20rpx;
border-radius: 999px;
margin-top: 16rpx;
}
}
}
&__bottom {
width: 100%;
padding: 12rpx 20rpx;
border-top: 1px solid #eee;
background-color: #fff;
box-sizing: border-box;
font-size: 20rpx;
line-height: 1.8;
color: #999;
z-index: 2;
}
&.gray {
// filter: grayscale(1);
}
}
</style>

View File

@ -0,0 +1,197 @@
<template>
<view :class="status !== 1 ? 'gray-scale' : ''" class="card flex mt-[20rpx]">
<view class="left text-center">
<view class="pt-[36rpx] text-[#ff2c3c]">
<text class="text-2xl text-semibold"></text
><text class="text-[40rpx] text-semibold">{{ item.money }}</text>
</view>
<view class="pt-[8rpx] pb-[32rpx] text-muted text-[24rpx] truncate">{{
item.coupon_describe
}}</view>
</view>
<view class="right flex-1 px-[20rpx] pt-[20rpx] bg-white">
<u-image
v-show="item.btn === 2 || showDoneBtn"
class="img"
src="@/static/images/received.png"
width="70"
height="54"
/>
<view class="title text-lg text-[#333] font-medium pb-[16rpx] truncate">{{
item.name
}}</view>
<view class="flex justify-between">
<view
class="date text-xs text-[#666] py-[11rpx]"
:class="{ 'w-[307rpx] break-all': showBtn }"
>
<view v-if="item.is_countdown && timeStamp >= 0" class="text-[#ff2c3c] flex">
即将过期仅剩余
<u-count-down
:timestamp="timeStamp"
format="HH:mm:ss"
:font-size="24"
:separator-size="26"
@end="timeStamp = 0"
/>
</view>
<text v-else>{{ item.use_time_desc }}</text>
</view>
<view v-if="showBtn" class="btns flex-1">
<!-- 1-立即领取;2-继续领取;3-已领完; -->
<button
class="btn-one text-[#ff2c3c] bg-white"
v-if="item.btn !== 3 && showDoneBtn"
@click="goPage"
>
去使用
</button>
<button
class="btn-two bg-[#ff2c3c] text-white"
v-if="item.btn === 1 && !showDoneBtn"
@click="toReceive(item.id)"
>
立即领取
</button>
<button
class="btn-two bg-[#ff2c3c] text-white"
v-if="item.btn === 2 && !showDoneBtn"
@click="toReceive(item.id)"
>
继续领取
</button>
<button class="btn-three bg-[#ccc] text-white" v-if="item.btn === 3" disabled>
已领完
</button>
</view>
</view>
<view class="footer flex justify-between py-[8rpx] mt-[16rpx]">
<!-- 1=全部商品2=指定商品3=指定商品不可用 -->
<view class="text-xs text-[#666]" v-if="item?.use_goods_type === 1"
>全部商品可用</view
>
<view class="text-xs text-[#666]" v-if="item?.use_goods_type === 2"
>指定商品可用</view
>
<view class="text-xs text-[#666]" v-if="item?.use_goods_type === 3"
>指定商品不可用</view
>
<u-icon
v-show="item?.use_goods_type !== 1 && !isShow"
name="arrow-up"
@click="isShow = !isShow"
/>
<u-icon
v-show="item?.use_goods_type !== 1 && isShow"
name="arrow-down"
@click="isShow = !isShow"
/>
</view>
</view>
</view>
<view class="bg-white py-[15rpx] px-[20rpx]" v-show="isShow">
<view class="text-[#999] text-xs pt-2">{{
item.use_goods_type === 3 ? '不可用商品:' : '可用商品:'
}}</view>
<view v-for="iten in item.goods" :key="iten.id" class="text-[#999] text-xs pt-2">{{
iten.name
}}</view>
</view>
</template>
<script lang="ts" setup>
import { ref, watchEffect } from 'vue'
// import { apiReceiveCoupon } from '@/api/coupon'
const props = withDefaults(
defineProps<{
item: any
status?: number | undefined | string // :1-使;2-使;3-
showBtn?: boolean //
}>(),
{
item: {},
status: 1,
showBtn: true
}
)
const timeStamp = ref<number | string | undefined>()
const StartTime = Date.now() / 1000
const EndTime = +props.item.invalid_time
watchEffect(() => {
timeStamp.value = Math.floor(EndTime - StartTime) * 1000
})
const isShow = ref(false)
//
const showDoneBtn = ref(false)
const toReceive = async (id) => {
// try {
// await apiReceiveCoupon({ id })
// uni.$u.toast('')
// showDoneBtn.value = true
// } catch (error) {
// uni.$u.toast(error)
// }
}
//
const goPage = () => {
uni.navigateTo({
url: '/pages/index/index'
})
}
</script>
<style lang="scss" scoped>
.card {
-webkit-mask-image: radial-gradient(circle at 178rpx 10rpx, transparent 10rpx, red 11rpx),
radial-gradient(closest-side circle at 50%, red 99%, transparent 100%);
-webkit-mask-size: 100%, 4rpx 8rpx;
-webkit-mask-repeat: repeat, repeat-y;
-webkit-mask-position: 0 -10rpx, 175rpx;
-webkit-mask-composite: source-out;
mask-composite: subtract;
.left {
box-sizing: border-box;
width: 180rpx;
background: linear-gradient(to right, #ff2c3c 11rpx, white 15rpx);
border-top-left-radius: 14rpx;
border-bottom-left-radius: 14rpx;
}
.right {
position: relative;
.img {
position: absolute;
right: 0;
top: 0;
}
.btn-one,
.btn-two,
.btn-three {
height: 52rpx;
line-height: 52rpx;
border-radius: 9999rpx;
border: 1rpx solid #ff2c3c;
font-weight: 400;
font-size: 24rpx;
}
.btn-three {
border: 1rpx solid #ccc;
}
.footer {
border-top: 1px solid #eaeaea;
}
}
}
.gray-scale {
filter: grayscale(1);
}
</style>

View File

@ -0,0 +1,328 @@
<template>
<view class="card flex mt-[20rpx]" :class="{ 'gray': isGray(item!) }">
<view class="card__top">
<view class="card__left overflow-hidden">
<!-- 领取次数多张 -->
<slot name="count"></slot>
<text class="font-bold text-xl name truncate">{{ item!.name }}</text>
<text>{{ couponDesc(item!) }}</text>
<text class="useTime">{{ timeText }}{{ parseTime(item) }}</text>
<!-- 详细信息 -->
<block>
<view
class="detail"
v-if="isVisibleCouponDetail(item!)"
@click="handleToggle(item!)"
>
<text>详细信息</text>
<text :class="['icon', { 'active': item!.isToggle }]"></text>
</view>
<view class="placeholder" v-else></view>
</block>
<slot name="selection"></slot>
</view>
<view class="card__right" :class="{ default: slots.selection }">
<text>
<text class="money">{{ item!.money }}</text>
</text>
<text class="label">{{ couponCondition(item!) }}</text>
<!-- 已使用/已过期 -->
<slot name="status"></slot>
<!-- 立即使用 -->
<slot name="use" :row="item"></slot>
<!-- 立即领取 -->
<slot name="manual"></slot>
<slot name="expiringSoon"></slot>
</view>
</view>
<!-- 详细信息内容 -->
<view class="card__bottom" v-show="item!.isToggle">
<view v-if="slots.manual">{{ '' + item!.getNum }}</view>
<view v-if="slots.manual">{{ item!.useTimeEnd }}</view>
<view v-if="isLen(item!)">使{{ parseDetailLabel(item!) }}</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { PropType, computed, unref, useSlots } from 'vue'
import {
CouponEnum,
UseConditionEnum,
conditions,
useGoodsTypeMap,
CouponCountMap,
CouponStatusEnum,
showExpiringSoonDays
} from '@/enums/appEnums'
interface CouponProp {
name: string
money: number
getType: number //
channelType: number ///
useGoodsType: number //
conditionType: number //使
sendTimeStart: string //
sendTimeEnd: string //
useTimeStart: string //使
useTimeEnd: string //使
sendTotal: number
sendTotalType: number //
getNum: number //
status: number //
conditionMoney: number | string //
distributors: any[] //
goodsCategoryDetailVos: any[] //
goodsDetailVos: any[] //
isToggle?: boolean //
goodsCategories: any[] //
goods: any[] //
useStatus: number //使
}
const props = defineProps({
item: Object as PropType<CouponProp>,
default: () => ({}),
activeTabName: String
})
const slots = useSlots()
const couponDesc = computed(() => (row: CouponProp) => generateCouponText(row))
/**无门槛:立减 有门槛满x元可用 */
const couponCondition = computed(() => (row: CouponProp) => {
const { conditionType, conditionMoney } = row
return conditionType === UseConditionEnum.NO_CONDITION ? '立减券' : `${conditionMoney}可用`
})
const isLen = computed(() => (row: CouponProp) => {
const { goodsCategories, goods } = row
return goodsCategories.length >= 1 || goods.length >= 1
})
/**
* 1通用券不需要
* 2useStatus1需要
*/
/**显示详细信息 */
const isVisibleCouponDetail = computed(() => (row: CouponProp) => {
return unref(isLen)(row) || slots.manual
})
/**已使用、已过期可见 */
const isGray = computed(() => {
return (row: CouponProp) => {
const { useStatus } = row
return (
useStatus === CouponEnum.ALIPAY ||
useStatus === CouponEnum.OVERDUE ||
props.activeTabName == CouponCountMap.notAvailableCount
)
}
})
const parseTime = computed(() => {
return (row: any) => {
const { useTimeStart, useTimeEnd } = row
// const [useTimeStartDate] = useTimeStart.split(' ')
// const [useTimeEndDate] = useTimeEnd.split(' ')
return `${parseSingleTime(useTimeStart)}${parseSingleTime(useTimeEnd)}`
}
})
const parseSingleTime = (t: string) => {
if (!t) return
try {
const [day, time] = t.split(' ')
let hour = time.split(':')[0]
hour.startsWith('0') ? (hour = hour.replace('0', '')) : ''
return `${day} ${hour}`
} catch {}
return t
}
const parseDetailLabel = computed(() => {
/**品类券仅可购买指定xx商品 商品券仅可购买xx部分商品 */
return (row: CouponProp) => {
const { goodsCategories, useGoodsType, goods } = row
if (!unref(isVisibleCouponDetail) || useGoodsType === 1) return
const catrgoryNames =
useGoodsType === 2 ? transformGoods(goodsCategories) : transformGoods(goods)
const text = useGoodsType === 2 ? `指定${catrgoryNames}` : `${catrgoryNames}`
return `仅可购买${text}服务`
}
})
const timeText = computed(() => (slots.manual ? '领取时间:' : '有效期:'))
const showExpiringSoon = computed(() => {
return (row: CouponProp) => {
const { useTimeEnd, useStatus } = row
const currentTime = new Date().getTime()
const endTime = Date.parse(useTimeEnd)
const daysDifference = Math.abs(endTime - currentTime) / (60 * 60 * 24 * 1000)
return daysDifference <= showExpiringSoonDays && useStatus == CouponStatusEnum.AVALIABLE
}
})
/**切换显示详细信息 */
const handleToggle = (item: CouponProp) => {
item.isToggle = !item.isToggle
}
function transformGoods(goods: any[], separator = '、') {
return goods.map(category => category.name).join(separator)
}
/*优惠券类型+有门槛/优惠券类型+无门槛*/
function generateCouponText(row: any) {
const { useGoodsType, conditionType, conditionMoney, money } = row
const flag = conditions.some(
condition =>
condition.useGoodsType === useGoodsType && condition.conditionType === conditionType
)
if (!flag) return
const condition =
conditionType === UseConditionEnum.NO_CONDITION
? `消费立减${money}`
: `${conditionMoney}${money}`
return `${useGoodsTypeMap[useGoodsType]}服务${condition}`
}
</script>
<style lang="scss" scoped>
.card {
display: flex;
flex-direction: column;
box-sizing: border-box;
box-shadow: 0 8rpx 20rpx 0 $white4;
margin: 30rpx;
overflow: hidden;
&__top {
display: flex;
position: relative;
.card__left {
flex: 1;
display: flex;
flex-direction: column;
line-height: 1.8;
background-color: $white;
padding: 0 20rpx 20rpx;
border-radius: 12rpx;
position: relative;
.name {
margin-top: 34rpx;
}
.useTime {
font-size: 21rpx;
}
.detail {
color: $gray2;
font-size: 20rpx;
display: flex;
justify-content: space-between;
align-items: center;
.icon {
width: 20rpx;
height: 20rpx;
background-color: $gray2;
border-radius: 999px;
position: relative;
&::before {
content: '';
width: 6rpx;
height: 6rpx;
border-left: 1px solid $white;
border-bottom: 1px solid $white;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%) rotate(-45deg);
}
&.active {
&::before {
transform: translate(-50%, -50%) rotate(135deg);
}
}
}
}
.placeholder {
height: 36rpx;
}
}
.card__right {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
// justify-content: center;
text-align: center;
background-image: linear-gradient(to left, $blue3 0%, $blue4 100%);
color: $white;
// background-image: linear-gradient(45deg, #FEF4F0 0%, #ffdde1 100%);
// color: #fc5531;
// padding: 20rpx 30rpx;
padding-top: 44rpx;
width: 90px;
border-radius: 12rpx;
.money {
font-size: 44rpx;
}
&::before {
content: '';
position: absolute;
top: -10rpx;
left: -10rpx;
width: 20rpx;
height: 20rpx;
background-color: $white1;
border-radius: 100%;
}
&::after {
content: '';
position: absolute;
bottom: -10rpx;
left: -10rpx;
width: 20rpx;
height: 20rpx;
background-color: $white1;
border-radius: 100%;
z-index: 1;
}
.label {
font-size: 24rpx;
}
&.default {
justify-content: center;
padding-top: 0;
}
}
}
&__bottom {
width: 100%;
padding: 12rpx 20rpx;
border-top: 1px solid $white3;
background-color: $white;
box-sizing: border-box;
font-size: 20rpx;
line-height: 1.8;
color: $gray2;
z-index: 2;
}
&.gray {
filter: grayscale(1);
opacity: 0.7;
}
}
</style>

View File

@ -0,0 +1,95 @@
<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 || 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-[200rpx] h-[310rpx] mr-[49rpx] flex bg-white"
@click="goPage(goodsItem.id)"
>
<u-image :src="goodsItem.image" width="200" 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 || goodsItem?.unit_name"
></price>
</view>
</view>
</block>
</template>
</template>
<script lang="ts" setup>
import Price from '@/components/price/index.vue'
/** 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,207 @@
<template>
<view title="热门服务">
<block v-for="item1 in lists" :key="item1.id">
<view
class="goods-item mr-[20rpx]"
@click="goPage(`/pages/goods/index?id=${item1.id}`)"
>
<view class="info-column">
<view style="text-align: left">
<view class="info-column-title">
<view class="goods-scribing-title" v-if="item1.scribingPrice > 0">
活动
</view>
<view class="goods-title overflow-ellipsis-line ellipsis-line1 flex-1">
{{ item1.name }}
</view>
</view>
<view class="goods-desc overflow-ellipsis-line ellipsis-line1">
{{ item1.remarks || ' ' }}
</view>
</view>
</view>
<view class="image-column">
<u-image
:src="item1.image"
class="good-img"
width="100%"
height="350"
border-radius="20"
></u-image>
</view>
<view class="price-column" v-if="item1.scribingPrice > 0">
<view>
<view class="price-wrap">
¥
<span class="price">{{ numFilter(item1.price) }}</span>
<view class="subprice line-through">
原价:¥{{ numFilter(item1.scribingPrice) }}
</view>
</view>
<view class="text-base normal mt-1" style="color: #b0aeae">
<text class="font-bold">{{ item1.orderNum }}</text>
人预约过
</view>
</view>
<view class="btn_con">
<view class="btn_scribing bg-primary">立即抢购</view>
</view>
</view>
<view class="price-column" v-else>
<view>
<view class="price-wrap">
¥
<span class="price">{{ numFilter(item1.price) }}</span>
</view>
<view class="text-base normal mt-1" style="color: #b0aeae">
<text class="font-bold">{{ item1.orderNum }}</text>
人预约过
</view>
</view>
<view class="btn_con">
<view class="btn bg-primary">立即抢购</view>
</view>
</view>
</view>
</block>
</view>
</template>
<script lang="ts" setup>
defineProps({
lists: {
type: Array,
default: []
}
})
const goPage = (url: string) => {
uni.navigateTo({ url: url })
}
//
const numFilter = value => {
const realVal = parseFloat(value).toFixed(2)
return realVal
}
</script>
<style lang="scss" scoped>
//
.goods-item {
display: flex;
flex-direction: column;
box-sizing: border-box;
// padding: 20rpx;
border-radius: 17rpx;
background-color: #fff;
//box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
margin: 20rpx;
.image-column {
margin-left: 20rpx;
margin-right: 20rpx;
width: 94%;
.good-img {
width: 100%;
//border-radius: 20rpx; //u-image
border-radius: 25px;
border-radius: 50%;
}
}
.info-column {
display: flex;
flex-direction: column;
border-radius: 10rpx;
justify-content: space-between;
text-align: left;
padding-left: 26rpx;
margin-top: 20rpx;
margin-bottom: 20rpx;
.info-column-title {
display: flex;
align-items: center;
margin-bottom: 10rpx;
.goods-title {
font-size: 1.1rem;
}
.goods-scribing-title {
font-size: 0.79rem;
// box-sizing: border-box;
margin-right: 15rpx;
color: white;
padding: 3rpx; //
background-color: #ff5856;
border-radius: 20rpx;
width: 80rpx;
display: flex;
justify-content: center;
align-items: center;
}
}
.goods-desc {
font-size: 0.775rem;
color: #808695;
}
}
.price-column {
display: flex;
flex-direction: row;
align-items: center;
border-radius: 10rpx;
justify-content: space-between;
margin: 20rpx;
.price-wrap {
display: flex;
flex-direction: row;
align-items: baseline;
color: #ff5856;
.price {
font-size: 1.5rem; //¥
display: flexbox;
}
.subprice {
color: #aeaeae;
margin-left: 20rpx;
}
}
.btn,
.btn_scribing {
margin-top: auto;
display: flex;
justify-content: center;
align-items: center;
color: #fff;
width: 180rpx;
height: 66rpx;
border-radius: 50rpx;
margin-right: 10rpx;
font-size: 0.79rem;
font-weight: bold;
}
.btn {
// background-color: #34afff;
}
.btn_scribing {
// background-color: $u-type-primary;
}
}
}
</style>

View File

@ -0,0 +1,51 @@
<!--
* @Author: micky 1254597151@qq.com
* @Date: 2023-08-14 15:38:40
* @LastEditors: micky 1254597151@qq.com
* @LastEditTime: 2023-11-24 14:36:17
* @FilePath: \housekeeping-uniapp\src\components\la-swiper\la-swiper.vue
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
-->
<template>
<view class="banner h-[560rpx] bg-white translate-y-0">
<swiper
class="swiper h-full"
:indicator-dots="content.length && content.length > 1"
indicator-active-color="#2468f2"
:autoplay="true"
v-if="content.length"
>
<swiper-item v-for="(item, index) in content" :key="index">
<u-image
mode="scaleToFill"
width="100%"
height="100%"
:src="item"
:lazy-load="true"
@click="handlePreviewImage(index)"
/>
</swiper-item>
</swiper>
<u-empty v-else></u-empty>
</view>
</template>
<script setup lang="ts">
const props = defineProps({
content: {
type: Array,
default: () => []
},
styles: {
type: Object,
default: () => ({})
}
})
const handlePreviewImage = (index: number) => {
if (!props.content.length) return
uni.previewImage({
urls: props.content as string[],
current: index
})
}
</script>

View File

@ -0,0 +1,97 @@
<template>
<view>
<u-popup v-model="showPopup" mode="bottom" border-radius="14" :mask-close-able="false">
<view class="h-[1000rpx] p-[40rpx]">
<view class="flex items-center">
<image
class="w-[100rpx] h-[100rpx] rounded"
mode="heightFix"
:src="logo"
></image>
<text class="text-3xl ml-5 font-bold">{{ title }}</text>
</view>
<view class="mt-5 text-muted">
建议使用您的微信头像和昵称以便获得更好的体验
</view>
<view class="mt-[30rpx]">
<form @submit="handleSubmit">
<u-form-item required label="头像" :labelWidth="120">
<view class="flex-1">
<avatar-upload v-model="avatar"></avatar-upload>
</view>
</u-form-item>
<u-form-item required label="昵称" :labelWidth="120">
<input
class="flex-1 h-[60rpx]"
name="nickname"
type="nickname"
placeholder="请输入昵称"
/>
</u-form-item>
<view class="mt-[80rpx]">
<button
class="bg-primary rounded-full text-white text-lg h-[80rpx] leading-[80rpx]"
hover-class="none"
form-type="submit"
>
确定
</button>
</view>
<view class="flex justify-center mt-[60rpx]">
<view class="text-muted" @click="showPopup = false">暂不登录</view>
</view>
</form>
</view>
</view>
</u-popup>
</view>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue'
const props = defineProps({
show: {
type: Boolean,
},
logo: {
type: String,
},
title: {
type: String,
},
})
const emit = defineEmits<{
(event: 'update:show', show: boolean): void
(event: 'update', value: any): void
}>()
const showPopup = computed({
get() {
return props.show
},
set(val) {
emit('update:show', val)
},
})
const avatar = ref()
const handleSubmit = (e: any) => {
const { nickname } = e.detail.value
if (!avatar.value)
return uni.$u.toast({
title: '请添加头像',
})
if (!nickname)
return uni.$u.toast({
title: '请输入昵称',
})
emit('update', {
avatar: avatar.value,
nickname,
})
}
</script>
<style lang="scss" scoped></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.intro
}}</view>
<view class="text-muted text-xs w-full flex justify-between mt-[12rpx]">
<view>{{ item.createTime }}</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.visit }}</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,598 @@
<template>
<view class="flex">
<!-- 联系客服 -->
<!-- <template v-if="contract">
<button
class="Btn mr-2 bg-white text-base text-black leading-[70rpx] h-[70rpx] rounded-full"
@click.stop="handleShowModal"
>
联系客服
</button>
</template> -->
<!-- 取消订单 -->
<template v-if="cancel">
<button
class="Btn mr-2 bg-white text-base text-black leading-[70rpx] h-[70rpx] rounded-full"
@click.stop="handleOrderCancel(orderId)"
>
取消订单
</button>
</template>
<!-- 删除订单 -->
<template v-if="orderStatus == 4 || del">
<button
class="Btn mr-2 bg-white text-base text-black leading-[70rpx] h-[70rpx] rounded-full"
@click.stop="handleOrderDel(orderId)"
>
删除订单
</button>
</template>
<!-- 去评价0-隐藏 1-显示 -->
<!-- @click.stop="goPage('/bundle/pages/evaluate_list/index')" -->
<template v-if="evaluate">
<button
class="Btn evaluate bg-white text-sm text-muted leading-[70rpx] h-[70rpx] rounded-full"
@click.stop="goPage(`/bundle/pages/evaluate_submit/index?id=${orderId}`)"
>
去评价
</button>
</template>
<!-- 联系师傅 -->
<!-- <template
v-if="
staffMobile?.length > 0 &&
!(confirmService || verification) &&
((isDispatch && orderStatus == 1) || orderStatus == 2)
"
>
<button
class="Btn flex-1 mr-2 bg-white text-sm text-black leading-[70rpx] h-[70rpx] rounded-full"
@click.stop="handleContactMobile()"
>
联系师傅
</button>
</template> -->
<!-- 联系客户 -->
<template v-if="mobile && (confirmService || verification)">
<button
class="Btn flex-1 mr-2 bg-white text-sm text-black leading-[70rpx] h-[70rpx] rounded-full"
@click.stop="handleCustomerMobile()"
>
联系客户
</button>
</template>
<!-- 去支付 -->
<template v-if="pay">
<button
class="bg-primary text-lg ml-[20rpx] text-white leading-[70rpx] h-[70rpx] rounded-full"
@click.stop="handlePayment"
>
去支付
</button>
</template>
<!-- 确认服务 -->
<template v-if="confirmService && isCanConfirmService">
<button
class="bg-primary text-lg text-white leading-[70rpx] h-[70rpx] rounded-full"
@click.stop="handleConfirmService(orderId)"
>
开始服务
</button>
</template>
<!-- 还不可以确认服务 -->
<template v-if="confirmService && !isCanConfirmService">
<button
class="BtnDisable text-lg text-[#cecece] leading-[70rpx] h-[70rpx] rounded-full"
@click.stop="handleNotConfirmService(orderId)"
>
开始服务
</button>
</template>
<!-- 核销订单 -->
<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>
<!-- 手动输入核销码MODAL -->
<u-modal
ref="uModalInput"
v-model="showInputCode"
show-cancel-button
confirmColor="#1296DB"
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>
<!-- 已预约用户确认服务 -->
<template v-if="orderStatus == 2">
<button
class="bg-primary text-lg text-white leading-[70rpx] h-[70rpx] rounded-full"
@click.stop="handleUserConfirm"
>
确认服务
</button>
</template>
</view>
<u-popup
v-model="showConfirmPop"
mode="center"
height="500"
border-radius="14"
:mask-close-able="false"
closeable
>
<view class="text-center w-[560rpx] py-[40rpx]">
<view class="w-[160rpx] h-[160rpx] px-[200rpx]">
<u-image width="160" height="160" :src="goodsImage" />
</view>
<view class="mt-[20rpx] truncate">{{ goodsName }}</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>
<u-popup v-model="isHide" mode="center" closeable border-radius="14" v-if="isHide">
<view class="text-center w-[660rpx] py-[40rpx]">
<view class="reason">
<view v-if="isShowScore" class="mb-[20rpx]"></view>
<!-- <view v-if="isShowScore" class="mb-[20rpx]">
上门服务前
<text class="text-error mx-[8rpx]">{{ state.intervalTime }}</text>
分钟内取消订单扣款比例
<text class="text-error mx-[8rpx]">{{ state.value }}</text>
%扣款金额
<text class="text-error mx-[8rpx]">{{ state.deductMoney }}</text>
</view> -->
<u-input
class="textarea"
v-model="reason"
type="textarea"
:maxlength="255"
:trim="true"
:border="true"
placeholder="请输入取消订单原因"
:height="200"
/>
<text class="limit">{{ reason.length }}/255</text>
</view>
<view class="operation-btn">
<button
@click="handleShowModal"
class="Btn flex-1 mr-2 bg-white text-sm text-black leading-[70rpx] h-[70rpx] rounded-full"
>
联系客服
</button>
<button
@click="handleConfirmModal"
class="bg-primary text-lg text-white leading-[70rpx] h-[70rpx] rounded-full"
>
确定
</button>
</view>
</view>
</u-popup>
</template>
<script lang="ts" setup>
import { ref, onUnmounted, onMounted, unref, reactive } from 'vue'
import {
apiOrderCancel,
apiStaffOrderConfirmService,
apiStaffOrderVerification,
apiOrderDel,
apiByteQueryRefund,
apiCancelOrderRule,
apiUserOrderVerification
} from '@/api/order'
import { OrderStatusEnum, PayFromType } from '@/enums/appEnums'
import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia'
import { getClient } from '@/utils/client'
import { isWeixinClient } from '@/utils/client'
import { useToggle } from '@/hooks/useLockFn'
import { toast } from '@/utils/util'
const emit = defineEmits(['refresh', 'backRefresh', 'contact'])
const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore)
const props = withDefaults(
defineProps<{
orderId?: any // ID
orderSn?: any //
cancel?: boolean | number | null //
evaluate?: boolean | number | null //
pay?: boolean | number | null //
confirmService?: boolean | number | null //
verification?: boolean | number | null //
goodsImage?: string | null //
goodsName?: string | null //
order_amount?: number | string | null //
mobile?: number | string | null //
contact?: number | boolean //
staffId?: any
type?: number | null //
orderStatus?: number | string | null // ;0-;1-;2-;3-;4-退;5-退
del?: boolean | number | null //
appointTime?: string //
appointTimeStartStr?: string //
isDispatch?: number | null //
staffMobile?: string | null //
contract: number | null
goodsId?: number | string
}>(),
{
orderId: '',
orderSn: '',
cancel: false,
evaluate: false,
pay: false,
confirmService: false,
verification: false,
goodsImage: '',
goodsName: '',
del: false,
contact: false,
mobile: '',
staffId: '',
type: 2,
orderStatus: '',
appointTime: '',
appointTimeStartStr: '',
isDispatch: 0, // 0= 1= 0
staffMobile: '',
contract: null,
goodsId: ''
}
)
const code = ref<number | string>('') //
const showInputCode = ref<boolean>(false) // () |
//
const goPage = (url: string) => {
uni.navigateTo({ url })
}
const isHide = ref(false)
const reason = ref('')
const isShowScore = ref(false)
const state = reactive({
intervalTime: '',
value: '',
deductMoney: ''
})
/**取消订单-取消 */
const handleCloseModal = () => {
reason.value = ''
}
/**取消订单-确定 */
const handleConfirmModal = async () => {
if (!unref(reason).length) return toast('请输入取消订单原因')
await cancelOrderAction()
isHide.value = false
}
//
const handleOrderCancel = async (id: number | string): Promise<void> => {
/**待支付 */
if (props.orderStatus === OrderStatusEnum.NO_PAYMENT) {
uni.showModal({
title: '温馨提示',
content: '确定取消订单?',
success: async res => {
if (res.cancel) return
await cancelOrderAction()
}
})
return
}
try {
const data = {
orderId: props.orderId,
goodsId: props.goodsId
}
const res = await apiCancelOrderRule(data)
isShowScore.value = !Object.keys(res).length ? false : true
if (isShowScore.value) {
state.intervalTime = res.intervalTime ?? '--'
state.value = res.value ?? '--'
state.deductMoney = res.deductMoney ?? '--'
}
isHide.value = true
} catch (error) {}
}
/**调用取消订单接口 */
async function cancelOrderAction() {
try {
const data = {
id: props.orderId,
userId: userInfo.value.id,
source: getClient(),
refundReason: unref(reason),
goodsId: props.goodsId,
serviceTimeBeforeMinute: state.intervalTime,
deductionRatio: state.value
}
await apiOrderCancel(data)
// #ifdef MP-TOUTIAO
if (props.orderStatus === 0 || props.orderStatus === 1) {
// MP-TOUTIAO
await apiByteQueryRefund({
order_id: props.orderId
})
}
// #endif
uni.$u.toast('操作成功')
} catch (error) {
console.log('错误信息:', error, props.orderId)
}
emit('refresh')
}
//
const handleOrderDel = async (id: number | string): Promise<void> => {
const modelRes = await uni.showModal({
title: '温馨提示',
content: '确认删除该订单吗?'
})
if (modelRes.cancel) return
try {
await apiOrderDel({ id })
uni.$u.toast('操作成功')
} catch (error) {
console.log('错误信息:', error, props.orderId)
}
emit('refresh')
}
//
const handlePayment = () => {
const param = {
order_id: props.orderId,
sn: props.orderSn,
from: PayFromType.ORDER,
order_amount: props.order_amount
}
uni.$emit('payment', param)
}
//
const handleContactMobile = () => {
uni.makePhoneCall({
phoneNumber: props.staffMobile //
})
}
//
const handleCustomerMobile = () => {
uni.makePhoneCall({
phoneNumber: props.mobile //
})
}
//
const handleConfirmService = async (id: number | string): Promise<void> => {
const modelRes = await uni.showModal({
title: '温馨提示',
content: '确认开始服务该订单吗?'
})
if (modelRes.cancel) return
await apiStaffOrderConfirmService({ id })
uni.$u.toast('操作成功')
emit('refresh')
}
const handleNotConfirmService = async (id: number | string): Promise<void> => {
const modelRes = await uni.showModal({
title: '温馨提示',
content: '预约前2小时才可以开始服务'
})
}
//
const handleScanQRCode = () => {
// #ifdef H5
if (!isWeixinClient()) return uni.$u.toast('h5暂不支持扫码')
showInputCode.value = true
// #endif
// #ifdef MP
wx.scanCode({
success: data => {
code.value = data.result
showConfirmPop.value = true
},
fail: err => {
console.log(err)
}
})
// #endif
}
//
const showConfirmPop = ref(false)
const confirmVerification = async () => {
try {
await apiStaffOrderVerification({ id: props.orderId, code: code.value })
code.value = ''
uni.$u.toast('订单核销成功')
showConfirmPop.value = false
emit('refresh')
} catch (error) {
uni.$u.toast(error)
}
}
//
const inputOrderCode = async (): Promise<void> => {
if (code.value === '') return uni.$u.toast('请输入核销码')
try {
await apiStaffOrderVerification({ id: props.orderId, code: code.value })
uni.$u.toast('订单核销成功')
emit('refresh')
} catch (error) {
console.log(error)
}
code.value = ''
}
//
const handleCancel = () => {
code.value = ''
emit('refresh')
}
//
const handleUserConfirm = async () => {
const modelRes = await uni.showModal({
title: '温馨提示',
content: '确认核销该订单吗?'
})
if (modelRes.cancel) return
await apiUserOrderVerification({ id: props.orderId })
uni.$u.toast('订单核销成功')
emit('refresh')
}
// 2
const isCanConfirmService = ref(false) // 2
const checkconfirmService = () => {
if (!props.confirmService) {
return
}
if (isCanConfirmService.value) {
return
}
const date = new Date(props.appointTime + ' ' + props.appointTimeStartStr)
const now = new Date()
const isBefore = now.getTime() < date.getTime()
const isAfter = date.getTime() < now.getTime()
const minutesBeforeAndAfter = Math.abs(now.getTime() - date.getTime()) / 1000 / 60 //
console.log(
'当前时间',
new Date(Date.parse(new Date().toString())),
props.appointTime + ' ' + props.appointTimeStartStr,
isBefore,
isAfter,
minutesBeforeAndAfter
)
//2
if (isBefore && minutesBeforeAndAfter < 60 * 2) {
console.log('即前2小时内')
isCanConfirmService.value = true
}
//10
// if (isAfter && minutesBeforeAndAfter < 60 * 10) {
if (isAfter) {
isCanConfirmService.value = true
}
// - 线
isCanConfirmService.value = true
}
/**联系客服 */
const handleShowModal = () => {
emit('contact')
}
const _timer = ref(null) //
onMounted(() => {
isCanConfirmService.value = false
if (props.confirmService) {
checkconfirmService()
_timer.value = setInterval(() => {
checkconfirmService()
}, 10000)
}
})
//
onUnmounted(() => {
clearInterval(_timer.value)
if (props.confirmService) {
console.log('--- 退出定时器 --', new Date(Date.parse(new Date().toString())))
}
})
</script>
<style lang="scss" scoped>
.reason {
padding: 30rpx;
position: relative;
font-size: 28rpx;
.limit {
position: absolute;
right: 50rpx;
bottom: 40rpx;
font-size: 28rpx;
}
}
.operation-btn {
display: flex;
gap: 20rpx;
padding: 0 30rpx;
> button {
flex: 1;
}
}
.Btn {
border: 1px solid rgba(187, 187, 187, 1);
background-color: rgba(255, 255, 255, 1);
color: rgba(16, 16, 16, 1);
text-align: center;
}
.evaluate {
border: 1px solid $blue5;
color: $blue5;
}
.BtnDisable {
border: 1px solid rgba(187, 187, 187, 1);
background-color: rgba(255, 255, 255, 1);
color: rgb(88, 88, 88);
text-align: center;
}
</style>

View File

@ -0,0 +1,62 @@
<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" 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,49 @@
<template>
<view
@click="onSelect(item.id)"
v-for="(item, index) in lists"
:key="index"
class="px-[20rpx] flex justify-between items-center h-[120rpx] mb-[2rpx] bg-white"
>
<image class="w-[48rpx] h-[48rpx]" :src="item.icon" />
<view class="flex-1 mx-[20rpx]">
<view class="text-base text-main">{{ item.pay_way_name }}</view>
<view class="text-muted text-xs mt-[6rpx]" v-if="item.id === 1"
>可用余额{{ user_money }}</view
>
</view>
<image
class="w-[34rpx] h-[34rpx]"
v-show="modelValue == item.id"
src="@/static/images/icon/icon_select.png"
/>
<image
class="w-[34rpx] h-[34rpx]"
v-show="modelValue != item.id"
src="@/static/images/icon/icon_null_select.png"
/>
</view>
</template>
<script lang="ts" setup>
const emit = defineEmits<{
(event: 'update:modelValue', value: number): void
}>()
const props = withDefaults(
defineProps<{
lists: any
user_money: string | number
modelValue: any
}>(),
{
lists: [],
user_money: 0,
modelValue: 1
}
)
console.log('props', props.lists)
const onSelect = (way: number) => {
emit('update:modelValue', way)
}
</script>

View File

@ -0,0 +1,525 @@
<template>
<u-popup
v-model="showPay"
mode="bottom"
safe-area-inset-bottom
:mask-close-able="false"
border-radius="14"
closeable
@close="handleClose"
>
<view class="h-[900rpx]">
<page-status :status="popupStatus" :fixed="false">
<template #error>
<u-empty text="订单信息错误,无法查询到订单信息" mode="order"></u-empty>
</template>
<template #default>
<view class="flex flex-col w-full h-full payment">
<view class="header py-[50rpx] flex flex-col items-center">
<price
:content="props.order_amount"
mainSize="44rpx"
minorSize="40rpx"
fontWeight="500"
color="#333"
></price>
<template v-if="payData.cancelTime">
<view
class="count-down flex items-center rounded-[30px] text-xs"
v-if="timeStamp >= 0"
>
<text>支付剩余时间</text>
<u-count-down
:timestamp="timeStamp"
format="HH:mm:ss"
:font-size="22"
:separator-size="26"
@end="payData.cancelTime = 0"
/>
</view>
</template>
</view>
<view class="main flex-1 mx-[20rpx]">
<view>
<view class="payway-lists">
<u-radio-group v-model="payWay" class="w-full">
<view
class="p-[20rpx] flex items-center w-full payway-item"
v-for="(item, index) in payData.payWayList"
:key="index"
@click="selectPayWay(item.payWay)"
>
<u-image
class="flex-none"
:src="item.payImage"
width="48"
height="48"
></u-image>
<view class="mx-[16rpx] flex-1">
<view class="payway-item--name flex-1">
{{ item.payName }}
</view>
<view
class="text-muted text-xs"
v-if="item.payWay === 2"
>
可用余额{{ payData.money }}
</view>
</view>
<u-radio
class="mr-[-20rpx]"
:name="item.payWay"
active-color="#5ac725"
></u-radio>
</view>
</u-radio-group>
</view>
</view>
</view>
<view class="submit-btn p-[20rpx]">
<u-button
@click="handlePay"
shape="circle"
type="primary"
:loading="isLock"
:custom-style="{ background: '#2468f2' }"
>
立即支付
</u-button>
</view>
</view>
</template>
</page-status>
</view>
</u-popup>
<u-popup
class="pay-popup"
v-model="showCheckPay"
round
mode="center"
borderRadius="10"
:maskCloseAble="false"
@close="handleClose"
>
<view class="content bg-white w-[560rpx] p-[40rpx]">
<view class="text-2xl font-medium text-center">支付确认</view>
<view class="pt-[30rpx] pb-[40rpx]">
<view>请在微信内完成支付如果您已支付成功请点击`已完成支付`按钮</view>
</view>
<view class="flex">
<view class="flex-1 mr-[20rpx]">
<button
class="Btn bg-white text-xs text-black leading-[60rpx] h-[60rpx] rounded-full"
@click="queryPayResult(false)"
>
重新支付
</button>
</view>
<view class="flex-1">
<button
class="bg-primary text-xs ml-[20rpx] text-white leading-[60rpx] h-[60rpx] rounded-full"
@click="queryPayResult()"
>
已完成支付
</button>
</view>
</view>
</view>
</u-popup>
</template>
<script lang="ts" setup>
import qs from 'qs'
import { pay, PayWayEnum } from '@/utils/pay/index'
import { computed, ref, watch, watchEffect } from 'vue'
import { useLockFn } from '@/hooks/useLockFn'
import { series } from '@/utils/util'
import { ClientEnum, PageStatusEnum, PayStatusEnum } from '@/enums/appEnums'
import { useUserStore } from '@/stores/user'
import { client } from '@/utils/client'
import { getClient } from '@/utils/client'
import {
apiPayPayWay,
apiPayPrepay,
apiQueryOrder,
apiH5PayPrepay,
apiBytePayPrepay,
apiByteQueryOrder,
apiQueryRecharge
} from '@/api/app'
import price from '@/components/price/price-v2.vue'
import { apiOrderClose } from '@/api/order'
/*
页面参数 orderId订单idfrom订单来源
*/
const props = defineProps({
show: {
type: Boolean,
required: true
},
showCheck: {
type: Boolean
},
// id
orderId: {
type: Number,
required: true
},
sn: {
type: [Number, String],
required: true
},
order_amount: {
type: String,
required: true
},
//
from: {
default: 'order',
type: String,
required: true
},
//h5
redirect: {
type: String
}
})
const emit = defineEmits(['update:showCheck', 'update:show', 'close', 'success', 'fail'])
const payWay = ref()
const popupStatus = ref(PageStatusEnum.LOADING)
const payData = ref<any>({
payWayList: [],
cancelTime: null,
money: 0
})
const showCheckPay = computed({
get() {
return props.showCheck
},
set(value) {
emit('update:showCheck', value)
}
})
const showPay = computed({
get() {
return props.show
},
set(value) {
emit('update:show', value)
}
})
const handleClose = () => {
showPay.value = false
emit('close')
}
const getPayData = async () => {
popupStatus.value = PageStatusEnum.LOADING
try {
// #ifdef MP-TOUTIAO
payData.value.payWayList = [
{
id: 2,
isDefault: 2,
payId: 2,
payImage:
'http://nvqryx.natappfree.cc/api/uploads/image/20230110/8608cf8d-555d-415a-9100-c87dac00fe8e.png',
payName: '抖音支付',
payWay: 2,
scene: 2,
status: 2
}
]
popupStatus.value = PageStatusEnum.NORMAL
payWay.value = 2
// #endif
payData.value = await apiPayPayWay({
scene: getClient(),
orderId: props.orderId,
type: props.from
})
popupStatus.value = PageStatusEnum.NORMAL
const checkPay =
payData.value.payWayList.find((item: any) => item.isDefault) ||
payData.value.payWayList[0]
payWay.value = checkPay?.payWay
} catch (error) {
uni.$u.toast(error)
popupStatus.value = PageStatusEnum.ERROR
}
}
const { bindMnp } = useUserStore().userInfo as { bindMnp: boolean }
const selectPayWay = (pay: number) => {
payWay.value = pay
}
const payment = (() => {
//
const checkIsBindWx = async () => {
if (
[ClientEnum.OA_WEIXIN, ClientEnum.MP_WEIXIN].includes(client) &&
!bindMnp &&
payWay.value == PayWayEnum.WECHAT
) {
showPay.value = false
const res: any = await uni.showModal({
title: '温馨提示',
content: '当前账号未绑定微信,无法完成支付',
confirmText: '去绑定'
})
if (res.confirm) {
uni.navigateTo({
url: '/pages/user_set/user_set'
})
}
return Promise.reject()
}
}
//
const prepayTask = async () => {
uni.showLoading({
title: '正在支付中'
})
let payChannel = ''
switch (client) {
case 1:
payChannel = 'mp_channel'
break
case 2:
payChannel = 'oa_channel'
break
case 3:
payChannel = 'h5_channel'
break
default:
payChannel = 'mp_channel'
break
}
try {
const params = {
orderId: props.orderId,
payChannel,
scene: props.from,
payWay: payWay.value // 1- 2-
}
// or
if (payChannel === 'mp_channel' || payChannel === 'oa_channel') {
const res = await apiPayPrepay({ ...params })
return { pay_way: payWay.value, config: res }
} else if (payChannel === 'h5_channel') {
const res = await apiH5PayPrepay({ ...params })
const redirect = `&redirect_url=${encodeURIComponent(
window.location.href + '&checkPay=' + true + '&order_id=' + props.orderId
)}`
return { pay_way: payWay.value, config: res.url + redirect }
}
} catch (err) {
return uni.$u.toast(err)
}
}
//
const payTask = async (data: any) => {
try {
const res = await pay.payment(data.pay_way, data.config)
console.log('resPayTask: ' + res)
return res
} catch (error) {
return Promise.reject(error)
}
}
const ttpay = async () => {}
// #ifdef MP-TOUTIAO
return series(ttpay)
// #endif
// #ifndef MP-TOUTIAO
return series(checkIsBindWx, prepayTask, payTask)
// #endif
})()
const { isLock, lockFn: handlePay } = useLockFn(async () => {
try {
// #ifdef MP-TOUTIAO
const ttpay = async () => {
const params = { order_id: props.orderId, pay_channel: 'byte_channel' }
const tempRes = await apiBytePayPrepay(params)
// debugger
console.log(tempRes.data)
console.log(tempRes?.data?.order_id)
console.log(tempRes?.data?.order_token)
let payResult: any = ''
try {
tt.pay({
orderInfo: {
order_id: tempRes.data.order_id,
order_token: tempRes.data.order_token
},
service: 5,
success(res) {
payResult = res
console.log('收到成功回调啦', res)
if (res.code === 0) {
console.log('支付成功啦~~')
const queryResult = apiByteQueryOrder({ order_id: props.orderId })
console.log(queryResult)
emit('success')
return 'success'
} else {
uni.$u.toast(res.msg)
emit('fail')
return 'fail'
}
},
fail(res) {
payResult = res
console.log('支付失败了', res)
emit('fail')
}
})
// debugger
// setTimeout(()=> {
// const res = {
// code: 200,
// data: [],
// msg: ""
// }
// // return Promise.resolve('success')
// emit('success')
// return "success"
// }, 1000)
} catch (e) {
console.log('ttpay catch:', e)
// Promise.reject('fail')
return 'fail'
}
}
// debugger
const res: PayStatusEnum = ttpay()
console.log('ttpay:', res)
// #endif
// #ifndef MP-TOUTIAO
const res: PayStatusEnum = await payment()
console.log('支付结果状态:', res)
handlePayResult(res)
// #endif
uni.hideLoading()
} catch (error) {
uni.hideLoading()
console.log(error)
}
})
const handlePayResult = async (status: PayStatusEnum) => {
await queryPayStatus()
switch (status) {
case PayStatusEnum.SUCCESS:
emit('success')
break
case PayStatusEnum.PENDING:
showCheckPay.value = true
break
case PayStatusEnum.FAIL:
emit('fail')
break
}
}
const queryPayResult = async (confirm = true) => {
const res = await queryPayStatus()
if (res.payStatus === 0) {
if (confirm == true) {
uni.$u.toast('您的订单还未支付,请重新支付')
}
showPay.value = true
// handlePayResult(PayStatusEnum.FAIL)
} else {
if (confirm == false) {
uni.$u.toast('您的订单已经支付,请勿重新支付')
}
handlePayResult(PayStatusEnum.SUCCESS)
}
showCheckPay.value = false
}
const queryPayStatus = async () => {
if (props.from === 'order' && payWay.value !== 2) {
const res = await apiQueryOrder({ id: props.orderId })
return res
} else if (props.from === 'recharge') {
await apiQueryRecharge({ id: props.orderId })
}
}
const timeStamp = ref<number | null>(0)
watchEffect(() => {
//
const endTimestamp = payData.value.cancelTime
const startTimestamp = new Date().getTime() / 1000
timeStamp.value = (endTimestamp - startTimestamp) * 1000
if (timeStamp.value === 0) orderClose()
})
//
const orderClose = async () => {
await apiOrderClose({
outTradeNo: props.orderId
})
}
watch(
() => props.show,
async value => {
if (value) {
if (!props.orderId) {
uni.$u.toast('订单信息错误,无法查询到订单信息')
return
}
getPayData()
}
},
{
immediate: true
}
)
</script>
<style lang="scss" scoped>
.payway-lists {
.payway-item {
border-bottom: 1px solid;
@apply border-page;
}
}
.Btn {
border: 1px solid rgba(187, 187, 187, 1);
background-color: rgba(255, 255, 255, 1);
color: rgba(16, 16, 16, 1);
}
.count-down {
color: #666666;
padding: 10rpx 30rpx 10rpx 30rpx;
background-color: #ffffff;
}
:deep(.u-btn--primary) {
background-color: $blue5 !important;
}
</style>

View File

@ -0,0 +1,38 @@
<template>
<view class="primary">
<text class="text-lg font-medium text">{{ price }}</text>
<text class="text-xs text">{{ desc }}</text>
<text v-if="scribingPrice" class="text-xs line-through ml-2 text-muted">
{{ scribingPrice }}{{ desc }}
</text>
</view>
</template>
<script setup lang="ts">
import { ref, withDefaults } from 'vue'
/** Props Start **/
const props = withDefaults(
defineProps<{
price?: string | number //
desc?: string //
scribingPrice?: string | number // 线
}>(),
{
price: '',
desc: '',
scribingPrice: ''
}
)
/** Props End **/
/** Methods Start **/
/** Methods End **/
</script>
<style lang="scss" scoped>
.text {
color: rgba(248, 113, 113);
}
</style>

View File

@ -0,0 +1,155 @@
<template>
<view class="price-container">
<view
:class="['price-wrap', { 'price-wrap--disabled': lineThrough }]"
:style="{ color: color }"
>
<!-- Prefix -->
<view class="fix-pre" :style="{ fontSize: minorSize }">
<slot name="prefix">{{ prefix }}</slot>
</view>
<!-- Content -->
<view :style="{ 'font-weight': fontWeight }">
<!-- Integer -->
<text :style="{ fontSize: mainSize }">{{ integer }}</text>
<!-- Decimals -->
<text :style="{ fontSize: minorSize }">{{ decimals }}</text>
</view>
<!-- Suffix -->
<view class="fix-suf" :style="{ fontSize: minorSize }">
<slot name="suffix">{{ suffix }}</slot>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
/**
* @description 价格展示适用于有前后缀小数样式不一
* @property {String|Number} content 价格 (必填项)
* @property {Number} prec 小数位 (默认: 2)
* @property {Boolean} autoPrec 自动小数位以prec为最大小数位 (默认: true)
* @property {String} color 颜色 (默认: 'unset')
* @property {String} mainSize 主要内容字体大小 (默认: 46rpx)
* @property {String} minorSize 主要内容字体大小 (默认: 32rpx)
* @property {Boolean} lineThrough 贯穿线 (默认: false)
* @property {String|Number} fontWeight 字重 (默认: normal)
* @property {String} prefix 前缀 (默认: )
* @property {String} suffix 后缀
* @example <price content="100" suffix="\/元" />
*/
import { ref, computed } from 'vue'
const props = withDefaults(
defineProps<{
content: string | number //
prec?: number //
autoPrec?: boolean //
color?: string //
mainSize?: string //
minorSize?: string //
lineThrough?: boolean // 穿线
fontWeight?: string //
prefix?: string //
suffix?: string //
}>(),
{
content: '',
prec: 2,
autoPrec: true,
color: '#FF2C3C',
mainSize: '36rpx',
minorSize: '28rpx',
lineThrough: false,
fontWeight: 'normal',
prefix: '¥',
suffix: ''
}
)
/**
* @description 格式化输出价格
* @param { string } price 价格
* @param { string } take 小数点操作
* @param { string } prec 小数位补
*/
const formatPrice = ({
price,
take = 'all',
prec = undefined
}: {
price: string | number
take: string
prec?: number
}) => {
let [integer, decimals = ''] = (price + '').split('.')
//
if (prec !== undefined) {
const LEN = decimals.length
for (let i = prec - LEN; i > 0; --i) decimals += '0'
decimals = decimals.substr(0, prec)
}
switch (take) {
case 'int':
return integer
case 'dec':
return decimals
case 'all':
return integer + '.' + decimals
}
}
/**
* @description 金额主体部分
*/
const integer = computed(() => {
return formatPrice({
price: props.content,
take: 'int'
})
})
/**
* @description 金额小数部分
*/
const decimals = computed(() => {
let decimals: any = formatPrice({
price: props.content,
take: 'dec',
prec: props.prec
})
// .10||.20||.30
decimals = decimals % 10 == 0 ? decimals.substr(0, decimals.length - 1) : decimals
return props.autoPrec ? (decimals * 1 ? '.' + decimals : '') : props.prec ? '.' + decimals : ''
})
</script>
<style lang="scss" scoped>
.price-container {
display: inline-block;
}
.price-wrap {
display: flex;
align-items: baseline;
&--disabled {
position: relative;
&::before {
position: absolute;
left: 0;
top: 50%;
right: 0;
transform: translateY(-50%);
display: block;
content: '';
height: 0.05em;
background-color: currentColor;
}
}
}
</style>

View File

@ -0,0 +1,231 @@
<template xlang="wxml" minapp="mpvue">
<view class="tki-qrcode">
<!-- #ifndef MP-ALIPAY -->
<canvas
class="tki-qrcode-canvas"
:canvas-id="cid"
:style="{ width: cpSize + 'px', height: cpSize + 'px' }"
/>
<!-- #endif -->
<!-- #ifdef MP-ALIPAY -->
<canvas :id="cid" :width="cpSize" :height="cpSize" class="tki-qrcode-canvas" />
<!-- #endif -->
<image
v-show="show"
:src="result"
:style="{ width: cpSize + 'px', height: cpSize + 'px' }"
/>
</view>
</template>
<script>
import QRCode from './qrcode.js'
let qrcode
export default {
name: 'tki-qrcode',
props: {
cid: {
type: String,
default: 'tki-qrcode-canvas'
},
size: {
type: Number,
default: 200
},
unit: {
type: String,
default: 'upx'
},
show: {
type: Boolean,
default: true
},
val: {
type: String,
default: 'asfsdfsdf'
},
background: {
type: String,
default: '#ffffff'
},
foreground: {
type: String,
default: '#000000'
},
pdground: {
type: String,
default: '#000000'
},
icon: {
type: String,
default: ''
},
iconSize: {
type: Number,
default: 40
},
lv: {
type: Number,
default: 3
},
onval: {
type: Boolean,
default: false
},
loadMake: {
type: Boolean,
default: false
},
usingComponents: {
type: Boolean,
default: true
},
showLoading: {
type: Boolean,
default: true
},
loadingText: {
type: String,
default: '二维码生成中'
}
},
data() {
return {
result: ''
}
},
methods: {
_makeCode() {
const that = this
if (!this._empty(this.val)) {
try {
qrcode = new QRCode({
context: that, //
canvasId: that.cid, // canvas-id
usingComponents: that.usingComponents, //
showLoading: that.showLoading, // loading
loadingText: that.loadingText, // loading
text: that.val, //
size: that.cpSize, //
background: that.background, //
foreground: that.foreground, //
pdground: that.pdground, //
correctLevel: that.lv, //
image: that.icon, //
imageSize: that.iconSize, //
cbResult: function (res) {
//
that._result(res)
}
})
} catch (e) {
console.log(e)
//TODO handle the exception
}
} else {
uni.showToast({
title: '二维码内容不能为空',
icon: 'none',
duration: 2000
})
}
},
_clearCode() {
this._result('')
qrcode.clear()
},
_saveCode() {
const that = this
if (this.result != '') {
uni.saveImageToPhotosAlbum({
filePath: that.result,
success: function () {
uni.showToast({
title: '二维码保存成功',
icon: 'success',
duration: 2000
})
}
})
}
},
_result(res) {
this.result = res
this.$emit('result', res)
},
_empty(v) {
let tp = typeof v,
rt = false
if (tp == 'number' && String(v) == '') {
rt = true
} else if (tp == 'undefined') {
rt = true
} else if (tp == 'object') {
if (JSON.stringify(v) == '{}' || JSON.stringify(v) == '[]' || v == null) rt = true
} else if (tp == 'string') {
if (v == '' || v == 'undefined' || v == 'null' || v == '{}' || v == '[]') rt = true
} else if (tp == 'function') {
rt = false
}
return rt
}
},
watch: {
size: {
handler(n, o) {
console.log(n, '为什么!!!!!!!!')
if (n != o && !this._empty(n)) {
this.cSize = n
if (!this._empty(this.val)) {
setTimeout(() => {
this._makeCode()
}, 100)
}
}
},
immediate: true
},
val(n, o) {
console.log(n, '为什么!!!!!!!!')
if (this.onval) {
if (n != o && !this._empty(n)) {
setTimeout(() => {
this._makeCode()
}, 0)
}
}
}
},
computed: {
cpSize() {
if (this.unit == 'upx') {
return uni.upx2px(this.size)
} else {
return this.size
}
}
},
mounted: function () {
if (this.loadMake) {
if (!this._empty(this.val)) {
setTimeout(() => {
this._makeCode()
}, 0)
}
}
}
}
</script>
<style>
.tki-qrcode {
position: relative;
}
.tki-qrcode-canvas {
position: fixed;
top: -99999upx;
left: -99999upx;
z-index: -99999;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,85 @@
<template>
<view
:class="{ active, inactive: !active, tab: true }"
:style="shouldShow ? '' : 'display: none;'"
>
<slot v-if="shouldRender"></slot>
</view>
</template>
<script lang="ts" setup>
import { ref, inject, watch, computed, onMounted, getCurrentInstance } from 'vue'
const props = withDefaults(
defineProps<{
dot?: boolean | string
name?: boolean | string
info?: any
}>(),
{
dot: false,
name: ''
}
)
const active = ref<boolean>(false)
const shouldShow = ref<boolean>(false)
const shouldRender = ref<boolean>(false)
const inited = ref(undefined)
// const updateTabs: any = inject('updateTabs')
// const handleChange: any = inject('handleChange')
const updateRender = (value: any) => {
inited.value = inited.value || value
active.value = value
shouldRender.value = inited.value!
shouldShow.value = value
}
// const update = () => {
// if (updateTabs) {
// updateTabs()
// }
// }
const instance = getCurrentInstance()
uni.$emit('handleChange', { event: instance?.props, fun: updateRender })
onMounted(() => {
// update()
uni.$emit('updateTabs')
})
const changeData = computed(() => {
const { dot, info } = props
return {
dot,
info
}
})
watch(
() => changeData.value,
() => {
uni.$emit('updateTabs')
// update()
}
)
watch(
() => props.name,
val => {
uni.$emit('updateTabs')
// update()
}
)
</script>
<style>
.tab.active {
height: auto;
}
.tab.inactive {
height: 0;
overflow: visible;
}
</style>

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