This commit is contained in:
2026-03-06 13:41:22 +08:00
commit f39c6a705f
394 changed files with 159599 additions and 0 deletions

23
.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
.DS_Store
node_modules/
unpackage/
dist/
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.project
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw*

24
README.md Normal file
View File

@ -0,0 +1,24 @@
# pet-home
## node
node v14+
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

81
babel.config.js Normal file
View File

@ -0,0 +1,81 @@
const webpack = require('webpack')
const plugins = []
if (process.env.UNI_OPT_TREESHAKINGNG) {
plugins.push(require('@dcloudio/vue-cli-plugin-uni-optimize/packages/babel-plugin-uni-api/index.js'))
}
if (
(
process.env.UNI_PLATFORM === 'app-plus' &&
process.env.UNI_USING_V8
) ||
(
process.env.UNI_PLATFORM === 'h5' &&
process.env.UNI_H5_BROWSER === 'builtin'
)
) {
const path = require('path')
const isWin = /^win/.test(process.platform)
const normalizePath = path => (isWin ? path.replace(/\\/g, '/') : path)
const input = normalizePath(process.env.UNI_INPUT_DIR)
try {
plugins.push([
require('@dcloudio/vue-cli-plugin-hbuilderx/packages/babel-plugin-console'),
{
file (file) {
file = normalizePath(file)
if (file.indexOf(input) === 0) {
return path.relative(input, file)
}
return false
}
}
])
} catch (e) { }
}
process.UNI_LIBRARIES = process.UNI_LIBRARIES || ['@dcloudio/uni-ui']
process.UNI_LIBRARIES.forEach(libraryName => {
plugins.push([
'import',
{
'libraryName': libraryName,
'customName': (name) => {
return `${libraryName}/lib/${name}/${name}`
}
}
])
})
if (process.env.UNI_PLATFORM !== 'h5') {
plugins.push('@babel/plugin-transform-runtime')
}
const config = {
presets: [
[
'@vue/app',
{
modules: webpack.version[0] > 4 ? 'auto' : 'commonjs',
useBuiltIns: process.env.UNI_PLATFORM === 'h5' ? 'usage' : 'entry'
}
]
],
plugins
}
const UNI_H5_TEST = '**/@dcloudio/uni-h5/dist/index.umd.min.js'
if (process.env.NODE_ENV === 'production') {
config.overrides = [{
test: UNI_H5_TEST,
compact: true,
}]
} else {
config.ignore = [UNI_H5_TEST]
}
module.exports = config

9
jsconfig.json Normal file
View File

@ -0,0 +1,9 @@
{
"compilerOptions": {
"types": [
"@dcloudio/types",
"miniprogram-api-typings",
"mini-types"
]
}
}

42359
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

117
package.json Normal file
View File

@ -0,0 +1,117 @@
{
"name": "pet-home",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "npm run dev:h5",
"build": "npm run build:h5",
"build:app-plus": "cross-env NODE_ENV=production UNI_PLATFORM=app-plus vue-cli-service uni-build",
"build:custom": "cross-env NODE_ENV=production uniapp-cli custom",
"build:h5": "cross-env NODE_ENV=production UNI_PLATFORM=h5 vue-cli-service uni-build",
"build:mp-360": "cross-env NODE_ENV=production UNI_PLATFORM=mp-360 vue-cli-service uni-build",
"build:mp-alipay": "cross-env NODE_ENV=production UNI_PLATFORM=mp-alipay vue-cli-service uni-build",
"build:mp-baidu": "cross-env NODE_ENV=production UNI_PLATFORM=mp-baidu vue-cli-service uni-build",
"build:mp-jd": "cross-env NODE_ENV=production UNI_PLATFORM=mp-jd vue-cli-service uni-build",
"build:mp-kuaishou": "cross-env NODE_ENV=production UNI_PLATFORM=mp-kuaishou vue-cli-service uni-build",
"build:mp-lark": "cross-env NODE_ENV=production UNI_PLATFORM=mp-lark vue-cli-service uni-build",
"build:mp-qq": "cross-env NODE_ENV=production UNI_PLATFORM=mp-qq vue-cli-service uni-build",
"build:mp-toutiao": "cross-env NODE_ENV=production UNI_PLATFORM=mp-toutiao vue-cli-service uni-build",
"build:mp-weixin": "cross-env NODE_ENV=production UNI_PLATFORM=mp-weixin vue-cli-service uni-build",
"build:mp-xhs": "cross-env NODE_ENV=production UNI_PLATFORM=mp-xhs vue-cli-service uni-build",
"build:quickapp-native": "cross-env NODE_ENV=production UNI_PLATFORM=quickapp-native vue-cli-service uni-build",
"build:quickapp-webview": "cross-env NODE_ENV=production UNI_PLATFORM=quickapp-webview vue-cli-service uni-build",
"build:quickapp-webview-huawei": "cross-env NODE_ENV=production UNI_PLATFORM=quickapp-webview-huawei vue-cli-service uni-build",
"build:quickapp-webview-union": "cross-env NODE_ENV=production UNI_PLATFORM=quickapp-webview-union vue-cli-service uni-build",
"dev:app-plus": "cross-env NODE_ENV=development UNI_PLATFORM=app-plus vue-cli-service uni-build --watch",
"dev:custom": "cross-env NODE_ENV=development uniapp-cli custom",
"dev:h5": "cross-env NODE_ENV=development UNI_PLATFORM=h5 vue-cli-service uni-serve",
"dev:mp-360": "cross-env NODE_ENV=development UNI_PLATFORM=mp-360 vue-cli-service uni-build --watch",
"dev:mp-alipay": "cross-env NODE_ENV=development UNI_PLATFORM=mp-alipay vue-cli-service uni-build --watch",
"dev:mp-baidu": "cross-env NODE_ENV=development UNI_PLATFORM=mp-baidu vue-cli-service uni-build --watch",
"dev:mp-jd": "cross-env NODE_ENV=development UNI_PLATFORM=mp-jd vue-cli-service uni-build --watch",
"dev:mp-kuaishou": "cross-env NODE_ENV=development UNI_PLATFORM=mp-kuaishou vue-cli-service uni-build --watch",
"dev:mp-lark": "cross-env NODE_ENV=development UNI_PLATFORM=mp-lark vue-cli-service uni-build --watch",
"dev:mp-qq": "cross-env NODE_ENV=development UNI_PLATFORM=mp-qq vue-cli-service uni-build --watch",
"dev:mp-toutiao": "cross-env NODE_ENV=development UNI_PLATFORM=mp-toutiao vue-cli-service uni-build --watch",
"dev:mp-weixin": "cross-env NODE_ENV=development UNI_PLATFORM=mp-weixin vue-cli-service uni-build --watch",
"dev:mp-xhs": "cross-env NODE_ENV=development UNI_PLATFORM=mp-xhs vue-cli-service uni-build --watch",
"dev:quickapp-native": "cross-env NODE_ENV=development UNI_PLATFORM=quickapp-native vue-cli-service uni-build --watch",
"dev:quickapp-webview": "cross-env NODE_ENV=development UNI_PLATFORM=quickapp-webview vue-cli-service uni-build --watch",
"dev:quickapp-webview-huawei": "cross-env NODE_ENV=development UNI_PLATFORM=quickapp-webview-huawei vue-cli-service uni-build --watch",
"dev:quickapp-webview-union": "cross-env NODE_ENV=development UNI_PLATFORM=quickapp-webview-union vue-cli-service uni-build --watch",
"info": "node node_modules/@dcloudio/vue-cli-plugin-uni/commands/info.js",
"serve:quickapp-native": "node node_modules/@dcloudio/uni-quickapp-native/bin/serve.js",
"test:android": "cross-env UNI_PLATFORM=app-plus UNI_OS_NAME=android jest -i",
"test:h5": "cross-env UNI_PLATFORM=h5 jest -i",
"test:ios": "cross-env UNI_PLATFORM=app-plus UNI_OS_NAME=ios jest -i",
"test:mp-baidu": "cross-env UNI_PLATFORM=mp-baidu jest -i",
"test:mp-weixin": "cross-env UNI_PLATFORM=mp-weixin jest -i"
},
"dependencies": {
"@dcloudio/uni-app": "^2.0.2-3080720230703001",
"@dcloudio/uni-app-plus": "^2.0.2-3080720230703001",
"@dcloudio/uni-h5": "^2.0.2-3080720230703001",
"@dcloudio/uni-i18n": "^2.0.2-3080720230703001",
"@dcloudio/uni-mp-360": "^2.0.2-3080720230703001",
"@dcloudio/uni-mp-alipay": "^2.0.2-3080720230703001",
"@dcloudio/uni-mp-baidu": "^2.0.2-3080720230703001",
"@dcloudio/uni-mp-jd": "^2.0.2-3080720230703001",
"@dcloudio/uni-mp-kuaishou": "^2.0.2-3080720230703001",
"@dcloudio/uni-mp-lark": "^2.0.2-3080720230703001",
"@dcloudio/uni-mp-qq": "^2.0.2-3080720230703001",
"@dcloudio/uni-mp-toutiao": "^2.0.2-3080720230703001",
"@dcloudio/uni-mp-vue": "^2.0.2-3080720230703001",
"@dcloudio/uni-mp-weixin": "^2.0.2-3080720230703001",
"@dcloudio/uni-mp-xhs": "^2.0.2-3080720230703001",
"@dcloudio/uni-quickapp-native": "^2.0.2-3080720230703001",
"@dcloudio/uni-quickapp-webview": "^2.0.2-3080720230703001",
"@dcloudio/uni-stacktracey": "^2.0.2-3080720230703001",
"@dcloudio/uni-stat": "^2.0.2-3080720230703001",
"@tencentcloud/chat": "^3.2.0",
"@vue/shared": "^3.0.0",
"core-js": "^3.6.5",
"crypto-js": "^4.2.0",
"flyio": "^0.6.2",
"moment": "^2.29.3",
"node-sass": "4.14.1",
"sass": "1.26.2",
"sass-loader": "8.0.2",
"sass-resources-loader": "^2.2.4",
"tim-upload-plugin": "^1.3.0",
"vue": "^2.6.11",
"vuex": "^3.2.0",
"vuex-persistedstate": "^4.1.0",
"wxml-to-canvas": "^1.1.1"
},
"devDependencies": {
"@dcloudio/types": "^3.3.2",
"@dcloudio/uni-automator": "^2.0.2-3080720230703001",
"@dcloudio/uni-cli-i18n": "^2.0.2-3080720230703001",
"@dcloudio/uni-cli-shared": "^2.0.2-3080720230703001",
"@dcloudio/uni-helper-json": "*",
"@dcloudio/uni-migration": "^2.0.2-3080720230703001",
"@dcloudio/uni-template-compiler": "^2.0.2-3080720230703001",
"@dcloudio/vue-cli-plugin-hbuilderx": "^2.0.2-3080720230703001",
"@dcloudio/vue-cli-plugin-uni": "^2.0.2-3080720230703001",
"@dcloudio/vue-cli-plugin-uni-optimize": "^2.0.2-3080720230703001",
"@dcloudio/webpack-uni-mp-loader": "^2.0.2-3080720230703001",
"@dcloudio/webpack-uni-pages-loader": "^2.0.2-3080720230703001",
"@vue/cli-plugin-babel": "~4.5.19",
"@vue/cli-service": "~4.5.19",
"babel-plugin-import": "^1.11.0",
"cross-env": "^7.0.2",
"jest": "^25.4.0",
"mini-types": "*",
"miniprogram-api-typings": "*",
"postcss-comment": "^2.0.0",
"uqrcodejs": "^3.6.0",
"vue-template-compiler": "^2.6.11"
},
"browserslist": [
"Android >= 4.4",
"ios >= 9"
],
"uni-app": {
"scripts": {}
}
}

27
postcss.config.js Normal file
View File

@ -0,0 +1,27 @@
const path = require('path')
const webpack = require('webpack')
const config = {
parser: require('postcss-comment'),
plugins: [
require('postcss-import')({
resolve (id, basedir, importOptions) {
if (id.startsWith('~@/')) {
return path.resolve(process.env.UNI_INPUT_DIR, id.substr(3))
} else if (id.startsWith('@/')) {
return path.resolve(process.env.UNI_INPUT_DIR, id.substr(2))
} else if (id.startsWith('/') && !id.startsWith('//')) {
return path.resolve(process.env.UNI_INPUT_DIR, id.substr(1))
}
return id
}
}),
require('autoprefixer')({
remove: process.env.UNI_PLATFORM !== 'h5'
}),
require('@dcloudio/vue-cli-plugin-uni/packages/postcss')
]
}
if (webpack.version[0] > 4) {
delete config.parser
}
module.exports = config

25
project.config.json Normal file
View File

@ -0,0 +1,25 @@
{
"setting": {
"es6": true,
"postcss": true,
"minified": true,
"uglifyFileName": false,
"enhance": true,
"packNpmRelationList": [],
"babelSetting": {
"ignore": [],
"disablePlugins": [],
"outputPath": ""
},
"useCompilerPlugins": false,
"minifyWXML": true
},
"compileType": "miniprogram",
"simulatorPluginLibVersion": {},
"packOptions": {
"ignore": [],
"include": []
},
"appid": "wx00e2dcdc7c02b23a",
"editorSetting": {}
}

View File

@ -0,0 +1,14 @@
{
"libVersion": "3.8.10",
"projectname": "pet_home_wxapp",
"setting": {
"urlCheck": true,
"coverView": true,
"lazyloadPlaceholderEnable": false,
"skylineRenderEnable": false,
"preloadBackgroundData": false,
"autoAudits": false,
"showShadowRootInWxmlPanel": true,
"compileHotReLoad": true
}
}

10
shime-uni.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
import Vue from 'vue'
declare module "vue/types/options" {
type Hooks = App.AppInstance & Page.PageInstance;
interface ComponentOptions<V extends Vue> extends Hooks {
/**
* 组件类型
*/
mpType?: string;
}
}

4
shime-vue.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
declare module "*.vue" {
import Vue from 'vue'
export default Vue
}

71
src/App.vue Normal file
View File

@ -0,0 +1,71 @@
<script>
import Store from "./store";
import { RouterInterceptor } from "./utils/common";
export default {
globalData: {
navBarHeight: uni.getSystemInfoSync().statusBarHeight + 44,
inviteCode: "",
},
onLaunch(options) {
const inviteCode = options?.query?.yaoqing_code || ''
const referrerID = options?.query?.referrerID || ''
console.log('小程序启动参数1111', options)
setTimeout(() => {
if (inviteCode) {
getApp().globalData.inviteCode = inviteCode
console.log('小程序启动参数---邀请码222', getApp().globalData.inviteCode)
}
// 扫码进入时如果带有 referrerID则写入 vuex登录时使用
if (referrerID) {
Store.dispatch('user/setReferrerID', Number(referrerID) || 0)
}
}, 200)
// 路由拦截
RouterInterceptor();
}
};
</script>
<style lang="scss">
/* 注意要写在第一行同时给style标签加入lang="scss"属性 */
@import "./styles/font.css";
/* 每个页面公共css */
@import "./styles/common.scss";
:root {
--app-font-scale: 1;
}
page {
height: 100vh;
// padding-bottom: constant(safe-area-inset-bottom);
// padding-bottom: env(safe-area-inset-bottom);
box-sizing: border-box;
// background: $app_color_background_color;
font-family: "Arial", sans-serif;
}
view,
text {
@extend .app-fs-main;
@extend .app-fc-main;
font-family: PingFangSC, "Arial", sans-serif;
font-display: swap; /* 或者使用 block, fallback */
}
/* 隐藏所有 scroll-view 滚动条 */
scroll-view::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
color: transparent;
}
/* 隐藏页面级滚动条(在 app.wxss 中设置) */
::-webkit-scrollbar {
width: 0;
display: none;
}
</style>

15
src/androidPrivacy.json Normal file
View File

@ -0,0 +1,15 @@
{
"version" : "1",
"prompt" : "template",
"title" : "服务协议和隐私政策",
"message" : "  请你务必审慎阅读、充分理解“服务协议”和“隐私政策”各条款,包括但不限于:为了更好的向你提供服务,我们需要收集你的设备标识、操作日志等信息用于分析、优化应用性能。<br/>  你可阅读<a href=\"\">《服务协议》</a>和<a href=\"\">《隐私政策》</a>了解详细信息。如果你同意,请点击下面按钮开始接受我们的服务。",
"buttonAccept" : "同意并接受",
"buttonRefuse" : "暂不同意",
"hrefLoader" : "system|default",
"second" : {
"title" : "确认提示",
"message" : "  进入应用前,你需先同意<a href=\"\">《服务协议》</a>和<a href=\"\">《隐私政策》</a>,否则将退出应用。",
"buttonAccept" : "同意并继续",
"buttonRefuse" : "退出应用"
}
}

70
src/api/address.js Normal file
View File

@ -0,0 +1,70 @@
import request from "../utils/request";
import { ADDRESS_LIST, ADDRESS_CREATE, ADDRESS_UPDATE, ADDRESS_EDIT, ADDRESS_INFO, ADDRESS_DEL, ADDRESS_DELETE } from "./url";
export const getAddressList = (data) => {
const { user_id } = data;
return request({
url: ADDRESS_LIST,
method: "post",
data: {
user_id,
},
});
};
// 创建地址
export const createAddress = (data) => {
return request({
url: ADDRESS_CREATE,
method: "post",
data: data
});
};
// 更新地址
export const updateAddress = (data) => {
return request({
url: ADDRESS_UPDATE,
method: "post",
data: data
});
};
export const editAddress = (data) => {
return request({
url: ADDRESS_EDIT,
method: "post",
data: data
});
};
export const getAddressInfo = (id) => {
const data = {
id: +id
}
return request({
url: ADDRESS_INFO,
method: "post",
data: data
});
};
export const delAddress = (id) => {
const data = {
address_id: id
}
return request({
url: ADDRESS_DEL,
method: "post",
data: data
});
};
// 删除地址(新接口)
export const deleteAddress = (data) => {
return request({
url: ADDRESS_DELETE,
method: "post",
data: data
});
};

7
src/api/app.js Normal file
View File

@ -0,0 +1,7 @@
// 文章协议
export const getArticleInfo = (code) => {
return request({
url: `/mp/article/${code}`,
method: "get",
});
};

27883
src/api/areas.js Normal file

File diff suppressed because it is too large Load Diff

12
src/api/article.js Normal file
View File

@ -0,0 +1,12 @@
import request from "../utils/request";
import { ARTICLE_DETAIL } from "./url";
export const getArticleDetail = (id) => {
return request({
url: ARTICLE_DETAIL,
method: "post",
data: {
article_id: id,
},
});
};

199
src/api/common.js Normal file
View File

@ -0,0 +1,199 @@
import request from "@/utils/request"
import * as URL from "@/api/url";
/**
* 获取轮播图
* @param type
* @returns {Promise<unknown>}
*/
export function getImageList(type = 1) {
return request({
url: URL.HOST_BANNER_LIST,
method: 'POST',
data: { type }
})
}
export function updateUserInfo() {
return request({
url: '/mp/auth/info',
method: 'GET'
})
}
// 获取地址
export function getRegionList(id) {
const data = {
parent_id: id
}
return request({
url: '/app/home/city_list',
method: 'POST',
data: data
})
}
// 上传图片
export function uploadImg(file) {
const formData = new FormData()
formData.append('image', file)
return request({
url: '/mp/file/uploadPicture',
method: 'POST',
data: formData
})
}
// 上传视频
export function uploadVideo() {
const formData = new FormData()
formData.append('image', file)
return request({
url: '/mp/file/uploadVideo',
method: 'POST',
data: formData
})
}
/**
* 获取宠物体重区间列表
*/
export function getWeightList() {
return request({
url: URL.WEIGHT_LIST,
method: 'POST',
data: {
p: 1,
num: 9999,
}
})
}
//获取宠物列表
export function getPetList(id) {
let data = {
user_id: id
}
return request({
url: URL.PET_LIST,
method: 'POST',
data: data
})
}
/**
* 切换宠物档案
* @param {Object} data - 请求参数 { pet_id: 宠物ID }
* @returns {Promise}
*/
export function switchPetArchive(data) {
return request({
url: URL.PET_ARCHIVES_SWITCH,
method: 'POST',
data: data
})
}
/**
* 获取宠物套餐列表
* @param {Object} data - 请求参数
* @returns {Promise}
*/
export function getHomeServices(data) {
return request({
url: URL.HOME_SERVICES,
method: 'POST',
data: data || {}
})
}
/**
* 获取领养列表
* @param {Object} data - 请求参数 { page: 1, page_size: 10 }
* @returns {Promise}
*/
export function getAdoptList(data) {
return request({
url: URL.ADOPTIONS_PET_LIST,
method: 'POST',
data: data || { page: 1, page_size: 10 }
})
}
/**
* 获取宠物详情
* @param {Object} data - 请求参数 { adoption_id: 宠物id }
* @returns {Promise}
*/
export function getPetDetail(data) {
return request({
url: URL.ADOPTIONS_PET_AGGREGATE,
method: 'POST',
data: data
})
}
/**
* 宠物收藏/取消收藏
* @param {Object} data - 请求参数 { adoption_id: 宠物id, action: 1收藏/0取消收藏 }
* @returns {Promise}
*/
export function togglePetFavorite(data) {
return request({
url: URL.ADOPTIONS_PET_FAVORITE,
method: 'POST',
data: data
})
}
/**
* 提交领养人信息
* @param {Object} data - 请求参数
* @returns {Promise}
*/
export function submitApplicantProfile(data) {
return request({
url: URL.ADOPTIONS_PET_APPLICANT_PROFILE,
method: 'POST',
data: data
})
}
/**
* 获取我的申请记录列表
* @param {Object} data - 请求参数 { page: 1, page_size: 10 }
* @returns {Promise}
*/
export function getMyApplications(data) {
return request({
url: URL.ADOPTIONS_PET_MYAPPLICATIONS,
method: 'POST',
data: data
})
}
/**
* 撤销申请
* @param {Object} data - 请求参数 { application_id: 1 }
* @returns {Promise}
*/
export function cancelApplication(data) {
return request({
url: URL.ADOPTIONS_PET_APPLICANT_CANCEL,
method: 'POST',
data: data
})
}
/**
* 撤销申请(新接口)
* @param {Object} data - 请求参数 { adoption_id: 1 }
* @returns {Promise}
*/
export function cancelPetApply(data) {
return request({
url: URL.ADOPTIONS_PET_APPLY_CANCEL,
method: 'POST',
data: data
})
}

86
src/api/community.js Normal file
View File

@ -0,0 +1,86 @@
import request from "../utils/request";
import {
COMMUNITY_DELETE,
COMMUNITY_DETAIL,
COMMUNITY_FORWARD,
COMMUNITY_LIST,
COMMUNITY_MYLIST,
COMMUNITY_ZAN,
} from "./url";
// 宠圈列表
export const getCommunityList = ({ p, num }) => {
return request({
url: COMMUNITY_LIST,
method: "post",
data: {
p,
num,
},
});
};
// 我的宠圈列表
export const getMyCommunityList = ({ p, num }) => {
return request({
url: COMMUNITY_MYLIST,
method: "post",
data: {
p,
num,
},
});
};
// 圈子详情
export const getCommunityDetail = ({
chongquan_id,
is_see = 0,
is_share = 0,
}) => {
return request({
url: COMMUNITY_DETAIL,
method: "post",
data: {
chongquan_id,
is_see,
is_share,
},
});
};
// 删除我的圈子
export const deleteCommunity = (id) => {
return request({
url: COMMUNITY_DELETE,
method: "post",
data: {
chongquan_id: id,
},
});
};
// 点赞圈子 type类型 1.点赞 2.取消
export const zanCommunity = ({ chongquan_id, type }) => {
return request({
url: COMMUNITY_ZAN,
method: "post",
data: {
chongquan_id,
type,
},
});
};
// 一键转发到宠圈
export const shareCommunity = ({ order_id, old_pic, new_pic }) => {
return request({
url: COMMUNITY_FORWARD,
method: "post",
data: {
order_id,
old_pic,
new_pic,
},
});
};

9
src/api/config.js Normal file
View File

@ -0,0 +1,9 @@
import request from "../utils/request";
import { CONFIG_INFO } from "./url";
export const getConfig = () => {
return request({
url: CONFIG_INFO,
method: "post",
});
};

151
src/api/coupon.js Normal file
View File

@ -0,0 +1,151 @@
import request from "../utils/request";
import {
CREATE_SERVICE_ORDER,
CREATE_SERVICE_ORDER_PAY,
GET_COUPON_DATA,
GET_COUPON_LIST,
GET_COUPON_LIST_OWN,
GET_SERVICE_COUPON_DATA,
GET_SERVICE_COUPON_DETAIL,
GET_SERVICE_COUPON_LIST,
GET_SERVICE_COUPON_LIST_BUY,
RECEIVE_COUPON,
} from "./url";
// 获取优惠券列表
export const getCouponData = ({ is_lingqu, p, num, type = '', is_xinren = '' }) => {
const data = {
is_lingqu,
p,
num,
}
if (type) {
data.type = type
}
if (is_xinren) {
data.is_xinren = is_xinren
}
return request({
url: GET_COUPON_LIST,
method: "post",
data: data,
});
};
// 我的优惠券列表
export const getOwnCouponData = ({ use_status, p, num, is_xinren, type }) => {
return request({
url: GET_COUPON_LIST_OWN,
method: "post",
data: {
use_status,
p,
num,
is_xinren,
type,
},
});
};
// 领取优惠券
export const receiveCoupon = (coupon_id) => {
return request({
url: RECEIVE_COUPON,
method: "post",
data: {
coupon_id,
},
});
};
// 服务券列表
export const getServiceCouponList = ({ p, num, type, weight_id }) => {
return request({
url: GET_SERVICE_COUPON_LIST,
method: "post",
data: {
p,
num,
type,
weight_id,
},
});
};
// 我的服务券列表
export const getMyServiceCouponList = ({ p, num, status }) => {
return request({
url: GET_SERVICE_COUPON_LIST_BUY,
method: "post",
data: {
p,
num,
status
},
});
};
// 服务券详情
export const getServiceCouponDetail = (fuwuquan_id) => {
return request({
url: GET_SERVICE_COUPON_DETAIL,
method: "post",
data: {
fuwuquan_id
}
});
};
// 服务券下单
export const serviceCouponCreateOrder = (fuwuquan_id) => {
return request({
url: CREATE_SERVICE_ORDER,
method: "post",
data: {
fuwuquan_id
}
});
}
// 服务券支付
export const serviceCouponOrderPay = (order_id) => {
return request({
url: CREATE_SERVICE_ORDER_PAY,
method: "post",
data: {
order_id,
pay_type: 1, // 支付类型 1.微信
}
});
}
/**
* 根据订单价格查询可用优惠券
* @param orderPrice 订单价格
* @param orderType 订单类型 1.商品 2.宠物
*/
export const getCouponListByOrderPrice = (userId, basePrice) => {
return request({
url: GET_COUPON_DATA,
method: "post",
data: {
user_id: userId,
base_price: basePrice
}
});
}
/**
* 根据体重id查询可用服务券
* @param weightId
*/
export const getServiceCouponListByWeightId = (weightId) => {
return request({
url: GET_SERVICE_COUPON_DATA,
method: "post",
data: {
weight_id: weightId
}
});
}

14
src/api/franchise.js Normal file
View File

@ -0,0 +1,14 @@
import request from "@/utils/request";
/**
* 加盟申请
* @param {Object} data - 申请表单数据
*/
export function franchiseApply(data) {
return request({
url: '/franchise/leads',
method: 'POST',
data: data
});
}

293
src/api/login.js Normal file
View File

@ -0,0 +1,293 @@
import request from "../utils/request";
import { LOGIN, GET_PHONE, USER_SHARE,USER_WALLET,RECHARGE_WALLET,USER_WXPAY,USER_TRANSACTION,USER_ADDITIONAL,MEMBER_TYPES,
USER_RECHARGE,USER_REDEEM,USER_HolderList,USER_MEMBERSHIP,USER_BINDPETS,USER_PETBINDING,USER_DISCOUNTFEE,USER_COUPONLIST,CANCEL_PET_ORDER,
GET_VIP_PRICE,POINTS_RECHARGE_LIST,POINTS_DONATE,POINTS_RECORDS,POINTS_RANK,OSS_STS,DONATION_SUMMARY,
USER_DISPATCHFEE,CANCEL_MALL_ORDER
} from "./url";
// 微信登陆
export const getCodeByWxLogin = () => {
return new Promise((resolve, reject) => {
uni.login({
provider: "weixin",
success: function (loginRes) {
resolve(loginRes.code);
},
fail: function (err) {
reject(err);
},
});
});
};
import Store from "../store";
// 登录鉴权
export const login = (phone) => {
return getCodeByWxLogin().then((code) => {
// 从 vuex 中获取 referrerID如果通过二维码扫描进入
const referrerID = Store.state.user?.referrerID || 0;
return request({
url: LOGIN,
method: "POST",
data: {
code: code,
// yaoqing_code: inviteCode || null,
phone: phone || null,
source: "wechat",
referrerID: Number(referrerID) || 0,
referrerType: "wechat"
},
}).then((res) => {
// 登录接口使用完 referrerID 后,清除一次,避免重复使用
if (referrerID) {
Store.dispatch('user/setReferrerID', 0);
}
return res;
});
});
};
// 获取手机号
export const getPhone = (code) => {
return request({
url: GET_PHONE,
method: "POST",
data: {
code: code
},
});
};
// 用户分享
export const userShare = (id) => {
return request({
url: USER_SHARE,
method: "POST",
data: {
member_id:id
},
});
};
// 用户钱包
export const userWllet = (id) => {
return request({
url: USER_WALLET,
method: "POST",
data: {
user_id: +id
},
});
};
// 兑换码
export const rechargeRedeem = (data) => {
return request({
url: USER_REDEEM,
method: "POST",
data: data
});
};
// 钱包充值列表
export const rechargeWallet = (data) => {
return request({
url: RECHARGE_WALLET,
method: "POST",
data: data
});
};
// 钱包充值
export const walletWxpay = (data) => {
return request({
url: USER_WXPAY,
method: "POST",
data: data
});
};
// 钱包金额
export const walletRechagre = (data) => {
return request({
url: USER_RECHARGE,
method: "POST",
data: data
});
};
// 积分充值列表
export const pointsRechargeList = (data) => {
return request({
url: POINTS_RECHARGE_LIST,
method: "POST",
data: data
});
};
// 捐赠积分
export const pointsDonate = (data) => {
return request({
url: POINTS_DONATE,
method: "POST",
data: data,
});
};
// 获取积分明细
export const getPointsRecords = (data) => {
return request({
url: POINTS_RECORDS,
method: "POST",
data: data,
});
};
// 获取排行榜
export const getPointsRank = () => {
return request({
url: POINTS_RANK,
method: "POST",
data: {},
});
};
// 获取捐赠汇总
export const getDonationSummary = () => {
return request({
url: DONATION_SUMMARY,
method: "POST",
data: {},
});
};
// 获取OSS配置
export const getOssConfig = () => {
return request({
url: OSS_STS,
method: "GET"
});
};
// 钱包消费
export const walletTransaction = (data) => {
return request({
url: USER_TRANSACTION,
method: "POST",
data: data
});
};
// 卡包列表
export const userHoldrlist = (data) => {
return request({
url: USER_HolderList,
method: "POST",
data: data
});
};
// 会员钱包支付接口
export const membershipWallet = (data) => {
return request({
url: USER_MEMBERSHIP,
method: "POST",
data: data
});
};
// 取消宠物订单
export const cancelPetOrderRefund = (data) => {
return request({
url: CANCEL_PET_ORDER,
method: "POST",
data: data
});
};
// 取消商城订单
export const cancelPetOrderMall = (data) => {
return request({
url: CANCEL_MALL_ORDER,
method: "POST",
data: data
});
};
// 绑定宠物列表
export const bindPets = (data) => {
return request({
url: USER_BINDPETS,
method: "POST",
data: data
});
};
// 绑定宠物
export const petBinding = (data) => {
return request({
url: USER_PETBINDING,
method: "POST",
data: data
});
};
// 获取夜间费调度费
export const gitMidnight = (data) => {
return request({
url: USER_DISPATCHFEE,
method: "POST",
data: data
});
};
// 获取基础价格
export const gitDiscountfee = (data) => {
return request({
url: USER_DISCOUNTFEE,
method: "POST",
data: data
});
};
// 获取会员卡信息
export const getVipPrice = (data) => {
return request({
url: GET_VIP_PRICE,
method: "POST",
data: data
});
};
// 优惠劵新接口
export const userCoupolistwo = (data) => {
return request({
url: USER_COUPONLIST,
method: "POST",
data: data
});
};
// 会员详情
export const memberType = (data) => {
return request({
url: MEMBER_TYPES,
method: "POST",
data: data
});
};
// 会员支付
export const memberWXPAY = (data) => {
return request({
url: USER_WXPAY,
method: "POST",
data: data
});
};
// 添加附加项
export const additionalItems = (data) => {
return request({
url: USER_ADDITIONAL,
method: "POST",
data: data
});
}

24
src/api/notice.js Normal file
View File

@ -0,0 +1,24 @@
import request from "../utils/request";
import { NEWS_DETAIL, NEWS_LIST } from "./url";
// 消息通知
export const getNoticeList = (data) => {
const { p, num } = data;
return request({
url: NEWS_LIST,
method: "post",
data: {
p,
num,
},
});
};
// 消息详情
export const getNoticeDetails = (id) => {
return request({
url: NEWS_DETAIL,
method: "post",
data: { notice_id: id },
});
};

206
src/api/order.js Normal file
View File

@ -0,0 +1,206 @@
import request from "@/utils/request"
import * as URL from "@/api/url";
//获取订单列表
export const getOrderList = (data) => {
return request({
url: URL.ORDER_LIST,
method: "post",
data
});
};
export const getOrderDetail = (id) => {
return request({
url: URL.ORDER_DETAIL,
method: "post",
data: {
order_id: id
},
});
};
// 获取订单详情(通用接口)
export const getOrderWideDetail = (data) => {
return request({
url: URL.ORDERS_WIDE_DETAIL,
method: "post",
data: data
});
};
// 卡片列表
export const getCardList = (id) => {
return request({
url: URL.CARD_LIST,
method: "post",
data: {
member_id: id
},
});
};
export const getYuYueTimeList = (time) => {
return request({
url: URL.RESERVATION_TIME_LIST,
method: "post",
data: {
date: time,
},
});
};
export const createOrder = (data) => {
return request({
url: URL.CREATE_ORDER,
method: "post",
data: data,
});
};
export const cancelOrder = (id) => {
return request({
url: URL.CANCEL_ORDER,
method: "post",
data: {
order_id: id
},
});
};
// 取消宠物订单
export const cancelPetOrder = (data) => {
return request({
url: URL.CANCEL_PET_ORDER,
method: "post",
data: data,
});
};
export const payOrder = (data) => {
return request({
url: URL.PAY_ORDER,
method: "post",
data,
});
};
export const addServicePay = (id, weightId) => {
return request({
url: URL.ADD_SERVICE_FEE,
method: "post",
data: {
order_id: id,
new_weight_id: weightId
},
});
};
// 城市是否开通服务
export const getCityIsOpen = (cityId) => {
return request({
url: URL.ADDRESS_IS_SERVICE,
method: "post",
data: {
area_id: cityId,
},
showErrToast: false,
});
};
// 创建训练订单(上门训练/寄养训练)
export const createTrainingOrder = (data) => {
return request({
url: URL.HOMETRAINING_ORDERS,
method: "post",
data,
});
};
// 创建上门喂养订单
export const createFeedOrder = (data) => {
return request({
url: URL.HOME_FEED_ORDERS,
method: "post",
data,
});
};
// 创建上门遛宠订单
export const createWalkOrder = (data) => {
return request({
url: URL.HOME_WALK_ORDERS,
method: "post",
data,
});
};
// 取消上门训练订单
export const cancelTrainingOrder = (data) => {
return request({
url: URL.HOMETRAINING_ORDERS_CANCEL,
method: "post",
data,
});
};
// 取消上门服务订单
export const cancelHomeOrder = (order_id) => {
return request({
url: URL.HOME_ORDERS_CANCEL,
method: "post",
data: {
order_id
},
});
};
// 支付完成后添加附加项
export const appendAdditionalServices = (data) => {
return request({
url: URL.ADDITIONAL_SERVICES_APPEND,
method: "post",
data: data,
});
};
// 获取宠物服务记录列表
export const getPetServiceRecords = (data) => {
return request({
url: URL.PET_SERVICE_RECORDS,
method: "post",
data: data,
});
};
// 获取宠物预检记录列表
export const getPetPrecheckRecords = (data) => {
return request({
url: URL.PET_PRECHECK_QUERY,
method: "post",
data: data,
});
};
// 校验服务码是否存在
export const checkWaExists = (data) => {
return request({
url: URL.CHECK_WA_EXISTS,
method: "post",
data: data,
});
};
// 校验节假日费用(参数 date
export const checkHolidayFee = (data) => {
return request({
url: URL.CHECK_HOLIDAY_FEE,
method: "post",
data: data,
});
};

81
src/api/record.js Normal file
View File

@ -0,0 +1,81 @@
import request from "../utils/request";
import { RECORD_WEIGHT, RECORD_TYPE, RECORD_YUYUE, RECORD_LIST, RECORD_EDIT, RECORD_INFO, RECORD_DEL, PET_DELETE, PET_UPDATE } from "./url";
export const getRecordWeightList = (data) => {
return request({
url: RECORD_WEIGHT,
method: "post",
data: data,
});
};
export const getRecordTypeList = (data) => {
return request({
url: RECORD_TYPE,
method: "post",
data: data,
});
};
export const getRecordList = (data) => {
const { p, num } = data;
return request({
url: RECORD_LIST,
method: "post",
data: {
p,
num,
},
});
};
export const editRecord = (data) => {
return request({
url: RECORD_EDIT,
method: "post",
data: data
});
};
export const getRecordInfo = (id, userId) => {
const data = {
pet_id : +id,
user_id: userId
}
return request({
url: RECORD_INFO,
method: "post",
data: data
});
};
export const delRecord = (id) => {
const data = {
chongwu_id: id
}
return request({
url: RECORD_DEL,
method: "post",
data: data
});
};
export const deletePet = (petId, userId) => {
const data = {
pet_id: petId,
user_id: userId
}
return request({
url: PET_DELETE,
method: "post",
data: data
});
};
export const updatePet = (data) => {
return request({
url: PET_UPDATE,
method: "post",
data: data
});
};

366
src/api/shop.js Normal file
View File

@ -0,0 +1,366 @@
// 商品相关接口
import request from "../utils/request";
import {
ADD_CART,
CART_LIST,
CREATE_CART_ORDER,
CREATE_ORDER_NEW,
DELETE_CART,
GET_GOODS_CATEGORY,
GET_GOODS_DETAIL,
GET_GOODS_LIST,
PAY_ORDER_NEW,
SHOP_COLLECT,
SHOP_COLLECT_LIST,
SHOP_ORDER_AFTER_SALE,
SHOP_ORDER_CANCEL,
SHOP_ORDER_CONFIRM,
SHOP_ORDER_DETAILS,
SHOP_ORDER_LIST,
SHOP_ORDER_REMARK,
SHOP_ORDER_REMARK_DETAILS,
SHOP_ORDER_REMARK_LIST,
COMMENTS_CREATE,
COMMENTS_SERVICE_IMGS,
SHOP_ORDER_REMIND_SLIVER,
SHOP_ORDER_SLIVERINFO,
UPDATE_CART_NUM,
UPDATE_CART_SELECT,
} from "./url";
// 获取商品分类
export const getGoodsClassify = () => {
return request({
url: GET_GOODS_CATEGORY,
method: "post"
});
};
// 商品列表
export const getGoodsListData = (data) => {
return request({
url: GET_GOODS_LIST,
method: "post",
data
});
};
// 商品详情
export const getGoodsDetail = ({product_id }) => {
return request({
url: GET_GOODS_DETAIL,
method: "post",
data: {
product_id
},
});
};
// 加入购物车
export const addCart = (data) => {
const { item_id, item_type, price_id, price_desc,item_name,item_price,number,item_pic,is_select,property } = data;
return request({
url: ADD_CART,
method: "post",
data: {
item_id,
item_type,
price_id,
price_desc,
item_name,
item_price,
number,
item_pic,
is_select,
property
},
});
};
// 购物车列表
export const getCartList = ({ p, num }) => {
return request({
url: CART_LIST,
method: "post",
data: {
p,
num,
},
});
};
// 购物车修改数量
export const updateCartNum = ({ cart_id, number }) => {
return request({
url: UPDATE_CART_NUM,
method: "post",
data: {
cart_id,
number,
},
});
};
// 选择购物车商品
export const updateCartSelect = ({ is_select,cart_id }) => {
return request({
url: UPDATE_CART_SELECT,
method: "post",
data: {
is_select,
cart_id
},
});
};
// 删除购物车
export const deleteCart = ({ cart_id }) => {
return request({
url: DELETE_CART,
method: "post",
data: {
cart_id,
},
});
};
// 创建订单
export const createOrder = ({
goods_id,
price_id,
dikou_id,
number,
shuxing_name,
liuyan,
address_id,
yuxiadan,
order_type,
chongwu_order_id
}) => {
return request({
url: CREATE_ORDER_NEW,
method: "post",
data: {
goods_id,
price_id,
dikou_id,
number,
shuxing_name,
liuyan,
address_id,
yuxiadan,
order_type,
chongwu_order_id
},
});
};
// 创建购物车订单
export const createCartOrder = ({
type = "" ,
original_price = "",
actual_price = "",
reduction_amount = 0,
pay_amount = "",
pay_type = "",
address_id = "",
address = "",
name = "",
phone = "",
note = "",
items = []
}) => {
return request({
url: CREATE_CART_ORDER,
method: "post",
data: {
type,
original_price,
actual_price,
reduction_amount,
pay_amount,
pay_type,
address_id,
address,
name,
phone,
note,
items
},
});
};
// 订单支付
export const payOrder = ({ type, total_fee,order_id,order_no }) => {
return request({
url: PAY_ORDER_NEW,
method: "post",
data: {
type,
total_fee,
order_id,
order_no,
},
});
};
// 商城订单列表
export const getShopOrderList = ({ p, num, status = 0, tui_status = 0 }) => {
return request({
url: SHOP_ORDER_LIST,
method: "post",
data: {
p,
num,
status,
tui_status,
},
});
};
// 商城订单详情
export const getShopOrderDetails = (order_id) => {
return request({
url: SHOP_ORDER_DETAILS,
method: "post",
data: { order_id },
});
};
// 取消订单
export const cancelOrder = (order_id) => {
return request({
url: SHOP_ORDER_CANCEL,
method: "post",
data: { order_id },
});
};
// 提醒发货
export const remindOrder = (order_id) => {
return request({
url: SHOP_ORDER_REMIND_SLIVER,
method: "post",
data: { order_id },
});
};
// 确认收货
export const confirmOrder = (order_id) => {
return request({
url: SHOP_ORDER_CONFIRM,
method: "post",
data: {
order_id,
status:6
},
});
};
// 查看物流
export const getSliverInfo = (order_id) => {
return request({
url: SHOP_ORDER_SLIVERINFO,
method: "post",
data: { order_id },
});
}
// 发起售后
export const createAfterSale = ({
order_id,
tui_yuanyin,
tui_desc,
tui_pic,
}) => {
return request({
url: SHOP_ORDER_AFTER_SALE,
method: "post",
data: { order_id, tui_yuanyin, tui_desc, tui_pic },
});
};
// 收藏
export const collectShop = ({ goods_id, type }) => {
return request({
url: SHOP_COLLECT,
method: "post",
data: {
goods_id,
type: type,
},
});
};
// 收藏列表
export const collectList = ({ p, num }) => {
return request({
url: SHOP_COLLECT_LIST,
method: "post",
data: {
p,
num,
},
});
};
// 评价
export const createRemark = ({
order_id,
type,
imgs,
content,
description,
attitude,
}) => {
return request({
url: COMMENTS_CREATE,
method: "post",
data: {
order_id,
type,
imgs,
content,
description,
attitude,
},
});
};
// 获取服务图片(洗护对比图)
export const getServiceImgs = (order_id) => {
return request({
url: COMMENTS_SERVICE_IMGS,
method: "post",
data: {
order_id,
},
});
};
// 评价列表
export const getRemarkList = ({ type, p, num, guanlian_id }) => {
return request({
url: SHOP_ORDER_REMARK_LIST,
method: "post",
data: {
type,
p,
num,
guanlian_id,
},
});
};
// 评价详情
export const getRemarkDetails = (id) => {
return request({
url: SHOP_ORDER_REMARK_DETAILS,
method: "post",
data: {
pinglun_id: id
},
});
};

306
src/api/url.js Normal file
View File

@ -0,0 +1,306 @@
import appConfig from "@/constants/app.config";
/* 登录注册 */
export const LOGIN = "/auth/login";
/* 获取手机号 */
export const GET_PHONE = "/auth/wx_phone";
// 用户信息
export const USER_INFO = "/user/info";
// 修改用户信息
export const UPDATE_USER_INFO = "/user/update";
// 退出登录
export const USER_LOGIN_OUT = "/app/member/login_out";
// 申请管家
export const APPLY_KEHU = "/app/shenqing/shenqing_info";
// 申请管家详情
export const APPLY_KEHU_DETAILS = '/app/shenqing/shenqing_show'
//分享接口
export const USER_SHARE = '/shared/info'
//钱包接口
export const RECHARGE_WALLET = '/wallet/details'
//钱包充值列表接口
export const USER_WALLET = '/wallet/info'
//钱包充值接口
export const USER_WXPAY = '/wxpay/jsapi'
//钱包充值金额
export const USER_RECHARGE = '/recharge/bonus-list'
//积分充值列表
export const POINTS_RECHARGE_LIST = '/points/recharge/list'
//捐赠积分接口
export const POINTS_DONATE = '/public_service/points/donate'
//积分明细接口
export const POINTS_RECORDS = '/user/points/records'
//排行榜接口
export const POINTS_RANK = '/public_service/points/rank'
//捐赠汇总接口
export const DONATION_SUMMARY = '/public_service/donation/summary'
//证书列表接口
export const USER_CERTIFICATES = '/user/certificates'
//OSS配置接口
export const OSS_STS = '/oss/sts'
//钱包消费接口
export const USER_TRANSACTION = '/wallet/transaction'
// 取消预约(宠物订单)
export const PET_ORDER_CANCEL = '/home/orders/cancel'
// 支付完成后添加附加项接口
export const ADDITIONAL_SERVICES_APPEND = '/order/additional_services/append'
// 取消预约订单接口
export const CANCEL_PET_ORDER = '/order/pet/cancel'
// 取消商城接口
export const CANCEL_MALL_ORDER = '/product/order/cancel'
// 卡包列表接口
export const USER_HolderList = '/membership/instances'
// 会员钱包支付接口
export const USER_MEMBERSHIP = '/wallet/transaction'
// 绑定宠物列表接口
export const USER_BINDPETS = '/membership/bindable_pets'
// 绑定宠物
export const USER_PETBINDING = '/membership/bind_pets'
// 获取折扣和免服务费
export const USER_DISCOUNTFEE = '/order/pet/base_price'
// 获取夜间费和调度费
export const USER_DISPATCHFEE = '/membership/pet_benefits'
// 获取会员价格
export const GET_VIP_PRICE = '/membership/pet_benefits'
// 兑换码
export const USER_REDEEM = '/coupon/redeem'
// 新优惠劵接口
export const USER_COUPONLIST = '/coupon/distribution'
//预约添加附加项接口
export const USER_ADDITIONAL = '/order/additional_services/by_pet'
//会员详情
export const MEMBER_TYPES = '/membership/cards'
// 消息列表
export const NEWS_LIST = "/app/notice/notice_list";
// 消息详情
export const NEWS_DETAIL = "/app/notice/notice_show";
// 地址列表
export const ADDRESS_LIST = "/user/addresses";
// 创建地址
export const ADDRESS_CREATE = "/user/addresses/create";
// 更新地址
export const ADDRESS_UPDATE = "/user/addresses/update";
// 地址列表新增或修改
export const ADDRESS_EDIT = "/app/address/address_info";
// 地址详情
export const ADDRESS_INFO = "/user/addresses/detail ";
// 删除地址
// export const ADDRESS_DEL = "/app/address/address_del";
// 删除地址(新接口)
export const ADDRESS_DELETE = "/user/addresses/delete";
// 地址是否开通服务
export const ADDRESS_IS_SERVICE = "/app/home/is_city";
//获取轮播图
export const HOST_BANNER_LIST = "/app/ad/ad_list";
//获取订单列表
export const ORDER_LIST = "/orders/wide/list";
//获取订单详情(通用接口)
export const ORDERS_WIDE_DETAIL = "/orders/wide/detail";
//获取体重列表
export const WEIGHT_LIST = "/pet/weights";
//获取宠物列表
export const PET_LIST = "/order/pet/by_user";
//获取宠物服务记录列表
export const PET_SERVICE_RECORDS = "/order/pet/by_pet";
// 预检记录查询
export const PET_PRECHECK_QUERY = "/pet/precheck/query";
//获取订单详情
export const ORDER_DETAIL = "/app/chongwu_order/order_show";
//卡片列表
export const CARD_LIST = "/app/card/card_list";
// 文章详情
export const ARTICLE_DETAIL = "/ad_page/detail";
// 获取配置(客服)
export const CONFIG_INFO = "/app/home/config";
// 宠物体重列表
export const RECORD_WEIGHT = "/pet/weights";
// 宠物品种列表
export const RECORD_TYPE = "/pet/breeds";
// 宠物预约时间
export const RECORD_YUYUE = "/app/chongwu/yuyue_time";
// 宠物列表
export const RECORD_LIST = "/app/chongwu/chongwu_list";
// 添加修改宠物
export const RECORD_EDIT = "/pet/add";
// 宠物更新
export const PET_UPDATE = "/pet/update";
// 宠物详情
export const RECORD_INFO = "/pet/info";
// 宠物删除
export const RECORD_DEL = "/app/chongwu/chongwu_del";
// 宠物删除(新接口)
export const PET_DELETE = "/pet/delete";
// 切换宠物档案
export const PET_ARCHIVES_SWITCH = "/pet/archives/switch";
//获取预约时间列表
export const RESERVATION_TIME_LIST = "/order/periods";
//创建订单
export const CREATE_ORDER = "/order/pet/create";
// 校验服务码是否存在
export const CHECK_WA_EXISTS = "/order/check_wa_exists";
// 创建训练订单(上门训练/寄养训练)
export const HOMETRAINING_ORDERS = "/hometraining/orders";
// 创建上门喂养订单
export const HOME_FEED_ORDERS = "/home/orders/create/feed";
// 创建上门遛宠订单
export const HOME_WALK_ORDERS = "/home/orders/create/walk";
// 取消上门服务订单
export const HOME_ORDERS_CANCEL = "/home/orders/cancel";
// 校验节假日费用
export const CHECK_HOLIDAY_FEE = "/check_holiday_fee";
//支付订单
export const PAY_ORDER = "/wxpay/jsapi";
//取消订单
export const CANCEL_ORDER = "/app/chongwu_order/order_quxiao";
//调整服务费
export const ADD_SERVICE_FEE = "/app/chongwu_order/order_fuwufei";
// 商品分类
export const GET_GOODS_CATEGORY = "/product/type/list";
// 商品列表
export const GET_GOODS_LIST = "/product/list";
// 商品详情
export const GET_GOODS_DETAIL = "/product/detail";
// 获取优惠券列表
export const GET_COUPON_LIST = "/app/coupon/coupon_list";
// 已领取的优惠券列表
export const GET_COUPON_LIST_OWN = "/app/coupon/fafang_list";
// 领取优惠券
export const RECEIVE_COUPON = "/app/coupon/coupon_lingqu";
// 服务券列表
export const GET_SERVICE_COUPON_LIST = "/app/fuwuquan/fuwuquan_list";
// 我的服务券列表
export const GET_SERVICE_COUPON_LIST_BUY = "/app/fuwuquan/order_list";
// 服务券详情
export const GET_SERVICE_COUPON_DETAIL = "/app/fuwuquan/fuwuquan_show";
// 服务券下单
export const CREATE_SERVICE_ORDER = "/app/fuwuquan/order_creat";
// 服务券支付
export const CREATE_SERVICE_ORDER_PAY = "/app/fuwuquan/order_pay";
// 加入购物车
export const ADD_CART = "/product/cart/add";
// 获取购物车
export const CART_LIST = "/product/cart/list";
// 修改购物车数量
export const UPDATE_CART_NUM = "/cart/update";
// 选择购物车商品
export const UPDATE_CART_SELECT = "/cart/select/all";
// 删除购物车
export const DELETE_CART = "/product/cart/delete";
// 创建订单
export const CREATE_ORDER_NEW = "/product/order/create";
// 创建购物车订单
export const CREATE_CART_ORDER = "/product/order/create";
// 订单支付
export const PAY_ORDER_NEW = "/wxpay/jsapi";
// 订单列表
export const SHOP_ORDER_LIST = "/product/order/list";
// 订单详情
export const SHOP_ORDER_DETAILS = "/product/order/show";
// 取消订单
export const SHOP_ORDER_CANCEL = '/app/order/order_quxiao'
// 提醒发货
export const SHOP_ORDER_REMIND_SLIVER = '/app/order/order_tixing'
// 确认收货
export const SHOP_ORDER_CONFIRM = '/product/order/update_status'
// 发起售后
export const SHOP_ORDER_AFTER_SALE = '/app/order/order_tui'
// 查看物流
export const SHOP_ORDER_SLIVERINFO = '/app/order/order_wuliu'
// 收藏商品
export const SHOP_COLLECT = "/app/goods/goods_shoucang";
// 收藏商品列表
export const SHOP_COLLECT_LIST = "/app/goods/shoucang_list";
// 创建评价
export const SHOP_ORDER_REMARK = '/app/pinglun/creat_pinglun'
export const COMMENTS_CREATE = '/comments/create'
export const COMMENTS_SERVICE_IMGS = '/comments/service_imgs'
// 评价列表
export const SHOP_ORDER_REMARK_LIST = '/app/pinglun/pinglun_list'
// 评价详情
export const SHOP_ORDER_REMARK_DETAILS = '/app/pinglun/pinglun_show'
// 圈子列表
export const COMMUNITY_LIST = '/app/chongquan/chongquan_list'
// 我的圈子列表
export const COMMUNITY_MYLIST = '/app/chongquan/my_list'
// 删除圈子
export const COMMUNITY_DELETE = '/app/chongquan/chongquan_del'
// 点赞圈子
export const COMMUNITY_ZAN = '/app/chongquan/zan_info'
// 圈子详情
export const COMMUNITY_DETAIL = '/app/chongquan/chongquan_show'
// 一键转发到宠圈
export const COMMUNITY_FORWARD = '/app/chongquan/chongquan_add'
//根据订单价格查询可用优惠券
export const GET_COUPON_DATA = '/order/coupons/usable'
//根据体重id查询可用服务券
export const GET_SERVICE_COUPON_DATA = '/app/fuwuquan/order_shiyong'
// 获取宠物套餐列表
export const HOME_SERVICES = '/home/services'
// 领养列表接口
export const ADOPTIONS_PET_LIST = '/adoptions/pet/list'
// 宠物详情接口
export const ADOPTIONS_PET_AGGREGATE = '/adoptions/pet/aggregate'
// 宠物收藏接口
export const ADOPTIONS_PET_FAVORITE = '/adoptions/pet/favorite'
// 提交领养人信息接口
export const ADOPTIONS_PET_APPLICANT_PROFILE = '/adoptions/pet/applicant/profile'
// 我的申请记录接口
export const ADOPTIONS_PET_MYAPPLICATIONS = '/adoptions/pet/myapplications'
// 撤销申请
export const ADOPTIONS_PET_APPLICANT_CANCEL = '/adoptions/pet/applicant/cancel'
// 撤销申请(新接口)
export const PET_APPLY_CANCEL = '/pet/apply/cancel'
// 撤销申请(上门训练订单)
export const HOMETRAINING_ORDERS_CANCEL = '/hometraining/orders/cancel'
// 撤销申请(领养申请)
export const ADOPTIONS_PET_APPLY_CANCEL = '/adoptions/pet/apply/cancel'

60
src/api/user.js Normal file
View File

@ -0,0 +1,60 @@
import request from "../utils/request";
import { USER_INFO, UPDATE_USER_INFO, USER_LOGIN_OUT, APPLY_KEHU, APPLY_KEHU_DETAILS, USER_SHARE, USER_CERTIFICATES } from "./url";
// 获取用户信息
export const getUserInfo = () => {
return request({
url: USER_INFO,
method: "POST",
data: {},
});
};
// 修改用户信息
export const updateUserInfo = (data) => {
return request({
url: UPDATE_USER_INFO,
method: "POST",
data: data,
});
};
// 退出登录
export const loginOut = (data) => {
return request({
url: USER_LOGIN_OUT,
method: "POST",
data: data,
});
};
// 申请管家
export const applyKeeper = ({ name, phone }) => {
return request({
url: APPLY_KEHU,
method: "POST",
data: {
name, phone
},
});
};
// 申请管家详情
export const applyKeeperDetail = () => {
return request({
url: APPLY_KEHU_DETAILS,
method: "POST",
data: {},
});
};
// 获取证书列表
export const getCertificates = (data) => {
return request({
url: USER_CERTIFICATES,
method: "POST",
data: data || {},
});
};

View File

@ -0,0 +1,88 @@
<template>
<view
class="flex-row-start common-cell"
:class="[{ 'common-cell-line': showSplitLine }]"
:style="fontScaleStyle"
@click="clickAction"
>
<image
v-if="showLeftIcon"
class="cell-title-icon"
:src="leftIcon"
/>
<text
class="app-fc-main fs-32 cell-content"
:style="textStyle"
>{{title}}</text>
<image
v-if="showRightIcon"
class="cell-right-icon"
mode="scaleToFill"
:src="rightIcon"
></image>
</view>
</template>
<script>
export default {
name: 'CommonCell',
props: {
title: {
type: String,
default: '标题',
},
showLeftIcon: {
type: Boolean,
default: false,
},
showRightIcon: {
type: Boolean,
default: true,
},
leftIcon: {
type: String,
},
rightIcon: {
type: String,
default: require('@/static/images/arrow_right.png'),
},
showSplitLine: {
type: Boolean,
default: true,
},
textStyle: {
type: String,
default: '',
},
},
methods: {
clickAction() {
this.$emit('clickAction')
},
},
}
</script>
<style lang="scss" scoped>
.common-cell {
box-sizing: border-box;
width: 100%;
padding: 40rpx 36rpx;
background: #fff;
&.common-cell-line {
border-bottom: 1rpx solid#f2f2f2;
}
.cell-content {
flex: 1;
margin: 0 20rpx;
}
.cell-title-icon {
width: 48rpx;
height: 48rpx;
}
.cell-right-icon {
width: 30rpx;
height: 30rpx;
}
}
</style>

View File

@ -0,0 +1,237 @@
<template>
<select-modal
class="good-info-modal"
title="洗护对比图"
@close="$emit('close', false)"
>
<view class="compare-content">
<view class="compare-wrapper">
<text class="fs-36 app-fc-normal">洗护前</text>
<view class="flex-row-start imgs-list">
<view
class="imgs-item"
:class="{ 'no-margin': index % 3 === 2 }"
v-for="(item, index) in beforeImgs"
:key="index"
@click="selectItem(item, index)"
>
<image class="item-img" :src="item.fullUrl" mode="aspectFit" />
<image
v-if="selectList.some(v => v.url === item.url)"
class="select-icon"
src="@/static/images/cart_checked.png"
/>
<view v-else class="select-icon un-select"></view>
</view>
</view>
<text class="fs-36 app-fc-normal">洗护后</text>
<view class="flex-row-start imgs-list">
<view
class="imgs-item"
:class="{ 'no-margin': index % 3 === 2 }"
v-for="(item, index) in afterImgs"
:key="index"
@click="selectItem(item, index)"
>
<image class="item-img" :src="item.fullUrl" mode="aspectFit" />
<image
v-if="selectList.some(v => v.url === item.url)"
class="select-icon"
src="@/static/images/cart_checked.png"
/>
<view v-else class="select-icon un-select"></view>
</view>
</view>
</view>
<view
class="flex-row-between compare-footer"
v-if="isCanRemark || isCanShare"
>
<!-- <view
v-if="isCanRemark"
class="flex-center fs-30 app-fc-white confirm-btn left"
@click="submit"
>
添加到评论
</view> -->
<view
v-if="isCanShare"
class="flex-center fs-30 app-fc-white confirm-btn"
@click="shareToCommunity"
>
一键转发到宠圈
</view>
</view>
</view>
</select-modal>
</template>
<script>
import SelectModal from "@/components/select-modal.vue";
export default {
props: {
isCanShare: {
type: Boolean,
default: false,
},
isCanRemark: {
type: Boolean,
default: false,
},
beforeImgs: {
type: Array,
default: () => [],
},
afterImgs: {
type: Array,
default: () => [],
},
selectImgs: {
type: Array,
default: () => [],
},
},
data() {
return {
selectList: [], // 选中的图片
};
},
components: {
SelectModal,
},
watch: {
selectImgs: {
handler(val) {
this.selectList = val;
},
immediate: true,
},
},
methods: {
submit() {
if (!this.selectList?.length) {
uni.showToast({
title: "请选择图片",
icon: "none",
});
return;
}
this.$emit("add", this.selectList);
},
shareToCommunity() {
const front = this.selectList
.filter((v) => this.beforeImgs.some((item) => v.url === item.url))
.map((v) => v.url)
const end = this.selectList
.filter((v) => this.afterImgs.some((item) => v.url === item.url))
.map((v) => v.url)
if (!front.length) {
uni.showToast({
title: "请至少选择一张洗护前图片",
icon: "none",
});
return
}
if (!end.length) {
uni.showToast({
title: "请至少选择一张洗护后图片",
icon: "none",
});
return
}
this.$emit("share", {
front: front,
end: end,
});
},
selectItem(item) {
const index = this.selectList.findIndex((v) => v.url === item.url);
if (index < 0) {
this.selectList.push(item);
} else {
this.selectList.splice(index, 1);
}
},
},
};
</script>
<style lang="scss" scoped>
::v-deep {
.selected-modal .model-container {
background-color: #fff0f5;
}
}
.compare-content {
padding: 48rpx 32rpx 0;
box-sizing: border-box;
.compare-wrapper {
max-height: 60vh;
overflow: auto;
}
.imgs-list {
flex-wrap: wrap;
margin-top: 28rpx;
margin-bottom: 40rpx;
.imgs-item {
width: 204rpx;
height: 204rpx;
border-radius: 20rpx;
position: relative;
margin-right: 14rpx;
margin-bottom: 14rpx;
background: #fff;
&.no-margin {
margin-right: 0;
}
.item-img {
width: 100%;
height: 100%;
border-radius: 20rpx;
}
.select-icon {
position: absolute;
top: 10rpx;
right: 10rpx;
width: 24rpx;
height: 24rpx;
.un-select {
border: 3rpx solid #beb5ba;
border-radius: 30rpx;
}
}
}
}
.compare-footer {
background: #fff;
padding: 20rpx 0 0;
.confirm-btn {
flex: 1;
height: 90rpx;
border-radius: 90rpx;
background: $app_color_main;
border-radius: 45rpx 45rpx 45rpx 45rpx;
color: #fff;
&.left {
margin-right: 32rpx;
}
}
}
}
</style>

View File

@ -0,0 +1,118 @@
<template>
<view class="flex-center contact-modal">
<view class="flex-center contact-modal-content">
<view class="flex-center contact-modal-inner">
<!-- <image
class="modal-title-icon"
src="@/static/images/contact_title_icon.png"
/> -->
<image
class="qrcode-img"
:src="details.kefu_pic"
:show-menu-by-longpress="true"
mode="widthFix"
/>
<text class="PingFangSC-Heavy fs-44 app-fc-main modal-tip">
扫码添加客服
</text>
<text class="fs-28 app-fc-normal PingFangSC-Medium">
客服在线时间{{ details.kefu_time }}
</text>
</view>
<image
class="close-icon"
src="@/static/images/modal_close.png"
@click="$emit('close')"
/>
</view>
</view>
</template>
<script>
import { getConfig } from "../api/config";
export default {
props: {
data: {
type: Object,
default: () => {},
},
},
data() {
return {
details: {},
};
},
watch: {
data(val) {
val && (this.details = val);
},
},
mounted() {
if (!this.data || !Object.keys(this.data).length) {
this.getConfig();
} else {
this.details = this.data;
}
},
methods: {
getConfig() {
getConfig().then((res) => {
this.details.kefu_pic = res.info.kefu_pic;
this.details.kefu_time = res.info.kefu_time;
this.$forceUpdate();
});
},
},
};
</script>
<style lang="scss" scoped>
.contact-modal {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 999;
.contact-modal-content {
margin-top: -50rpx;
.contact-modal-inner {
width: 646rpx;
padding: 134rpx 0 80rpx;
box-sizing: border-box;
background: #fff;
position: relative;
border-radius: 36rpx;
.modal-title-icon {
position: absolute;
top: 0;
left: 50%;
transform: translate(-50%, -50%);
width: 140rpx;
height: 140rpx;
}
.qrcode-img {
width: 260rpx;
height: 260rpx;
padding: 14rpx;
box-sizing: border-box;
}
.modal-tip {
margin: 76rpx 0 56rpx;
}
}
.close-icon {
width: 80rpx;
height: 80rpx;
margin-top: 50rpx;
}
}
}
</style>

View File

@ -0,0 +1,184 @@
<template>
<view class="custom-picker">
<view class="mask" @click="handleClose"></view>
<view class="picker-content">
<!-- 自定义头部 -->
<view class="picker-header">
<view class="header-hidden"></view>
<text class="title app-text-ellipse ali-puhui-bold">{{ title }}</text>
<image
src="@/static/images/close_icon.png"
class="close-icon"
@click="handleClose"
/>
</view>
<!-- 选择器主体 -->
<picker-view
:value="currentValue"
class="picker-view"
:style="{ height: rowHeight * visibleRows + 'px' }"
@change="handleChange"
:indicator-style="`height: ${rowHeight}px`"
>
<picker-view-column>
<view
class="picker-item"
v-for="(item, index) in options"
:key="index"
:style="{ height: rowHeight + 'px', lineHeight: rowHeight + 'px' }"
>
{{ item[labelKey] }}
</view>
</picker-view-column>
</picker-view>
<!-- 自定义底部插槽 -->
<view
class="flex-center PingFangSC-Semibold fs-30 confirm-btn"
@click="handleConfirm"
>
确定
</view>
</view>
</view>
</template>
<script>
export default {
name: "CustomPicker",
props: {
title: {
type: String,
default: "",
},
options: {
type: Array,
default: () => [],
},
value: {
type: [Number, Array],
default: 0,
},
visibleRows: {
type: Number,
default: 3,
},
rowHeight: {
type: Number,
default: 60,
},
labelKey: {
type: String,
default: "label",
},
valueKey: {
type: String,
default: "value",
},
},
data() {
return {
currentValue: [""],
};
},
watch: {
value: {
handler(val) {
this.currentValue = Array.isArray(val) ? val : [val];
},
immediate: true,
},
},
methods: {
handleChange(e) {
this.currentValue = e.detail.value;
this.$emit("change", e.detail.value[0]);
},
handleClose() {
this.$emit("cancel");
},
handleConfirm() {
const index = this.currentValue[0] || 0;
const _index = index < 0 ? 0 : index;
const value = this.options[_index][this.valueKey];
this.$emit("confirm", value);
},
},
};
</script>
<style lang="scss" scoped>
.custom-picker {
position: fixed;
left: 0;
right: 0;
bottom: 0;
top: 0;
z-index: 999;
.mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.4);
}
.picker-content {
position: absolute;
left: 0;
right: 0;
bottom: 0;
background: #fff;
border-radius: 20rpx 20rpx 0 0;
.picker-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 52rpx 30rpx;
.header-hidden {
width: 52rpx;
height: 52rpx;
visibility: hidden;
}
.close-icon {
width: 52rpx;
height: 52rpx;
}
.title {
font-size: 32rpx;
// font-weight: 500;
flex: 1;
text-align: center;
}
}
.confirm-btn {
width: 630rpx;
height: 90rpx;
border-radius: 90rpx;
background: #FF19A0;
border-radius: 45rpx 45rpx 45rpx 45rpx;
color: #fff;
margin: 20rpx auto 60rpx;
}
.picker-view {
width: 100%;
.picker-item {
display: flex;
justify-content: center;
align-items: center;
font-size: 32rpx;
}
}
}
}
</style>

View File

@ -0,0 +1,219 @@
<template>
<view class="datetime-picker">
<view
class="mask"
:class="{ show: open }"
@touchmove.stop.prevent
catchtouchmove="true"
@click="changeState(false)"
></view>
<view class="wrap" :class="{ show: open }">
<view
class="picker-header title"
@touchmove.stop.prevent
catchtouchmove="true"
>
<view class="PingFangSC-Bold fs-36 btn-picker">{{ title }}</view>
<image
class="close-icon"
src="@/static/images/close.png"
@click="changeState(false)"
/>
</view>
<view class="picker-body">
<DateTimePicker
:mode="modeValue"
:minDate="minDate"
:maxDate="maxDate"
:defaultDate="defaultDate"
@onChange="onDateChange"
/>
</view>
<!-- <view class="picker-header" @touchmove.stop.prevent catchtouchmove="true">
<view class="submit" @click="submit">确定</view>
</view> -->
<view class="picker-bottom">
<view
class="fs-30 PingFangSC-Semibold app-fc-white flex-center picker-confirm"
@click="submit"
>
确定
</view>
</view>
</view>
</view>
</template>
<script>
import DateTimePicker from "@/components/dengrq-datetime-picker/dateTimePicker/index.vue";
export default {
name: "DateTimePicker",
components: {
DateTimePicker,
},
props: {
title: {
type: String,
default: "",
},
// 日期模式1年月日默认2年月3年份4年月日时分秒5时分秒6时分
mode: {
type: Number,
default: 4,
},
// 可选的最小日期,默认十年前
minDate: {
type: String,
default: "1940-01-01",
},
// 可选的最大日期,默认十年后
maxDate: {
type: String,
default: new Date().format("yyyy-MM-dd"),
},
// 默认选中日期(注意要跟日期模式对应)
defaultDate: {
type: String,
default: "1970-01-01",
},
visible: {
type: Boolean,
default: false,
},
},
data() {
return {
open: false,
date: new Date().format("yyyy-MM-dd"),
modeValue: 4,
};
},
watch: {
mode: {
handler(val) {
this.modeValue = val;
},
immediate: true,
},
visible: {
handler(value) {
this.open = value;
},
immediate: true,
},
defaultDate: {
handler(value) {
this.date = value;
},
immediate: true,
},
},
mounted() {},
methods: {
changeState(value) {
this.open = value;
this.$emit("onModalStateChange", value);
},
submit() {
this.$emit("onSubmit", this.date || new Date().format("yyyy-MM-dd"));
},
onDateChange(value) {
this.date = value;
},
},
};
</script>
<style lang="scss" scoped>
$transition: all 0.3s ease;
$primary: #488ee9;
.datetime-picker {
position: relative;
z-index: 999;
picker-view {
height: 100%;
}
.mask {
position: fixed;
z-index: 1000;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.6);
visibility: hidden;
opacity: 0;
transition: $transition;
&.show {
visibility: visible;
opacity: 1;
}
}
.wrap {
z-index: 1001;
position: fixed;
bottom: 0;
left: 0;
width: 100%;
transition: $transition;
transform: translateY(100%);
&.show {
transform: translateY(0);
}
}
.picker-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 50rpx;
box-sizing: border-box;
// background-color: darken(#fff, 2%);
background-color: #fff;
position: relative;
&.title {
border-top-right-radius: 30rpx;
border-top-left-radius: 30rpx;
}
.close-icon {
position: absolute;
top: 50rpx;
right: 50rpx;
width: 52rpx;
height: 52rpx;
}
}
.picker-body {
width: 100%;
overflow: hidden;
background-color: #fff;
}
.submit {
width: 100%;
text-align: center;
margin-bottom: 0;
margin-bottom: constant(safe-area-inset-bottom);
margin-bottom: env(safe-area-inset-bottom);
}
.btn-picker {
width: 100%;
text-align: center;
color: #3e4055;
}
.picker-bottom {
width: 100%;
background: #fff;
padding: 20rpx 0 40rpx;
.picker-confirm {
width: 630rpx;
height: 90rpx;
border-radius: 90rpx;
background: $app_color_main;
margin: 0 auto;
}
}
}
</style>

340
src/components/FormCell.vue Normal file
View File

@ -0,0 +1,340 @@
<template>
<view class="form-cell-wrapper">
<view class="form-cell" :class="[{ 'no-border': noBorder }]" @click="clickAction">
<text class="required" v-if="isRequired">*</text>
<text class="title">
{{ title }}
</text>
<view class="flex-center form-content">
<input v-if="type === 'input'" :type="inputType" :disabled="disabled"
class="fs-24 app-fc-main form-inner app-text-ellipse" :class="{ disabled: disabled }"
placeholder-class="fs-24 form-placeholder" v-model="curValue" :placeholder="placeholderText"
@input="onChange" @confirm="onChangeDone" />
<text v-else-if="['jumpToMap'].includes(type)" class="fs-24 app-fc-main form-inner"
:class="{ 'form-placeholder': !value, 'fs-26': !value }" @click="jumpToLoationAction">
{{ value || placeholderText || "请选择" }}
</text>
<text v-else-if="
['checkDate', 'checkPicker', 'checkAddress'].includes(type)
" class="fs-24 form-inner" :class="{ 'form-placeholder': !value, 'fs-26': !value }"
@click="showModal(type)">
{{
((type === "checkPicker"
? labelKey
? pickerData[pickerIndex][labelKey]
: pickerData[pickerIndex]
: value) || "") + showText ||
placeholderText ||
"请选择"
}}
</text>
<slot v-else-if="type === 'custom'" name="right"></slot>
</view>
<image v-show="showRightArrow" class="form-arrow" :src="`${imgPrefix}right-arrow.png`"
@click="$emit('arrowClick')" />
</view>
<DateTimePicker :mode="mode" :visible="showDatePicker" :title="title.replace('*', '')" :minDate="minDate"
:maxDate="maxDate" :defaultDate="defaultDate" @onSubmit="onDataChange"
@onModalStateChange="onModalStateChange" />
<CustomePicker v-if="showPicker" :title="title.replace('*', '')" :options="pickerData" :value="pickerIndex"
:labelKey="labelKey" :valueKey="valueKey" @confirm="onDataChange" @cancel="showPicker = false" />
<template v-if="showAreaPicker">
<RegionPicker :title="title.replace('*', '')" :list="regionList" :defaultValue="defaultRegionList"
@confirm="onDataChange" @cancel="showAreaPicker = false" />
</template>
</view>
</template>
<script>
import DateTimePicker from "@/components/DateTimePicker.vue";
import CustomePicker from "@/components/CustomePicker.vue";
import RegionPicker from "@/components/RegionPicker.vue";
import {
getRegionList
} from "@/api/common.js";
import {
jumpToLoation
} from "@/utils/common";
import {
pcaData
} from "@/api/areas.js";
import {
imgPrefix
} from "@/utils/common";
export default {
name: "form-cell",
props: {
// input/checkDate/checkPicker/checkAddress
type: {
default: "input",
type: String,
},
// type===checkPicker ? 索引 : value值
value: {
default: "",
type: String | Number,
},
inputType: {
default: "text",
type: String,
},
valueKey: {
default: "value",
type: String,
},
showText: {
default: "",
type: String,
},
labelKey: {
default: "",
type: String,
},
disabled: {
default: false,
type: Boolean,
},
// type===checkPicker时有效
pickerData: {
default: () => [],
type: Array,
},
placeholderText: {
type: String,
default: "请填写",
},
// 日期模式1年月日默认2年月3年份4年月日时分秒5时分秒6时分
title: {
type: String,
default: "",
},
mode: {
type: Number,
default: 1,
},
// 可选的最小日期,默认十年前
minDate: {
type: String,
default: "1940-01-01",
},
// 可选的最大日期,默认十年后
maxDate: {
type: String,
default: new Date().format("yyyy-MM-dd"),
},
// 默认选中日期(注意要跟日期模式对应)
defaultDate: {
type: String,
default: "1970-01-01",
},
// 默认区域
defaultRegion: {
type: Array,
default: () => [0, 0, 0],
},
extraData: {
type: Object,
default: () => {},
},
showRightArrow: {
type: Boolean,
default: true,
},
noBorder: {
type: Boolean,
default: false,
},
isRequired: {
type: Boolean,
default: false,
}
},
components: {
DateTimePicker,
CustomePicker,
RegionPicker
},
data() {
return {
curValue: "",
showDatePicker: false,
showPicker: false,
showAreaPicker: false,
regionList: [],
defaultRegionList: [0, 0, 0],
imgPrefix
};
},
computed: {
pickerIndex() {
let index = 0;
if (this.type === "checkPicker") {
const _index = this.pickerData.findIndex(
(v) => v[this.valueKey] === this.curValue
);
index = _index;
}
return index;
},
},
watch: {
value: {
handler(value) {
this.curValue = value;
},
immediate: true,
},
defaultRegion: {
handler(val) {
const [province, city, area] = val;
this.defaultRegionList = [
province > -1 ? province : 0,
city > -1 ? city : 0,
area > -1 ? area : 0,
];
},
},
},
mounted() {},
methods: {
jumpToLoationAction() {
if (this.disabled) return;
jumpToLoation(this.extraData.latitude, this.extraData.longitude);
},
formatRegionData(list) {
return list.map((p) => {
const _p = Object.assign({}, {
label: p.area_name,
value: p.area_id
});
if (p.children) {
_p.children = p.children.map((city) => {
const _city = Object.assign({}, {
label: city.area_name,
value: city.area_id,
});
if (city.children) {
_city.children = city.children.map((area) => {
return Object.assign({}, {
label: area.area_name,
value: area.area_id,
});
});
}
return _city;
});
}
return _p;
});
},
onChange(e) {
this.$emit("onChange", e.detail.value);
},
onChangeDone(e) {
this.$emit("onChangeDone", e.detail.value);
},
onModalStateChange(value) {
this.showDatePicker = value;
},
async showModal(type) {
if (this.disabled) return;
switch (type) {
case "checkDate":
this.showDatePicker = true;
break;
case "checkPicker":
this.showPicker = true;
break;
case "checkAddress":
uni.showLoading({
title: "加载中",
});
// const res = await getRegionList(-1);
const res = pcaData;
this.regionList = this.formatRegionData(res || []);
this.showAreaPicker = true;
uni.hideLoading();
break;
}
},
onDataChange(data) {
let _data = data;
this.showDatePicker = false;
this.showPicker = false;
this.showAreaPicker = false;
this.$emit("onChange", _data);
},
clickAction() {
this.$emit("clickAction");
},
},
};
</script>
<style lang="scss" scoped>
.form-cell-wrapper {
height: 96rpx;
}
.form-cell {
height: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
border-bottom: 1rpx solid #ececec;
.title {
color: #3D3D3D;
font-size: 24rpx;
}
&.no-border {
border-bottom: 0;
}
.form-content {
flex: 1;
margin: 0 10rpx;
.form-inner {
width: 100%;
height: 100%;
border: none;
text-align: right;
&.disabled {
color: #999999;
}
}
.form-placeholder {
color: #8f9090;
}
}
.form-arrow {
width: 11rpx;
height: 18rpx;
}
::v-deep {
.u-datetime-picker {
.u-picker-header {
border-top-left-radius: 40rpx;
border-top-right-radius: 40rpx;
height: unset;
padding: 50rpx 40rpx;
}
}
}
}
.required {
color: #FF19A0;
}
</style>

View File

@ -0,0 +1,235 @@
<template>
<scroll-view class="list-template-wrapper" :scroll-y="!disableScroll" :refresher-enabled="true"
:refresher-triggered="refreshTriggered" @refresherrefresh="onRefresh" @scrolltolower="onLoadMore">
<slot name="top" />
<view class="list-content" v-if="list.length > 0">
<view v-for="(item, index) in list" :key="item[idKey]" :class="{ left: index % 2 === 0 }"
class="flex-column-start news-item" @click="clickCell(item)">
<slot style="width: 100%" name="item" :data="{
...item,
...listExtraFields,
deleteSelect: !!item.deleteSelect,
}" />
</view>
<uni-load-more v-if="isLoading || (!isLoading && total && total === list.length)"
:status="isLoading ? 'loading' : 'nomore'"></uni-load-more>
</view>
<view v-else class="empty-container">
<image class="home-ration" mode="widthFix" :src="emptyImage || defaultEmptyImage" />
<text v-if="emptyText" class="empty-text">{{ emptyText }}</text>
</view>
<slot name="bottom" />
</scroll-view>
</template>
<script>
export default {
name: "ListPageTemp",
props: {
getDataPromise: {
type: Function,
},
// 格式化请求结果的方法
resultFormatFunc: {
type: Function,
},
requestData: {
defult: () => { },
type: Object,
},
// 列表元素额外字段
listExtraFields: {
type: Object,
default: () => { },
},
reloadFlag: {
default: 0,
type: Number,
},
// 是否分页
isPagiantion: {
type: Boolean,
default: true,
},
// 分页数量, 不分页时有效
pageSize: {
type: Number,
default: 999,
},
defaultList: {
type: Array,
default: () => [],
},
disableScroll: {
type: Boolean,
default: false,
},
idKey: {
type: String,
default: "id",
},
// 占位图片地址
emptyImage: {
type: String,
default: "",
},
// 空状态文本(传入时才显示)
emptyText: {
type: String,
default: "",
}
},
data() {
return {
isLoading: false,
total: 0,
list: [],
refreshTriggered: false,
p: 1,
num: 10,
defaultEmptyImage: 'https://activity.wagoo.live/empty.png', // 默认占位图片
};
},
watch: {
reloadFlag: {
handler(value) {
if (value) {
this.p = 1;
this.num = 10;
this.list = [];
this.total = 0;
this.getList();
}
},
immediate: true,
},
requestData: {
handler(data) {
if (data) {
this.p = 1;
this.num = 10;
this.list = [];
this.total = 0;
this.getList();
}
},
deep: true,
},
defaultList: {
handler(list) {
this.list = list;
this.$forceUpdate();
},
immediate: true,
},
},
methods: {
onRefresh() {
if (this.refreshTriggered) return;
this.refreshTriggered = true;
this.p = 1;
this.num = 10;
// this.list = [];
this.total = 0;
this.getList();
},
onLoadMore() {
if (!this.isPagiantion) {
return;
}
if (!this.isLoading && this.total > this.list.length) {
this.p++;
this.getList();
}
},
clickCell(item) {
this.$emit("clickCell", item);
},
getList() {
if (this.isLoading) return;
this.isLoading = true;
// TODO: 删除测试数据
if (!this.getDataPromise) {
this.list = [0, 1, 2, 3, 4, 5, 6].map((v, k) => {
return {
title: "消息名称0000sssss撒大苏打大苏打" + v,
id: k,
len: 10,
};
});
this.total = 9;
this.isLoading = false;
this.refreshTriggered = false;
uni.stopPullDownRefresh();
return;
}
const data = Object.assign(
{},
{ p: this.p, num: !this.isPagiantion ? this.pageSize : this.num },
this.requestData
);
this.getDataPromise(data)
.then((res) => {
const list =
this.p === 1
? res?.data || []
: [...this.list, ...(res?.data || [])];
this.list = this.resultFormatFunc
? this.resultFormatFunc(list)
: list;
this.total = res?.count || 0;
this.$emit("getList", this.list, res);
// console.log(this.list,'???')
})
.finally(() => {
this.isLoading = false;
this.refreshTriggered = false;
uni.stopPullDownRefresh();
});
},
},
};
</script>
<style lang="scss" scoped>
.list-template-wrapper {
height: 100%;
width: 100%;
.list-content {
width: 100%;
}
.news-item {
width: 100%;
}
.empty-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 400rpx auto 0;
padding: 0 40rpx;
}
.home-ration {
width: 160px;
height: 174px;
display: block;
margin-bottom: 24rpx;
}
.empty-text {
font-size: 28rpx;
color: #999;
text-align: center;
}
}
</style>

52
src/components/NavBar.vue Normal file
View File

@ -0,0 +1,52 @@
<template>
<view class="nav-container">
<view :style="{ height: ststuaBarHeight + 'px' }"></view>
<view
class="flex-row-center nav-title ali-puhui-bold"
:style="{ height: menuButtonHeight + 'px' }"
>
{{ title }}
</view>
</view>
</template>
<script>
export default {
props: {
title: {
type: String,
default: "",
},
},
data() {
return {
navBarHeight: 0,
};
},
computed: {
ststuaBarHeight() {
const systemInfo = wx.getSystemInfoSync();
return systemInfo.statusBarHeight;
},
menuButtonHeight() {
const menuButtonInfo = wx.getMenuButtonBoundingClientRect();
return (
(menuButtonInfo.top - this.ststuaBarHeight) * 2 + menuButtonInfo.height
);
},
},
};
</script>
<style lang="scss" scoped>
.nav-container {
background: #fff;
.nav-title {
font-size: 14px;
// font-weight: bold;
color: #000;
height: 100%;
}
}
</style>

40
src/components/NoData.vue Normal file
View File

@ -0,0 +1,40 @@
<template>
<view class="flex-center no-data">
<image class="no-data-icon" :src="imgUrl" mode="widthFix" />
<text class="no-data-tip app-fc-normal app-fs-base">{{ tip }}</text>
</view>
</template>
<script>
export default {
name: 'NoData',
props: {
imgUrl: {
type: String,
// default: 'https://oss.chuanglingmeng.com/cl/common/no_data.png'
},
tip: {
type: String,
default: '暂无记录'
}
}
}
</script>
<style lang="scss" scoped>
.no-data {
width: 100%;
padding: 60rpx 0;
display: flex;
flex-direction: column;
justify-content: flex-center;
align-items: center;
.no-data-icon {
width: 160rpx;
height: 160rpx;
}
.no-data-tip {
margin: 10rpx 0;
}
}
</style>

View File

@ -0,0 +1,85 @@
<template>
<view class="flex-center pop-up-modal">
<view class="pop-content">
<view
class="app-text-center fs-36 app-fc-main PingFangSC-Medium pop-inner"
>
{{ content }}
</view>
<view class="flex-row-between pop-bottom">
<view
class="fs-32 PingFangSC-Regular flex-center pop-btn cancel"
@click="$emit('cancel')"
>
取消
</view>
<view
class="fs-32 PingFangSC-Regular flex-center app-fc-white pop-btn confirm"
@click="$emit('confirm')"
>
确定
</view>
</view>
</view>
</view>
</template>
<script>
export default {
props: {
content: {
type: String,
default: "",
},
},
data() {
return {};
},
};
</script>
<style lang="scss" scoped>
.pop-up-modal {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.5);
.pop-content {
background: #fff;
width: 620rpx;
border-radius: 40rpx 40rpx 40rpx 40rpx;
box-sizing: border-box;
padding: 60rpx 76rpx;
.pop-inner {
padding: 24rpx 0;
}
.pop-bottom {
margin-top: 60rpx;
.pop-btn {
flex: 1;
height: 80rpx;
background: #f2f2f2;
border-radius: 8rpx 8rpx 8rpx 8rpx;
&.cancel  {
color: #3e4055;
background: #f2f2f2;
margin-right: 36rpx;
}
&.confirm {
background: $app_color_main;
}
}
}
}
}
</style>

View File

@ -0,0 +1,239 @@
<template>
<view class="custom-picker">
<view class="mask" @click="handleClose"></view>
<view class="picker-content">
<!-- 头部 -->
<view class="picker-header">
<view class="header-hidden"></view>
<text class="title app-text-ellipse ali-puhui-bold">{{ title }}</text>
<image
src="@/static/images/close_icon.png"
class="close-icon"
@click="handleClose"
/>
</view>
<!-- 选择器主体 -->
<picker-view
:value="currentValue"
class="picker-view"
:style="{ height: rowHeight * visibleRows + 'px' }"
@change="handleChange"
:indicator-style="`height: ${rowHeight}px`"
>
<!-- 省份列 -->
<picker-view-column>
<view
class="picker-item"
v-for="(item, index) in list"
:key="index"
:style="{ height: rowHeight + 'px', lineHeight: rowHeight + 'px' }"
>
{{ item.label }}
</view>
</picker-view-column>
<!-- 城市列 -->
<picker-view-column>
<view
class="picker-item"
v-for="(item, index) in cityList"
:key="index"
:style="{ height: rowHeight + 'px', lineHeight: rowHeight + 'px' }"
>
{{ item.label }}
</view>
</picker-view-column>
<!-- 区县列 -->
<picker-view-column>
<view
class="picker-item"
v-for="(item, index) in areaList"
:key="index"
:style="{ height: rowHeight + 'px', lineHeight: rowHeight + 'px' }"
>
{{ item.label }}
</view>
</picker-view-column>
</picker-view>
<!-- 底部确认按钮 -->
<view
class="flex-center PingFangSC-Semibold fs-30 confirm-btn"
@click="handleConfirm"
>
确定
</view>
</view>
</view>
</template>
<script>
export default {
name: "RegionPicker",
props: {
title: {
type: String,
default: "选择地区",
},
list: {
type: Array,
default: () => [],
},
defaultValue: {
type: Array,
default: () => [0, 0, 0],
},
visibleRows: {
type: Number,
default: 3,
},
rowHeight: {
type: Number,
default: 60,
},
},
data() {
return {
currentValue: [0, 0, 0],
cityList: [],
areaList: [],
};
},
watch: {
defaultValue: {
handler(val) {
this.currentValue = val;
this.updateCityAndArea();
},
immediate: true,
},
list: {
handler() {
this.updateCityAndArea();
},
immediate: true,
},
},
methods: {
updateCityAndArea() {
if (this.list.length > 0) {
const [provinceIndex, cityIndex] = this.currentValue;
// 更新城市列表
this.cityList = this.list[provinceIndex]?.children || [];
// 更新区县列表
this.areaList = this.cityList[cityIndex]?.children || [];
}
},
handleChange(e) {
const newValue = e.detail.value;
// 判断省份是否改变
if (newValue[0] !== this.currentValue[0]) {
newValue[1] = 0;
newValue[2] = 0;
}
// 判断城市是否改变
else if (newValue[1] !== this.currentValue[1]) {
newValue[2] = 0;
}
this.currentValue = newValue;
this.updateCityAndArea();
},
handleClose() {
this.$emit("cancel");
},
handleConfirm() {
const [provinceIndex, cityIndex, areaIndex] = this.currentValue;
this.$emit("confirm", [
this.list[provinceIndex],
this.cityList[cityIndex],
this.areaList[areaIndex],
]);
},
},
};
</script>
<style lang="scss" scoped>
.custom-picker {
position: fixed;
left: 0;
right: 0;
bottom: 0;
top: 0;
z-index: 999;
.mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.4);
}
.picker-content {
position: absolute;
left: 0;
right: 0;
bottom: 0;
background: #fff;
border-radius: 20rpx 20rpx 0 0;
.picker-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 52rpx 30rpx;
.header-hidden {
width: 52rpx;
height: 52rpx;
visibility: hidden;
}
.close-icon {
width: 52rpx;
height: 52rpx;
}
.title {
font-size: 32rpx;
// font-weight: 500;
flex: 1;
text-align: center;
}
}
.confirm-btn {
width: 630rpx;
height: 90rpx;
border-radius: 90rpx;
background: $app_color_main;
border-radius: 45rpx 45rpx 45rpx 45rpx;
color: #fff;
margin: 20rpx auto 60rpx;
}
.picker-view {
width: 100%;
.picker-item {
display: flex;
justify-content: center;
align-items: center;
font-size: 32rpx;
}
}
}
}
</style>

View File

@ -0,0 +1,359 @@
<template>
<view class="service-record" :class="{ 'service-record--empty': !isLoading && recordList.length === 0 }">
<scroll-view class="record-scroll" scroll-y :show-scrollbar="false" :enhanced="true" @scrolltolower="loadMore">
<view class="record-list">
<view class="record-item" v-for="(item, index) in recordList" :key="index" @click="goToDetail(item)">
<view class="record-content">
<view class="pet-info">
<text class="pet-name">{{ petInfo.name || '--' }}</text>
<text class="pet-gender">{{ formatGender(petInfo.gender) }}</text>
</view>
<view class="divider"></view>
<view class="order-info">
<view class="info-row">
<text class="label">订单编号</text>
<text class="value">{{ item.order_no || '--' }}</text>
</view>
<view class="info-row">
<text class="label">车牌号</text>
<text class="value">{{ item.license_plate || '--' }}</text>
</view>
<view class="info-row">
<text class="label">预约时间</text>
<text class="value">{{ formatTimeRange(item) }}</text>
</view>
</view>
</view>
<view class="status-badge">
<text class="status-text">预检</text>
</view>
</view>
<!-- 加载更多 -->
<view class="load-more" v-if="isLoading">
<uni-load-more status="loading" :show-text="false" />
</view>
<view class="load-more" v-if="isNoMore && recordList.length > 0">
<text class="no-more-text">没有更多了</text>
</view>
<view class="empty-state" v-if="!isLoading && recordList.length === 0">
<image class="empty-img" :src="imgPrefix + 'certificatePlaceholder.png'" mode="widthFix" />
<text class="empty-text">暂无记录</text>
</view>
</view>
</scroll-view>
</view>
</template>
<script>
import { getPetPrecheckRecords } from "@/api/order";
import { imgPrefix } from "@/utils/common";
export default {
name: 'ServiceRecord',
props: {
// 宠物ID
petId: {
type: [Number, String],
required: true
}
},
data() {
return {
imgPrefix,
recordList: [],
page: 1,
pageSize: 10,
isLoading: false,
isNoMore: false,
petInfo: {}
};
},
mounted() {
this.loadData();
},
watch: {
petId: {
handler() {
this.resetData();
this.loadData();
},
immediate: false
}
},
methods: {
// 重置数据
resetData() {
this.page = 1;
this.recordList = [];
this.isNoMore = false;
},
// 加载数据
loadData() {
if (this.isLoading || this.isNoMore) {
return;
}
if (!this.petId) {
return;
}
this.isLoading = true;
getPetPrecheckRecords({
pet_id: [this.petId],
order_id: 0
})
.then((res) => {
this.isLoading = false;
if (res.code === 0 && res.data) {
const list = res.data.list || []
if (this.page === 1) {
this.recordList = list;
} else {
this.recordList = [...this.recordList, ...list];
}
this.isNoMore = list.length < this.pageSize;
this.petInfo = res.data.pet;
} else {
this.isNoMore = true;
}
})
.catch((err) => {
this.isLoading = false;
console.error('获取服务记录失败:', err);
uni.showToast({
title: err?.msg || err?.message || '获取服务记录失败',
icon: 'none'
});
});
},
// 加载更多
loadMore() {
if (this.isNoMore || this.isLoading) {
return;
}
this.page++;
this.loadData();
},
formatGender(val) {
if (!val) return '--';
const v = String(val).toLowerCase();
if (v === 'male' || v === '男') return '男生';
if (v === 'female' || v === '女') return '女生';
return val;
},
formatTimeRange(item) {
const time = item.reservation_time || item.created_at;
if (!time) return '--';
if (typeof time === 'string' && /^\d{4}-\d{2}-\d{2}[\s\S]+/.test(time)) {
return time;
}
return this.formatTime(time);
},
formatTime(time) {
if (!time) return '--';
if (typeof time === 'number') {
const date = new Date(time * 1000);
return this.formatDateTime(date);
}
const date = new Date(time);
return this.formatDateTime(date);
},
formatDateTime(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}`;
},
// 跳转到预检详情页面
goToDetail(item) {
if (!item) {
return;
}
// 获取订单ID可能是 order_id 或 id
const orderId = item.order_id || item.id;
if (!orderId) {
uni.showToast({
title: '订单信息异常',
icon: 'none'
});
return;
}
// 跳转到预检详情页面(筛查报告)
uni.navigateTo({
url: `/pages/client/screening/details?orderId=${orderId}&petId=${this.petId}`
});
}
}
};
</script>
<style lang="scss" scoped>
.service-record {
width: 100%;
height: 100%;
box-sizing: border-box;
&.service-record--empty {
background: #fff;
}
.record-scroll {
width: 100%;
height: 100%;
}
.record-list {
width: calc(100% - 40rpx);
margin: 0 auto;
background: #fff;
padding: 0rpx 20rpx;
padding-top: 20rpx;
box-sizing: border-box;
.record-item {
display: flex;
background: #fff;
border-radius: 24rpx;
margin-bottom: 24rpx;
overflow: hidden;
position: relative;
border: 2rpx solid #FF19A0;
box-sizing: border-box;
cursor: pointer;
.record-content {
flex: 1;
padding: 32rpx 88rpx 32rpx 32rpx;
display: flex;
align-items: center;
gap: 32rpx;
min-width: 0;
position: relative;
z-index: 3;
.pet-info {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8rpx;
flex-shrink: 0;
width: 100rpx;
.pet-name {
font-size: 24rpx;
color: #272427;
line-height: 1.3;
}
.pet-gender {
font-size: 24rpx;
color: #9B939A;
line-height: 1.3;
}
}
.divider {
width: 2rpx;
height: 88rpx;
background: #ececec;
flex-shrink: 0;
}
.order-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 20rpx;
align-items: flex-start;
min-width: 0;
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16rpx;
width: 100%;
.label {
font-size: 24rpx;
color: #9B939A;
flex-shrink: 0;
}
.value {
font-size: 28rpx;
color: #272427;
text-align: right;
font-weight: 500;
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
.status-badge {
width: 60rpx;
background: #FF19A0;
display: flex;
align-items: center;
justify-content: center;
writing-mode: vertical-rl;
text-orientation: upright;
position: absolute;
right: 0;
top: 0;
bottom: 0;
z-index: 2;
border-radius: 0 22rpx 22rpx 0;
.status-text {
font-size: 28rpx;
color: #fff;
font-weight: 500;
letter-spacing: 4rpx;
}
}
}
.load-more {
display: flex;
justify-content: center;
align-items: center;
padding: 40rpx 0;
.no-more-text {
font-size: 24rpx;
color: #999999;
}
}
.empty-state {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 120rpx 0;
.empty-img {
width: 426rpx;
height: 320rpx;
margin-bottom: 24rpx;
}
.empty-text {
font-size: 28rpx;
color: #999999;
}
}
}
}
</style>

View File

@ -0,0 +1,29 @@
<template>
<view class="split-view" :style="{ height: height, background:bgColor }"></view>
</template>
<script>
export default {
props: {
bgColor: {
default: '#F0F0F0',
type: String,
},
height: {
default: '10rpx',
type: String,
},
},
data() {
return {}
},
mounted() {},
methods: {},
}
</script>
<style lang="scss" scoped>
.split-view {
width: 100%;
}
</style>

View File

@ -0,0 +1,112 @@
<template>
<view class="pay-success-modal" @click.stop="">
<view class="body-view" @click.stop="">
<image
v-if="showImg"
:src="imgSrc"
class="pay-success-img"
:style="[imgStyle]"
mode="aspectFit"
/>
<text class="app-fc-main fs-40 app-font-bold-700">{{ title }}</text>
<text class="app-fc-normal fs-28 tip-text">{{ message }}</text>
<view class="handle-btn" @click.stop="okAction">
<text class="app-fc-white fs-32">{{okText}}</text>
</view>
</view>
</view>
</template>
<script>
export default {
props: {
title: {
type: String,
default: "",
},
message: {
type: String,
default: "",
},
showImg: {
type: Boolean,
default: true,
},
imgSrc: {
type: String,
default: require("@/static/images/success_icon.png"),
},
imgStyle: {
type: Object,
default: () => {
return {};
},
},
okText: {
type: String,
default: "确定",
},
},
data() {
return {};
},
methods: {
closeAction() {
this.$emit("close");
},
okAction() {
this.$emit("ok");
},
},
};
</script>
<style lang="scss" scoped>
.pay-success-modal {
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.5);
position: fixed;
z-index: 100;
top: 0;
left: 0;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
.body-view {
width: calc(100% - 132rpx);
padding: 60rpx 10rpx;
background-color: #fff;
border-radius: 40rpx;
margin-bottom: 10vh;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
.pay-success-img {
width: 100rpx;
height: 100rpx;
flex-shrink: 0;
margin-bottom: 42rpx;
}
.tip-text {
margin-top: 32rpx;
}
.handle-btn {
margin-top: 52rpx;
width: 216rpx;
height: 80rpx;
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
background-color: $app_color_main;
}
}
}
</style>

176
src/components/TabBar.vue Normal file
View File

@ -0,0 +1,176 @@
<template>
<view class="tab-bar" id="tab-bar">
<view class="tab-bar-inner">
<view v-for="item in tabList" :key="item.pageId" class="tab-item" :class="{
'tab-item--active': activePageId === item.pageId,
'tab-item--special': item.isSpecial
}" @click="handleTabClick(item.pageId)">
<image class="tab-icon" :src="activePageId === item.pageId ? item.selectedIconPath : item.iconPath"
mode="aspectFit" />
<text v-if="!item.isSpecial" class="tab-text"
:class="activePageId === item.pageId ? 'tab-text--active' : 'tab-text--inactive'">
{{ item.text }}
</text>
</view>
</view>
</view>
</template>
<script>
/**
* TabBar 配置
* 集中管理所有 Tab 项的配置信息
*/
import {
imgPrefix
} from "@/utils/common";
const TAB_CONFIG = [{
pageId: "homePage",
iconPath: `${imgPrefix}tabBar-home.png`,
selectedIconPath: `${imgPrefix}tabBar-selectedHome.png`,
text: "首页",
isSpecial: false,
},
{
pageId: "filesPage",
iconPath: `${imgPrefix}tabBar-record.png`,
selectedIconPath: `${imgPrefix}tabBar-selectedRecord.png`,
text: "档案",
isSpecial: false,
},
{
pageId: "reservationPage",
iconPath: `${imgPrefix}tabbar-reservation.gif`,
selectedIconPath: `${imgPrefix}tabbar-bath.png`,
text: "立即预约",
isSpecial: true, // 特殊样式标记(中间突出的按钮)
},
{
pageId: "shopPage",
iconPath: `${imgPrefix}tabBar-mall.png`,
selectedIconPath: `${imgPrefix}tabBar-selectdMall.png`,
text: "商城",
isSpecial: false,
},
{
pageId: "minePage",
iconPath: `${imgPrefix}tabBar-mine.png`,
selectedIconPath: `${imgPrefix}tabBar-selectedMine.png`,
text: "我的",
isSpecial: false,
},
];
export default {
name: "TabBar",
props: {
activePageId: {
type: String,
default: "",
},
},
computed: {
tabList() {
return TAB_CONFIG;
},
},
methods: {
/**
* 处理 Tab 点击事件
* @param {String} pageId - 页面ID
*/
handleTabClick(pageId) {
if (this.activePageId === pageId) {
return; // 避免重复点击
}
this.$emit("changeTab", [pageId]);
},
},
};
</script>
<style lang="scss" scoped>
.tab-bar {
width: 100vw;
box-sizing: border-box;
position: fixed;
left: var(--window-left);
right: var(--window-right);
bottom: 0;
z-index: 998;
background: #fff;
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
border-radius: 32rpx 32rpx 0px 0px;
box-shadow: 0px 0px 22px 0px rgba(0, 0, 0, 0.1);
.tab-bar-inner {
background: #fff;
border-radius: 16rpx 16rpx 0px 0px;
display: flex;
align-items: center;
justify-content: space-around;
position: relative;
}
.tab-item {
padding: 0rpx 50rpx;
padding-top: 24rpx;
flex: 1;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
position: relative;
background: transparent;
transition: all 0.3s ease;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
.tab-icon {
width: 46rpx;
height: 46rpx;
margin-bottom: 9rpx;
transition: transform 0.3s ease;
}
.tab-text {
font-size: 20rpx;
transition: color 0.3s ease;
&--active {
color: $app_fc_mark;
}
&--inactive {
color: #AAA2A9;
}
}
// 激活状态
&--active {
.tab-icon {
transform: scale(1.1);
}
}
// 特殊样式(中间突出的按钮)
&--special {
.tab-icon {
position: absolute;
left: 0;
bottom: -76rpx;
width: 120rpx;
height: 120rpx;
color: #FFFFFF;
padding: 20rpx 22rpx;
}
}
// 点击反馈
&:active {
opacity: 0.7;
}
}
}
</style>

192
src/components/TabsList.vue Normal file
View File

@ -0,0 +1,192 @@
<template>
<view class="flex-row-start tabs-list" :style="[customStyle]">
<view class="tabs-scroll-view">
<scroll-view
:scroll-x="isScroll"
:scroll-with-animation="isScroll"
:scroll-left="tabScrollWidth"
@scroll="onTabScroll"
>
<view class="list-wrapper" :class="flexClass">
<view
class="tabs-item"
:class="{ first: index === 0, last: index === list.length - 1 }"
v-for="(item, index) in list"
:key="item[idKey]"
@click="changeTab(item, index)"
>
<view
class="flex-center fs-28 item-inner"
:class="{ 'fs-32 app-fc-main active ali-puhui-bold': index === curIndex }"
>
{{ item[nameKey] }}
<view class="item-border" v-if="showBorder"></view>
</view>
</view>
</view>
</scroll-view>
</view>
<view v-if="showFilterIcon" class="tabs-right" @click="showFilterView">
<!-- <image class="right-icon" src="https://oss.chuanglingmeng.com/cl/common/more.png" /> -->
</view>
<slot v-else name="right" />
</view>
</template>
<script>
export default {
name: "TabsList",
props: {
list: {
type: Array,
default: () => [],
},
isScroll: {
type: Boolean,
default: false,
},
currentIndex: {
type: Number,
default: 0,
},
nameKey: {
type: String,
default: "name",
},
idKey: {
type: String,
default: "id",
},
flexClass: {
type: String,
default: "flex-row-start",
},
showFilterIcon: {
type: Boolean,
default: false,
},
customStyle: {
type: Object,
default: () => {},
},
showBorder: {
type: Boolean,
default: true,
},
},
data() {
return {
curIndex: 0,
tabScrollWidth: 0,
};
},
watch: {
currentIndex: {
handler(value) {
this.curIndex = value;
},
immediate: true,
},
curIndex: {
async handler(value) {
if (!this.isScroll) return;
// 计算scroll-view应该移动的距离
const listData = await this.getElementStyle(".list-wrapper");
const tabItemData = await this.getElementStyle(".tabs-item");
const listWidth = listData?.[0]?.width || 0;
const listLeft = listData?.[0]?.left || 0;
const tabItemWidth = tabItemData?.[value]?.width || 0;
const tabItemLeft = tabItemData?.[value]?.left || 0;
this.tabScrollWidth =
tabItemLeft + tabItemWidth / 2 - listWidth / 2 - listLeft;
},
immediate: true,
},
},
methods: {
onTabScroll() {},
changeTab(item, index) {
this.curIndex = index;
this.$emit("change", item, index);
},
getElementStyle(el) {
return new Promise((resolve) => {
this.$nextTick(() => {
const query = uni.createSelectorQuery().in(this);
query
.selectAll(el)
.boundingClientRect()
.exec((data) => {
resolve(data[0]);
});
});
});
},
showFilterView() {
this.$emit("showFilterView");
},
},
};
</script>
<style lang="scss" scoped>
.tabs-list {
width: 100%;
box-sizing: border-box;
background: #fff;
border-top: 2rpx solid #f4f4f4;
.tabs-scroll-view {
flex: 1;
width: 100%;
}
.list-wrapper {
padding: 0;
.tabs-item {
padding: 0 20rpx;
height: 100rpx;
box-sizing: border-box;
position: relative;
.item-inner {
white-space: nowrap;
text-align: center;
position: relative;
height: 100%;
box-sizing: border-box;
color: #666666;
.item-border {
position: absolute;
bottom: 12rpx;
left: 50%;
transform: translateX(-50%);
width: 24rpx;
height: 10rpx;
border-radius: 100px;
background: transparent;
}
&.active {
// font-weight: bold;
color: $app_fc_main;
.item-border {
background: $app_color_main;
}
}
}
}
}
.tabs-right {
width: 90rpx;
height: 90rpx;
background: #fff;
box-shadow: -5rpx 1rpx 6rpx 0 rgba(0, 0, 0, 0.04);
display: flex;
justify-content: center;
align-items: center;
.right-icon {
width: 32rpx;
height: 32rpx;
}
}
}
</style>

View File

@ -0,0 +1,532 @@
<template>
<view class="coupon-item" :class="{ disabled: disabled, disabled: past}" @click.stop="$emit('jumpToDetails', data)">
<view class="flex-row-start" :class="[data.expire_coupon == 1 ? 'item-top2' : 'item-top']">
<view v-if="data.tab" class="tab-label">{{ data.tab }}</view>
<image v-if="data.expire_coupon == 1" class="expireCoupons"
src="https://activity.wagoo.live/expire-coupons.png" mode="aspectFit" />
<image v-if="data.new_flag == 1" class="initiation" src="https://activity.wagoo.live/initiation-rite.png"
mode="aspectFit" />
<view class="coupon-content-wrapper">
<view class="coupon-info-left">
<text class="coupon-title">{{ data.coupon_title }}</text>
<text class="coupon-time">
有效期至{{ data.end_time || data.start_time }}
</text>
</view>
<view class="coupon-info-right">
<view class="coupon-price-wrapper">
<text class="coupon-price-number">{{ couponPrice }}</text>
<text class="coupon-price-unit"></text>
</view>
<text class="coupon-condition">
{{ `${upPrice}` === "0" ? "无门槛立减" : `${couponPrice}${upPrice}` }}
</text>
</view>
</view>
<view v-if="showOptBtn" class="flex-center app-fc-white item-btn" @click="useCoupon">
{{ optBtnText }}
</view>
<view v-if="showSelectIcon && !showOptBtn" class="flex-center">
<image v-if="isSelected" class="select-icon-img" src="@/static/images/cart_checked.png"
mode="aspectFit" />
<image v-else class="select-icon-img" src="@/static/images/unchecked.png" mode="aspectFit" />
</view>
<image v-if="disabled" class="used-stamp" :src="usedStampImg" mode="aspectFit" />
<image v-if="past" class="used-stamp" :src="imgPrefix + 'pastDue.png'" mode="aspectFit" />
<view v-else>
<slot name="status" />
</view>
</view>
<view class="item-bottom">
<view class="flex-row-between" @click.stop="showGuide = !showGuide">
<view class="flex-row-start" @click.stop="showGuide = !showGuide">
<text class="rules-label">使用规则</text>
<image class="arrow-down" :class="{ 'arrow-up': showGuide }" src="@/static/images/arrow_up.png"
mode="widthFix" />
</view>
<view v-if="showUseLink && !disabled && !past" class="use-btn" @click.stop="$emit('useCoupon', data)">
<text class="use-btn-text">去使用</text>
</view>
</view>
<view v-if="showGuide" class="guide-details">
{{ data.coupon_desc }}
</view>
<view class="circle"></view>
<view class="circle right"></view>
</view>
</view>
</template>
<script>
import moment from "moment";
import { imgPrefix } from "@/utils/common";
export default {
props: {
data: {
type: Object,
default: () => { },
},
// 是否展示操作按钮:使用/领取
showOptBtn: {
type: Boolean,
default: true,
},
// 操作按钮文本
optBtnText: {
type: String,
default: "使用",
},
disabled: {
type: Boolean,
default: false,
},
showCountDown: {
type: Boolean,
default: false,
},
showSelectIcon: {
type: Boolean,
default: false,
},
// 是否展示底部“去使用”按钮
showUseLink: {
type: Boolean,
default: true,
},
isSelected: {
type: Boolean,
default: false,
},
past: {
type: Boolean,
default: false,
}
},
data() {
return {
showGuide: false,
usedStampImg: require('@/pages/client/coupon/static/coupon_used.png'),
imgPrefix,
};
},
computed: {
couponPrice() {
return +this.data.coupon_amount || 0;
},
upPrice() {
return +this.data.coupon_min_spend || 0;
},
startTime() {
return moment(this.data.add_time * 1000).format("YYYY-MM-DD");
},
endTime() {
return moment(this.data.guoqi_time ? this.data.guoqi_time * 1000 : this.data.end_time * 1000).format(
"YYYY-MM-DD");
},
time() {
const seconds = this.data.daojishi || 0;
const day = Math.floor(seconds / 3600 / 24) || 0;
const hour = Math.floor((seconds - day * 24 * 3600) / 3600) || 0;
const minutes = Math.floor((seconds - hour * 3600) / 60) || 0;
const second = seconds - hour * 3600 - minutes * 60 || 0;
return {
day,
hour,
minutes,
second,
};
},
},
mounted() {
console.log(this.data, '???22')
},
methods: {
useCoupon() {
this.$emit("useCoupon", this.data);
},
},
};
</script>
<style lang="scss" scoped>
.coupon-item {
width: 686rpx;
background: #ffffff;
display: flex;
flex-direction: column;
justify-self: flex-start;
align-items: stretch;
overflow: hidden;
border-radius: 16rpx;
&.disabled,
&.expired {
.tab-label {
background: #AFA5AE !important;
border-radius: 8px 0px 8px 0px;
}
}
&.expired {
.item-top2 .tab-label {
background: #AFA5AE !important;
border-radius: 8px 0px 8px 0px;
}
}
&.disabled {
.coupon-title {
color: #999 !important;
}
.coupon-time {
color: #bbb !important;
}
.coupon-price-number,
.coupon-price-unit,
.coupon-condition {
color: #bdbdbd !important;
}
.item-bottom {
border-top: 2rpx dashed #e0e0e0;
}
.rules-label,
.guide-details {
color: #bbb !important;
}
.arrow-down {
opacity: 0.6;
}
}
.item-top {
flex: 1;
padding: 40rpx 32rpx 28rpx;
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
.tab-label {
position: absolute;
left: 0;
top: 0;
padding: 10rpx 24rpx;
background: #FF19A0;
color: #fff;
font-size: 24rpx;
z-index: 2;
border-radius: 8px 0px 8px 0px;
}
.initiation {
position: absolute;
left: 0;
top: 0;
width: 69px;
height: 15px;
z-index: 1;
}
.coupon-content-wrapper {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
margin-left: 0;
padding-top: 20rpx;
}
.coupon-info-left {
display: flex;
flex-direction: column;
align-items: flex-start;
flex: 1;
min-width: 0;
.coupon-title {
font-size: 32rpx;
font-weight: 500;
color: #272427;
line-height: 1.4;
}
.coupon-time {
font-size: 24rpx;
color: #999;
margin-top: 12rpx;
line-height: 1.4;
}
}
.coupon-info-right {
display: flex;
flex-direction: column;
align-items: flex-end;
margin-left: 24rpx;
flex-shrink: 0;
.coupon-price-wrapper {
display: flex;
align-items: baseline;
line-height: 1;
.coupon-price-number {
font-size: 64rpx;
font-weight: 500;
color: #FF19A0;
line-height: 1;
}
.coupon-price-unit {
font-size: 24rpx;
color: #FF19A0;
margin-left: 4rpx;
}
}
.coupon-condition {
font-size: 24rpx;
color: #FF19A0;
margin-top: 8rpx;
text-align: right;
}
}
.item-btn {
width: 116rpx;
height: 60rpx;
background: $app_color_main;
border-radius: 60rpx;
margin-left: 24rpx;
flex-shrink: 0;
}
.select-icon-img {
width: 32rpx;
height: 32rpx;
flex-shrink: 0;
margin-left: 24rpx;
}
.used-stamp {
position: absolute;
left: 60%;
top: 50%;
transform: translate(-50%, -50%);
width: 128rpx;
height: 128rpx;
z-index: 3;
}
}
.item-top2 {
flex: 1;
padding: 40rpx 32rpx 28rpx;
opacity: 0.6;
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
.tab-label {
position: absolute;
left: 0;
top: 0;
padding: 10rpx 24rpx;
background: #FF19A0;
color: #fff;
font-size: 24rpx;
z-index: 2;
border-top-left-radius: 40rpx;
border-bottom-left-radius: 40rpx;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.initiation {
position: absolute;
left: 0;
top: 0;
width: 69px;
height: 15px;
z-index: 1;
}
.expireCoupons {
position: absolute;
right: 47rpx;
top: 46rpx;
width: 112rpx;
height: 70rpx;
z-index: 1;
}
.coupon-content-wrapper {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
margin-left: 0;
padding-top: 20rpx;
}
.coupon-info-left {
display: flex;
flex-direction: column;
align-items: flex-start;
flex: 1;
min-width: 0;
.coupon-title {
font-size: 32rpx;
font-weight: 500;
color: #272427;
line-height: 1.4;
}
.coupon-time {
font-size: 24rpx;
color: #999;
margin-top: 12rpx;
line-height: 1.4;
}
}
.coupon-info-right {
display: flex;
flex-direction: column;
align-items: flex-end;
margin-left: 24rpx;
flex-shrink: 0;
.coupon-price-wrapper {
display: flex;
align-items: baseline;
line-height: 1;
.coupon-price-number {
font-size: 64rpx;
font-weight: 500;
color: #FF19A0;
line-height: 1;
}
.coupon-price-unit {
font-size: 24rpx;
color: #FF19A0;
margin-left: 4rpx;
}
}
.coupon-condition {
font-size: 24rpx;
color: #FF19A0;
margin-top: 8rpx;
text-align: right;
}
}
.item-btn {
width: 116rpx;
height: 60rpx;
background: $app_color_main;
border-radius: 60rpx;
margin-left: 24rpx;
flex-shrink: 0;
}
.select-icon-img {
width: 32rpx;
height: 32rpx;
flex-shrink: 0;
margin-left: 24rpx;
}
.used-stamp {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 100rpx;
height: 100rpx;
z-index: 3;
}
}
.item-bottom {
border-top: 2rpx solid #f7f3f7;
margin: 0 32rpx;
padding: 20rpx 0;
position: relative;
.rules-label {
font-size: 24rpx;
color: #666;
}
.arrow-down {
width: 20rpx;
height: 20rpx;
transform: rotate(180deg);
margin-left: 12rpx;
&.arrow-up {
transform: rotate(0deg);
}
}
.reset-time {
margin-right: 10rpx;
}
.guide-details {
font-size: 26rpx;
color: #666;
margin: 20rpx 0;
line-height: 1.5;
}
.circle {
position: absolute;
top: 0%;
left: -32rpx;
transform: translate(-50%, -50%);
width: 40rpx;
height: 40rpx;
background: #ffecf3;
clip-path: polygon(0 0, 0 100%, 100% 50%);
&.right {
right: -32rpx;
left: unset;
transform: translate(50%, -50%);
clip-path: polygon(100% 0, 100% 100%, 0 50%);
}
}
.use-btn {
display: flex;
align-items: center;
justify-content: center;
background: #FF19A0;
border-radius: 100rpx;
padding: 8rpx 16rpx;
flex-shrink: 0;
}
.use-btn-text {
font-size: 20rpx;
color: #FFFFFF;
text-shadow: 0 0 8rpx rgba(255, 255, 255, 0.6);
}
}
}
</style>

View File

@ -0,0 +1,165 @@
<template>
<view>
<select-modal class="coupon-list-modal" title="优惠券" @close="$emit('close', false)">
<!-- <view>{{couponList.title}}</view> -->
<image class="coupon-icon" src="https://activity.wagoo.live/coupon-icon.png" />
<view class="coupon-list-content">
<text class="fs-28 app-fc-normal coupon-list-title">
待使用{{ couponList.length }}
</text>
<view
v-for="item in couponList"
:key="item.distribution_id || item.id || item"
class="coupon-item-wrapper"
>
<coupon-item
:data="item"
@useCoupon="useCoupon"
@jumpToDetails="useCoupon"
:showOptBtn="showOptBtn"
:is-selected="selectedItem.distribution_id === item.distribution_id"
:show-select-icon="true"
:show-use-link="false"
/>
</view>
<!-- <template v-else>
<coupon-item
v-for="item in availableList"
:key="item"
:data="item"
@useCoupon="useCoupon"
@jumpToDetails="useCoupon"
:showOptBtn="showOptBtn"
:is-selected="selectedItem.fafang_id === item.fafang_id"
:show-select-icon="true"
/>
<coupon-item
v-for="item in disableList"
:key="item"
:data="item"
:showOptBtn="showOptBtn"
:disabled="true"
:show-select-icon="false"
/>
</template> -->
</view>
</select-modal>
</view>
</template>
<script>
import SelectModal from "../select-modal.vue";
import CouponItem from "./CouponItem.vue";
export default {
props: {
couponList: {
type: Array,
default: () => [],
},
isShowAll: {
type: Boolean,
default: false,
},
price: {
type: Number,
default: 0,
},
selectedItem: {
type: Object,
default: () => {
return {};
},
},
showOptBtn: {
type: Boolean,
default: false,
}
},
components: {
SelectModal,
CouponItem,
},
options: {
styleIsolation: "shared",
},
data() {
return {
showGuide: false,
couponGuide: "",
};
},
computed: {
//可用列表
availableList() {
return this.couponList.filter((data) => Number(this.price) >= Number(data.up_money))
},
//不可以列表
disableList() {
return this.couponList.filter((data) => Number(this.price) < Number(data.up_money))
}
},
mounted() {
console.log(this.couponList, '???11')
},
methods: {
useCoupon(data) {
this.$emit("useCoupon", data);
},
},
};
</script>
<style lang="scss" scoped>
.coupon-list-modal {
.coupon-icon {
position: absolute;
top: 0;
left: 40rpx;
width: 218rpx;
height: 210rpx;
z-index: 0;
}
.coupon-list-content {
padding: 44rpx 32rpx 24rpx;
max-height: 956rpx;
overflow-y: auto;
box-sizing: border-box;
position: relative;
z-index: 1;
.coupon-list-title {
display: block;
margin-bottom: 24rpx;
}
.coupon-item-wrapper + .coupon-item-wrapper {
margin-top: 24rpx;
}
}
::v-deep {
.selected-modal .model-container {
background: #fff0f5;
position: relative;
}
}
}
.coupon-guide-modal {
::v-deep {
.use-guide-modal {
z-index: 100;
}
}
.guide-content {
max-height: 400rpx;
overflow-y: auto;
}
}
</style>

View File

@ -0,0 +1,179 @@
<template>
<view class="service-coupon" @click.stop="$emit('jumpToDetails', serviceCoupon)">
<view class="service-img">
<image :src="serviceCoupon.fuwuquan_pic" mode="aspectFill"/>
</view>
<view class="service-info">
<view class="service-name">
<text class="fs-28 app-fc-main ali-puhui-bold">
{{ serviceCoupon.name }}
</text>
</view>
<view class="service-tags">
<text v-for="(item, index) in serviceTags" :key="index" class="tag fs-20" :class="{'active-tag': index===0}">
{{item}}
</text>
</view>
<view class="service-weight fs-24">适用于{{ serviceCoupon.weight_name }}</view>
<view class="service-price-info">
<view class="price-container">
<text class="current-price-unit fs-24 ali-puhui-bold">¥</text>
<text class="current-price fs-44 ali-puhui-bold">{{ serviceCoupon.price }}</text>
<text class="original-price fs-24">¥{{ serviceCoupon.old_price }}</text>
</view>
</view>
<view class="service-sales-info">
<view class="sales-info fs-20 ali-puhui-regular">
已售 {{ serviceCoupon.xiaoliang }} | 剩余 {{ serviceCoupon.kucun }}
</view>
</view>
</view>
<view class="buy-btn" @click.stop="buyService">购买</view>
</view>
</template>
<script>
export default {
props: {
serviceCoupon: {
type: Object,
default: () => {
return {}
},
},
},
data() {
return {};
},
computed: {
serviceTags() {
if (this.serviceCoupon?.label) {
return this.serviceCoupon.label.split(',').filter(v => !!v)
}
return []
}
},
methods: {
buyService() {
this.$emit('buyService', this.serviceCoupon)
}
},
}
</script>
<style lang="scss" scoped>
.service-coupon {
width: 100%;
background-color: #fff;
border-radius: 40rpx;
margin-bottom: 20rpx;
padding: 20rpx;
display: flex;
flex-direction: row;
align-items: center;
position: relative;
box-sizing: border-box;
.service-img {
width: 220rpx;
height: 220rpx;
margin-right: 24rpx;
border-radius: 40rpx;
overflow: hidden;
background-color: #FFF0F5;
image {
width: 100%;
height: 100%;
}
}
.service-info {
display: flex;
flex-direction: column;
flex: 1;
.service-name {
// font-weight: bold;
margin-bottom: 16rpx;
}
.service-tags {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 15rpx;
.tag {
border: 1rpx solid #FE019B;
background-color: #FFF;
color: #FE019B;
padding: 2rpx 6rpx;
border-radius: 6rpx;
margin-right: 12rpx;
}
.active-tag {
background-color: #FE019B;
color: #ffffff;
}
}
.service-weight {
color: #726E71;
margin-bottom: 15rpx;
}
.service-price-info {
display: flex;
align-items: center;
margin-bottom: 10rpx;
.price-container {
.current-price-unit {
color: #FE019B;
// font-weight: bold;
}
.current-price {
color: #FE019B;
// font-weight: bold;
}
.original-price {
color: #726E71;
text-decoration: line-through;
margin-left: 12rpx;
}
}
}
.service-sales-info {
display: flex;
align-items: center;
.sales-info {
flex: 1;
color: #9B939A;
// font-weight: 400;
}
}
}
.buy-btn {
position: absolute;
bottom: 18rpx;
right: 24rpx;
width: 136rpx;
height: 60rpx;
background-color: #FE019B;
color: #fff;
font-size: 24rpx;
border-radius: 30rpx;
display: flex;
align-items: center;
justify-content: center;
}
}
</style>

View File

@ -0,0 +1,238 @@
<template>
<view
class="coupon-item"
:class="{ disabled: disabled }"
@click.stop="$emit('jumpToDetails', data)"
>
<view class="flex-row-start item-top">
<view class="flex-center coupon-left">
<text class="fs-24 app-fc-danger app-font-bold coupon-price">
¥<text class="fs-52 app-fc-danger app-font-bold coupon-price">{{
couponPrice
}}</text>
</text>
<text class="fs-24 app-fc-normal coupon-tip">
适用于{{ data.weight_name || "" }}
</text>
</view>
<view class="flex-center flex-1 coupon-info">
<text class="fs-32 app-fc-main">{{ data.name }}</text>
<text class="fs-24 app-fc-normal coupon-time">
{{ startTime }}{{ endTime }}
</text>
<view v-if="showOrderBtn" class="flex-row-end order-btn-wrapper">
<view
class="fs-28 app-fc-white flex-center order-btn"
@click="jumpToReservation"
>
立即预约
</view>
</view>
</view>
<view
v-if="showOptBtn"
class="flex-center app-fc-white item-btn"
@click="useCoupon"
>
{{ optBtnText }}
</view>
<view v-if="showSelectIcon && !showOptBtn" class="flex-center">
<image
v-if="isSelected"
class="select-icon-img"
src="@/static/images/cart_checked.png"
mode="aspectFit"
/>
<image
v-else
class="select-icon-img"
src="@/static/images/unchecked.png"
mode="aspectFit"
/>
</view>
<slot name="status" />
</view>
<view class="item-bottom" v-if="data.desc">
<view class="flex-row-start" @click="showGuide = !showGuide">
<text class="fs-24 app-fc-normal">使用规则</text>
<image
class="arrow-down"
:class="{ 'arrow-up': showGuide }"
src="@/static/images/arrow_up.png"
mode="widthFix"
/>
</view>
<view v-if="showGuide" class="fs-26 app-fc-normal guide-details">
{{ data.desc }}
</view>
<view class="circle"></view>
<view class="circle right"></view>
</view>
</view>
</template>
<script>
import moment from "moment";
export default {
props: {
data: {
type: Object,
default: () => {},
},
// 是否展示操作按钮:使用/领取
showOptBtn: {
type: Boolean,
default: true,
},
// 是否展示预约按钮
showOrderBtn: {
type: Boolean,
default: false,
},
showSelectIcon: {
type: Boolean,
default: false,
},
isSelected: {
type: Boolean,
default: false,
},
// 操作按钮文本
optBtnText: {
type: String,
default: "使用",
},
disabled: {
type: Boolean,
default: false,
},
},
data() {
return {
showGuide: false,
};
},
computed: {
couponPrice() {
return +this.data.price || 0;
},
startTime() {
return moment(this.data.add_time * 1000).format("YYYY-MM-DD");
},
endTime() {
return moment(this.data.youxiao_time * 1000).format("YYYY-MM-DD");
},
},
methods: {
useCoupon() {
this.$emit("useCoupon", this.data);
},
jumpToReservation() {
uni.navigateTo({
url: "/pageHome/reservation/index",
});
},
},
};
</script>
<style lang="scss" scoped>
.coupon-item {
width: 686rpx;
background: #ffffff;
border-radius: 40rpx 40rpx 40rpx 40rpx;
display: flex;
flex-direction: column;
justify-self: flex-start;
align-items: stretch;
margin-top: 24rpx;
overflow: hidden;
&.disabled {
opacity: 0.5;
}
.item-top {
flex: 1;
padding: 40rpx;
.item-btn {
width: 116rpx;
height: 60rpx;
background: $app_color_main;
border-radius: 60rpx;
}
.coupon-tip {
margin-top: 12rpx;
}
.coupon-info {
align-items: flex-start;
margin-left: 30rpx;
}
.coupon-time {
margin-top: 26rpx;
}
.order-btn-wrapper {
width: 100%;
.order-btn {
padding: 6rpx 12rpx;
border-radius: 10rpx;
background: $app_color_main;
margin-top: 18rpx;
float: right;
}
}
.coupon-left {
width: 30%;
}
.select-icon-img {
width: 32rpx;
height: 32rpx;
flex-shrink: 0;
}
}
.item-bottom {
border-top: 2rpx solid #f7f3f7;
margin: 0 32rpx;
padding: 20rpx 0;
position: relative;
.arrow-down {
width: 20rpx;
height: 20rpx;
transform: rotate(180deg);
margin-left: 12rpx;
&.arrow-up {
transform: rotate(0deg);
}
}
.guide-details {
margin: 20rpx 0;
}
.circle {
position: absolute;
top: 0%;
left: -32rpx;
transform: translate(-50%, -50%);
width: 30rpx;
height: 30rpx;
background: #fff0f5;
border-radius: 30rpx;
&.right {
right: -32rpx;
left: unset;
transform: translate(50%, -50%);
}
}
}
}
</style>

View File

@ -0,0 +1,141 @@
<template>
<view>
<select-modal
class="coupon-list-modal"
title="服务券"
@close="$emit('close', false)"
>
<image class="coupon-icon" src="https://activity.wagoo.live/coupon-icon.png"/>
<view class="coupon-list-content">
<text class="fs-28 app-fc-normal coupon-list-title">
待使用{{ availableList.length || 0 }}
</text>
<ServiceCouponItem
v-for="item in availableList"
:key="item"
:data="item"
@jumpToDetails="useService"
@useCoupon="useService"
:showOptBtn="false"
:disabled="false"
:is-selected="selectedItem.order_id === item.order_id"
:show-select-icon="true"
/>
<ServiceCouponItem
v-for="item in disableList"
:key="item"
:data="item"
:showOptBtn="false"
:disabled="true"
:show-select-icon="false"
/>
</view>
</select-modal>
</view>
</template>
<script>
import SelectModal from "../select-modal.vue";
import ServiceCouponItem from "@/components/coupon/ServiceCouponItem.vue";
export default {
props: {
serviceList: {
type: Array,
default: () => [],
},
selectedItem: {
type: Object,
default: () => {
return {};
},
},
weightId: {
type: Number,
default: 0,
},
price: {
type: Number,
default: 0,
}
},
components: {
ServiceCouponItem,
SelectModal,
},
options: {
styleIsolation: "shared",
},
data() {
return {
showGuide: false,
couponGuide: "",
};
},
computed: {
//可用列表
availableList() {
return this.serviceList.filter((data) => this.weightId === data.weight_id && Number(this.price) >= Number(data.price))
},
//不可以列表
disableList() {
return this.serviceList.filter((data) => this.weightId !== data.weight_id || Number(this.price) < Number(data.price))
}
},
mounted() {
},
methods: {
useService(data) {
this.$emit("useService", data);
},
},
};
</script>
<style lang="scss" scoped>
.coupon-list-modal {
.coupon-icon {
position: absolute;
top: 0;
left: 40rpx;
width: 218rpx;
height: 210rpx;
z-index: 0;
}
.coupon-list-content {
padding: 44rpx 32rpx 24rpx;
max-height: 956rpx;
overflow-y: auto;
box-sizing: border-box;
position: relative;
z-index: 1;
.coupon-list-title {
margin-bottom: 24rpx;
}
}
::v-deep {
.selected-modal .model-container {
background: #fff0f5;
position: relative;
}
}
}
.coupon-guide-modal {
::v-deep {
.use-guide-modal {
z-index: 100;
}
}
.guide-content {
max-height: 400rpx;
overflow-y: auto;
}
}
</style>

View File

@ -0,0 +1,95 @@
<template>
<view class="use-guide-modal" @click.stop="">
<view class="body-view" @click.stop="">
<text class="app-fc-main fs-36 app-font-bold-700">{{ title }}</text>
<view class="guide-content" v-html="content"></view>
<view class="flex-center handle-btn" @click.stop="okAction">
<text class="app-fc-white fs-32">{{ confirmText }}</text>
</view>
</view>
</view>
</template>
<script>
export default {
props: {
title: {
type: String,
default: "",
},
content: {
type: String,
default: "",
},
confirmText: {
type: String,
default: "确定",
},
},
data() {
return {};
},
methods: {
okAction() {
this.$emit("ok");
},
},
};
</script>
<style lang="scss" scoped>
.use-guide-modal {
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.5);
position: fixed;
top: 0;
left: 0;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
.body-view {
width: calc(100% - 132rpx);
padding: 60rpx 10rpx;
background-color: #fff;
border-radius: 40rpx;
margin-bottom: 10vh;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
.pay-success-img {
width: 100rpx;
height: 100rpx;
flex-shrink: 0;
margin-bottom: 42rpx;
}
.tip-text {
margin-top: 32rpx;
}
.handle-btn {
margin-top: 52rpx;
width: 490rpx;
height: 92rpx;
background: $app_color_main;
border-radius: 92rpx;
}
.guide-content {
width: 100%;
padding: 0 32rpx;
box-sizing: border-box;
max-height: 500rpx;
min-height: 200rpx;
overflow-y: auto;
overflow-x: hidden;
margin-top: 24rpx;
}
}
}
</style>

View File

@ -0,0 +1,27 @@
.picker-view {
height: 356rpx;
padding: 0 110rpx;
}
.picker-view-column {
font-size: 14px;
line-height: 34px;
text-align: center;
color: #3E4055;
font-family: PingFangSC-Bold;
}
.picker-view-column-innner {
/* height: calc(356rpx / 3);
line-height: calc(356rpx / 3); */
line-height: 60px;
}
/* 覆盖默认样式,样式可以按需自己改 */
.uni-picker-view-indicator {
background-color: rgba(106, 123, 255, 0.1);
}
.uni-picker-view-indicator::before,
.uni-picker-view-indicator::after {
content: none;
}

View File

@ -0,0 +1,53 @@
export default {
data() {
return {};
},
props: {
// 所有列选项数据
columns: {
type: Array,
default: () => []
},
// 每一列默认选中值数组,不传默认选中第一项
selectVals: {
type: Array,
default: () => []
}
},
computed: {
// 每一列选中项的索引,当默认选中值变化的时候这个值也要变化
indexArr: {
// 多维数组,深度监听
cache: false,
get() {
if (this.selectVals.length > 0) {
return this.columns.map((col, cIdx) => {
return col.findIndex((i) => i == this.selectVals[cIdx]);
});
} else {
return [].fill(0, 0, this.columns.length);
}
}
}
},
methods: {
onChange(e) {
const { value } = e.detail;
let ret = this.columns.map((item, index) => {
let idx = value[index];
if (idx < 0) {
idx = 0;
}
if (idx > item.length - 1) {
idx = item.length - 1;
}
return item[idx];
});
this.$emit('onChange', {
value: ret
});
}
}
};

View File

@ -0,0 +1,11 @@
<template>
<picker-view class="picker-view" :value="indexArr" @change="onChange" :indicator-style="`height: 60px`">
<picker-view-column class="picker-view-column" v-for="(col, colIdx) in columns" :key="colIdx">
<view class="picker-view-column-innner" v-for="(item, idx) in col" :key="idx">{{ item }}</view>
</picker-view-column>
</picker-view>
</template>
<script src="./index.js"></script>
<style lang="css" scoped src="./index.css"></style>

View File

@ -0,0 +1,57 @@
.date-selector {
width: 100%;
font-size: 12px;
color: #333;
}
.select-date-wrapper {
margin-bottom: 8px;
display: flex;
justify-content: space-between;
align-items: center;
}
.select-date {
padding: 10px;
flex: 1;
border-radius: 2px;
border: 1rpx solidrgba(6, 7, 46, 0.05);
font-size: 12px;
}
.select-date.active {
border-color: #6a7bff;
}
.select-date-placeholder {
color: rgba(6, 7, 46, 0.3);
}
.btn-group {
display: flex;
margin: 48rpx 0;
justify-content: space-between;
}
.btn-confirm {
width: 180px;
height: 40px;
line-height: 40px;
background: rgba(33, 58, 255, 0.85);
border-radius: 4px;
font-size: 14px;
color: #fff;
text-align: center;
}
.btn-cancel {
width: 144px;
height: 40px;
line-height: 38px;
text-align: center;
background: #fff;
border-radius: 4px;
border: 1rpx solid#979797;
font-size: 14px;
color: #06072e;
}

View File

@ -0,0 +1,209 @@
import DateTimePicker from '../dateTimePicker/index.vue';
import DateUtil from '../dateTimePicker/dateUtil';
import { DATE_TYPES } from '../dateTimePicker/constant';
export default {
components: {
DateTimePicker
},
data() {
return {
showStartDatePicker: false,
showEndDatePicker: false,
startDate: '',
endDate: '',
activeDate: 'startDate' // 正在处理哪一个日期值startDate/endDate
};
},
props: {
// 日期筛选模式1年月日默认2年月34年月日时分秒5时分秒6时分
mode: {
type: Number,
default: DATE_TYPES.YMD
},
// 默认开始日期
defaultStartDate: {
type: String,
default: ''
},
// 默认结束日期
defaultEndDate: {
type: String,
default: ''
},
// 可选的最小日期
minDate: {
type: String,
default: ''
},
// 可选的最大日期
maxDate: {
type: String,
default: ''
}
},
watch: {
mode() {
// 筛选模式更换时清空一下数据
this.resetData();
},
startDate() {
this.$emit('onChange', {
startDate: this.startDate,
endDate: this.endDate
});
},
endDate() {
this.$emit('onChange', {
startDate: this.startDate,
endDate: this.endDate
});
},
defaultStartDate: {
handler(defaultStartDate) {
if (!defaultStartDate) {
return;
}
if (this.mode == DATE_TYPES.HMS || this.mode == DATE_TYPES.HM) {
console.error('时分秒/时分模式不支持设置默认开始时间');
return;
}
if (DateUtil.isBefore(defaultStartDate, this.minDate)) {
console.warn(
`默认开始日期不可小于最小可选日期,已把默认开始日期设为最小可选日期。默认开始日期:${defaultStartDate},最小可选日期:${this.minDate}`
);
this.startDate = this.getModeFormatDateString(this.minDate);
} else {
this.startDate = this.getModeFormatDateString(defaultStartDate);
}
},
immediate: true
},
defaultEndDate: {
handler(defaultEndDate) {
if (!defaultEndDate) {
return;
}
if (this.mode == DATE_TYPES.HMS || this.mode == DATE_TYPES.HM) {
console.error('时分秒/时分模式不支持设置默认结束时间');
return;
}
if (DateUtil.isAfter(defaultEndDate, this.maxDate)) {
console.warn(
`默认结束日期不可大于最大可选日期,已把默认结束日期设为最大可选日期。默认结束日期:${defaultEndDate},最大可选日期:${this.maxDate}`
);
this.endDate = this.getModeFormatDateString(this.maxDate);
} else {
this.endDate = this.getModeFormatDateString(defaultEndDate);
}
},
immediate: true
},
minDate(val) {
if ((val && this.mode == DATE_TYPES.HMS) || this.mode == DATE_TYPES.HM) {
console.error('时分秒/时分模式不支持设置最小可选时间');
return;
}
},
maxDate(val) {
if ((val && this.mode == DATE_TYPES.HMS) || this.mode == DATE_TYPES.HM) {
console.error('时分秒/时分模式不支持设置最大可选时间');
return;
}
}
},
methods: {
onTapStartDate() {
this.showEndDatePicker = false;
if (!this.startDate) {
this.startDate = this.getModeFormatDateString(new Date());
}
this.activeDate = 'startDate';
this.showStartDatePicker = true;
},
onTapEndDate() {
this.showStartDatePicker = false;
if (!this.endDate) {
this.endDate = this.startDate;
}
this.activeDate = 'endDate';
this.showEndDatePicker = true;
},
onChangeStartDate(date) {
this.startDate = date;
},
onChangeEndDate(date) {
this.endDate = date;
},
validateInput() {
if (!this.startDate) {
uni.showToast({
title: '请选择开始时间',
icon: 'none'
});
return false;
} else if (!this.endDate) {
uni.showToast({
title: '请选择结束时间',
icon: 'none'
});
return false;
} else if (DateUtil.isAfter(this.startDate, this.endDate)) {
uni.showToast({
title: '结束时间不能小于开始时间',
icon: 'none'
});
return false;
}
return true;
},
onCancel() {
this.resetData();
},
onConfirm() {
if (this.validateInput()) {
this.$emit('onSubmit', {
startDate: this.startDate,
endDate: this.endDate
});
this.showStartDatePicker = false;
this.showEndDatePicker = false;
}
},
resetData() {
this.startDate = '';
this.endDate = '';
this.activeDate = 'startDate';
this.showStartDatePicker = false;
this.showEndDatePicker = false;
},
// 返回对应日期模式的时间字符串
getModeFormatDateString(date) {
let fmt = 'YYYY-MM-DD';
switch (this.mode) {
case DATE_TYPES.YM:
fmt = 'YYYY-MM';
break;
case DATE_TYPES.Y:
fmt = 'YYYY';
break;
case DATE_TYPES['YMD-HMS']:
fmt = 'YYYY-MM-DD HH:mm:ss';
break;
case DATE_TYPES.HMS:
fmt = 'HH:mm:ss';
break;
case DATE_TYPES.HM:
fmt = 'HH:mm';
break;
default:
break;
}
return DateUtil.formatDate(date, fmt);
}
}
};

View File

@ -0,0 +1,41 @@
<template>
<view class="date-selector">
<view class="select-date-wrapper">
<view class="select-date" :class="{ active: activeDate == 'startDate' }" @tap="onTapStartDate">
<view class="select-date-value" v-if="startDate">{{ startDate }}</view>
<view class="select-date-placeholder" v-else>请选择时间</view>
</view>
<view style="margin: 0 16px"></view>
<view class="select-date" :class="{ active: activeDate == 'endDate' }" @tap="onTapEndDate">
<view class="select-date-value" v-if="endDate">{{ endDate }}</view>
<view class="select-date-placeholder" v-else>请选择时间</view>
</view>
</view>
<DateTimePicker
v-if="showStartDatePicker"
@onChange="onChangeStartDate"
:defaultDate="startDate"
:minDate="minDate || ''"
:maxDate="endDate || maxDate || ''"
:mode="mode"
/>
<DateTimePicker
v-if="showEndDatePicker"
@onChange="onChangeEndDate"
:defaultDate="endDate"
:minDate="startDate || minDate || ''"
:maxDate="maxDate || ''"
:mode="mode"
/>
<view class="btn-group" v-if="showStartDatePicker || showEndDatePicker">
<view class="btn-cancel" @tap="onCancel">取消</view>
<view class="btn-confirm" @tap="onConfirm">确定</view>
</view>
</view>
</template>
<script src="./index.js"></script>
<style lang="css" scoped src="./index.css"></style>

View File

@ -0,0 +1,15 @@
// 日期模式
export const DATE_TYPES = {
// 年月日
YMD: 1,
// 年月
YM: 2,
// 年份
Y: 3,
// 年月日时分秒
'YMD-HM': 4,
// 时分秒
HMS: 5,
// 时分
HM: 6
};

View File

@ -0,0 +1,93 @@
/**
* 日期时间格式化
* @param {Date} date 要格式化的日期对象
* @param {String} fmt 格式化字符串egYYYY-MM-DD HH:mm:ss
* @returns 格式化后的日期字符串
*/
function formatDate(date, fmt) {
if (typeof date == 'string') {
date = new Date(handleDateStr(date));
}
const o = {
'M+': date.getMonth() + 1, // 月份
'd+': date.getDate(), // 日
'D+': date.getDate(), // 日
'H+': date.getHours(), // 小时
'h+': date.getHours(), // 小时
'm+': date.getMinutes(), // 分
's+': date.getSeconds(), // 秒
'q+': Math.floor((date.getMonth() + 3) / 3), // 季度
S: date.getMilliseconds() // 毫秒
};
if (/([y|Y]+)/.test(fmt)) {
fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').slice(4 - RegExp.$1.length));
}
for (const k in o) {
if (new RegExp('(' + k + ')').test(fmt)) {
fmt = fmt.replace(RegExp.$1, RegExp.$1.length == 1 ? o[k] : ('00' + o[k]).slice(('' + o[k]).length));
}
}
return fmt;
}
/**
* 处理时间字符串兼容ios下new Date()返回NaN问题
* @param {*} dateStr 日期字符串
* @returns
*/
function handleDateStr(dateStr) {
return dateStr.replace(/\-/g, '/');
}
/**
* 判断日期1是否在日期2之前即日期1小于日期2
* @param {Date} date1
* @param {Date} date2
* @returns
*/
function isBefore(date1, date2) {
if (typeof date1 == 'string') {
date1 = new Date(handleDateStr(date1));
}
if (typeof date2 == 'string') {
date2 = new Date(handleDateStr(date2));
}
return date1.getTime() < date2.getTime();
}
/**
* 判断日期1是否在日期2之后即日期1大于日期2
* @param {Date} date1
* @param {Date} date2
* @returns
*/
function isAfter(date1, date2) {
if (typeof date1 == 'string') {
date1 = new Date(handleDateStr(date1));
}
if (typeof date2 == 'string') {
date2 = new Date(handleDateStr(date2));
}
return date1.getTime() > date2.getTime();
}
/**
* 检查传入的字符串是否能转换为有效的Date对象
* @param {String} date
* @returns {Boolean}
*/
function isValid(date) {
return new Date(date) !== 'Invalid Date' && !isNaN(new Date(date));
}
export default {
formatDate,
handleDateStr,
isBefore,
isAfter,
isValid
};

View File

@ -0,0 +1,377 @@
import CustomPickerView from '../customPickerView/index.vue';
import DateUtil from '../dateTimePicker/dateUtil';
import { DATE_TYPES } from './constant';
export default {
components: {
CustomPickerView
},
props: {
// 日期模式1年月日默认2年月3年份4年月日时分5时分秒6时分
mode: {
type: Number,
default: DATE_TYPES.YMD
},
// 可选的最小日期,默认十年前
minDate: {
type: String,
default: ''
},
// 可选的最大日期,默认十年后
maxDate: {
type: String,
default: ''
},
// 默认选中日期(注意要跟日期模式对应)
defaultDate: {
type: String,
default: ''
}
},
data() {
return {
selectYear: new Date().getFullYear(),
selectMonth: new Date().getMonth() + 1, // 选中的月份1~12
selectDay: new Date().getDate(),
selectHour: new Date().getHours(),
selectMinute: new Date().getMinutes(),
selectSecond: new Date().getSeconds()
};
},
watch: {
defaultDate: {
immediate: true,
handler(val) {
if (val) {
if (this.mode == DATE_TYPES.YM && val.replace(/\-/g, '/').split('/').length == 2) {
// 日期模式为年月时有可能传进来的defaultDate是2022-02这样的格式在ios下new Date会报错加上日期部分做兼容
val += '-01';
} else if (this.mode == DATE_TYPES.HMS || this.mode == DATE_TYPES.HM) {
// 只有时分秒或者只有时分是不能调用new Date生成Date对象的先加上一个假设的年月日就取当年一月一日来兼容
const now = new Date();
val = `${now.getFullYear()}-01-01 ${val}`;
}
let date = new Date(DateUtil.handleDateStr(val));
this.selectYear = date.getFullYear();
this.selectMonth = date.getMonth() + 1;
this.selectDay = date.getDate();
this.selectHour = date.getHours();
this.selectMinute = date.getMinutes();
this.selectSecond = date.getSeconds();
}
}
}
},
computed: {
minDateObj() {
let minDate = this.minDate;
if (minDate) {
if (this.mode == DATE_TYPES.YM && minDate.replace(/\-/g, '/').split('/').length == 2) {
// 日期模式为年月时有可能传进来的minDate是2022-02这样的格式在ios下new Date会报错加上日期部分做兼容
minDate += '-01';
} else if (this.mode == DATE_TYPES.HMS || this.mode == DATE_TYPES.HM) {
// 只有时分秒或者只有时分是不能调用new Date生成Date对象的先加上一个假设的年月日就取当年一月一日来兼容
const now = new Date();
minDate = `${now.getFullYear()}-01-01 ${minDate}`;
}
return new Date(DateUtil.handleDateStr(minDate));
} else {
// 没有传最小日期,默认十年前
let year = new Date().getFullYear() - 10;
minDate = new Date(year, 0, 1);
return minDate;
}
},
maxDateObj() {
let maxDate = this.maxDate;
if (maxDate) {
if (this.mode == DATE_TYPES.YM && maxDate.replace(/\-/g, '/').split('/').length == 2) {
// 日期模式为年月时有可能传进来的maxDate是2022-02这样的格式在ios下new Date会报错加上日期部分做兼容
maxDate += '-01';
} else if (this.mode == DATE_TYPES.HMS || this.mode == DATE_TYPES.HM) {
// 只有时分秒或者只有时分是不能调用new Date生成Date对象的先加上一个假设的年月日就取当年一月一日来兼容
const now = new Date();
maxDate = `${now.getFullYear()}-01-01 ${maxDate}`;
}
return new Date(DateUtil.handleDateStr(maxDate));
} else {
// 没有传最大日期,默认十年后
let year = new Date().getFullYear() + 10;
maxDate = new Date(year, 11, 31);
return maxDate;
}
},
years() {
let years = [];
let minYear = this.minDateObj.getFullYear();
let maxYear = this.maxDateObj.getFullYear();
for (let i = minYear; i <= maxYear; i++) {
years.push(i);
}
return years;
},
months() {
let months = [];
let minMonth = 1;
let maxMonth = 12;
// 如果选中的年份刚好是最小可选日期的年份,那月份就要从最小日期的月份开始
if (this.selectYear == this.minDateObj.getFullYear()) {
minMonth = this.minDateObj.getMonth() + 1;
}
// 如果选中的年份刚好是最大可选日期的年份,那月份就要在最大日期的月份结束
if (this.selectYear == this.maxDateObj.getFullYear()) {
maxMonth = this.maxDateObj.getMonth() + 1;
}
for (let i = minMonth; i <= maxMonth; i++) {
months.push(i);
}
return months;
},
days() {
// 一年中12个月每个月的天数
let monthDaysConfig = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
// 闰年2月有29天
if (this.selectMonth == 2 && this.selectYear % 4 == 0) {
monthDaysConfig[1] = 29;
}
let minDay = 1;
let maxDay = monthDaysConfig[this.selectMonth - 1];
if (this.selectYear == this.minDateObj.getFullYear() && this.selectMonth == this.minDateObj.getMonth() + 1) {
minDay = this.minDateObj.getDate();
}
if (this.selectYear == this.maxDateObj.getFullYear() && this.selectMonth == this.maxDateObj.getMonth() + 1) {
maxDay = this.maxDateObj.getDate();
}
let days = [];
for (let i = minDay; i <= maxDay; i++) {
days.push(i);
}
return days;
},
hours() {
let hours = [];
let minHour = 0;
let maxHour = 23;
if (
this.selectYear == this.minDateObj.getFullYear() &&
this.selectMonth == this.minDateObj.getMonth() + 1 &&
this.selectDay == this.minDateObj.getDate()
) {
minHour = this.minDateObj.getHours();
}
if (
this.selectYear == this.maxDateObj.getFullYear() &&
this.selectMonth == this.maxDateObj.getMonth() + 1 &&
this.selectDay == this.maxDateObj.getDate()
) {
maxHour = this.maxDateObj.getHours();
}
for (let i = minHour; i <= maxHour; i++) {
hours.push(i);
}
return hours;
},
minutes() {
let mins = [];
let minMin = 0;
let maxMin = 59;
if (
this.selectYear == this.minDateObj.getFullYear() &&
this.selectMonth == this.minDateObj.getMonth() + 1 &&
this.selectDay == this.minDateObj.getDate() &&
this.selectHour == this.minDateObj.getHours()
) {
minMin = this.minDateObj.getMinutes();
}
if (
this.selectYear == this.maxDateObj.getFullYear() &&
this.selectMonth == this.maxDateObj.getMonth() + 1 &&
this.selectDay == this.maxDateObj.getDate() &&
this.selectHour == this.maxDateObj.getHours()
) {
maxMin = this.maxDateObj.getMinutes();
}
for (let i = minMin; i <= maxMin; i++) {
mins.push(i);
}
return mins;
},
seconds() {
let seconds = [];
let minSecond = 0;
let maxSecond = 59;
if (
this.selectYear == this.minDateObj.getFullYear() &&
this.selectMonth == this.minDateObj.getMonth() + 1 &&
this.selectDay == this.minDateObj.getDate() &&
this.selectHour == this.minDateObj.getHours() &&
this.selectMinute == this.minDateObj.getMinutes()
) {
minSecond = this.minDateObj.getSeconds();
}
if (
this.selectYear == this.maxDateObj.getFullYear() &&
this.selectMonth == this.maxDateObj.getMonth() + 1 &&
this.selectDay == this.maxDateObj.getDate() &&
this.selectHour == this.maxDateObj.getHours() &&
this.selectMinute == this.maxDateObj.getMinutes()
) {
maxSecond = this.maxDateObj.getSeconds();
}
for (let i = minSecond; i <= maxSecond; i++) {
seconds.push(i);
}
return seconds;
},
// 传给pickerView组件的数组根据mode来生成不同的数据
dateConfig() {
let years = this.years.map((y) => y + '年');
let months = this.months.map((m) => m + '月');
let days = this.days.map((d) => d + '日');
let hours = this.hours.map((h) => h + '时');
let minutes = this.minutes.map((m) => m + '分');
let seconds = this.seconds.map((s) => s + '秒');
let ret = [];
switch (this.mode) {
case DATE_TYPES.YM:
ret = [years, months];
break;
case DATE_TYPES.Y:
ret = [years];
break;
case DATE_TYPES['YMD-HM']:
ret = [years, months, days, hours, minutes];
break;
case DATE_TYPES.HMS:
ret = [hours, minutes, seconds];
break;
case DATE_TYPES.HM:
ret = [hours, minutes];
break;
default:
ret = [years, months, days];
break;
}
return ret;
},
selectVals() {
let ret = [];
switch (this.mode) {
case DATE_TYPES.YM:
ret = [this.selectYear + '年', this.selectMonth + '月'];
break;
case DATE_TYPES.Y:
ret = [this.selectYear + '年'];
break;
case DATE_TYPES['YMD-HM']:
ret = [
this.selectYear + '年',
this.selectMonth + '月',
this.selectDay + '日',
this.selectHour + '时',
// this.selectMinute + '分',
];
break;
case DATE_TYPES.HMS:
ret = [this.selectHour + '时', this.selectMinute + '分', this.selectSecond + '秒'];
break;
case DATE_TYPES.HM:
ret = [this.selectHour + '时', this.selectMinute + '分'];
break;
default:
ret = [this.selectYear + '年', this.selectMonth + '月', this.selectDay + '日'];
break;
}
return ret;
}
},
methods: {
onChangePickerValue(e) {
const { value } = e;
if (this.mode == DATE_TYPES.YM && value[0] && value[1]) {
// 年月模式
this.selectYear = Number(value[0].replace('年', ''));
this.selectMonth = Number(value[1].replace('月', ''));
} else if (this.mode == DATE_TYPES.Y && value[0]) {
// 只有年份模式
this.selectYear = Number(value[0].replace('年', ''));
} else if (this.mode == DATE_TYPES['YMD-HM'] && value[0] && value[1] && value[2] != '' && value[3] && value[4]) {
// 年月日时分秒模式
this.selectYear = Number(value[0].replace('年', ''));
this.selectMonth = Number(value[1].replace('月', ''));
this.selectDay = Number(value[2].replace('日', ''));
this.selectHour = Number(value[3].replace('时', ''));
this.selectMinute = Number(value[4].replace('分', ''));
// this.selectSecond = Number(value[5].replace('秒', ''));
} else if (this.mode == DATE_TYPES.HMS && value[0] && value[1] && value[2]) {
// 时分秒模式
this.selectHour = Number(value[0].replace('时', ''));
this.selectMinute = Number(value[1].replace('分', ''));
this.selectSecond = Number(value[2].replace('秒', ''));
} else if (this.mode == DATE_TYPES.HM && value[0] && value[1]) {
// 时分模式
this.selectHour = Number(value[0].replace('时', ''));
this.selectMinute = Number(value[1].replace('分', ''));
} else if (value[0] && value[1] && value[2]) {
// 默认,年月日模式
this.selectYear = Number(value[0].replace('年', ''));
this.selectMonth = Number(value[1].replace('月', ''));
this.selectDay = Number(value[2].replace('日', ''));
} else {
// 其他情况可能是pickerView返回的数据有问题不处理
console.log('onChangePickerValue其他情况');
return;
}
let formatTmpl = 'YYYY-MM-DD';
switch (this.mode) {
case DATE_TYPES.YM:
formatTmpl = 'YYYY-MM';
break;
case DATE_TYPES.Y:
formatTmpl = 'YYYY';
break;
case DATE_TYPES['YMD-HM']:
formatTmpl = 'YYYY-MM-DD HH:mm';
break;
case DATE_TYPES.HMS:
formatTmpl = 'HH:mm:ss';
break;
case DATE_TYPES.HM:
formatTmpl = 'HH:mm';
break;
default:
break;
}
this.$emit(
'onChange',
DateUtil.formatDate(
new Date(`${this.selectYear}/${this.selectMonth}/${this.selectDay} ${this.selectHour}:${this.selectMinute}:${this.selectSecond}`),
formatTmpl
)
);
}
}
};

View File

@ -0,0 +1,9 @@
<template>
<view class="datetime-picker">
<CustomPickerView :columns="dateConfig" :selectVals="selectVals" @onChange="onChangePickerValue" />
</view>
</template>
<script src="./index.js"></script>
<style scoped lang="css"></style>

View File

@ -0,0 +1,228 @@
<template>
<select-modal title="选择规格" @close="$emit('change', false)">
<view>{{data.prices.product_name}}</view>
<view class="add-goods-content">
<view class="flex-row-start good-info">
<image class="good-img" :src="data.product_pic" />
<view class="flex-column-between good-info-inner">
<view class="fs-32 app-fc-main app-text-ellipse">{{data.product_name || ''}}</view>
<view>
<text class="fs-24 app-fc-mark"></text>
<text class="fs-44 app-font-bold app-fc-mark">{{data.prices[0].actual_price || data.prices[0].product_actual_price }}</text>
<text class="fs-24 origin-price">{{data.prices[0].original_price || data.prices[0].product_original_price }}</text>
</view>
</view>
</view>
<!-- <view class="fs-28 size-title">选择{{ data.shuxing_name || "" }}</view> -->
<view class="flex-row-start size-list">
<view
class="size-item"
:class="{ 'app-fc-mark active ali-puhui-bold': item.name === selectQuality }"
v-for="(item, i) in data.shuxing_list"
:key="i"
@click="selectQuality = item.name"
>
{{ item.name || "" }}
</view>
</view>
<!-- <view class="fs-28 size-title">商品规格</view>
<view class="flex-row-start size-list">
<view
class="size-item"
:class="{ 'active ali-puhui-bold': item.price_id === selectPriceId }"
v-for="item in data.price_list"
:key="item.price_id"
@click="selectPriceId = item.price_id"
>
{{ item.price_name || "" }}
</view>
</view> -->
<view class="fs-28 size-title">选择数量</view>
<view class="flex-row-start">
<image
class="card-num-icon"
src="@/static/images/cart_decrease.png"
@click="decrease"
/>
<text class="fs-28 app-fc-main cart-num-text">{{ curGoodsNum }}</text>
<image
class="card-num-icon"
src="@/static/images/cart_add.png"
@click="add"
/>
</view>
<view class="flex-center app-fc-white fs-30 opt-btn" @click="optAction">
{{ optText }}
</view>
</view>
</select-modal>
</template>
<script>
import selectModal from "../select-modal.vue";
export default {
props: {
data: {
type: Object,
default: () => {},
},
qualityName: {
type: String,
default: "",
},
priceId: {
type: Number,
default: -1,
},
goodsNum: {
type: Number,
default: 1,
},
optText: {
type: String,
default: "",
},
},
components: {
selectModal,
},
data() {
return {
selectQuality: "",
selectPriceId: "",
curGoodsNum: 1,
};
},
computed: {
// 商品规格库存
priceStore() {
const priceInfo = this.data?.prices?.find(
(v) => v.id === this.selectPriceId
);
return priceInfo?.stock || 0;
},
priceInfo() {
const priceInfo = this.data?.prices?.find(
(v) => v.id === this.selectPriceId
);
return {
price: ((priceInfo?.actual_price || 0) * this.curGoodsNum).toFixed(2)
}
}
},
watch: {
qualityName(val) {
this.selectQuality = val;
},
priceId(val) {
this.selectPriceId = val;
},
goodsNum(val) {
this.curGoodsNum = val;
},
},
mounted() {
this.selectQuality = this.data?.shuxing_list?.[0]?.name || "";
this.selectPriceId = this.data?.prices?.[0]?.id || "";
},
methods: {
add() {
if (this.priceStore > this.curGoodsNum) {
this.curGoodsNum += 1;
} else {
uni.showToast({
title: "库存不足",
icon: "none",
});
}
},
decrease() {
if (this.curGoodsNum > 1) {
this.curGoodsNum -= 1;
}
},
optAction() {
this.$emit("optAction", {
...this.data,
price_id: this.selectPriceId,
number: this.curGoodsNum
});
},
},
};
</script>
<style lang="scss" scoped>
.add-goods-content {
padding: 30rpx 52rpx 0;
box-sizing: border-box;
.good-info {
margin-bottom: 40rpx;
align-items: stretch;
.good-img {
width: 160rpx;
height: 160rpx;
border-radius: 20rpx;
margin-right: 24rpx;
}
.good-info-inner {
flex: 1;
overflow: hidden;
align-items: flex-start;
}
.origin-price {
color: #726E71;
margin-left: 12rpx;
text-decoration: line-through;
}
}
.size-title {
margin: 20rpx 0 36rpx;
}
.size-list {
flex-wrap: wrap;
.size-item {
padding: 18rpx 38rpx;
background: transparent;
border: 2rpx solid #f5f5f5;
color: #726e71;
background: #f5f5f5;
margin-right: 28rpx;
margin-bottom: 18rpx;
border-radius: 24rpx;
&.active {
background: #fee9f3;
border: 2rpx solid $app_color_main;
color: $app_color_main;
// font-weight: bold;
}
}
}
.card-num-icon {
width: 64rpx;
height: 64rpx;
}
.cart-num-text {
min-width: 78rpx;
margin: 0 8rpx;
text-align: center;
}
.opt-btn {
width: 630rpx;
height: 92rpx;
background: #fe019b;
border-radius: 200rpx;
margin: 66rpx auto 0;
}
}
</style>

View File

@ -0,0 +1,119 @@
<template>
<view class="remark-item">
<view class="flex-row-start remark-item-info">
<image class="info-avator" :src="data.head_pic" />
<view class="info-center">
<view class="app-text-ellipse fs-26 app-fc-main">{{
data.nick_name || ""
}}</view>
<view class="flex-row-start star-list">
<image
v-for="item in [1, 2, 3, 4, 5]"
:key="item"
class="star-item"
:src="
item <= data.star
? require('@/static/images/star.png')
: require('@/static/images/star_dark.png')
"
/>
</view>
</view>
<text class="fs-24 app-fc-normal"> {{ formatTime(data.add_time) }} </text>
</view>
<text class="fs-26 app-fc-normal">
{{ data.content || "" }}
</text>
<view class="flex-row-start remark-imgs">
<image
v-for="(item, i) in imgList"
:key="item"
class="remark-img"
:class="{ 'remark-img-right': i % 3 === 2 }"
:src="item"
@click="preview(i)"
mode="aspectFill"
/>
</view>
</view>
</template>
<script>
import moment from "moment";
export default {
props: {
data: {
type: Object,
default: () => {},
},
},
data() {
return {};
},
computed: {
imgList() {
return this.data?.pic_list || [];
},
},
mounted() {},
methods: {
formatTime(time) {
return moment(time * 1000).format("YYYY/MM/DD");
},
preview(index) {
uni.previewImage({
urls: this.imgList,
current: index,
})
}
},
};
</script>
<style lang="scss" scoped>
.remark-item {
padding: 40rpx 24rpx;
background: #fff;
border-radius: 20rpx;
box-sizing: border-box;
margin: 0 0 20rpx 32rpx;
width: calc(100vw - 32rpx * 2);
.remark-item-info {
margin-bottom: 20rpx;
align-items: stretch;
.info-avator {
width: 60rpx;
height: 60rpx;
border-radius: 60rpx;
}
.info-center {
flex: 1;
margin: 0 20rpx;
overflow: hidden;
}
.star-item {
width: 20rpx;
height: 20rpx;
margin-right: 6rpx;
margin-top: 6rpx;
}
}
.remark-imgs {
margin-top: 20rpx;
flex-wrap: wrap;
.remark-img {
width: calc((100vw - 32rpx * 2 - 24rpx * 2 - 14rpx * 2) / 3);
height: calc((100vw - 32rpx * 2 - 24rpx * 2 - 14rpx * 2) / 3);
border-radius: 20rpx;
margin-right: 14rpx;
margin-bottom: 14rpx;
&.remark-img-right {
margin-right: 0;
}
}
}
}
</style>

View File

@ -0,0 +1,836 @@
<template>
<view class="htz-image-upload-list">
<view class="htz-image-upload-Item" v-for="(item,index) in uploadLists" :key="index">
<view class="htz-image-upload-Item-video" v-if="isVideo(item)">
<!-- #ifndef APP-PLUS -->
<video :disabled="false" :controls="false" :src="getFileUrl(item)">
<cover-view class="htz-image-upload-Item-video-fixed" @click="previewVideo(getFileUrl(item))">
</cover-view>
<!-- <cover-view class="htz-image-upload-Item-del-cover" v-if="remove && previewVideoSrc==''" @click="imgDel(index)">×</cover-view> -->
</video>
<!-- #endif -->
<!-- #ifdef APP-PLUS -->
<view class="htz-image-upload-Item-video-fixed" @click="previewVideo(getFileUrl(item))"></view>
<image v-if="dataType==1 && item.cover" class="htz-image-upload-Item-video-app-poster" mode="widthFix" :src="item.cover"></image>
<image v-else class="htz-image-upload-Item-video-app-poster" mode="widthFix" :src="appVideoPoster"></image>
<!-- #endif -->
</view>
<image v-else :src="getFileUrl(item)" @click="imgPreview(getFileUrl(item))"></image>
<view class="htz-image-upload-Item-del" v-if="remove" @click="imgDel(index)">×</view>
</view>
<view class="htz-image-upload-Item htz-image-upload-Item-add" v-if="uploadLists.length<max && add" @click="chooseFile">
+
</view>
<view class="preview-full" v-if="previewVideoSrc!=''">
<video :autoplay="true" :src="previewVideoSrc" :show-fullscreen-btn="false">
<cover-view class="preview-full-close ali-puhui-bold" @click="previewVideoClose"> ×
</cover-view>
</video>
</view>
<!-- -->
</view>
</template>
<style>
.ceshi {
width: 100%;
height: 100%;
position: relative;
top: 0;
left: 0;
bottom: 0;
right: 0;
background-color: #FFFFFF;
color: #2C405A;
opacity: 0.5;
z-index: 100;
}
</style>
<script>
export default {
name: 'htz-image-upload',
props: {
max: { //展示图片最大值
type: Number,
default: 1,
},
chooseNum: { //选择图片数
type: Number,
default: 9,
},
name: { //发到后台的文件参数名
type: String,
default: 'file',
},
dataType: { //v-model的数据结构类型
type: Number,
default: 0, // 0: ['http://xxxx.jpg','http://xxxx.jpg'] 1:[{type:0,url:'http://xxxx.jpg'}] type 0 图片 1 视频 url 文件地址 此类型是为了给没有文件后缀的状况使用的
},
remove: { //是否展示删除按钮
type: Boolean,
default: true,
},
add: { //是否展示添加按钮
type: Boolean,
default: true,
},
disabled: { //是否禁用
type: Boolean,
default: false,
},
sourceType: { //选择照片来源 【psH5就别费劲了设置了也没用。不是我说的官方文档就这样
type: Array,
default: () => ['album', 'camera'],
},
action: { //上传地址 如需使用uniCloud服务设置为uniCloud即可
type: String,
default: '',
},
headers: { //上传的请求头部
type: Object,
default: () => {},
},
formData: { //HTTP 请求中其他额外的 form data
type: Object,
default: () => {},
},
compress: { //是否需要压缩
type: Boolean,
default: true,
},
quality: { //压缩质量范围0100
type: Number,
default: 80,
},
// #ifndef VUE3
value: { //受控图片列表
type: Array,
default: () => [],
},
// #endif
// #ifdef VUE3
modelValue: { //受控图片列表
type: Array,
default: () => [],
},
// #endif
uploadSuccess: {
default: (res) => {
return {
success: false,
url: ''
}
},
},
mediaType: { //文件类型 image/video/all
type: String,
default: 'image',
},
maxDuration: { //拍摄视频最长拍摄时间,单位秒。最长支持 60 秒。 (只针对拍摄视频有用)
type: Number,
default: 60,
},
camera: { //'front'、'back',默认'back'(只针对拍摄视频有用)
type: String,
default: 'back',
},
appVideoPoster: { //app端视频展示封面 只对app有效
type: String,
default: '/static/htz-image-upload/play.png',
},
},
data() {
return {
uploadLists: [],
mediaTypeData: ['image', 'video', 'all'],
previewVideoSrc: '',
}
},
mounted: function() {
this.$nextTick(function() {
// #ifndef VUE3
this.uploadLists = this.value;
// #endif
// #ifdef VUE3
this.uploadLists = this.modelValue;
// #endif
if (this.mediaTypeData.indexOf(this.mediaType) == -1) {
uni.showModal({
title: '提示',
content: 'mediaType参数不正确',
showCancel: false,
success: function(res) {
if (res.confirm) {
//console.log('用户点击确定');
} else if (res.cancel) {
//console.log('用户点击取消');
}
}
});
}
});
},
watch: {
// #ifndef VUE3
value(val, oldVal) {
//console.log('value',val, oldVal)
this.uploadLists = val;
},
// #endif
// #ifdef VUE3
modelValue(val, oldVal) {
//console.log('value',val, oldVal)
this.uploadLists = val;
},
// #endif
},
methods: {
isVideo(item) {
let isPass = false
if ((!/.(gif|jpg|jpeg|png|gif|jpg|png)$/i.test(item) && this.dataType == 0) || (this.dataType == 1 && item
.type == 1)) {
isPass = true
}
return isPass
},
getFileUrl(item) {
var url = item;
if (this.dataType == 1) {
url = item.url
}
//console.log('url', url)
return url
},
previewVideo(src) {
this.previewVideoSrc = src;
// this.previewVideoSrc =
// 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-fbd63a76-dc76-485c-b711-f79f2986daeb/ba804d82-860b-4d1a-a706-5a4c8ce137c3.mp4'
},
previewVideoClose() {
this.previewVideoSrc = ''
//console.log('previewVideoClose', this.previewVideoSrc)
},
imgDel(index) {
uni.showModal({
title: '提示',
content: '您确定要删除么?',
success: (res) => {
if (res.confirm) {
// this.uploadLists.splice(index, 1)
// this.$emit("input", this.uploadLists);
// this.$emit("imgDelete", this.uploadLists);
let delUrl = this.uploadLists[index]
this.uploadLists.splice(index, 1)
// #ifndef VUE3
this.$emit("input", this.uploadLists);
// #endif
// #ifdef VUE3
this.$emit("update:modelValue", this.uploadLists);
// #endif
this.$emit("imgDelete", {
del: delUrl,
tempFilePaths: this.uploadLists
});
} else if (res.cancel) {}
}
});
},
imgPreview(index) {
var imgData = []
this.uploadLists.forEach(item => {
if (!this.isVideo(item)) {
imgData.push(this.getFileUrl(item))
}
})
//console.log('imgPreview', imgData)
uni.previewImage({
urls: imgData,
current: index,
loop: true,
});
},
chooseFile() {
if (this.disabled) {
return false;
}
switch (this.mediaTypeData.indexOf(this.mediaType)) {
case 1: //视频
this.videoAdd();
break;
case 2: //全部
uni.showActionSheet({
itemList: ['相册', '视频'],
success: (res) => {
if (res.tapIndex == 1) {
this.videoAdd();
} else if (res.tapIndex == 0) {
this.imgAdd();
}
},
fail: (res) => {
console.log(res.errMsg);
}
});
break;
default: //图片
this.imgAdd();
break;
}
//if(this.mediaType=='image'){
},
videoAdd() {
//console.log('videoAdd')
let nowNum = Math.abs(this.uploadLists.length - this.max);
let thisNum = (this.chooseNum > nowNum ? nowNum : this.chooseNum) //可选数量
uni.chooseVideo({
compressed: this.compress,
sourceType: this.sourceType,
camera: this.camera,
maxDuration: this.maxDuration,
success: (res) => {
// console.log('videoAdd', res)
// console.log(res.tempFilePath)
this.chooseSuccessMethod([res.tempFilePath], 1)
//this.imgUpload([res.tempFilePath]);
//console.log('tempFiles', res)
// if (this.action == '') { //未配置上传路径
// this.$emit("chooseSuccess", res.tempFilePaths);
// } else {
// if (this.compress && (res.tempFiles[0].size / 1024 > 1025)) { //设置了需要压缩 并且 文件大于1M进行压缩上传
// this.imgCompress(res.tempFilePaths);
// } else {
// this.imgUpload(res.tempFilePaths);
// }
// }
}
});
},
imgAdd() {
//console.log('imgAdd')
let nowNum = Math.abs(this.uploadLists.length - this.max);
let thisNum = (this.chooseNum > nowNum ? nowNum : this.chooseNum) //可选数量
//console.log('nowNum', nowNum)
//console.log('thisNum', thisNum)
// #ifdef APP-PLUS
if (this.sourceType.length > 1) {
uni.showActionSheet({
itemList: ['拍摄', '从手机相册选择'],
success: (res) => {
if (res.tapIndex == 1) {
this.appGallery(thisNum);
} else if (res.tapIndex == 0) {
this.appCamera();
}
},
fail: (res) => {
console.log(res.errMsg);
}
});
}
if (this.sourceType.length == 1 && this.sourceType.indexOf('album') > -1) {
this.appGallery(thisNum);
}
if (this.sourceType.length == 1 && this.sourceType.indexOf('camera') > -1) {
this.appCamera();
}
// #endif
//#ifndef APP-PLUS
uni.chooseImage({
count: thisNum,
sizeType: ['original', 'compressed'], //可以指定是原图还是压缩图,默认二者都有
sourceType: this.sourceType,
success: (res) => {
this.chooseSuccessMethod(res.tempFilePaths, 0)
//console.log('tempFiles', res)
// if (this.action == '') { //未配置上传路径
// this.$emit("chooseSuccess", res.tempFilePaths);
// } else {
// if (this.compress && (res.tempFiles[0].size / 1024 > 1025)) { //设置了需要压缩 并且 文件大于1M进行压缩上传
// this.imgCompress(res.tempFilePaths);
// } else {
// this.imgUpload(res.tempFilePaths);
// }
// }
}
});
// #endif
},
appCamera() {
var cmr = plus.camera.getCamera();
var res = cmr.supportedImageResolutions[0];
var fmt = cmr.supportedImageFormats[0];
//console.log("Resolution: " + res + ", Format: " + fmt);
cmr.captureImage((path) => {
//alert("Capture image success: " + path);
this.chooseSuccessMethod([path], 0)
},
(error) => {
//alert("Capture image failed: " + error.message);
console.log("Capture image failed: " + error.message)
}, {
resolution: res,
format: fmt
}
);
},
appGallery(maxNum) {
plus.gallery.pick((res) => {
this.chooseSuccessMethod(res.files, 0)
}, function(e) {
//console.log("取消选择图片");
}, {
filter: "image",
multiple: true,
maximum: maxNum
});
},
chooseSuccessMethod(filePaths, type) {
if (this.action == '') { //未配置上传路径
this.$emit("chooseSuccess", filePaths, type); //filePaths 路径 type 0 为图片 1为视频
} else {
if (type == 1) {
this.imgUpload(filePaths, type);
} else {
if (this.compress) { //设置了需要压缩
this.imgCompress(filePaths, type);
} else {
this.imgUpload(filePaths, type);
}
}
}
},
imgCompress(tempFilePaths, type) { //type 0 为图片 1为视频
uni.showLoading({
title: '压缩中...'
});
let compressImgs = [];
let results = [];
tempFilePaths.forEach((item, index) => {
compressImgs.push(new Promise((resolve, reject) => {
// #ifndef H5
uni.compressImage({
src: item,
quality: this.quality,
success: res => {
//console.log('compressImage', res.tempFilePath)
results.push(res.tempFilePath);
resolve(res.tempFilePath);
},
fail: (err) => {
//console.log(err.errMsg);
reject(err);
},
complete: () => {
//uni.hideLoading();
}
})
// #endif
// #ifdef H5
this.canvasDataURL(item, {
quality: this.quality / 100
}, (base64Codes) => {
//this.imgUpload(base64Codes);
results.push(base64Codes);
resolve(base64Codes);
})
// #endif
}))
})
Promise.all(compressImgs) //执行所有需请求的接口
.then((results) => {
uni.hideLoading();
//console.log('imgCompress', type)
this.imgUpload(results, type);
})
.catch((res, object) => {
uni.hideLoading();
});
},
imgUpload(tempFilePaths, type) { //type 0 为图片 1为视频
// if (this.action == '') {
// uni.showToast({
// title: '未配置上传地址',
// icon: 'none',
// duration: 2000
// });
// return false;
// }
if (this.action == 'uniCloud') {
this.uniCloudUpload(tempFilePaths, type)
return
}
uni.showLoading({
title: '上传中'
});
//console.log('imgUpload', tempFilePaths)
let uploadImgs = [];
tempFilePaths.forEach((item, index) => {
uploadImgs.push(new Promise((resolve, reject) => {
//console.log(index, item)
const uploadTask = uni.uploadFile({
url: this.action, //仅为示例,非真实的接口地址
filePath: item,
name: this.name,
fileType: 'image',
formData: this.formData,
header: this.headers,
success: (uploadFileRes) => {
//uni.hideLoading();
//console.log(typeof this.uploadSuccess)
//console.log('')
uploadFileRes.fileType = type
if (typeof this.uploadSuccess == 'function') {
let thisUploadSuccess = this.uploadSuccess(
uploadFileRes)
if (thisUploadSuccess.success) {
let keyName = '';
// #ifndef VUE3
keyName = 'value'
// #endif
// #ifdef VUE3
keyName = 'modelValue'
// #endif
if (this.dataType == 0) {
this[keyName].push(thisUploadSuccess.url)
} else {
this[keyName].push({
type: type,
url: thisUploadSuccess.url,
...thisUploadSuccess
})
}
//this.$emit("input", this.uploadLists);
// #ifndef VUE3
this.$emit("input", this.uploadLists);
// #endif
// #ifdef VUE3
this.$emit("update:modelValue", this.uploadLists);
// #endif
}
}
resolve(uploadFileRes);
this.$emit("uploadSuccess", uploadFileRes);
},
fail: (err) => {
console.log(err);
//uni.hideLoading();
reject(err);
this.$emit("uploadFail", err);
},
complete: () => {
//uni.hideLoading();
}
});
}))
})
Promise.all(uploadImgs) //执行所有需请求的接口
.then((results) => {
uni.hideLoading();
})
.catch((res, object) => {
uni.hideLoading();
this.$emit("uploadFail", res);
});
// uploadTask.onProgressUpdate((res) => {
// //console.log('',)
// uni.showLoading({
// title: '上传中' + res.progress + '%'
// });
// if (res.progress == 100) {
// uni.hideLoading();
// }
// });
},
uniCloudUpload(tempFilePaths, type) {
uni.showLoading({
title: '上传中'
});
console.log('uniCloudUpload', tempFilePaths);
let uploadImgs = [];
tempFilePaths.forEach((item, index) => {
uploadImgs.push(new Promise((resolve, reject) => {
uniCloud.uploadFile({
filePath: item,
cloudPath: this.guid() + '.' + this.getFileType(item, type),
success(uploadFileRes) {
if (uploadFileRes.success) {
resolve(uploadFileRes.fileID);
}
},
fail(err) {
console.log(err);
reject(err);
},
complete() {}
});
}))
})
Promise.all(uploadImgs) //执行所有需请求的接口
.then((results) => {
uni.hideLoading();
// console.log('then', results)
uniCloud.getTempFileURL({
fileList: results,
success: (res) => {
//console.log('success',res.fileList)
res.fileList.forEach(item => {
//console.log(item.tempFileURL)
//this.value.push(item.tempFileURL)
// #ifndef VUE3
this.value.push(item.tempFileURL)
this.$emit("input", this.value);
// #endif
// #ifdef VUE3
this.modelValue.push(item.tempFileURL)
this.$emit("update:modelValue", this.modelValue);
// #endif
})
},
fail() {},
complete() {}
});
})
.catch((res, object) => {
uni.hideLoading();
});
},
getFileType(path, type) { //手机端默认图片为jpg 视频为mp4
// #ifdef H5
var result = type == 0 ? 'jpg' : 'mp4';
// #endif
// #ifndef H5
var result = path.split('.').pop().toLowerCase();
// #ifdef MP
if (this.compress) { //微信小程序压缩完没有后缀
result = type == 0 ? 'jpg' : 'mp4';
}
// #endif
// #endif
return result;
},
guid() {
return 'xxxxxxxx-date-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random() * 16 | 0,
v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
}).replace(/date/g, function(c) {
return Date.parse(new Date());
});
},
canvasDataURL(path, obj, callback) {
var img = new Image();
img.src = path;
img.onload = function() {
var that = this;
// 默认按比例压缩
var w = that.width,
h = that.height,
scale = w / h;
w = obj.width || w;
h = obj.height || (w / scale);
var quality = 0.8; // 默认图片质量为0.8
//生成canvas
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
// 创建属性节点
var anw = document.createAttribute("width");
anw.nodeValue = w;
var anh = document.createAttribute("height");
anh.nodeValue = h;
canvas.setAttributeNode(anw);
canvas.setAttributeNode(anh);
ctx.drawImage(that, 0, 0, w, h);
// 图像质量
if (obj.quality && obj.quality <= 1 && obj.quality > 0) {
quality = obj.quality;
}
// quality值越小所绘制出的图像越模糊
var base64 = canvas.toDataURL('image/jpeg', quality);
// 回调函数返回base64的值
callback(base64);
}
},
}
}
</script>
<style>
.preview-full {
position: fixed;
top: 0;
left: 0;
bottom: 0;
width: 100%;
height: 100%;
z-index: 1002;
}
.preview-full video {
width: 100%;
height: 100%;
z-index: 1002;
}
.preview-full-close {
position: fixed;
right: 32rpx;
top: 25rpx;
width: 80rpx;
height: 80rpx;
line-height: 60rpx;
text-align: center;
z-index: 1003;
/* background-color: #808080; */
color: #fff;
font-size: 65rpx;
/* font-weight: bold; */
text-shadow: 1px 2px 5px rgb(0 0 0);
}
/* .preview-full-close-before,
.preview-full-close-after {
position: absolute;
top: 50%;
left: 50%;
content: '';
height: 60rpx;
margin-top: -30rpx;
width: 6rpx;
margin-left: -3rpx;
background-color: #FFFFFF;
z-index: 20000;
}
.preview-full-close-before {
transform: rotate(45deg);
}
.preview-full-close-after {
transform: rotate(-45deg);
} */
.htz-image-upload-list {
display: flex;
flex-wrap: wrap;
}
.htz-image-upload-Item {
width: 160rpx;
height: 160rpx;
margin: 13rpx;
border-radius: 10rpx;
position: relative;
}
.htz-image-upload-Item image {
width: 100%;
height: 100%;
border-radius: 10rpx;
}
.htz-image-upload-Item-video {
width: 100%;
height: 100%;
border-radius: 10rpx;
position: relative;
}
.htz-image-upload-Item-video-fixed {
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 100%;
height: 100%;
border-radius: 10rpx;
z-index: 996;
}
.htz-image-upload-Item video {
width: 100%;
height: 100%;
border-radius: 10rpx;
}
.htz-image-upload-Item-add {
font-size: 105rpx;
/* line-height: 160rpx; */
text-align: center;
border: 1px dashed #d9d9d9;
color: #d9d9d9;
}
.htz-image-upload-Item-del {
background-color: #f5222d;
font-size: 24rpx;
position: absolute;
width: 35rpx;
height: 35rpx;
line-height: 35rpx;
text-align: center;
top: 0;
right: 0;
z-index: 997;
color: #fff;
}
.htz-image-upload-Item-del-cover {
background-color: #f5222d;
font-size: 24rpx;
position: absolute;
width: 35rpx;
height: 35rpx;
text-align: center;
top: 0;
right: 0;
color: #fff;
/* #ifdef APP-PLUS */
line-height: 25rpx;
/* #endif */
/* #ifndef APP-PLUS */
line-height: 35rpx;
/* #endif */
z-index: 997;
}
.htz-image-upload-Item-video-app-poster {
width: 100%;
height: 100%;
}
</style>

View File

@ -0,0 +1,228 @@
<template>
<select-modal @close="closeAction" title="调整服务费">
<view class="add-pay-container">
<view class="top-container">
<detail-cell v-if="weightName" title="下单宠物重量区间" :content="weightName"/>
<detail-cell title="已支付费用" :content="`¥${price}`"/>
<!-- <detail-cell v-if="orderInfo.dikou_id" title="优惠券" :content="`-¥${diKouPrice}`"/>-->
<!-- <detail-cell v-if="orderInfo.fuwuquan_id" title="服务券金额" :content="`¥${servicePrice}`"/>-->
<view class="top-cell" @click.stop="selectedWeight">
<view class="info-view">
<text class="app-fc-main fs-30">实际宠物重量区间</text>
<text class="app-fc-main fs-30 app-font-bold-500">{{ realWeightName }}</text>
</view>
<image class="right-arrow" mode="aspectFit" src="/static/images/arrow_right_black.png"/>
</view>
<view class="top-cell">
<view class="info-view">
<text v-if="needRefund" class="app-fc-main fs-30">差价退款金额</text>
<text v-else class="app-fc-main fs-30">差价补偿金额</text>
</view>
<text class="fs-30 app-fc-alarm app-font-bold-500">{{needRefund ? '-' : ''}}{{ `¥${diffPrice}` }}</text>
</view>
</view>
<view class="line-view"/>
<view class="bottom-container">
<view class="price-container">
<view class="price-view">
<text class="app-fc-main fs-30 app-font-bold-500"></text>
<text class="app-fc-main fs-40 app-font-bold-700">{{ diffPrice }}</text>
</view>
<text class="fs-24 app-fc-normal">差价补退</text>
</view>
<view class="submit-btn" @click.stop="paymentConfirm">
<text v-if="needRefund" class="app-fc-white fs-30">提交</text>
<text v-else class="app-fc-white fs-30">确认支付</text>
</view>
</view>
</view>
</select-modal>
</template>
<script>
import SelectModal from "@/components/select-modal.vue";
import DetailCell from "@/components/petOrder/detail-cell.vue";
export default {
components: { DetailCell, SelectModal },
props: {
orderInfo: {
type: Object,
default: () => {
return {};
}
},
newWeight: {
type: Object,
default: () => {
return {};
}
}
},
computed: {
price() {
return this.orderInfo.price || '';
},
diKouPrice() {
return this.orderInfo.dikou_price || '';
},
servicePrice() {
return +this.orderInfo.fuwuquan_price || 0;
},
weightName() {
return this.orderInfo.new_weight_name || this.orderInfo?.weight_name || '';
},
realWeightName() {
return this.newWeight?.weight_name || '请选择';
},
diffPrice() {
if (Object.keys(this.newWeight).length > 0) {
const newPrice = Number(this.newWeight?.price || 0);
const orderPrice = Number(this.orderInfo?.price || 0);
const payPrice = Number(this.orderInfo?.pay_price || 0);
let d = Math.abs(newPrice - orderPrice);
if (d === 0) {
return '0'
} else if (newPrice > orderPrice) {
//需要补交钱
return d.toFixed(2);
} else {
//退款
if (this.orderInfo.fuwuquan_id) {
//使用服务券
if ((orderPrice - newPrice) >= this.servicePrice) {
return this.servicePrice.toFixed(2);
} else {
return d.toFixed(2);
}
} else {
if ((orderPrice - newPrice) >= payPrice) {
return payPrice.toFixed(2);
} else {
return d.toFixed(2);
}
}
}
}
return '0';
},
needRefund() {
const newPrice = Number(this.newWeight?.price || 0);
const orderPrice = Number(this.orderInfo?.price || 0);
return orderPrice > newPrice;
},
},
data() {
return {};
},
methods: {
selectedWeight() {
this.$emit('changeWeight')
},
closeAction() {
this.$emit('close');
},
paymentConfirm() {
// console.log(this.orderInfo,'--')
if (Object.keys(this.newWeight).length === 0) {
uni.showToast({
icon: 'none',
title: '请选择实际宠物重量区间'
});
return;
}
this.$emit('paymentConfirm', {
needRefund: this.needRefund,
diffPrice:this.diffPrice,
order_id:this.orderInfo.order_id
});
}
},
}
</script>
<style lang="scss" scoped>
.add-pay-container {
width: 100%;
box-sizing: border-box;
display: flex;
flex-direction: column;
.top-container {
width: 100%;
padding: 30rpx 40rpx 0;
box-sizing: border-box;
.top-cell {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
padding: 40rpx;
box-sizing: border-box;
border-radius: 30rpx;
background-color: #F9F7F9;
margin-bottom: 20rpx;
margin-top: 12rpx;
.info-view {
display: flex;
flex: 1;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.right-arrow {
width: 32rpx;
height: 32rpx;
flex-shrink: 0;
margin-left: 12rpx;
}
}
}
.line-view {
width: 100%;
height: 2rpx;
background-color: #ECECEC;
margin-top: 10rpx;
}
.bottom-container {
width: 100%;
padding: 20rpx 32rpx 30rpx;
box-sizing: border-box;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
.price-container {
display: flex;
flex: 1;
flex-direction: column;
align-items: flex-start;
.price-view {
display: flex;
align-items: flex-end;
flex-direction: row;
}
}
.submit-btn {
flex-shrink: 0;
width: 260rpx;
height: 90rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 45rpx;
background-color: $app_color_main;
}
}
}
</style>

View File

@ -0,0 +1,118 @@
<template>
<view class="call-modal" @click.stop="closeAction">
<view class="call-body-view" @click.stop="">
<view class="phone-view">
<image src="@/static/images/phone.png" mode="aspectFit" class="phone-icon"/>
<text class="app-fc-main fs-52 app-font-bold-500">{{ phoneNumber }}</text>
</view>
<view class="handle-container">
<view class="handle-btn" @click.stop="closeAction">
<text class="app-fc-main fs-32">取消</text>
</view>
<view class="handle-btn ok-btn" @click.stop="callAction">
<text class="app-fc-white fs-32">确定</text>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
props: {
phoneNumber: {
type: String,
default: ''
}
},
data() {
return {};
},
methods: {
closeAction() {
this.$emit('close');
},
callAction() {
if (this.phoneNumber) {
uni.makePhoneCall({
phoneNumber: this.phoneNumber,
success: (res) => {
console.log(res);
this.$emit('close');
},
fail: (err) => {
console.log(err);
}
})
} else {
uni.showToast({
title: '电话号码为空',
icon: 'none'
})
}
}
},
}
</script>
<style lang="scss" scoped>
.call-modal {
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.5);
position: fixed;
top: 0;
left: 0;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
.call-body-view {
width: calc(100% - 132rpx);
padding: 90rpx 0 60rpx;
background-color: #fff;
border-radius: 40rpx;
margin-bottom: 10vh;
box-sizing: border-box;
.phone-view {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
.phone-icon {
width: 52rpx;
height: 52rpx;
margin-right: 20rpx;
}
}
}
.handle-container {
width: 100%;
display: flex;
align-items: center;
margin-top: 78rpx;
justify-content: space-around;
padding: 0 40rpx;
box-sizing: border-box;
.handle-btn {
width: 216rpx;
height: 80rpx;
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
background-color: #F2F2F2;
}
.ok-btn {
background-color: $app_color_main;
}
}
}
</style>

View File

@ -0,0 +1,103 @@
<template>
<view class="detail-cell-container" @click.stop="clickAction">
<view class="title-view">
<text class="app-fc-main fs-24" v-if="titleMark">{{ title }}</text>
<text class="app-fc-normal fs-24" v-else>{{ title }}</text>
</view>
<view class="content-view">
<text class="app-fc-main fs-24 app-font-bold-500" :style="[customContentStyle]" v-if="contentMark">
{{ content }}
</text>
<text class="app-fc-main fs-24" :style="[customContentStyle]" v-else>{{ content }}</text>
</view>
<view class="arrow-view" v-if="isShowArrow">
<image src="@/static/images/arrow_right_black.png" mode="widthFix" class="arrow-img"></image>
</view>
</view>
</template>
<script>
export default {
props: {
title: {
type: String,
default: ''
},
content: {
type: String,
default: ''
},
titleMark: {
type: Boolean,
default: false
},
contentMark: {
type: Boolean,
default: true
},
customContentStyle: {
type: Object,
default: () => {
return {}
}
},
isShowArrow: {
type: Boolean,
default: false
}
},
data() {
return {};
},
methods: {
clickAction() {
this.$emit('clickAction')
}
},
}
</script>
<style lang="scss" scoped>
.detail-cell-container {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 20rpx 0;
.title-view {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
flex-shrink: 0;
}
.content-view {
margin-left: 40rpx;
display: flex;
flex: 1;
flex-shrink: 0;
flex-direction: column;
align-items: flex-end;
justify-content: center;
word-wrap: break-word;
word-break: break-all;
}
.arrow-view {
margin-left: 10rpx;
width: 20rpx;
height: 20rpx;
display: flex;
align-items: center;
justify-content: center;
.arrow-img {
width: 100%;
}
}
}
</style>

View File

@ -0,0 +1,541 @@
<template>
<view class="order-cell" @click.stop="gotoDetail">
<view class="order-title-view">
<!-- <view>{{orderInfo.address}}</view> -->
<text class="fs-24 app-fc-normal">{{ `订单编号:${orderNumber}` }}</text>
<view class="state-btn" :class="{ 'done-state-btn': isCompleted || isCanceled }">
<text class="fs-20">{{ orderState }}</text>
</view>
</view>
<view class="line-view" />
<view class="info-row">
<text class="info-label">{{ petsCountLabel }}</text>
<text class="info-value">{{ petsNames }}</text>
</view>
<view class="detail-cell detail-time-cell">
<view class="detail-title-cell">
<text class="fs-24 app-fc-normal">{{ timeTitle }}</text>
</view>
<view class="detail-info-cell">
<text class="fs-24 app-fc-main">{{ reservationTime }}</text>
</view>
</view>
<view class="detail-cell detail-time-cell">
<view class="detail-title-cell">
<text class="fs-24 app-fc-normal">{{ "服务地址" }}</text>
</view>
<view class="detail-info-cell">
<text class="fs-24 app-fc-main">{{ address }}</text>
</view>
</view>
<!-- 预检报告提示条 -->
<view v-if="isShowReport" class="precheck-report-bar" @click.stop="gotoReport">
<text class="precheck-report-text">该订单宠物预检报告已生成</text>
<view class="precheck-report-action">
<text class="precheck-report-link">立即查看</text>
<text class="precheck-report-arrow"></text>
</view>
</view>
<view class="detail-cell detail-time-cell" v-if="orderInfo.fuwuquan_name">
<view class="detail-title-cell">
<text class="fs-24 app-fc-normal">服务券<text class="fs-28" style="color: transparent"></text></text>
</view>
<view class="detail-info-cell">
<text class="fs-24 app-fc-main">{{
orderInfo.fuwuquan_name || "--"
}}</text>
</view>
</view>
<!-- <view class="line-view-2" v-if="!isCanceled " /> -->
<view class="handle-view" v-if="!isCanceled || isCanceled">
<!-- 已取消状态显示再次预约按钮 -->
<view v-if="isCanceled" class="handle-btn" @click.stop="reserveAgain">
<text class="app-fc-white fs-24">再次预约</text>
</view>
<!-- 其他状态的按钮 -->
<template v-else>
<!-- 未支付状态显示取消预约和立即支付按钮 -->
<template v-if="isUnPay">
<view class="cancel-btn" @click.stop="cancelOrder">
<text class="app-fc-main fs-24">取消预约</text>
</view>
<view class="handle-btn" @click.stop="payNow">
<text class="app-fc-white fs-24">立即支付</text>
</view>
</template>
<!-- 其他状态 -->
<template v-else>
<view v-if="orderInfo.status != 5" class="cancel-btn" @click.stop="cancelOrder">
<text class="app-fc-main fs-24">取消预约</text>
</view>
<!-- <view class="handle-btn" v-if="isService" @click.stop="showGoods">
<text class="app-fc-main fs-24">随车购商品</text>
</view> -->
<!-- <view class="handle-btn" v-if="(isReceived || isService) && !isHasNewWeight" @click.stop="clickAddService">
<text class="app-fc">调整服务项</text>
</view> -->
<view class="handle-btn" v-if="isReceived || isService" @click.stop="jumpTo1(orderInfo)">
<text class="app-fc">添加附加项</text>
</view>
<!-- <view class="handle-btn" v-if="isCompleted" @click.stop="callPhone">
<text class="app-fc-main fs-24">联系售后</text>
</view> -->
<view :class="orderInfo.comment_id > 0 ? 'handle-btn' : 'cancel-btn'" v-if="isCompleted" @click.stop="gotoComparisonChart">
<text :class="orderInfo.comment_id > 0 ? 'app-fc-white' : 'app-fc-main'" class="fs-24">洗护对比图</text>
</view>
<view class="handle-btn" v-if="isCompleted && !(orderInfo.comment_id > 0)" @click.stop="gotoEvaluate">
<text class="app-fc-white fs-24">去评价</text>
</view>
</template>
</template>
</view>
</view>
</template>
<script>
import {
ORDER_STATUS_CANCELED,
ORDER_STATUS_COMPLETED,
ORDER_STATUS_RESERVED,
ORDER_STATUS_SEND,
ORDER_STATUS_SERVICE,
ORDER_STATUS_UNPAY,
orderStatusList,
} from "@/pageHome/constants/home";
import {
ORDER_TYPE_SITE,
PET_TYPE_CAT
} from "@/constants/app.business";
import moment from "moment/moment";
import {
gitDiscountfee
} from "../../api/login";
export default {
props: {
orderInfo: {
type: Object,
default: () => {
return {};
},
},
isShowReport: {
type: Boolean,
default: false,
},
},
computed: {
orderNumber() {
return this.orderInfo?.order_no || "--";
},
orderState() {
return (
orderStatusList.find(
(data) => `${data.value}` === `${this.orderInfo?.status}`
)?.label || "--"
);
},
//已预约
isReceived() {
return (
this.orderInfo?.status === ORDER_STATUS_RESERVED ||
this.orderInfo?.status === ORDER_STATUS_SEND
);
},
//已完成
isCompleted() {
return this.orderInfo?.status === ORDER_STATUS_COMPLETED;
},
//已取消
isCanceled() {
return this.orderInfo?.status === ORDER_STATUS_CANCELED;
},
//未支付
isUnPay() {
return this.orderInfo?.status === ORDER_STATUS_UNPAY;
},
//服务中
isService() {
return this.orderInfo?.status === ORDER_STATUS_SERVICE;
},
// 宠物列表:取 order_list 数组,无则用单宠 pet_id/pet_name 包装成数组
petsList() {
const list = this.orderInfo?.pet_id;
if (Array.isArray(list) && list.length > 0) {
return list;
}
return [];
},
// 标签单宠显示「宠物」多宠显示「X只宠物」
petsCountLabel() {
const n = this.petsList.length;
return `${n}只宠物`;
},
// 宠物名称:用 、 拼接
petsNames() {
const names = this.petsList.map((p) => p.pet_name || "").filter(Boolean);
return names.length > 0 ? names.join("、") : "--";
},
timeTitle() {
if (`${this.orderInfo?.order_type}` === `${ORDER_TYPE_SITE}`) {
return "下单时间";
}
return "预约时间";
},
reservationTime() {
if (`${this.orderInfo?.order_type}` === `${ORDER_TYPE_SITE}`) {
return this.orderInfo.created_at
}
const formattedPeriod = this.formatPeriodName(this.orderInfo?.period_name || "");
return `${this.orderInfo?.order_date} ${formattedPeriod}`;
},
address() {
return this.orderInfo?.address || "--";
},
petIcon() {
return this.orderInfo?.type === PET_TYPE_CAT ?
require("@/static/images/dog.png") :
require("@/static/images/dog.png");
},
isHasNewWeight() {
return this.orderInfo?.new_weight_name && this.orderInfo?.weight_chajia;
},
isNewWeightNoPay() {
return (
this.isHasNewWeight &&
`${this.orderInfo.new_weight_status}` !== `${ORDER_STATUS_RESERVED}`
);
},
isHasWashImgs() {
return (
this.isCompleted &&
(this.orderInfo.json_hou || this.orderInfo.json_qian)
);
},
},
data() {
return {};
},
methods: {
formatPeriodName(periodName) {
if (!periodName) return "";
// 将 "15:00:00 ~ 18:00:00" 转换成 "15:00 - 18:00"
return periodName
.replace(/(\d{2}:\d{2}):00/g, "$1") // 去掉秒数,保留时:分格式
.replace(/\s*~\s*/g, " - ") // 将 ~ 替换为 -
.trim();
},
jumpTo(url) {
uni.navigateTo({
url,
});
},
jumpTo1(url) {
const data = {
region_id: 1,
pet_id: url.pet_id.map(item => item.pet_id),
order_date : url.order_date
};
gitDiscountfee(data).then((res) => {
const petIdStr = encodeURIComponent(JSON.stringify(url.pet_id));
uni.navigateTo({
url: `/pageHome/order/additional?id=1&order_id=${url.order_id
}&order_no=${url.order_no}&user_id=${url.member_id}&hair=${url.hair}&breed_id=${url.breed_id}&type=${url.type}&weight_id=${url.weight_id}&discount2=${res.data.discount ? res.data.discount / 10 : 0
}&pet_id=${petIdStr}`,
});
// this.discount = res.data.discount/10;
});
},
clickAddService() {
this.$emit("addService");
},
callPhone() {
this.$emit("callPhone");
},
cancelOrder() {
this.$emit("cancelOrder", this.orderInfo);
},
gotoDetail() {
this.$emit("gotoDetail");
},
gotoEvaluate() {
this.$emit("gotoEvaluate");
},
gotoComparisonChart() {
const orderId = this.orderInfo?.order_id || this.orderInfo?.pet_order?.order_id;
if (!orderId) {
uni.showToast({
title: "订单ID不存在",
icon: "none",
});
return;
}
uni.navigateTo({
url: `/pages/client/order/wash-compare?orderid=${orderId}`,
});
},
showGoods() {
this.$emit("showGoods");
},
gotoReport() {
const orderId = this.orderInfo?.order_id || this.orderInfo?.pet_order?.order_id;
const petsList = this.petsList.filter(item => item.has_precheck) || [];
if (!orderId) {
uni.showToast({
title: "订单ID不存在",
icon: "none",
});
return;
}
if (petsList.length === 0) {
uni.showToast({
title: "暂无宠物信息",
icon: "none",
});
return;
}
if (petsList.length > 1) {
const petsListStr = encodeURIComponent(JSON.stringify(petsList));
const createdAt = this.orderInfo?.created_at || '';
uni.navigateTo({
url: `/pages/client/screening/pet-list?orderId=${orderId}&petsList=${petsListStr}&created_at=${encodeURIComponent(createdAt)}`,
});
} else {
const petId = petsList[0]?.pet_id || petsList[0]?.id || petsList[0]?.chongwu_id || '';
uni.navigateTo({
url: `/pages/client/screening/details?orderId=${orderId}&petId=${petId}`,
});
}
},
reserveAgain() {
// 清除当前页面栈,跳转到首页并切换到预约 tab
uni.reLaunch({
url: '/pages/client/index/index?activePageId=reservationPage'
});
},
payNow() {
// 跳转到订单详情页面进行支付
// orderInfo 可能直接包含 order_id也可能在 pet_order 对象中
const orderId = this.orderInfo?.order_id || this.orderInfo?.pet_order?.order_id;
const source = this.orderInfo?.source || 'pet_order';
if (!orderId) {
uni.showToast({
title: '订单信息错误',
icon: 'none'
});
return;
}
uni.navigateTo({
url: `/pageHome/order/order-detail-page?source=${source}&order_id=${orderId}`,
events: {
refreshList: () => this.$emit('refresh-list'),
},
});
},
},
};
</script>
<style lang="scss" scoped>
.order-cell {
width: calc(100% - 40rpx);
margin: auto;
padding: 20rpx;
border-radius: 30rpx;
background-color: #fff;
display: flex;
flex-direction: column;
box-sizing: border-box;
margin-top: 20rpx;
margin-bottom: 20rpx;
.order-title-view {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
.state-btn {
width: 104rpx;
height: 48rpx;
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: center;
background-color: #fef6ff;
text {
color: $app_fc_mark;
}
}
.done-state-btn {
background-color: #f7f7f7;
text {
color: #afa5ae;
}
}
}
.line-view {
width: 100%;
height: 2rpx;
background-color: #ececec;
margin: 20rpx 0rpx;
}
.info-row {
display: flex;
align-items: center;
.info-label {
font-size: 24rpx;
color: #999;
margin-right: 20rpx;
flex-shrink: 0;
}
.info-value {
font-size: 24rpx;
color: #333;
flex: 1;
word-break: break-all;
}
}
.detail-cell {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
.pet-icon {
width: 24rpx;
height: 24rpx;
}
.detail-title-cell {
display: flex;
align-items: center;
width: fit-content;
flex-shrink: 0;
}
.detail-info-cell {
display: flex;
flex: 1;
align-items: center;
margin-left: 20rpx;
}
}
.detail-time-cell {
margin-top: 16rpx;
}
.precheck-report-bar {
margin-top: 12rpx;
width: 100%;
height: 56rpx;
border-radius: 8rpx;
background-color: #ffe6f0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24rpx;
box-sizing: border-box;
margin-top: 20rpx;
.precheck-report-text {
font-size: 24rpx;
color: #ff19a0;
}
.precheck-report-action {
display: flex;
align-items: center;
}
.precheck-report-link {
font-size: 24rpx;
color: #ff19a0;
margin-right: 8rpx;
}
.precheck-report-arrow {
font-size: 28rpx;
color: #ff19a0;
}
}
.line-view-2 {
width: 100%;
height: 2rpx;
background-color: #ececec;
margin-top: 32rpx;
margin-bottom: 30rpx;
}
.handle-view {
width: 100%;
display: flex;
align-items: center;
justify-content: flex-end;
margin-top: 20rpx;
.cancel-btn {
padding: 12rpx 18rpx;
border-radius: 100px;
display: flex;
align-items: center;
justify-content: center;
border: 2rpx solid #ff19a0;
margin-left: 24rpx;
border: 1px solid #9B939A;
}
.handle-btn {
padding: 12rpx 18rpx;
border-radius: 100px;
border: 0.5px solid #ff19a0;
display: flex;
align-items: center;
justify-content: center;
border: 2rpx solid #ff19a0;
margin-left: 24rpx;
.app-fc {
color: #ff19a0;
font-family: PingFang SC;
font-size: 12px;
}
// width: 152rpx;
// height: 64rpx;
// border-radius: 32rpx;
// display: flex;
// align-items: center;
// justify-content: center;
// border: 2rpx solid #9B939A;
// margin-left: 24rpx;
}
.handle-mark-btn {
border: 2rpx solid $app_color_main;
background-color: $app_color_main;
}
// 最后一个按钮的特殊样式(无论是 cancel-btn 还是 handle-btn
>view:last-child {
border: 2rpx solid #FF19A0 !important;
text {
color: #FF19A0 !important;
}
}
}
}
</style>

View File

@ -0,0 +1,81 @@
<template>
<view class="pay-success-modal" @click.stop="">
<view class="body-view" @click.stop="">
<image src="@/static/images/success_icon.png" class="pay-success-img" mode="aspectFit"/>
<text class="app-fc-main fs-40 app-font-bold-700">提交成功</text>
<text class="app-fc-normal fs-28 tip-text">预计0-3个工作日原路退回支付账户</text>
<view class="handle-btn" @click.stop="okAction">
<text class="app-fc-white fs-32">确定</text>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {};
},
methods: {
closeAction() {
this.$emit('close');
},
okAction() {
this.$emit('ok');
}
},
}
</script>
<style lang="scss" scoped>
.pay-success-modal {
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.5);
position: fixed;
top: 0;
left: 0;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
.body-view {
width: calc(100% - 132rpx);
padding: 60rpx 10rpx;
background-color: #fff;
border-radius: 40rpx;
margin-bottom: 10vh;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
.pay-success-img {
width: 100rpx;
height: 100rpx;
flex-shrink: 0;
margin-bottom: 42rpx;
}
.tip-text {
margin-top: 32rpx;
}
.handle-btn {
margin-top: 52rpx;
width: 216rpx;
height: 80rpx;
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
background-color: $app_color_main;
}
}
}
</style>

View File

@ -0,0 +1,92 @@
<template>
<select-modal @close="closeAction" title="宠物体重">
<view class="select-weight-container">
<picker-view :value="selectedIndex" @change="onChangeAction" class="picker-container"
:indicator-style="indicatorStyle" :immediate-change="true">
<picker-view-column>
<view class="item-view" v-for="(item,index) in weightList" :key="item.weight_id">
{{ item.weight_name }}
</view>
</picker-view-column>
</picker-view>
<view class="submit-btn" @click.stop="changeWeight">
<text class="app-fc-white fs-30">确定</text>
</view>
</view>
</select-modal>
</template>
<script>
import SelectModal from "@/components/select-modal.vue";
import CustomPickerView from "@/components/dengrq-datetime-picker/customPickerView/index.vue";
export default {
components: { CustomPickerView, SelectModal },
props: {
petWeight: {
type: Object,
default: () => {
return {};
}
},
weightList: {
type: Array,
default: () => {
return [];
}
}
},
data() {
return {
indicatorStyle: `height: 60px;`,
selectedIndex: [0]
};
},
mounted() {
console.log("==========this.weightList======>", this.weightList);
const index = this.weightList.findIndex((data) => data?.weight_id === this.petWeight?.weight_id)
this.selectedIndex = index >= 0 ? [index] : [0]
},
methods: {
closeAction() {
this.$emit('close');
},
onChangeAction(e) {
this.selectedIndex = e.detail.value;
},
changeWeight() {
this.$emit('changeWeight', this.weightList[this.selectedIndex[0]])
}
},
}
</script>
<style lang="scss" scoped>
.select-weight-container {
width: 100%;
padding: 0 60rpx 10rpx;
box-sizing: border-box;
.picker-container {
width: 100%;
height: 180px;
.item-view {
width: 100%;
line-height: 60px;
text-align: center;
}
}
.submit-btn {
width: 100%;
height: 90rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 45rpx;
background-color: $app_color_main;
}
}
</style>

View File

@ -0,0 +1,89 @@
<template>
<view class="selected-modal" @click.stop="closeAction">
<view class="model-container" @click.stop="">
<view class="title-view">
<view class="cancel-image"/>
<view class="title-info">
<text class="fs-36 app-fc-main app-font-bold title-text">{{ title }}</text>
</view>
<image src="/static/images/close.png" mode="aspectFit" class="cancel-image" @click.stop="closeAction"/>
</view>
<view class="content-view">
<slot></slot>
</view>
</view>
</view>
</template>
<script>
export default {
props: {
title: {
type: String,
default: '标题'
}
},
data() {
return {};
},
methods: {
closeAction() {
this.$emit('close');
}
},
}
</script>
<style lang="scss" scoped>
.selected-modal {
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.5);
position: fixed;
top: 0;
left: 0;
display: flex;
flex-direction: column;
justify-content: flex-end;
z-index: 999;
.model-container {
width: 100%;
box-sizing: border-box;
background-color: #fff;
border-radius: 40rpx 40rpx 0px 0px;
padding-bottom: 0;
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
.title-view {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
padding: 50rpx 52rpx 8rpx 52rpx;
box-sizing: border-box;
.title-info {
display: flex;
flex: 1;
align-items: center;
justify-content: center;
}
.cancel-image {
width: 52rpx;
height: 52rpx;
}
}
.content-view {
width: 100%;
display: flex;
flex-direction: column;
}
}
}
</style>

View File

@ -0,0 +1,119 @@
/**
* 下载图片的类型
*/
export const imageType = {
homeAd: 1, //首页轮播
order: 2, // 下单轮播
}
export const ORDER_TYPE_RESERVATION = '1'; //预约单
export const ORDER_TYPE_SITE = '2'; //现场单
export const COUPON_TYPE_GOODS= '1'; //商品
export const COUPON_TYPE_PET = '2'; //宠物
export const ORDER_TYPE_GOODS = 1; //商品
export const ORDER_TYPE_PET_SERVICE = 2; //宠物
export const PET_TYPE_DOG = 1 //狗
export const PET_TYPE_CAT = 2 //猫
export const PET_SEX_MALE = 1 //公
export const PET_SEX_FEMALE = 2 //母
export const PET_HAIR_LONG = 1 //长发
export const PET_HAIR_SHORT = 2 //短发
/**
* 是否绝育
*/
export const PET_IS_STERILIZE_YES = 1 //是
export const PET_IS_STERILIZE_NO = 2 //否
/**
* 文章类型
* @type {number}
*/
//关于我们
export const ARTICLE_TYPE_ABOUT_US = 1
//使用帮助
export const ARTICLE_TYPE_HELP = 2
//预约狗详情
export const ARTICLE_TYPE_RESERVATION_DOG = 3
//预约猫详情
export const ARTICLE_TYPE_RESERVATION_CAT = 4
//平台服务协议
export const ARTICLE_TYPE_SERVICE_AGREEMENT = 5
//隐私协议
export const ARTICLE_TYPE_PRIVACY_AGREEMENT = 6
/**
* 商城订单状态
*/
export const SHOP_ORDER_UNPAY = 1 // 待支付
export const SHOP_ORDER_UNSLIVER = 2 // 待发货
export const SHOP_ORDER_UNRECEIVE = 3 // 待收货
export const SHOP_ORDER_UNREMARK = 4 // 待评价
export const SHOP_ORDER_DONE = 6 // 已完成
export const SHOP_ORDER_CANCEL = 5 // 取消订单
export const SHOP_ORDER_STATUS = {
[SHOP_ORDER_UNPAY]: '待支付',
[SHOP_ORDER_UNSLIVER]: '待发货',
[SHOP_ORDER_UNRECEIVE]: '待收货',
[SHOP_ORDER_DONE]: '已完成',
[SHOP_ORDER_CANCEL]: '已取消',
[SHOP_ORDER_UNREMARK]: '已签收'
}
// 商城订单类型
export const ORDER_TYPE_ADDRESS = 1; // 快递
export const ORDER_TYPE_BYCAR = 2; // 随车订单
export const ORDER_TYPE_BYPET = 3; // 随车购
/**
* 商城订单售后状态
*/
export const SHOP_ORDER_AFTERSALE = 1 // 售后中
export const SHOP_ORDER_AFTERSALE_DONE = 2 // 已退款
export const SHOP_ORDER_AFTERSALE_REJECT = 3 // 已驳回
export const SHOP_ORDER_AFTERSALE_STATUS = {
[SHOP_ORDER_AFTERSALE]: '售后中',
[SHOP_ORDER_AFTERSALE_DONE]: '已退款',
[SHOP_ORDER_AFTERSALE_REJECT]: '已驳回',
}
//服务列表-宠物类型
export const PET_TYPE_LIST = [
{
id: '',
name: '全部',
type: '',
maofa: ''
},
{
id: '2',
name: '猫(长毛)',
type: PET_TYPE_CAT,
maofa: PET_HAIR_LONG
},
{
id: '3',
name: '猫(短毛)',
type: PET_TYPE_CAT,
maofa: PET_HAIR_SHORT
},
{
id: '1',
name: '狗',
type: PET_TYPE_DOG,
maofa: ''
},
]

View File

@ -0,0 +1,55 @@
// 应用的配置页面
export default {
appName: "Wagoo",
appShareName: "Wagoo",
appId: "wx00e2dcdc7c02b23a",
// apiBaseUrl: 'https://api.wagoo.cc', // 服务端生产地址
apiBaseUrl: "https://api.wagoo.me/api/v1", // 服务端测试地址
// apiBaseUrl: "https://api.wagoo.pet/api/v1", // 服务端生产地址
// apiBaseUrl: "http:192.168.30.79", //本地接口
tencentMapKey: "WSBBZ-7OXK4-46QUC-KFB7B-4N3W7-M2BXM",
tencentSecret: "vb7D0PGj7xUvmOLuJz2Jd7ykTMpjiWRJ",
qiWeId: "ww285b04a185307dc9",
qiWeLink: "https://work.weixin.qq.com/kfid/kfcbb4c893c4d38abf2",
router: {
/*
名词解释:“强制登录页”
在打开定义的需强制登录的页面之前会自动检查前端校验token的值是否有效,
如果无效会自动跳转到登录页面
两种模式:
1.needLogin黑名单模式。枚举游客不可访问的页面。
2.visitor白名单模式。枚举游客可访问的页面。
* 注意:黑名单与白名单模式二选一
*/
visitor: [
"/", //注意入口页必须直接写 "/"
"/pages/client/auth/index",
"/pages/client/index/index",
"/pages/client/mine/index",
"/pages/client/mine/help",
"/pages/client/mine/aboutus",
"/pages/richText/index",
"/pages/client/mine/index",
"/pages/client/service/details",
"/pages/client/shop/details",
"/pageHome/reservation/index",
"/pageHome/service/index",
"/pageHome/service/feeding",
"/pageHome/service/training-booking",
"/pageHome/franchise/index",
"/pages/client/category/index",
"/pages/client/coupon/service-list",
"/pages/client/coupon/list",
"/pages/client/record/list",
"/pages/client/order/list",
"/pages/client/collect/list",
"/pages/client/order/after-sale",
"/pages/client/news/index",
"/pages/client/address/index",
],
},
debug: false,
};

45
src/main.js Normal file
View File

@ -0,0 +1,45 @@
import Vue from 'vue'
import App from './App'
import Store from './store'
import NavBar from "./components/NavBar.vue";
import ListPageTemp from './components/ListPageTemp.vue'
import SplitView from './components/SplitView.vue'
Vue.config.productionTip = false
Vue.component('NavBar', NavBar)
Vue.component('ListPageTemp', ListPageTemp)
Vue.component('SplitView', SplitView)
Vue.prototype.$store = Store;
Date.prototype.format = function (fmt) {
var o = {
"M+": this.getMonth() + 1, // 月份
"d+": this.getDate(), // 日
"h+": this.getHours(), // 小时
"m+": this.getMinutes(), // 分
"s+": this.getSeconds(), // 秒
"q+": Math.floor((this.getMonth() + 3) / 3), // 季度
"S": this.getMilliseconds() // 毫秒
};
if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length));
for (var k in o)
if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
return fmt;
}
// 设置默认字号
const fontScale = uni.getStorageSync('fontScale') == '' ? 1 : uni.getStorageSync('fontScale');
Store.dispatch('app/setFontScale', fontScale)
// 设置登录信息
const loginInfo = uni.getStorageSync('loginInfo')
loginInfo && Store.dispatch('user/setLoginInfo', loginInfo)
App.mpType = 'app'
const app = new Vue({
...App,
})
app.$mount()

94
src/manifest.json Normal file
View File

@ -0,0 +1,94 @@
{
"name" : "Wagoo",
"appid" : "__UNI__F0AB43B",
"description" : "init",
"versionName" : "1.0.0",
"versionCode" : "100",
"transformPx" : false,
"app-plus" : {
/* 5+App */
"compatible" : {
"ignoreVersion" : true
},
"usingComponents" : true,
"splashscreen" : {
"alwaysShowBeforeRender" : true,
"waiting" : true,
"autoclose" : true,
"delay" : 0
},
"modules" : {},
/* */
"distribute" : {
/* */
"android" : {
/* android */
"permissions" : [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.READ_CONTACTS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.WRITE_CONTACTS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.CALL_PHONE\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
]
},
"ios" : {},
/* ios */
"sdkConfigs" : {},
"splashscreen" : {
"useOriginalMsgbox" : true
}
}
},
/* SDK */
"quickapp" : {},
/* */
"mp-weixin" : {
"appid" : "wx00e2dcdc7c02b23a",
"setting" : {
"urlCheck" : false,
"minified" : true,
"postcss" : true,
"es6" : true,
"minifyJS" : true,
"minifyWXML" : true,
"minifyWXSS" : true
},
"usingComponents" : true,
"lazyCodeLoading" : "requiredComponents",
"optimization" : {
"subPackages" : true
},
"requiredPrivateInfos" : [],
"permission" : {},
"plugins" : {}
},
"mp-alipay" : {
"usingComponents" : true
},
"mp-baidu" : {
"usingComponents" : true
},
"mp-toutiao" : {
"usingComponents" : true
},
"mp-qq" : {
"usingComponents" : true
}
}

View File

@ -0,0 +1,356 @@
<template>
<view class="order-info-cell" @click.stop="clickAction">
<template v-if="cellType === 'text'">
<view class="info-top-view">
<!-- <image :src="infoIcon" mode="aspectFit" class="cell-icon" /> -->
<text v-if="isRequired" class="required">*</text>
<view class="title-view">
<text class="app-fc-main fs-24">{{ title }}</text>
</view>
<view class="info-view" v-if="info">
<text class="app-fc-main fs-24">{{ info }}</text>
</view>
<view class="info-view" v-else>
<text style="color: #9B939A;" class="app-fc-main fs-24">{{ placeholder }}</text>
</view>
<image v-if="isCanClick" class="right-icon" :src="`${imgPrefix}right-arrow.png`" mode="widthFix">
</image>
<view v-else class="right-icon" />
</view>
</template>
<!-- 时间信息-->
<template v-if="cellType === 'time'">
<view class="info-top-view">
<!-- <image :src="infoIcon" mode="aspectFit" class="cell-icon" /> -->
<text class="required">*</text>
<view class="title-view">
<text class="app-fc-main fs-24">{{ title }}</text>
</view>
<view class="info-bottom-view" v-if="info">
<text class="app-fc-main fs-24">{{ info }}</text>
</view>
<view class="info-view" v-else>
<text style="color: #9B939A;" class="app-fc-main fs-24">{{ placeholder }}</text>
</view>
<image class="right-icon" :src="`${imgPrefix}right-arrow.png`" mode="widthFix"></image>
</view>
<text v-if="infoTime == 5" class="s">(该时间段需收取夜间费)</text>
</template>
<!-- 地址信息-->
<template v-if="cellType === 'address'">
<view class="info-top-view">
<!-- <image :src="infoIcon" mode="aspectFit" class="cell-icon" /> -->
<text class="required">*</text>
<view class="title-view">
<text class="app-fc-main fs-24">{{ title }}</text>
</view>
<view class="info-bottom-view" style="max-width: 60%;" v-if="addressInfo">
<view class="user-info-view">
<text class="app-fc-main fs-24 app-font-bold">{{
addressInfo.recipient_name
}}</text>
<text class="app-fc-main fs-24 phone-text app-font-bold">{{
addressInfo.phone
}}</text>
</view>
<view class="address-view">
<text class="app-fc-normal fs-24 address-text">{{
`${addressInfo.area_name} ${addressInfo.full_address}`
}}</text>
</view>
</view>
<view class="info-view" v-else>
<text style="color: #9B939A;" class="app-fc-main fs-24">{{ placeholder }}</text>
</view>
<image class="right-icon" :src="`${imgPrefix}right-arrow.png`" mode="widthFix"></image>
</view>
<text class="nightFee">以下区域需收取调度费奉贤区嘉定区青浦区松江区崇明区金山区</text>
</template>
<!-- 价格-->
<template v-if="cellType === 'price'">
<view class="info-top-view">
<view class="title-view">
<text class="app-fc-main fs-28">{{ title }}</text>
</view>
<text v-if="price" class="app-fc-mark fs-32 app-font-bold-700">{{
`¥${price}`
}}</text>
<text v-else class="app-fc-main fs-28">---</text>
</view>
</template>
<template v-if="cellType === 'park'">
<view class="info-top-view">
<view class="title-view">
<text class="app-fc-main fs-24">{{ title }}</text>
</view>
</view>
<view class="list-view">
<view class="list-card" v-for="item in park_conditions" :key="item"
:class="{ 'selected-list-card': parkState === item }" @click.stop="changeParkState(item)">
<text class="fs-24 app-fc-main" :class="{ 'app-fc-mark': parkState === item }">{{ item }}</text>
</view>
</view>
<view class="input-container" v-if="parkState === '其他'">
<textarea :focus="true" :value="otherParkState" class="input-view fs-24 app-fc-main"
placeholder="点击输入停车信息" placeholder-class="placeholder-class" @input="onChange" />
</view>
</template>
</view>
</template>
<script>
import {
imgPrefix
} from "@/utils/common";
/**
* info-cell 预约信息显示
* @property {String} cellType - 显示类型 默认text text/address/time/price/park
* @value text 文本类型
* @value address 地址类型
* @value time 时间类型
* @value price 价格类型
* @value park 停车状况
* @property {String} infoIcon - 左侧图标
* @property {String} title - 标题
* @property {String} info - 显示内容
* @property {Object} addressInfo - 地址信息
* @property {String} price - 价格
* @property {String} parkState - 停车状况
*
* @property {String} placeholder - 占位
*
* @event {Function} clickAction 点击cell触发事件
* @event {Function} changeParkState 停车状况改变触发事件
*/
export default {
props: {
isCanClick: {
type: Boolean,
default: true,
},
cellType: {
type: String,
default: "info",
},
infoIcon: {
type: String | null,
default: require("@/static/images/arrow_right.png"),
},
title: {
type: String,
default: "",
},
infoTime: {
type: String,
default: "",
},
info: {
type: String,
default: "",
},
addressInfo: {
type: Object | null,
default: () => {
return null;
},
},
price: {
type: String,
default: "",
},
parkState: {
type: String,
default: "",
},
otherParkState: {
type: String,
default: "",
},
placeholder: {
type: String,
default: "",
},
isRequired: {
type: Boolean,
default: false,
}
},
data() {
return {
park_conditions: ["小区", "地库", "路边", "其他"],
imgPrefix
};
},
methods: {
clickAction() {
this.isCanClick && this.$emit("clickAction");
},
changeParkState(state) {
this.$emit("changeParkState", state);
},
onChange(e) {
this.$emit("changeOtherParkState", e.detail.value);
},
},
};
</script>
<style lang="scss" scoped>
.order-info-cell {
width: 100%;
background-color: #fff;
display: flex;
flex-direction: column;
padding: 36rpx 20rpx;
box-sizing: border-box;
position: relative;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 20rpx;
right: 20rpx;
height: 2rpx;
background-color: #ececec;
}
.info-top-view {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
box-sizing: border-box;
.cell-icon {
width: 36rpx;
height: 36rpx;
flex-shrink: 0;
margin-right: 20rpx;
}
.title-view {
display: flex;
flex: 1;
align-items: center;
}
.info-view {
display: flex;
flex: 1;
align-items: center;
justify-content: flex-end;
}
.right-icon {
margin-left: 16rpx;
width: 10rpx;
height: 5rpx;
flex-shrink: 0;
}
}
.info-bottom-view {
// width: calc(100% - 56rpx);
// margin-top: 32rpx;
// margin-left: 56rpx;
// padding-right: 104rpx;
display: flex;
flex-direction: column;
box-sizing: border-box;
.user-info-view {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
.phone-text {
margin-left: 24rpx;
}
}
.address-view {
width: 100%;
display: flex;
align-items: center;
.address-text {
margin-top: 12rpx;
}
}
}
.nightFee {
color: #ff19a0;
font-family: PingFangSC;
font-size: 20rpx;
font-weight: normal;
margin-top: 20rpx;
}
.s {
color: #ff19a0;
font-family: PingFangSC;
font-size: 24rpx;
margin: 6rpx 0 0 52rpx;
}
.list-view {
margin-top: 16rpx;
width: 100%;
padding-right: 30rpx;
box-sizing: border-box;
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
gap: 24rpx;
.list-card {
padding: 12rpx 20rpx;
border-radius: 182px;
background-color: #f5f5f5;
color: #000;
}
.selected-list-card {
background-color: #fee9f3;
}
}
.input-container {
width: 100%;
height: 200rpx;
padding: 0 30rpx 0 0;
box-sizing: border-box;
margin-top: 32rpx;
.input-view {
width: 100%;
height: 100%;
padding: 32rpx;
border-radius: 30rpx;
color: #333;
box-sizing: border-box;
background-color: #f9f7f9;
}
.placeholder-class {
color: #666262;
font-size: 28rpx;
}
}
}
.required {
color: #FF19A0;
}
</style>

View File

@ -0,0 +1,184 @@
<template>
<select-modal @close="closeAction" title="随车购商品" class="order-goods-modal">
<view class="goods-container">
<scroll-view class="goods-scroll-view" :scroll-y="true">
<view class="goods-item" v-for="item in goodsList" :key="item.order_id" @click.stop="gotoDetail(item)">
<image mode="aspectFit" class="goods-img" :src="item.goods_pic"/>
<view class="goods-info-view">
<text class="app-fc-main fs-32 app-font-bold-500 app-text-ellipse">
{{ item.goods_name || '--' }}
</text>
<text class="app-fc-normal fs-24 goods-num-text">{{ `数量X${item.number}` }}</text>
<text class="app-fc-main fs-28 goods-price-text" >实付款
<text class="fs-28 app-fc-alarm">{{ `¥${item.goods_price}` }}</text>
</text>
</view>
<image src="@/static/images/arrow_right_black.png" mode="aspectFit" class="good-arrow"/>
</view>
</scroll-view>
</view>
<view class="goods-price-container">
<view class="goods-left-view">
<view class="price-view">
<text class="app-fc-main fs-28">¥</text>
<text class="app-fc-main app-font-bold-700 fs-50 price-text">{{ price }}</text>
</view>
<view class="price-tips-view">
<image src="/pageHome/static/tips.png" mode="aspectFit" class="tips-img"/>
<text class="fs-24">商品总价</text>
</view>
</view>
<view class="submit-btn" @click.stop="closeAction">
<text class="app-fc-white fs-30">确定</text>
</view>
</view>
</select-modal>
</template>
<script>
import SelectModal from "@/components/select-modal.vue";
export default {
components: { SelectModal },
props: {
goodsList: {
type: Array,
default: () => []
},
price: {
type: Number,
default: 0
}
},
data() {
return {};
},
options: {
styleIsolation: "shared",
},
methods: {
closeAction() {
this.$emit('close');
},
gotoDetail(item){
this.$emit('gotoDetail', item.goods_id);
},
},
}
</script>
<style lang="scss" scoped>
.order-goods-modal {
::v-deep {
.selected-modal .model-container {
background: #fff0f5;
position: relative;
}
}
.goods-container {
width: 100%;
height: 800rpx;
position: relative;
.goods-scroll-view {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
padding: 0 32rpx;
box-sizing: border-box;
.goods-item {
width: 100%;
height: 200rpx;
display: flex;
align-items: center;
background-color: #fff;
border-radius: 40rpx;
margin-top: 32rpx;
padding: 20rpx;
box-sizing: border-box;
.goods-img {
width: 160rpx;
height: 160rpx;
margin-right: 24rpx;
}
.goods-info-view {
display: flex;
flex-direction: column;
width: calc(100% - 160rpx - 24rpx - 40rpx);
.goods-num-text {
margin-top: 16rpx;
}
.goods-price-text {
margin-top: 40rpx;
}
}
.good-arrow {
width: 20rpx;
height: 20rpx;
margin-left: 20rpx;
}
}
}
}
.goods-price-container {
width: 100%;
background-color: #fff;
padding: 26rpx 32rpx;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
.goods-left-view {
display: flex;
flex-direction: column;
.price-view {
display: flex;
flex-direction: row;
align-items: flex-end;
.price-text {
margin-left: 8rpx;
line-height: 50rpx;
}
}
.price-tips-view {
margin-top: 10rpx;
display: flex;
flex-direction: row;
align-items: center;
.tips-img {
width: 24rpx;
height: 24rpx;
margin-right: 2rpx;
}
}
}
.submit-btn {
width: 260rpx;
height: 92rpx;
display: flex;
align-items: center;
justify-content: center;
background-color: #FE019B;
border-radius: 46rpx;
}
}
}
</style>

View File

@ -0,0 +1,49 @@
<template>
<select-modal @close="closeAction" title="价格说明">
<view class="price-description-container">
<view class="price-info">
<text class="app-fc-main fs-32">{{priceInfo.desc}}</text>
</view>
</view>
</select-modal>
</template>
<script>
import SelectModal from "@/components/select-modal.vue";
export default {
components: { SelectModal },
props: {
priceInfo: {
type: Object,
default: () => {
return {};
}
}
},
data() {
return {};
},
methods: {
closeAction() {
this.$emit('close');
},
},
}
</script>
<style lang="scss" scoped>
.price-description-container {
width: 100%;
padding: 0 40rpx 10rpx;
box-sizing: border-box;
.price-info {
width: 100%;
padding: 36rpx 0;
box-sizing: border-box;
border-bottom: 1px solid #F7F3F7;
}
}
</style>

View File

@ -0,0 +1,230 @@
<template>
<select-modal @close="closeAction" title="选择服务地址">
<view class="select-address-container">
<view v-if="!isLoading && addressList.length" class="address-container">
<scroll-view class="scroll-view" scroll-y @scrolltolower="loadMoreAction"
refresher-background="transparent">
<view class="address-item" v-for="(item, index) in addressList" :key="item.id"
@click.stop="changeAddress(item)">
<view class="address-info-view">
<view class="address-name-view">
<text v-if="item.is_default" class="default-address fs-18">默认</text>
<text class="app-fc-main fs-30 app-font-bold name-text">{{ item.recipient_name }}</text>
<text class="app-fc-main fs-30 app-font-bold">{{ item.phone }}</text>
</view>
<text class="app-fc-normal fs-26">{{ getAddressText(item) }}</text>
</view>
<image @click.stop="goToEditAddress(item)" src="@/static/images/address_edit.png" mode="aspectFit" class="address-edit-icon"/>
</view>
</scroll-view>
</view>
<view class="address-container flex-center" v-if="isLoading">
<uni-load-more status="loading" :show-text="false"/>
</view>
<view v-if="addressList.length === 0 && !isLoading" class="address-container">
<image src="https://activity.wagoo.live/no_address.png" class="no-address-img" mode="widthFix"/>
<!-- <text class="app-fc-normal fs-32 no-text">暂无地址信息</text> -->
</view>
<view class="add-btn" @click.stop="gotoAddAddress">
<image class="add-icon" src="@/static/images/add.png" mode="aspectFit"/>
<text class="app-fc-white fs-30">添加地址</text>
</view>
</view>
</select-modal>
</template>
<script>
import SelectModal from "@/components/select-modal.vue";
import { getAddressList } from "@/api/address";
export default {
components: { SelectModal },
props: {
selectAddress: {
type: Object,
default: () => {
return {};
}
}
},
data() {
return {
addressList: [],
isLoading: true,
pageNumber: 1,
pageSize: 10,
isLoadMore: false,
isNoMore: false,
};
},
computed: {
userInfo() {
return this.$store.state?.user?.userInfo || {};
}
},
mounted() {
this.pageNumber = 1;
this.getData();
},
methods: {
closeAction() {
this.$emit('close');
},
changeAddress(address) {
this.$emit('changeAddress', address);
},
loadMoreAction() {
if (this.isNoMore || this.isLoadMore) {
return;
}
this.pageNumber++;
this.isLoadMore = true;
this.getData()
},
getData() {
getAddressList({ user_id: this.userInfo.userID }).then((res) => {
let list = res?.data || [];
if (this.pageNumber === 1) {
this.addressList = list;
} else {
this.addressList = [...this.addressList, ...list];
}
this.isLoading = false;
this.isLoadMore = false;
this.isNoMore = list.length < this.pageSize;
}).catch(() => {
if (this.pageNumber !== 1) {
this.pageNumber--;
}
this.isLoading = false;
this.isLoadMore = false;
})
},
getAddressText(item) {
// 组合地址信息:省市区 + 详细地址
const region = [item.province, item.city, item.district].filter(Boolean).join('');
return region ? `${region}${item.full_address}` : item.full_address;
},
gotoAddAddress() {
uni.navigateTo({
url: `/pages/client/address/edit?isAdd=1`,
events: {
refreshAddress: () => {
this.isLoading = true;
this.isNoMore = false;
this.isLoadMore = false;
this.pageNumber = 1;
this.getData();
},
},
})
},
goToEditAddress(item){
uni.navigateTo({
url: `/pages/client/address/edit?id=${item?.id || ""}`,
events: {
refreshAddress: () => {
this.isLoading = true;
this.isNoMore = false;
this.isLoadMore = false;
this.pageNumber = 1;
this.getData();
},
},
})
}
}
}
</script>
<style lang="scss" scoped>
.select-address-container {
width: 100%;
padding: 0 56rpx 10rpx;
box-sizing: border-box;
.address-container {
width: 100%;
height: 444rpx;
display: flex;
flex-direction: column;
margin-bottom: 18rpx;
.no-address-img {
margin-top: 30rpx;
width: 348rpx;
align-self: center;
}
.no-text {
margin: 0 0 24rpx;
align-self: center;
}
.scroll-view {
width: 100%;
height: 100%;
}
.address-item {
width: 100%;
display: flex;
align-items: center;
padding: 30rpx 0rpx;
box-sizing: border-box;
border-bottom: 2rpx solid #F7F3F7;
.address-info-view {
display: flex;
flex: 1;
flex-direction: column;
margin-right: 30rpx;
.address-name-view {
display: flex;
flex: 1;
align-items: center;
margin-bottom: 12rpx;
.default-address {
padding: 2rpx 6rpx;
color: #40ae36;
background: rgba(64, 174, 54, 0.2);
border-radius: 6rpx;
margin-right: 16rpx;
}
.name-text {
margin-right: 16rpx;
}
}
}
.address-edit-icon {
width: 48rpx;
height: 48rpx;
}
}
}
.add-btn {
width: 100%;
height: 90rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 45rpx;
background-color: $app_color_main;
.add-icon {
width: 34rpx;
height: 34rpx;
flex-shrink: 0;
margin-right: 18rpx;
}
}
}
</style>

View File

@ -0,0 +1,201 @@
<template>
<select-modal @close="closeAction" title="选择宠物">
<view class="select-pet-container">
<view v-if="!isLoading && petList.length" class="pet-container">
<scroll-view class="scroll-view" scroll-y @scrolltolower="loadMoreAction"
refresher-background="transparent">
<view class="pet-item" v-for="(item, index) in petList" :key="item.chongwu_id" @click.stop="changePet(item)">
<image v-if="item.chongwu_pic_url" class="pet-icon" mode="scaleToFill" :src="item.chongwu_pic_url"/>
<image v-else mode="scaleToFill" class="pet-icon" src="https://activity.wagoo.live/record_avator.png"/>
<view class="pet-name-view">
<text class="app-fc-main fs-32 app-font-bold-500">{{ item.name }}</text>
</view>
<image
v-if="selectPetInfo.chongwu_id === item.chongwu_id"
src="@/static/images/cart_checked.png"
mode="widthFix"
class="select-icon"
/>
<image
v-else
src="@/static/images/unchecked.png"
mode="widthFix"
class="select-icon"
/>
</view>
</scroll-view>
</view>
<view class="pet-container flex-center" v-if="isLoading">
<uni-load-more status="loading" :show-text="false"/>
</view>
<view v-if="petList.length === 0 && !isLoading" class="pet-container" @click.stop="gotoAddPet">
<image src="https://activity.wagoo.live/no_pet.png" class="no-pet-img" mode="heightFix" />
</view>
<view class="add-btn" @click.stop="gotoAddPet">
<image class="add-icon" src="@/static/images/add.png" mode="aspectFit"/>
<text class="app-fc-white fs-30">添加宠物</text>
</view>
</view>
</select-modal>
</template>
<script>
import SelectModal from "@/components/select-modal.vue";
import { getPetList } from "@/api/common";
import appConfig from '../../constants/app.config';
export default {
components: { SelectModal },
props: {
petType: {
type: Number,
default: 0
},
selectPetInfo: {
type: Object,
default: () => {
return {};
}
}
},
data() {
return {
isLoading: true,
pageNumber: 1,
pageSize: 10,
petList: [],
isLoadMore: false,
isNoMore: false,
appConfig,
};
},
mounted() {
this.pageNumber = 1;
this.getData();
},
methods: {
closeAction() {
this.$emit('close');
},
changePet(pet) {
this.$emit('changePet', pet);
},
loadMoreAction() {
if (this.isNoMore || this.isLoadMore) {
return;
}
this.pageNumber++;
this.isLoadMore = true;
this.getData()
},
getData() {
getPetList(null, this.pageNumber, this.pageSize).then((res) => {
let list = res?.info || [];
if (this.pageNumber === 1) {
this.petList = list;
} else {
this.petList = [...this.petList, ...list];
}
this.isLoading = false;
this.isLoadMore = false;
this.isNoMore = list.length < this.pageSize;
}).catch(() => {
if (this.pageNumber !== 1) {
this.pageNumber--;
}
this.isLoading = false;
this.isLoadMore = false;
})
},
gotoAddPet() {
uni.navigateTo({
url: `/pages/client/record/edit?type=${this.petType }&typeId=aaa`,
events: {
addPetSuccess: () => {
this.isLoading = true;
this.isNoMore = false;
this.isLoadMore = false;
this.pageNumber = 1;
this.getData();
},
},
})
}
}
}
</script>
<style lang="scss" scoped>
.select-pet-container {
width: 100%;
padding: 0 60rpx 10rpx;
box-sizing: border-box;
.pet-container {
width: 100%;
height: 444rpx;
display: flex;
flex-direction: column;
margin-bottom: 18rpx;
.no-pet-img {
height: calc(1289 / 1363 * 550rpx);
align-self: center;
}
.no-text {
margin: 44rpx 0 26rpx;
align-self: center;
}
.scroll-view {
width: 100%;
height: 100%;
}
.pet-item {
width: 100%;
display: flex;
align-items: center;
padding: 34rpx 22rpx;
box-sizing: border-box;
border-bottom: 2rpx solid #F7F3F7;
.pet-icon {
width: 80rpx;
height: 80rpx;
border-radius: 40rpx;
overflow: hidden;
}
.pet-name-view {
display: flex;
flex: 1;
margin-left: 24rpx;
}
.select-icon {
width: 27rpx;
}
}
}
.add-btn {
width: 100%;
height: 90rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 45rpx;
background-color: $app_color_main;
.add-icon {
width: 34rpx;
height: 34rpx;
flex-shrink: 0;
margin-right: 18rpx;
}
}
}
</style>

View File

@ -0,0 +1,262 @@
<template>
<select-modal @close="closeAction" title="选择预约时间">
<view class="select-time-container">
<view class="calendar-container">
<uni-calendar
:insert="true"
class="uni-calendar--hook"
:start-date="startDate"
:end-date="endDate"
:date="selectDate"
:showMonth="false"
@change="changeDate"/>
</view>
<view class="tips-view">
<image src="/pageHome/static/tips.png" class="tips-img" mode="aspectFit"></image>
<text class="fs-24 app-fc-normal">
{{isAfternoon? '可以预约的时间范围是后天及之后的 14 天' : '可以预约的时间范围是明天及之后的 14 天' }}
</text>
</view>
<text class="fs-32 app-fc-main time-header-text">可预约时段</text>
<view class="time-list-container">
<scroll-view v-if="!isLoading && dateList.length" class="list-scroll-view" :scroll-y="true">
<view
@click.stop="changeTimeAction(item)"
class="time-item"
:class="{'selected-time-item': selectTimeRange.shiduan_id === item.shiduan_id, 'disabled-time-item': item.num === 0}"
v-for="item in dateList"
:key="item.shiduan_id">
<text class="fs-28 app-fc-main">{{ `${item.start}-${item.end}` }}</text>
<text class="fs-28 app-fc-main">{{ item.num === 0 ? '已约满' : `剩余${item.num}` }}</text>
</view>
</scroll-view>
<view v-if="isLoading" class="loading-view">
<uni-load-more status="loading"/>
</view>
<view v-if="!isLoading && dateList.length === 0" class="loading-view">
<text class="fs-28 app-fc-main">无可预约时间</text>
</view>
</view>
<view class="submit-btn" @click.stop="changeReservationTime">
<text class="app-fc-white fs-30">确定</text>
</view>
</view>
</select-modal>
</template>
<script>
import SelectModal from "@/components/select-modal.vue";
import moment from "moment";
import "moment/locale/zh-cn";
import { getYuYueTimeList } from "@/api/order";
export default {
components: { SelectModal },
props: {
selectTime: {
type: Object,
default: () => {
return {};
}
}
},
data() {
return {
selectTimeRange: {},
isLoading: false,
selectDate: moment().isAfter(moment().format('YYYY-MM-DD 12:00:00')) ? moment().add(2, 'days').format('YYYY-MM-DD') : moment().add(1, 'days').format('YYYY-MM-DD'),
startDate: moment().isAfter(moment().format('YYYY-MM-DD 12:00:00')) ? moment().add(2, 'days').format('YYYY-MM-DD') : moment().add(1, 'days').format('YYYY-MM-DD'),
endDate: moment().isAfter(moment().format('YYYY-MM-DD 12:00:00')) ? moment().add(16, 'days').format('YYYY-MM-DD') : moment().add(15, 'days').format('YYYY-MM-DD'),
dateList: [],
//是下午
isAfternoon: moment().isAfter(moment().format('YYYY-MM-DD 12:00:00'))
};
},
mounted() {
moment.locale('zh-cn'); // 设置中文本地化
if (Object.keys(this.selectTime).length > 0) {
this.selectDate = this.selectTime.date;
this.selectTimeRange = this.selectTime;
this.getTimeArray(this.selectTime.date)
} else {
this.getTimeArray(this.selectDate)
}
},
methods: {
closeAction() {
this.$emit('close');
},
changeDate(e) {
console.log(e,'--')
this.selectTimeRange = {};
this.selectDate = e.fulldate;
this.getTimeArray(e.fulldate)
},
changeTimeAction(item) {
if (item.num === 0) {
uni.showToast({
title: '当前时间已约满',
icon: 'none'
})
} else {
this.selectTimeRange = item;
}
},
changeReservationTime() {
if (this.dateList.length === 0) {
uni.showToast({
title: '暂无可预约时间',
icon: 'none'
})
return;
}
if (Object.keys(this.selectTimeRange).length === 0) {
uni.showToast({
title: '请选择一个预约时段',
icon: 'none'
})
return;
}
let dateLabel;
if (moment().format('YYYY-MM-DD') === moment(this.selectDate).format('YYYY-MM-DD')) {
dateLabel = `今天${moment(this.selectDate).format('M月D日')}`
} else if (moment().add(1, "days").format('YYYY-MM-DD') === moment(this.selectDate).format('YYYY-MM-DD')) {
dateLabel = `明天${moment(this.selectDate).format('M月D日')}`
} else if (moment().add(2, "days").format('YYYY-MM-DD') === moment(this.selectDate).format('YYYY-MM-DD')) {
dateLabel = `后天${moment(this.selectDate).format('M月D日')}`
} else {
dateLabel = `${moment(this.selectDate).format('M月D日')}`
}
this.$emit('changeReservationTime', {
dateLabel,
date: this.selectDate,
...this.selectTimeRange,
});
},
getTimeArray(t) {
this.isLoading = true
this.dateList = [];
let isCurrent = moment().format('YYYY-MM-DD') === t;
return getYuYueTimeList(t).then((res) => {
let list = (res?.info || []).map((item) => {
return {
...item,
start: item.start.substring(0, 5),
end: item.end.substring(0, 5),
};
});
this.dateList = list.filter((item) => {
let end = moment(item.end, 'HH:mm');
let now = moment();
if (isCurrent) {
return end.isAfter(now);
} else {
return true;
}
});
this.isLoading = false;
return Promise.resolve();
}).catch(() => {
this.isLoading = false
})
}
},
}
</script>
<style lang="scss" scoped>
.select-time-container {
width: 100%;
height: 82vh;
box-sizing: border-box;
display: flex;
flex-direction: column;
.calendar-container {
width: 100%;
}
.tips-view {
width: 100%;
display: flex;
align-items: center;
padding: 24rpx 30rpx;
box-sizing: border-box;
background-color: #F5F5F5;
margin-bottom: 40rpx;
.tips-img {
width: 24rpx;
height: 24rpx;
margin-right: 6rpx;
}
}
.time-header-text {
margin-left: 28rpx;
}
.time-list-container {
margin-left: 28rpx;
width: calc(100% - 56rpx);
margin-top: 20rpx;
display: flex;
flex: 1;
position: relative;
.loading-view {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.list-scroll-view {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
.time-item {
width: 100%;
height: 102rpx;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
border-radius: 20rpx;
border: 2rpx solid #E5E7EB;
background-color: #fff;
opacity: 1;
margin-bottom: 22rpx;
padding: 0 30rpx;
box-sizing: border-box;
}
.selected-time-item {
border: 2rpx solid $app_color_main;
background-color: $app_color_mark_bg_color;
}
.disabled-time-item {
background-color: #FBF7FC;
opacity: 0.5;
}
}
}
.submit-btn {
width: calc(100% - 120rpx);
margin-left: 60rpx;
margin-bottom: 10rpx;
height: 90rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 45rpx;
background-color: $app_color_main;
}
}
</style>

View File

@ -0,0 +1,79 @@
export const ORDER_STATUS_UNPAY = 1; // 未支付
export const ORDER_STATUS_RESERVED = 2; // 已预约
export const ORDER_STATUS_SEND = 3; // 已派单
export const ORDER_STATUS_SERVICE = 4; //服务中
export const ORDER_STATUS_COMPLETED = 5; // 已完成
export const ORDER_STATUS_CANCELED = 6; // 已取消
export const ORDER_STATUS_REFUND = 7; // 退款中
export const PRICE_DIFF_TYPE_SERVICE = '1'; //差价类型 服务
export const orderStatusList = [
{
value: ORDER_STATUS_UNPAY,
label: '未支付',
},
{
value: ORDER_STATUS_RESERVED,
label: '已预约',
},
{
value: ORDER_STATUS_SEND,
label: '已派单',
},
{
value: ORDER_STATUS_SERVICE,
label: '服务中',
},
{
value: ORDER_STATUS_COMPLETED,
label: '已完成',
},
{
value: ORDER_STATUS_CANCELED,
label: '已取消',
},
{
value: ORDER_STATUS_REFUND,
label: '退款中',
},
]
//展示的状态
export const showOrderStatus = [
{
value: '',
label: '全部',
},
{
value: ORDER_STATUS_RESERVED,
label: '已预约',
},
{
value: ORDER_STATUS_SERVICE,
label: '服务中',
},
{
value: ORDER_STATUS_COMPLETED,
label: '已完成',
},
{
value: ORDER_STATUS_CANCELED,
label: '已取消',
},
]
//充值相关
export const rechargeStatus = [
{
value: '1',
label: '充值',
},
{
value: '2',
label: '充值记录',
}
]

View File

@ -0,0 +1,851 @@
<template>
<view class="order-detail-container">
<view class="detail-container">
<uni-load-more v-if="isLoading" status="loading" :show-text="false"/>
<scroll-view v-if="!isLoading" class="scroll-view" scroll-y>
<view class="info-cell info-cell-guanjia" v-if="carNumber || managerPhoneNum || managerName">
<image v-if="managerPic" :src="managerPic" mode="aspectFit" class="head-icon"/>
<image v-else src="/pageHome/static/head.png" mode="aspectFit" class="head-icon"/>
<view class="guan-jia-info-container">
<text class="app-fc-main fs-32 app-font-bold-700">{{ carNumber }}</text>
<text class="app-fc-normal fs-24">{{ `${managerName}管家` }}</text>
</view>
<view class="phone-view" v-if="managerPhoneNum" @click.stop="isShowCallManagerModal = true">
<image src="../static/phone.png" mode="aspectFit" class="call-img"/>
<text class="app-fc-main fs-24">电话</text>
</view>
</view>
<view class="info-cell">
<view class="info-title-view">
<image src="@/static/images/address.png" mode="aspectFit" class="info-icon"/>
<text class="app-fc-main fs-28">{{ addressInfo.name }}</text>
<text class="app-fc-main fs-28 phone-text">{{ addressInfo.phone }}</text>
</view>
<view class="address-view">
<view class="info-icon"/>
<text class="app-fc-normal fs-24 address-text">
{{ orderInfo.address || `${addressInfo.area_name}${addressInfo.address}` || '--' }}
</text>
</view>
</view>
<view class="info-cell pet-info-container">
<view class="pet-info-cell">
<image v-if="petInfo.chongwu_pic" mode="scaleToFill" class="pet-icon"
:src="petInfo.chongwu_pic"/>
<image v-else mode="scaleToFill" class="pet-icon" src="https://activity.wagoo.live/record_avator.png"/>
<view class="pet-info-view">
<text class="fs-26 app-fc-main app-font-bold-700">{{ petInfo.name }}</text>
<text class="app-fc-normal fs-24">{{ petDesc }}</text>
<text class="app-fc-normal fs-24">{{ `出生日期:${petInfo.birthday}` }}</text>
</view>
</view>
<view class="order-goods-container" v-if="goodsList.length" @click.stop="isShowGoodsModal = true">
<view class="order-goods-view">
<scroll-view class="goods-scroll-view" scroll-x>
<image :src="goods.goods_pic" mode="aspectFit" class="good-icon" v-for="goods in goodsList"
:key="goods.goods_id"/>
</scroll-view>
</view>
<view class="order-goods-num-view">
<text class="app-fc-main fs-24">{{ `随车购商品x${goodsNum}` }}</text>
<image class="arrow-icon" src="@/static/images/arrow_right_black.png" mode="aspectFit"/>
</view>
</view>
</view>
<view class="info-cell info-cell-gap-20">
<detail-cell title="实际宠物重量区间" :content="orderInfo.new_weight_name || orderInfo.weight_name"/>
<view class="order-price-view">
<detail-cell title="预约支付金额" :content="`¥${orderInfo.price}`" :content-mark="false"/>
<detail-cell v-if="orderInfo.dikou_id" title="优惠券" :content="`-¥${couponPrice}`" :content-mark="false"/>
<detail-cell v-if="orderInfo.fuwuquan_id" title="服务券金额" :content="`¥${servicePrice}`"
:content-mark="false"/>
</view>
<template v-if="orderInfo.new_weight_name && orderInfo.weight_chajia">
<detail-cell v-if="Number(orderInfo.weight_chajia) < 0" title="附加服务费退款金额"
:content="`¥${orderInfo.weight_chajia}`"/>
<detail-cell v-else title="附加服务费支付金额" :content="`¥${orderInfo.weight_chajia}`"/>
</template>
<view class="line-view"/>
<detail-cell title="金额合计" :content="`¥${totalMoney}`" :title-mark="true" :content-mark="true"
:custom-content-style="{'color': '#E70E0E'}"/>
</view>
<view class="info-cell info-cell-gap-20">
<detail-cell title="服务订单编号" :content="orderInfo.order_no" :content-mark="false"/>
<view class="line-view"/>
<detail-cell v-if="isShowReservationTime" title="预约时间" :content="reservationTime" :content-mark="false"/>
<detail-cell v-if="!isUnPay && payTime" title="付款时间" :content="payTime" :content-mark="false"/>
<detail-cell v-if="serviceStartTime" title="服务开始时间" :content="serviceStartTime" :content-mark="false"/>
<detail-cell v-if="serviceEndTime" title="服务完成时间" :content="serviceEndTime" :content-mark="false"/>
<view class="line-view"/>
<detail-cell title="停车状况" :content="orderInfo.tingche_desc" :content-mark="false"/>
</view>
</scroll-view>
<add-service-pay-modal
v-if="isShowPayModal"
@close="hidePayModal"
:order-info="orderInfo"
:new-weight="otherWeight"
@changeWeight="selectWeight"
@paymentConfirm="paymentConfirm"
/>
<select-weight-modal
v-if="isShowWeight"
:weight-list="weightList"
:pet-weight="otherWeight"
@close="closeWeightModal"
@changeWeight="changeOtherWeight"
/>
<pay-success-modal
v-if="isShowPaySuccessModal"
@close="isShowPaySuccessModal = false"
@ok="okPayAction"
/>
</view>
<view class="bottom-view" v-if="isHasHandle">
<view class="handle-btn" v-if="isCanCancelOrder" @click.stop="clickCancel">
<text class="fs-24 app-fc-main">取消预约</text>
</view>
<view class="handle-btn" v-if="isService" @click.stop="showGoods">
<text class="app-fc-main fs-24">随车购商品</text>
</view>
<view class="handle-btn handle-mark-btn" v-if="isCanAddService" @click.stop="showAddService">
<text class="fs-24 app-fc-white">调整服务费</text>
</view>
<view class="handle-btn" v-if="isUnPay" @click.stop="payOrderAction">
<text class="fs-24 app-fc-main">立即支付</text>
</view>
<view class="handle-btn" v-if="isNeedPayService" @click.stop="payNewWeightOrderAction">
<text class="fs-24 app-fc-main">支付服务费</text>
</view>
<view class="handle-btn" v-if="isCompleted" @click.stop="isShowCallModal = true">
<text class="app-fc-main fs-24">联系售后</text>
</view>
<view class="handle-btn " v-if="isHasWashImgs" @click.stop="isShowCompareModal = true">
<text class="app-fc-main fs-24">洗护对比图</text>
</view>
<view class="handle-btn handle-mark-btn" v-if="isCompleted" @click.stop="gotoEvaluate">
<text class="fs-24 app-fc-white">{{ orderInfo.pinglun_id ? '查看评价' : '去评价' }}</text>
</view>
</view>
<call-modal
:phone-number="orderInfo.phone"
v-if="isShowCallModal"
@close="isShowCallModal = false"
/>
<call-modal
:phone-number="managerPhoneNum"
v-if="isShowCallManagerModal"
@close="isShowCallManagerModal = false"
/>
<order-goods-modal
v-if="isShowGoodsModal"
@close="isShowGoodsModal = false"
@gotoDetail="gotoGoodsDetail"
:goods-list="goodsList"
:price="goodsPrice"
/>
<CompareModal
v-if="isShowCompareModal"
@close="isShowCompareModal = false"
:is-can-share="true"
:is-can-remark="!orderInfo.pinglun_id"
:after-imgs="afterImages"
:before-imgs="beforeImages"
@add="selectImgsChange"
@share="shareToCommunity"
/>
<pop-up-modal
v-if="isShowCancelModal"
content="确定要取消预约吗?"
@confirm="cancelOrder"
@cancel="isShowCancelModal = false"
/>
<success-modal
v-if="showSuccessModal"
title="转发成功"
message="请前往宠圈查看"
@ok="jumpToCommunity"
/>
</view>
</template>
<script>
import DetailCell from "@/components/petOrder/detail-cell.vue";
import {
ORDER_STATUS_CANCELED,
ORDER_STATUS_COMPLETED,
ORDER_STATUS_RESERVED,
ORDER_STATUS_SEND,
ORDER_STATUS_SERVICE,
ORDER_STATUS_UNPAY,
PRICE_DIFF_TYPE_SERVICE
} from "@/pageHome/constants/home";
import { addServicePay, cancelOrder, getOrderDetail, payOrder } from "@/api/order";
import {
ORDER_TYPE_PET_SERVICE, ORDER_TYPE_RESERVATION, PET_HAIR_LONG,
PET_IS_STERILIZE_YES,
PET_SEX_MALE,
PET_TYPE_CAT,
PET_TYPE_DOG
} from "@/constants/app.business";
import { shareCommunity } from '../../api/community';
import { cancelPetOrderRefund } from "../../api/login";
import AddServicePayModal from "@/components/petOrder/add-service-pay-modal.vue";
import SelectWeightModal from "@/components/petOrder/select-weight-modal.vue";
import { getWeightList } from "@/api/common";
import appConfig from '@/constants/app.config'
import PaySuccessModal from "@/components/petOrder/pay-success-modal.vue";
import moment from "moment";
import CallModal from "@/components/petOrder/call-modal.vue";
import OrderGoodsModal from "@/pageHome/components/order-goods-modal.vue";
import CompareModal from "@/components/CompareModal.vue";
import PopUpModal from "@/components/PopUpModal.vue";
import SuccessModal from "@/components/SuccessModal.vue";
export default {
components: {
PopUpModal,
CompareModal,
OrderGoodsModal,
CallModal,
SelectWeightModal,
AddServicePayModal,
DetailCell,
PaySuccessModal,
SuccessModal
},
onLoad(option) {
uni.$on('createRemarkSuccess', this.refreshOrderDetail)
if (option.orderId) {
this.orderId = option.orderId;
}
},
onShow() {
this.getData();
},
data() {
return {
orderId: '',
isLoading: true,
orderInfo: {},
isShowPayModal: false,
otherWeight: {},
isShowWeight: false,
allWeightList: [],
appConfig,
isShowPaySuccessModal: false,
isShowCallModal: false,
isShowGoodsModal: false,
isShowCallManagerModal: false,
isShowCompareModal: false,
isShowCancelModal: false,
showSuccessModal: false,
};
},
computed: {
goodsList() {
return this.orderInfo?.order_list || [];
},
goodsPrice() {
const price = this.goodsList.reduce((total, item) => {
return total + item.goods_price * item.number;
}, 0);
return +price.toFixed(2)
},
goodsNum() {
let num = 0
this.goodsList.forEach(item => {
num = num + item.number;
})
return num;
},
addressInfo() {
return this.orderInfo?.address_info || {};
},
petInfo() {
return this.orderInfo?.chongwu_info || {};
},
weightInfo() {
return this.orderInfo?.weight_info || {};
},
varietyInfo() {
return this.orderInfo?.pinzhong_info || {};
},
isShowReservationTime() {
return `${this.orderInfo?.order_type}` === `${ORDER_TYPE_RESERVATION}`
},
reservationTime() {
return `${this.orderInfo?.yuyue_date} ${this.orderInfo?.yuyue_time}`
},
petDesc() {
const varietyName = this.varietyInfo?.pinzhong_name || '';
const sex = `${this.petInfo.sex}` === `${PET_SEX_MALE}` ? '男生' : '女生';
const pet = `${this.petInfo.type}` === `${PET_TYPE_CAT}` ? '猫' : '狗';
let hair = '';
if (`${this.petInfo.type}` === `${PET_TYPE_CAT}`) {
hair = `/${`${this.petInfo.maofa}` === `${PET_HAIR_LONG}` ? '长毛' : '短毛'}`
}
const sterilize = `${this.petInfo.is_jueyu}` === `${PET_IS_STERILIZE_YES}` ? '已绝育' : '未绝育';
const birthDate = new Date(this.petInfo.birthday);
const today = new Date();
let age = today.getFullYear() - birthDate.getFullYear();
const monthDiff = today.getMonth() - birthDate.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
age--;
}
const weightName = this.weightInfo?.weight_name || '';
return `${varietyName}${hair}/${sex}${pet}/${sterilize}/${age}岁/${weightName}`;
},
isCompleted() {
return this.orderInfo?.status === ORDER_STATUS_COMPLETED
},
isReceived() {
return this.orderInfo?.status === ORDER_STATUS_RESERVED || this.orderInfo?.status === ORDER_STATUS_SEND
},
//已取消
isCanceled() {
return this.orderInfo?.status === ORDER_STATUS_CANCELED
},
//服务中
isService() {
return this.orderInfo?.status === ORDER_STATUS_SERVICE
},
isCanCancelOrder() {
return this.isReceived
},
isCanAddService() {
return (this.isReceived || this.isService) && !this.isHasNewWeight
},
isUnPay() {
return this.orderInfo?.status === ORDER_STATUS_UNPAY
},
isNeedPayService() {
return this.isReceived && this.isHasNewWeight && `${this.orderInfo.new_weight_status}` !== `${ORDER_STATUS_RESERVED}`
},
weightList() {
if (this.allWeightList.length && this.orderInfo) {
//使用优惠券后付款为0后 只增不减
if (this.orderInfo.dikou_id && Number(this.orderInfo.pay_price) === 0) {
const currentWeightSort = this.allWeightList.find((item) => item.weight_id === this.orderInfo.weight_id)?.sort || 0;
if (this.orderInfo.type === PET_TYPE_DOG) {
return this.allWeightList.filter((item) => item.type === this.orderInfo.type && item.weight_id !== this.orderInfo.weight_id && Number(item.sort) > Number(currentWeightSort));
} else {
return this.allWeightList.filter((item) => item.type === this.orderInfo.type && item.weight_id !== this.orderInfo.weight_id && item.maofa === this.petInfo.maofa && Number(item.sort) > Number(currentWeightSort));
}
} else {
if (this.orderInfo.type === PET_TYPE_DOG) {
return this.allWeightList.filter((item) => item.type === this.orderInfo.type && item.weight_id !== this.orderInfo.weight_id);
} else {
return this.allWeightList.filter((item) => item.type === this.orderInfo.type && item.weight_id !== this.orderInfo.weight_id && item.maofa === this.petInfo.maofa);
}
}
} else {
return []
}
},
isHasNewWeight() {
return this.orderInfo?.new_weight_name && this.orderInfo?.weight_chajia
},
totalMoney() {
let changeMoney = Number(this.orderInfo.weight_chajia || 0);
let price = Number(this.orderInfo.pay_price || 0);
return (price + changeMoney).toFixed(2);
},
isHasHandle() {
return !this.isLoading && (this.isCanCancelOrder || this.isCanAddService || this.isNeedPayService || this.isUnPay || this.isCompleted || this.isService)
},
payTime() {
if (this.orderInfo.pay_time) {
return moment.unix(this.orderInfo.pay_time).format('YYYY-MM-DD HH:mm');
}
return ''
},
couponPrice() {
return +this.orderInfo.dikou_price || 0;
},
servicePrice() {
return +this.orderInfo.fuwuquan_price || 0;
},
carNumber() {
return this.orderInfo?.car_info?.car_no || ''
},
managerPhoneNum() {
return this.orderInfo?.guanjia_info?.mobile || ''
},
managerName() {
return this.orderInfo?.guanjia_info?.name || ''
},
isHasWashImgs() {
return this.isCompleted && (this.orderInfo.json_hou || this.orderInfo.json_qian);
},
afterImages() {
const houUrlList = (this.orderInfo?.json_hou || '').split(',')
const houFullUrlList = this.orderInfo?.hou_list || []
return houUrlList.map((v, i) => ({
url: v,
fullUrl: houFullUrlList[i]
}))
},
beforeImages() {
const qianUrlList = (this.orderInfo?.json_qian || '').split(',')
const qianFullUrlList = this.orderInfo?.qian_list || []
return qianUrlList.map((v, i) => ({
url: v,
fullUrl: qianFullUrlList[i]
}))
},
serviceStartTime() {
if (this.orderInfo.fuwu_time) {
return moment.unix(this.orderInfo.fuwu_time).format('YYYY-MM-DD HH:mm');
}
return ''
},
serviceEndTime() {
if (this.orderInfo.over_time) {
return moment.unix(this.orderInfo.over_time).format('YYYY-MM-DD HH:mm');
}
return ''
},
managerPic() {
return this.orderInfo?.guanjia_info?.guanjia_pic || ''
}
},
beforeDestroy() {
uni.$off('createRemarkSuccess', this.refreshOrderDetail);
const eventChannel = this.getOpenerEventChannel();
eventChannel.emit('refreshList');
},
methods: {
// 一键转发到宠圈
shareToCommunity(data) {
const { front = [], end = [] } = data;
uni.showLoading({
title: "处理中...",
mask: true,
})
shareCommunity({
order_id: this.orderInfo.order_id,
old_pic: front.join(','),
new_pic: end.join(',')
}).then(() => {
uni.hideLoading()
this.isShowCompareModal = false
this.showSuccessModal = true
})
},
jumpToCommunity() {
this.showSuccessModal = false
uni.navigateTo({
url: '/pages/client/index/index?activePageId=communityPage'
})
},
refreshOrderDetail() {
this.isLoading = true;
this.getData();
},
getData() {
getWeightList().then((res) => {
this.allWeightList = res?.info || [];
})
getOrderDetail(this.orderId).then((res) => {
this.orderInfo = res?.info || {};
this.isLoading = false;
}).catch(() => {
this.isLoading = false;
})
},
showAddService() {
this.isShowPayModal = true;
},
hidePayModal() {
this.isShowPayModal = false;
this.otherWeight = {};
},
selectWeight() {
this.isShowPayModal = false;
this.isShowWeight = true;
},
closeWeightModal() {
this.isShowWeight = false;
this.isShowPayModal = true;
},
changeOtherWeight(data) {
this.otherWeight = data;
this.isShowWeight = false;
this.isShowPayModal = true;
},
paymentConfirm(data) {
this.isShowPayModal = false;
uni.showLoading({
title: '处理中',
mask: true
})
addServicePay(this.orderId, this.otherWeight.weight_id)
.then((res) => {
const { code, msg } = res;
if (`${code}` === '-121') {
uni.hideLoading();
uni.showToast({
title: msg,
icon: 'none',
duration: 4000
})
} else {
this.wxPayAction(data.needRefund, PRICE_DIFF_TYPE_SERVICE)
}
})
.catch((err) => {
uni.showToast({
title: err || '创建订单失败',
icon: 'none'
})
uni.hideLoading();
})
},
clickCancel() {
this.isShowCancelModal = true;
},
cancelOrder() {
this.isShowCancelModal = false;
uni.showLoading({
title: '正在取消订单',
mask: true
});
const data = {
business_id:this.orderId,
business_type:2
}
cancelPetOrderRefund(data).then(() => {
uni.hideLoading();
this.isShowPaySuccessModal = true;
}).catch((err) => {
uni.hideLoading();
uni.showToast({
title: err || '取消订单失败',
icon: 'none'
});
})
},
payOrderAction() {
uni.showLoading({
title: '正在支付',
mask: true
});
this.wxPayAction(false);
},
payNewWeightOrderAction() {
uni.showLoading({
title: '正在支付',
mask: true
});
this.wxPayAction(false, PRICE_DIFF_TYPE_SERVICE);
},
wxPayAction(needRefund, chaJiaType) {
if (needRefund) {
uni.hideLoading();
this.isShowPaySuccessModal = true;
return;
}
payOrder(this.orderId, chaJiaType).then((res) => {
const payData = res?.info?.pay_data || {};
uni.requestPayment({
provider: 'wxpay',
timeStamp: payData.timeStamp,
nonceStr: payData.nonceStr,
package: payData.package,
signType: payData.signType,
paySign: payData.sign,
success: (res) => {
console.log('success:' + JSON.stringify(res));
uni.hideLoading();
uni.showToast({
title: '支付成功',
icon: 'none'
})
uni.navigateBack()
},
fail: (err) => {
console.log('fail:' + JSON.stringify(err));
uni.showToast({
title: err?.msg || '支付失败',
icon: 'none'
})
uni.hideLoading();
}
});
}).catch((err) => {
uni.showToast({
title: err || '支付失败',
icon: 'none'
})
})
},
okPayAction() {
this.isShowPaySuccessModal = false;
uni.navigateBack()
},
gotoEvaluate() {
if (this.orderInfo.pinglun_id) {
uni.navigateTo({
url: `/pages/client/remark/details?remarkId=${this.orderInfo.pinglun_id}`,
});
} else {
uni.navigateTo({
url: `/pages/client/order/remark?orderId=${this.orderId}&orderType=${ORDER_TYPE_PET_SERVICE}`,
});
}
},
showGoods() {
uni.navigateTo({
url: `/pages/client/category/index?petOrderId=${this.orderId}&addressId=${this.addressInfo.address_id}`,
});
},
gotoGoodsDetail(goods_id) {
uni.navigateTo({
url: `/pages/client/shop/details?id=${goods_id}&petOrderId=${this.orderId}&petOrderAddressId=${this.addressInfo.address_id}`,
});
},
selectImgsChange(imgs) {
uni.navigateTo({
url: `/pages/client/order/remark?orderId=${this.orderId}&orderType=${ORDER_TYPE_PET_SERVICE}`,
success: (res) => {
res.eventChannel.emit('remarkInfo', {
remarkImages: imgs,
afterImages: this.afterImages,
beforeImages: this.beforeImages,
})
}
});
},
},
}
</script>
<style lang="scss" scoped>
.order-detail-container {
width: 100%;
height: 100%;
box-sizing: border-box;
background-color: #F7F8FA;
display: flex;
flex-direction: column;
.detail-container {
display: flex;
flex-direction: column;
flex: 1;
position: relative;
box-sizing: border-box;
margin: 20rpx 32rpx;
align-items: center;
justify-content: center;
.scroll-view {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
.info-cell {
display: flex;
flex-direction: column;
width: 100%;
padding: 40rpx;
box-sizing: border-box;
border-radius: 30rpx;
background-color: #FFFFFF;
margin-bottom: 32rpx;
.order-price-view {
width: 100%;
padding: 20rpx 24rpx;
box-sizing: border-box;
background-color: #F5F5F5;
border-radius: 30rpx;
}
.info-title-view {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
.phone-text {
margin-left: 24rpx;
}
}
.address-view {
width: 100%;
display: flex;
align-items: center;
.address-text {
margin-top: 12rpx;
}
}
.info-icon {
width: 36rpx;
height: 36rpx;
flex-shrink: 0;
margin-right: 20rpx;
}
.line-view {
width: 100%;
height: 2rpx;
background-color: #ECECEC;
margin: 20rpx 0 18rpx;
}
.head-icon {
width: 80rpx;
height: 80rpx;
flex-shrink: 0;
margin-right: 16rpx;
overflow: hidden;
border-radius: 40rpx;
}
.guan-jia-info-container {
display: flex;
flex: 1;
flex-direction: column;
justify-content: space-around;
}
.phone-view {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.call-img {
width: 60rpx;
height: 60rpx;
margin-bottom: 12rpx;
}
}
}
.info-cell-guanjia {
display: flex;
flex-direction: row;
align-items: center;
}
.info-cell-gap-20 {
padding: 20rpx 36rpx;
}
.pet-info-container {
padding: 40rpx 36rpx;
}
.pet-info-cell {
display: flex;
width: 100%;
flex-direction: row;
.pet-icon {
width: 136rpx;
height: 136rpx;
flex-shrink: 0;
border-radius: 20rpx;
overflow: hidden;
}
.pet-info-view {
display: flex;
flex: 1;
flex-direction: column;
justify-content: space-around;
margin-left: 24rpx;
}
}
.order-goods-container {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 30rpx;
.order-goods-view {
display: flex;
flex: 1;
height: 76rpx;
flex-direction: row;
position: relative;
.goods-scroll-view {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
white-space: nowrap;
.good-icon {
width: 76rpx;
height: 76rpx;
margin-right: 20rpx;
background-color: #E5E7EB;
}
}
}
.order-goods-num-view {
display: flex;
flex-shrink: 0;
flex-direction: row;
align-items: center;
margin-left: 20rpx;
.arrow-icon {
width: 18rpx;
height: 18rpx;
margin-left: 10rpx;
}
}
}
}
}
.bottom-view {
width: 100%;
padding: 36rpx 16rpx;
box-sizing: border-box;
display: flex;
flex-direction: row;
justify-content: flex-end;
background-color: #FFFFFF;
.handle-btn {
width: 152rpx;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
border: 2rpx solid #9B939A;
border-radius: 32rpx;
margin-right: 24rpx;
}
.handle-mark-btn {
border: 2rpx solid $app_color_main;
background-color: $app_color_main;
}
}
}
</style>

View File

@ -0,0 +1,939 @@
<template>
<view class="reservation-container">
<nav-bar title="预约" />
<view class="body-container">
<scroll-view class="scroll-view" :scroll-y="true">
<view class="form-content">
<view class="order-tab-list">
<view :class="orderType === ORDER_TYPE_RESERVATION ? 'activeItem' : 'tabItem'"
@click.stop="selectOrderType(ORDER_TYPE_RESERVATION)">
预约单
</view>
<view :class="orderType === ORDER_TYPE_SITE ? 'activeItem' : 'tabItem'"
@click.stop="selectOrderType(ORDER_TYPE_SITE)">
现场单
</view>
</view>
<view class="reservation-info-container">
<!-- 其他内容 -->
<view class="formWrapper">
<view class="info-top-view" v-if="orderType != ORDER_TYPE_RESERVATION">
<view class="top">
<view class="title-view">
<text class="required">*</text>
<text class="app-fc-main fs-24">服务码</text>
</view>
<view class="info-view">
<input class="identity-input fs-24" type="number" v-model="xwID"
placeholder="请输入或者扫一扫服务码">
<image :src='`${imgPrefix}scan.png`' mode="aspectFit" class="scan-img"
@click="scanCode" />
</view>
</view>
</view>
<view class="form-section form-section-pet">
<view class="form-label-row">
<text class="required">*</text>
<text class="form-label">选择宠物</text>
</view>
<view class="add-pet-wrapper">
<view v-for="(pet, index) in selectedPetsDisplay" :key="pet.id || index"
class="selected-pet-avatar">
<image class="pet-avatar-img"
:src="(pet.chongwu_pic || pet.pet_avatar || pet.avatar) || `${imgPrefix}record_avator.png`"
mode="aspectFill" />
<view class="remove-pet-btn" @click.stop="removePet(index)">×</view>
<text class="pet-avatar-name">{{ getPetShortName(pet) }}</text>
</view>
<view class="add-pet-cell" @click="goToSelectPet">
<view class="add-pet-btn">
<text class="plus-icon">+</text>
</view>
<text class="add-pet-text">添加宠物</text>
</view>
</view>
</view>
<!-- <info-cell key="weight" cell-type="text" title="宠物重量区间" :info="petInfo.weight_name"
placeholder='先选择宠物' :is-can-click="false" />
<info-cell v-if="selectedPetType === PET_TYPE_CAT" key="mofa" cell-type="text" title="宠物毛发"
:info="petInfo.hairName" placeholder='先选择宠物' :is-can-click="false" /> -->
<info-cell v-if="orderType === ORDER_TYPE_RESERVATION" key="time" cell-type="time"
title="选择预约时间" :info="Object.keys(reservationTime).length > 0
? `${reservationTime.dateLabel} ${reservationTime.start}-${reservationTime.end}`
: ''
" :infoTime="reservationTime.shiduan_id" @clickAction="goToSelectTime" placeholder='请选择' />
<info-cell key="address" cell-type="address"
title="选择服务地址" :address-info="address" @clickAction="goToSelectAddress"
placeholder='请选择' />
<info-cell v-if="orderType != ORDER_TYPE_SITE" key="park" cell-type="park" title="停车状况"
:park-state="parkState" :other-park-state="otherParkState"
@changeParkState="changeParkState" @changeOtherParkState="changeOtherParkState" />
</view>
<view v-if="tip" class="tip-view">
<image class="tip-icon" :src="imgPrefix + 'reservationTime-notice.png'" mode="aspectFit" />
<text class="tip-text">{{ tip }}</text>
</view>
<view class="payFooter">
<view class="leftPay">
<view class="priceWrapper">
<text class="text">预估</text>
<text class="unitText">
¥<text class="price">{{ totalDisplayPrice }}</text>
</text>
</view>
<!-- <view class="vipPrice" v-if="discount === 0">
<view>
<image :src="`${imgPrefix}vipPrice.png`" mode="widthFix"
class="vip-price-img" />
<text style="font-size: 24rpx;">¥{{ price && discount ? (price *
discount).toFixed(2) : (price ? (price *
0.8).toFixed(2) : "0.00") }}</text>
</view>
</view> -->
</view>
<view class="payBtn" @click.stop="paymentConfirm">
下一步
</view>
</view>
</view>
</view>
<!-- 广告 -->
<view class="ad-container">
<image :src='imgPrefix + "1.png"' mode="widthFix" class="ad-image"></image>
<image :src='imgPrefix + "2.png"' mode="widthFix" class="ad-image"></image>
<image :src='imgPrefix + "3.png"' mode="widthFix" class="ad-image"></image>
<image :src='imgPrefix + "4.png"' mode="widthFix" class="ad-image"></image>
<image :src='imgPrefix + "5.png"' mode="widthFix" class="ad-image"></image>
<image :src='imgPrefix + "6.png"' mode="widthFix" class="ad-image"></image>
</view>
</scroll-view>
</view>
</view>
</template>
<script>
import InfoCell from "@/page-reser/components/info-cell.vue";
import {
gitDiscountfee
} from "../../api/login";
import {
ARTICLE_TYPE_RESERVATION_CAT,
ARTICLE_TYPE_RESERVATION_DOG,
ORDER_TYPE_RESERVATION,
ORDER_TYPE_SITE,
PET_HAIR_LONG,
PET_TYPE_CAT,
PET_TYPE_DOG,
} from "@/constants/app.business";
import {
getArticleDetail
} from "@/api/article";
import appConfig from "@/constants/app.config";
import {
getCityIsOpen,
checkWaExists
} from "@/api/order";
import {
imgPrefix
} from "@/utils/common";
export default {
components: {
InfoCell,
},
data() {
return {
imgPrefix,
xwID: '',
PET_TYPE_CAT,
PET_TYPE_DOG,
ORDER_TYPE_RESERVATION,
ORDER_TYPE_SITE,
orderType: ORDER_TYPE_RESERVATION,
selectedPetType: PET_TYPE_CAT, // 默认选中“猫”
petInfo: {}, // 兼容下游,取 selectedPets[0]
selectedPets: [], // 多选宠物列表
parkState: "",
otherParkState: "",
price: "",
discount: '',
discount_price: [], // 接口返回的折扣价数组,展示为数组之和
reservationTime: {},
address: null,
catHtmlData: "",
dogHtmlData: "",
tip: ''
};
},
mounted() {
this.initData();
},
computed: {
// 展示价格discount_price 数组相加的总和
totalDisplayPrice() {
const arr = Array.isArray(this.discount_price) ? this.discount_price : [];
const sum = arr.reduce((s, p) => s + Number(p || 0), 0);
return sum.toFixed(2);
},
selectedPetsDisplay() {
return this.selectedPets && this.selectedPets.length > 0 ? this.selectedPets : [];
}
},
methods: {
getPetShortName(pet) {
const n = pet.name || pet.pet_name || pet.pet_nickname || '';
return n.length > 2 ? n.slice(0, 2) + '…' : n.slice(0, 2);
},
initData() {
getArticleDetail(ARTICLE_TYPE_RESERVATION_CAT).then((res) => {
this.catHtmlData = this.processHtmlContent(res?.info?.content || "");
});
},
processHtmlContent(html) {
return html.replace(/max-width/g, "width");
},
selectOrderType(orderType) {
if (this.orderType === orderType) {
return;
}
this.reservationTime = {};
this.parkState = "";
this.address = null;
this.price = "";
this.discount_price = [];
this.tip = "";
this.petInfo = {};
this.selectedPets = [];
this.orderType = orderType;
},
changeParkState(state) {
this.parkState = state;
this.otherParkState = "";
},
changeOtherParkState(state) {
this.otherParkState = state;
},
goToSelectPet() {
// 跳转到选择宠物页面(多选:每次返回添加一只)
const selectPetInfo = this.selectedPets.length > 0
? encodeURIComponent(JSON.stringify(this.selectedPets[0]))
: '';
uni.navigateTo({
url: `/pageHome/selectPet/index?petType=${this.selectedPetType}${selectPetInfo ? `&selectPetInfo=${selectPetInfo}` : ''}`,
events: {
changePet: (pet) => {
this.changePet(pet);
}
}
});
},
changePet(item) {
item.hairName = item.hair === 1 ? '长毛' : '短毛';
const exists = this.selectedPets.some(p => p.id === item.id);
if (!exists) {
this.selectedPets.push(item);
}
this.petInfo = this.selectedPets[0] || {};
this.tryFetchDiscount();
},
removePet(index) {
this.selectedPets.splice(index, 1);
this.petInfo = this.selectedPets[0] || {};
this.tryFetchDiscount();
},
goToSelectAddress() {
const selectAddress = this.address && Object.keys(this.address).length > 0
? encodeURIComponent(JSON.stringify(this.address))
: '';
uni.navigateTo({
url: `/pageHome/selectAddress/index${selectAddress ? `?selectAddress=${selectAddress}` : ''}`,
events: {
changeAddress: (address) => {
this.changeAddress(address);
}
}
});
},
changeAddress(address) {
address.area_name = `${address.province} ${address.city} ${address.district}`;
this.address = address;
this.tryFetchDiscount();
},
scanCode() {
uni.scanCode({
success: (res) => {
this.xwID = res.result;
uni.showToast({
title: '扫码成功',
icon: 'success'
});
},
fail: (err) => {
console.error('扫码失败:', err);
uni.showToast({
title: '扫码失败',
icon: 'none'
});
}
});
},
goToSelectTime() {
const selectTime = this.reservationTime && Object.keys(this.reservationTime).length > 0 ?
encodeURIComponent(JSON.stringify(this.reservationTime)) :
'';
uni.navigateTo({
url: `/pageHome/selectTime/index${selectTime ? `?selectTime=${selectTime}` : ''}`,
events: {
changeReservationTime: (timeData) => {
this.changeReservationTime(timeData);
}
}
});
},
changeReservationTime(timeData) {
this.reservationTime = timeData;
this.tryFetchDiscount();
},
// 仅当「选择宠物」「选择服务地址」「选择预约时间」三者都选好时调用 gitDiscountfee
tryFetchDiscount() {
const hasPet = this.selectedPets && this.selectedPets.length > 0;
const petIds = hasPet ? this.selectedPets.map(p => +p.id) : [];
const hasAddress = this.address && Object.keys(this.address).length > 0;
const hasTime = this.reservationTime && Object.keys(this.reservationTime).length > 0;
const regionId = this.address?.region_id;
// 现场单:只需宠物即可拉取价格
if (this.orderType === this.ORDER_TYPE_SITE) {
if (hasPet && hasAddress) {
gitDiscountfee({
pet_id: petIds,
region_id: regionId
}).then((res) => {
const discountArr = Array.isArray(res.data.discount_price) ? res.data.discount_price : [res.data.discount_price];
const originArr = Array.isArray(res.data.price) ? res.data.price : [res.data.price];
// 按顺序将价格赋值给对应宠物:原价 + 折后价
discountArr.forEach((discountPrice, index) => {
if (this.selectedPets[index]) {
const originPrice = originArr[index] != null ? originArr[index] : discountPrice;
this.selectedPets[index].basePrice = Number(originPrice || 0); // 原价
this.selectedPets[index].discountBasePrice = Number(discountPrice || 0); // 折后价
}
});
// 计算总价:用折后价数组
this.price = discountArr.reduce((sum, p) => sum + Number(p || 0), 0);
this.discount = res.data.discount;
this.discount_price = Array.isArray(res.data.discount_price) ? res.data.discount_price : (res.data.discount_price != null ? [res.data.discount_price] : []);
this.tip = res.data.tip;
});
}
return;
}
// 预约单:三个都选了才调用
if (hasPet && hasAddress && hasTime) {
gitDiscountfee({
region_id: regionId,
pet_id: petIds,
order_date: this.reservationTime.date
}).then((res) => {
const discountArr = Array.isArray(res.data.discount_price) ? res.data.discount_price : [res.data.discount_price];
const originArr = Array.isArray(res.data.price) ? res.data.price : [res.data.price];
// 按顺序将价格赋值给对应宠物:原价 + 折后价
discountArr.forEach((discountPrice, index) => {
if (this.selectedPets[index]) {
const originPrice = originArr[index] != null ? originArr[index] : discountPrice;
this.selectedPets[index].basePrice = Number(originPrice || 0); // 原价
this.selectedPets[index].discountBasePrice = Number(discountPrice || 0); // 折后价
}
});
// 计算总价:用折后价数组
this.price = discountArr.reduce((sum, p) => sum + Number(p || 0), 0);
this.discount = res.data.discount;
this.discount_price = Array.isArray(res.data.discount_price) ? res.data.discount_price : (res.data.discount_price != null ? [res.data.discount_price] : []);
this.tip = res.data.tip;
});
}
},
paymentConfirm() {
if (!this.selectedPets.length) {
uni.showToast({
title: "请选择宠物",
icon: "none",
});
return;
}
if (
Object.keys(this.reservationTime).length === 0 &&
this.orderType === ORDER_TYPE_RESERVATION
) {
uni.showToast({
title: "请选择预约时间",
icon: "none",
});
return;
}
// 预约单、现场单均需校验服务地址
if (!this.address) {
uni.showToast({
title: "请选择服务地址",
icon: "none",
});
return;
}
// 仅预约单校验停车状况
if (this.orderType !== ORDER_TYPE_SITE) {
if (!this.parkState) {
uni.showToast({
title: "请选择停车状况",
icon: "none",
});
return;
}
if (this.parkState === "其他" && !this.otherParkState) {
uni.showToast({
title: "请输入停车信息",
icon: "none",
});
return;
}
}
// 如果是现场单,需要校验服务码
if (this.orderType === ORDER_TYPE_SITE) {
if (!this.xwID || !this.xwID.trim()) {
uni.showToast({
title: "请输入服务码",
icon: "none",
});
return;
}
// 校验服务码是否为数字
const waCode = Number(this.xwID.trim());
if (isNaN(waCode) || waCode <= 0) {
uni.showToast({
title: "服务码格式不正确",
icon: "none",
});
return;
}
// 调用接口校验服务码
uni.showLoading({
title: "校验服务码中...",
mask: true,
});
checkWaExists({ wa_code: waCode })
.then((res) => {
if (!res.data.exists) {
uni.showToast({
title: "服务码错误",
icon: "none",
});
return;
} else {
this.navigateToAdditional();
}
uni.hideLoading();
})
.catch((err) => {
uni.hideLoading();
uni.showToast({
title: err.msg || "服务码校验失败,请检查服务码是否正确",
icon: "none",
duration: 2000,
});
});
return;
}
// 非现场单,直接跳转
this.navigateToAdditional();
},
navigateToAdditional() {
uni.navigateTo({
url: "/pageHome/order/additional",
// url: '/pageHome/reservation/payment-confirm-page',
success: (res) => {
res.eventChannel.emit("reservationInfo", {
petInfo: this.petInfo,
selectedPets: this.selectedPets,
orderInfo: {
parkState: this.otherParkState || this.parkState,
orderType: this.orderType,
xwID: this.xwID
},
addresInfo: this.address,
reservationTime: this.reservationTime,
price: this.totalDisplayPrice, // 全部价格
discount: this.discount,
discount_price: this.discount_price,
});
},
events: {
clearData: () => {
this.selectedPetType = PET_TYPE_CAT;
this.petInfo = {};
this.selectedPets = [];
this.parkState = "";
this.price = "";
this.reservationTime = {};
this.address = null;
},
},
});
},
// paymentConfirm() {
// if (!this.petInfo.chongwu_id) {
// uni.showToast({
// title: "请选择宠物",
// icon: "none",
// });
// return;
// }
// if (
// Object.keys(this.reservationTime).length === 0 &&
// this.orderType === ORDER_TYPE_RESERVATION
// ) {
// uni.showToast({
// title: "请选择预约时间",
// icon: "none",
// });
// return;
// }
// if (!this.address) {
// uni.showToast({
// title: "请选择服务地址",
// icon: "none",
// });
// return;
// }
// if (!this.parkState) {
// uni.showToast({
// title: "请选择停车状况",
// icon: "none",
// });
// return;
// }
// if (this.parkState === "其他" && !this.otherParkState) {
// uni.showToast({
// title: "请输入停车信息",
// icon: "none",
// });
// return;
// }
// const data = {
// user_id:this.petInfo.member_id,
// pet_id:this.petInfo.chongwu_id,
// };
// isPet(data).then((res) => {
// if(res.data){
// uni.navigateTo({
// url: "/pageHome/order/additional",
// success: (res) => {
// res.eventChannel.emit("reservationInfo", {
// petInfo: this.petInfo,
// parkState: this.otherParkState || this.parkState,
// petWeight: this.petWeight,
// reservationTime: this.reservationTime,
// address: this.address,
// price: this.price,
// discount:this.discount,
// estimatePrice: this.totalDisplayPrice,
// orderType: this.orderType,
// chongwu_id: this.petInfo.chongwu_id,
// });
// },
// events: {
// clearData: () => {
// this.selectedPetType = PET_TYPE_CAT;
// this.petInfo = {};
// this.parkState = "";
// this.petWeight = {};
// this.price = "";
// this.reservationTime = {};
// this.address = null;
// },
// },
// });
// }else{
// uni.showModal({
// content: '该宠物必须与会员卡绑定才能享受会员折扣',
// showCancel: true,
// cancelText: '取消',
// confirmText: '去绑定',
// success: res => {
// if (res.confirm) {
// uni.navigateTo({
// url: `/pages/client/recharge/my-card?user_id=${this.petInfo.member_id}`,
// })
// } else {
// console.log('用户取消操作');
// }
// }
// });
// }
// });
// },
},
onShareAppMessage(res) {
return {
title: appConfig.appShareName,
path: "/pages/client/index/index",
};
},
};
</script>
<style lang="scss" scoped>
.reservation-container {
display: flex;
flex: 1;
flex-direction: column;
width: 100%;
height: 100%;
box-sizing: border-box;
background-color: #ffecf3;
.body-container {
display: flex;
flex-direction: column;
flex: 1;
box-sizing: border-box;
position: relative;
.scroll-view {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
box-sizing: border-box;
.form-content {
padding: 0 20rpx;
box-sizing: border-box;
}
.order-tab-list {
display: flex;
align-items: flex-end;
margin-top: 20rpx;
.tabItem {
flex: 1;
text-align: center;
background-color: #ffd8e6;
border-radius: 16rpx 16rpx 0px 0px;
height: 92rpx;
line-height: 92rpx;
font-size: 24rpx;
}
.activeItem {
background-color: #fff;
flex: 1;
text-align: center;
height: 92rpx;
line-height: 92rpx;
border-radius: 16rpx 16rpx 0px 0px;
font-size: 28rpx;
font-weight: 500;
}
}
.reservation-info-container {
display: flex;
flex-direction: column;
width: 100%;
box-sizing: border-box;
.formWrapper {
background-color: #fff;
padding: 0;
border-radius: 0px 0px 16rpx 16rpx;
overflow: hidden;
.info-top-view {
width: 100%;
display: flex;
flex-direction: column;
// flex-direction: row;
// align-items: center;
box-sizing: border-box;
height: 104rpx;
padding: 0 20rpx;
position: relative;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 20rpx;
right: 20rpx;
height: 2rpx;
background-color: #ececec;
}
.top {
width: 100%;
height: 100%;
display: flex;
// flex-direction: column;
flex-direction: row;
align-items: center;
box-sizing: border-box;
.title-view {
display: flex;
flex: 1;
align-items: center;
}
.info-view {
display: flex;
flex: 1;
align-items: center;
justify-content: flex-end;
.identity-input {
text-align: right;
// direction: rtl;
width: 100%;
}
}
}
.right-icon {
margin-left: 16rpx;
width: 10rpx;
height: 5rpx;
flex-shrink: 0;
}
}
.form-section-pet {
display: flex;
flex-direction: column;
width: calc(100% - 40rpx);
margin: 0 auto;
padding: 36rpx 0;
border-bottom: 2rpx solid #ececec;
}
.form-label-row {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 0;
}
.form-label {
font-size: 28rpx;
color: #333;
margin-left: 4rpx;
}
.add-pet-wrapper {
display: flex;
// align-items: center;
gap: 16rpx;
margin-top: 20rpx;
flex-wrap: wrap;
}
.selected-pet-avatar {
position: relative;
width: 64rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
}
.pet-avatar-img {
width: 64rpx;
height: 64rpx;
border-radius: 50%;
background-color: #f0f0f0;
}
.pet-avatar-name {
font-size: 24rpx;
color: #666;
margin-top: 8rpx;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 64rpx;
}
.remove-pet-btn {
position: absolute;
top: -8rpx;
right: -8rpx;
width: 32rpx;
height: 32rpx;
border-radius: 50%;
background-color: #FF19A0;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
line-height: 1;
border: 2rpx solid #fff;
}
.add-pet-cell {
display: flex;
flex-direction: column;
align-items: center;
}
.add-pet-btn {
width: 64rpx;
height: 64rpx;
border: 1rpx dashed #3D3D3D;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: #EBEBEB;
}
.plus-icon {
font-size: 60rpx;
color: #999;
line-height: 1;
}
.add-pet-text {
font-size: 24rpx;
color: #666;
margin-top: 8rpx;
}
}
// .content-section :last-child {
// border-bottom:none;
// /* CSS 规则 */
// }
.tip-view {
background-color: #fff;
padding: 12rpx 20rpx;
margin-top: 16rpx;
border-radius: 16rpx;
display: flex;
align-items: center;
gap: 12rpx;
}
.tip-icon {
width: 30rpx;
height: 30rpx;
flex-shrink: 0;
}
.tip-text {
color: #FF19A0;
font-size: 24rpx;
flex: 1;
}
.payFooter {
background-color: #fff;
border-radius: 16rpx;
margin-top: 16rpx;
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx;
.leftPay {
.priceWrapper {
.text {
font-size: 24rpx;
color: #272427;
}
.unitText {
font-size: 24rpx;
font-weight: 500;
color: #FF19A0;
}
.price {
color: #FF19A0;
font-size: 48rpx;
}
}
.vipPrice {
margin-top: 14rpx;
font-size: 20rpx;
color: #9B939A;
view {
display: flex;
align-items: center;
}
.vip-price-img {
width: 43px;
height: 14px;
margin-right: 8rpx;
}
}
}
.payBtn {
background-color: #FF19A0;
color: #fff;
padding: 30rpx 80rpx;
border-radius: 100px;
font-size: 32rpx;
}
}
}
.ad-container {
width: 100%;
padding: 0;
box-sizing: border-box;
display: flex;
flex-direction: column;
margin-top: 16rpx;
padding-bottom: 20rpx;
.ad-image {
width: 100%;
height: auto;
display: block;
}
.ad-image:last-child {
margin-bottom: 0;
}
.ad-view {
width: 100%;
}
}
}
}
}
.required {
color: #FF19A0;
}
.scan-img {
width: 40rpx;
height: 40rpx;
margin-left: 8rpx;
}
</style>

View File

@ -0,0 +1,447 @@
<template>
<view class="payment-confirm-container" v-if="!initializing">
<view class="payment-body-container">
<view class="info-cell" v-if="reservationInfo.address">
<view class="info-title-view">
<image src="@/static/images/address.png" mode="aspectFit" class="info-icon"/>
<text class="app-fc-main fs-28">{{ reservationInfo.address.name }}</text>
<text class="app-fc-main fs-28 phone-text">{{ reservationInfo.address.phone }}</text>
</view>
<view class="address-view">
<view class="info-icon"/>
<text class="app-fc-normal fs-24 address-text">
{{ `${reservationInfo.address.area_name} ${reservationInfo.address.address}` }}
</text>
</view>
</view>
<view class="info-cell">
<view class="info-title-view">
<image src="/pageHome/static/time.png" mode="aspectFit" class="info-icon"/>
<text class="app-fc-main fs-28">{{ reservationTime }}</text>
</view>
</view>
<view class="info-cell info-pet-cell">
<detail-cell title="宠物类型" :content="petType"/>
<detail-cell v-if="petHair" title="毛型分类" :content="petHair"/>
<detail-cell title="宠物姓名" :content="petName"/>
<detail-cell title="宠物重量区间" :content="petWeight.weight_name"/>
<detail-cell v-if="Object.keys(selectCoupon).length === 0" title="优惠券"
:content="couponList.length === 0 ? '暂无可用' : '未选优惠券'" :is-show-arrow="true"
:custom-content-style="{'color': '#9B939A'}" @clickAction="isShowCouponModal=true"/>
<detail-cell v-else title="优惠券" :content="showCoupon" :is-show-arrow="true"
:custom-content-style="{'color': '#FF19A0'}" @clickAction="isShowCouponModal=true"/>
<detail-cell v-if="Object.keys(selectService).length === 0" title="服务券"
:content="serviceList.length === 0 ? '暂无可用' : '未选服务券'"
:is-show-arrow="true"
:custom-content-style="{'color': '#9B939A'}" @clickAction="isShowServiceModal=true"/>
<detail-cell v-else title="服务券" :content="showService" :is-show-arrow="true"
:custom-content-style="{'color': '#FF19A0'}" @clickAction="isShowServiceModal=true"/>
<view class="line-view"></view>
<detail-cell title="费用预估" :content="`¥${showPrice || '0.00'}`" :title-mark="true"/>
</view>
<view class="info-cell info-pet-cell">
<detail-cell title="停车状况" :content="reservationInfo.parkState" :content-mark="false" :title-mark="true"/>
</view>
</view>
<view class="handle-view">
<view class="handle-info">
<view class="handle-info-cell">
<text class="app-fc-main fs-30">¥</text>
<text class="app-fc-main fs-40 app-font-bold">{{ showPrice || '0.00' }}</text>
</view>
<view class="handle-info-cell tip-cell">
<image src="/static/images/notice.png" mode="aspectFit" class="tips-icon"/>
<text class="app-fc-normal fs-24">以实际服务宠物的重量为准</text>
</view>
</view>
<view class="handle-btn" @click.stop="createPetOrder">
<text class="app-fc-white fs-30">确认支付</text>
</view>
</view>
<RechargeCouponModal
v-if="isShowCouponModal"
:couponList="couponList"
:price="reservationInfo.price"
:selected-item="selectCoupon"
@useCoupon="useCoupon"
@close="isShowCouponModal = false"
/>
<ServiceCouponModal
v-if="isShowServiceModal"
:service-list="serviceList"
@useService="useService"
@close="isShowServiceModal = false"
:weight-id="petWeight.weight_id"
:price="reservationInfo.price"
:selected-item="selectService"
/>
<success-modal
v-if="showSuccessModal"
:img-src="'https://activity.wagoo.live/order_success.png'"
:img-style="{width: '200rpx', height: '200rpx'}"
title="预约成功"
message="预约状态可在订单中进行查看"
@ok="paySuccessAction"
/>
</view>
</template>
<script>
import DetailCell from "@/components/petOrder/detail-cell.vue";
import {
COUPON_TYPE_PET,
ORDER_TYPE_SITE,
PET_HAIR_LONG,
PET_TYPE_CAT
} from "@/constants/app.business";
import { createOrder, payOrder } from "@/api/order";
import {
getCouponListByOrderPrice,
getServiceCouponListByWeightId
} from "@/api/coupon";
import RechargeCouponModal from "@/components/coupon/RechargeCouponModal.vue";
import ServiceCouponModal from "@/components/coupon/ServiceCouponModal.vue";
import SuccessModal from "@/components/SuccessModal.vue";
import moment from "moment";
import { ORDER_STATUS_RESERVED } from "@/pageHome/constants/home";
export default {
components: { SuccessModal, ServiceCouponModal, RechargeCouponModal, DetailCell },
onLoad(option) {
const eventChannel = this.getOpenerEventChannel();
eventChannel.on('reservationInfo', (data) => {
this.initializing = false;
this.reservationInfo = data;
Promise.all([this.getMyServiceList(), this.getMyCouponList()]).then((result) => {
if (this.serviceList.length) {
let selectService = this.serviceList[0];
this.serviceList.map((data) => {
if (Number(data.price) > Number(selectService.price)) {
selectService = data;
}
});
this.selectService = selectService;
} else if (this.couponList.length) {
let selectCoupon = this.couponList[0]
this.couponList.map((data) => {
if (Number(data.card_money) > Number(selectCoupon.card_money)) {
selectCoupon = data;
}
});
this.selectCoupon = selectCoupon;
}
})
})
},
computed: {
petType() {
return `${this.reservationInfo?.petInfo?.type}` === `${PET_TYPE_CAT}` ? '喵喵' : '汪汪'
},
petName() {
return this.reservationInfo?.petInfo?.name || '';
},
petWeight() {
return this.reservationInfo?.petWeight || {};
},
petHair() {
if (`${this.reservationInfo?.petInfo?.type}` === `${PET_TYPE_CAT}`) {
return `${this.reservationInfo?.petInfo.maofa}` === `${PET_HAIR_LONG}` ? '长毛' : '短毛';
} else {
return ''
}
},
reservationTime() {
if (this.reservationInfo.orderType === ORDER_TYPE_SITE) {
return '今天 ' + moment().format('M月D日');
}
return `${this.reservationInfo?.reservationTime?.dateLabel} ${this.reservationInfo?.reservationTime?.start}-${this.reservationInfo?.reservationTime?.end}`
},
showService() {
return this.selectService.name || '';
},
showCoupon() {
return '-¥' + (+this.selectCoupon.card_money || 0);
},
showPrice() {
if (Object.keys(this.selectService).length) {
return '0.00'
} else {
let couponMoney = this.selectCoupon?.card_money || 0;
return (Number(this.reservationInfo.price) - couponMoney).toFixed(2);
}
},
},
data() {
return {
reservationInfo: {},
initializing: true,
serviceList: [],
selectService: {},
isShowServiceModal: false,
couponList: [],
selectCoupon: {},
isShowCouponModal: false,
showSuccessModal: false
};
},
methods: {
getMyCouponList() {
return getCouponListByOrderPrice(this.reservationInfo.price,COUPON_TYPE_PET).then((res)=>{
this.couponList = res?.info || [];
return Promise.resolve();
}).catch(()=>{
this.couponList = [];
this.selectCoupon = {};
return Promise.resolve();
})
},
getMyServiceList() {
return getServiceCouponListByWeightId(this.reservationInfo?.petWeight?.weight_id).then((res)=>{
this.serviceList = res?.info || [];
return Promise.resolve();
}).catch(()=>{
this.serviceList = []
this.selectService = {};
return Promise.resolve();
})
},
useCoupon(item) {
if (this.selectCoupon.fafang_id === item.fafang_id) {
this.selectCoupon = {};
} else {
this.selectCoupon = item;
this.isShowCouponModal = false;
}
},
useService(item) {
if (this.selectService.order_id === item.order_id) {
this.selectService = {};
} else {
this.selectService = item;
this.isShowServiceModal = false;
}
},
createPetOrder() {
if (Object.keys(this.selectService).length && Object.keys(this.selectCoupon).length) {
uni.showToast({
title: '服务券和优惠券不能同时使用',
icon: 'none'
})
return;
}
uni.showLoading({
title: '正在创建订单',
mask: true
});
const data = {
chongwu_id: this.reservationInfo?.petInfo?.chongwu_id,
type: this.reservationInfo?.petInfo?.type,
weight_id: this.reservationInfo?.petWeight?.weight_id,
address_id: this.reservationInfo?.address?.address_id,
shiduan_id: this.reservationInfo.orderType === ORDER_TYPE_SITE ? 0 : this.reservationInfo?.reservationTime?.shiduan_id,
yuyue_date: this.reservationInfo.orderType === ORDER_TYPE_SITE ? moment().format('YYYY-MM-DD') : this.reservationInfo?.reservationTime?.date,
tingche_desc: this.reservationInfo.parkState,
desc: '',
order_type: this.reservationInfo.orderType,
}
if (this.selectCoupon.fafang_id) {
data.dikou_id = this.selectCoupon.fafang_id;
}
if (this.selectService.order_id) {
data.fuwuquan_id = this.selectService.order_id;
}
if (`${this.reservationInfo?.petInfo?.type}` === `${PET_TYPE_CAT}`) {
data.maofa = this.reservationInfo?.petInfo.maofa;
}
createOrder(data).then((res) => {
if (Object.keys(this.selectService).length || `${this.showPrice}` === '0.00') {
const eventChannel = this.getOpenerEventChannel();
eventChannel.emit('clearData')
uni.hideLoading();
this.showSuccessModal = true;
} else {
this.weixinPay(res.info)
}
}).catch(() => {
uni.showToast({
title: '创建订单失败',
icon: 'none'
})
uni.hideLoading();
});
},
weixinPay(orderId) {
payOrder(orderId).then((res) => {
const payData = res?.info?.pay_data || {};
uni.requestPayment({
provider: 'wxpay',
timeStamp: payData.timeStamp,
nonceStr: payData.nonceStr,
package: payData.package,
signType: payData.signType,
paySign: payData.sign,
success: (res) => {
uni.hideLoading();
const eventChannel = this.getOpenerEventChannel();
eventChannel.emit('clearData')
this.showSuccessModal = true;
},
fail: (err) => {
uni.showToast({
title: '创建订单失败',
icon: 'none'
})
uni.hideLoading();
}
});
}).catch(() => {
uni.showToast({
title: '创建订单失败',
icon: 'none'
})
uni.hideLoading();
})
},
paySuccessAction() {
uni.redirectTo({
url:'/pages/client/petOrder/index',
complete() {
uni.$emit('changeTabBar', {
pageId: 'orderPage',
orderState: ORDER_STATUS_RESERVED
})
}
});
// uni.navigateBack({
// delta: 1,
// complete() {
// uni.$emit('changeTabBar', {
// pageId: 'orderPage',
// orderState: ORDER_STATUS_RESERVED
// })
// }
// })
}
}
}
</script>
<style lang="scss" scoped>
.payment-confirm-container {
width: 100%;
height: 100%;
box-sizing: border-box;
background-color: #FBF8FC;
display: flex;
flex-direction: column;
.payment-body-container {
display: flex;
flex-direction: column;
flex: 1;
padding: 20rpx 32rpx;
.info-cell {
width: 100%;
padding: 40rpx;
box-sizing: border-box;
border-radius: 30rpx;
background-color: #FFFFFF;
margin-bottom: 32rpx;
.info-title-view {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
.phone-text {
margin-left: 24rpx;
}
}
.address-view {
width: 100%;
display: flex;
align-items: center;
.address-text {
margin-top: 12rpx;
}
}
.info-icon {
width: 36rpx;
height: 36rpx;
flex-shrink: 0;
margin-right: 20rpx;
}
.info-view {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin-bottom: 40rpx;
}
.line-view {
width: 100%;
height: 2rpx;
background-color: #ECECEC;
margin: 20rpx 0;
}
}
.info-pet-cell {
padding: 20rpx 40rpx;
}
}
.handle-view {
width: 100%;
padding: 20rpx 36rpx 20rpx;
box-sizing: border-box;
background-color: #fff;
display: flex;
flex-direction: row;
align-items: center;
.handle-info {
display: flex;
flex: 1;
flex-direction: column;
justify-content: space-between;
.handle-info-cell {
display: flex;
flex-direction: row;
align-items: flex-end;
width: 100%;
.tips-icon {
width: 24rpx;
height: 24rpx;
}
}
.tip-cell {
align-items: center;
}
}
.handle-btn {
width: 260rpx;
height: 90rpx;
border-radius: 45rpx;
background-color: $app_color_main;
display: flex;
align-items: center;
justify-content: center;
}
}
}
</style>

View File

@ -0,0 +1,299 @@
<template>
<view class="order-info-cell" @click.stop="clickAction">
<template v-if="cellType === 'text'">
<view class="info-top-view">
<image :src="infoIcon" mode="aspectFit" class="cell-icon"/>
<view class="title-view">
<text class="app-fc-main fs-28">{{ title }}</text>
</view>
<view class="info-view" v-if="info">
<text class="app-fc-main fs-28">{{ info }}</text>
</view>
<image v-if="isCanClick" class="right-icon" src="@/static/images/right_arrow2.png" mode="widthFix"></image>
<view v-else class="right-icon"/>
</view>
</template>
<!-- 时间信息-->
<template v-if="cellType === 'time'">
<view class="info-top-view">
<image :src="infoIcon" mode="aspectFit" class="cell-icon"/>
<view class="title-view">
<text class="app-fc-main fs-28">{{ title }}</text>
</view>
<image class="right-icon" src="@/static/images/right_arrow2.png" mode="widthFix"></image>
</view>
<view class="info-bottom-view" v-if="info">
<text class="app-fc-main fs-28">{{ info }}</text>
</view>
</template>
<!-- 地址信息-->
<template v-if="cellType === 'address'">
<view class="info-top-view">
<image :src="infoIcon" mode="aspectFit" class="cell-icon"/>
<view class="title-view">
<text class="app-fc-main fs-28">{{ title }}</text>
</view>
<image class="right-icon" src="@/static/images/right_arrow2.png" mode="widthFix"></image>
</view>
<view class="info-bottom-view bottom-address-container" v-if="addressInfo">
<view class="user-info-view">
<text class="app-fc-main fs-28 app-font-bold">{{ addressInfo.name }}</text>
<text class="app-fc-main fs-28 phone-text app-font-bold">{{ addressInfo.phone }}</text>
</view>
<view class="address-view">
<text class="app-fc-normal fs-24 address-text">{{ `${addressInfo.area_name} ${addressInfo.address}` }}</text>
</view>
</view>
</template>
<!-- 价格-->
<template v-if="cellType === 'price'">
<view class="info-top-view">
<view class="title-view">
<text class="app-fc-main fs-28">{{ title }}</text>
</view>
<text v-if="price" class="app-fc-mark fs-32 app-font-bold-700">{{ `¥${price}` }}</text>
<text v-else class="app-fc-main fs-28">---</text>
</view>
</template>
<template v-if="cellType === 'park'">
<view class="info-top-view">
<view class="title-view">
<text class="app-fc-main fs-28">{{ title }}</text>
</view>
</view>
<view class="list-view">
<view class="list-card" v-for="item in park_conditions" :key="item"
:class="{'selected-list-card': parkState === item}" @click.stop="changeParkState(item)">
<text class="fs-28 app-fc-normal" :class="{'app-fc-mark': parkState === item}">{{ item }}</text>
</view>
</view>
<view class="input-container" v-if="parkState === '其他'">
<textarea
:focus="true"
:value="otherParkState"
class="input-view fs-28 app-fc-main"
placeholder="点击输入停车信息"
placeholder-class="placeholder-class"
@input="onChange"
/>
</view>
</template>
</view>
</template>
<script>
/**
* info-cell 预约信息显示
* @property {String} cellType - 显示类型 默认text text/address/time/price/park
* @value text 文本类型
* @value address 地址类型
* @value time 时间类型
* @value price 价格类型
* @value park 停车状况
* @property {String} infoIcon - 左侧图标
* @property {String} title - 标题
* @property {String} info - 显示内容
* @property {Object} addressInfo - 地址信息
* @property {String} price - 价格
* @property {String} parkState - 停车状况
*
* @event {Function} clickAction 点击cell触发事件
* @event {Function} changeParkState 停车状况改变触发事件
*/
export default {
props: {
isCanClick: {
type: Boolean,
default: true
},
cellType: {
type: String,
default: 'info'
},
infoIcon: {
type: String | null,
default: require('@/static/images/arrow_right.png'),
},
title: {
type: String,
default: ''
},
info: {
type: String,
default: ''
},
addressInfo: {
type: Object | null,
default: () => {
return null
}
},
price: {
type: String,
default: ''
},
parkState: {
type: String,
default: ''
},
otherParkState: {
type: String,
default: ''
}
},
data() {
return {
park_conditions: ['小区', '地库', '路边', '其他'],
};
},
methods: {
clickAction() {
this.isCanClick && this.$emit('clickAction')
},
changeParkState(state) {
this.$emit('changeParkState', state)
},
onChange(e) {
this.$emit('changeOtherParkState', e.detail.value)
}
}
}
</script>
<style lang="scss" scoped>
.order-info-cell {
width: 100%;
background-color: #fff;
display: flex;
flex-direction: column;
padding: 36rpx 0;
box-sizing: border-box;
border-bottom: 1px solid #ECECEC;
.info-top-view {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
box-sizing: border-box;
.cell-icon {
width: 36rpx;
height: 36rpx;
flex-shrink: 0;
margin-right: 20rpx;
}
.title-view {
display: flex;
flex: 1;
align-items: center;
}
.info-view {
display: flex;
flex: 1;
align-items: center;
justify-content: flex-end;
}
.right-icon {
margin-left: 16rpx;
width: 10rpx;
height: 5rpx;
flex-shrink: 0;
}
}
.info-bottom-view {
width: calc(100% - 56rpx);
margin-top: 32rpx;
margin-left: 56rpx;
padding-right: 104rpx;
display: flex;
flex-direction: column;
box-sizing: border-box;
.user-info-view {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
.phone-text {
margin-left: 24rpx;
}
}
.address-view {
width: 100%;
display: flex;
align-items: center;
.address-text {
margin-top: 12rpx;
}
}
}
.bottom-address-container {
margin-top: 28rpx;
}
.list-view {
margin-top: 32rpx;
width: 100%;
padding-right: 30rpx;
box-sizing: border-box;
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
.list-card {
padding: 18rpx 32rpx 18rpx 30rpx;
border-radius: 24rpx;
background-color: #F5F5F5;
}
.selected-list-card {
border: 2rpx solid $app_color_main;
background-color: #FEE9F3;
}
}
.input-container {
width: 100%;
height: 200rpx;
padding: 0 30rpx 0 0;
box-sizing: border-box;
margin-top: 32rpx;
.input-view {
width: 100%;
height: 100%;
padding: 32rpx;
border-radius: 30rpx;
color: #333;
box-sizing: border-box;
background-color: #F9F7F9;
}
.placeholder-class {
color: #666262;
font-size: 28rpx;
}
}
}
</style>

View File

@ -0,0 +1,184 @@
<template>
<select-modal @close="closeAction" title="随车购商品" class="order-goods-modal">
<view class="goods-container">
<scroll-view class="goods-scroll-view" :scroll-y="true">
<view class="goods-item" v-for="item in goodsList" :key="item.order_id" @click.stop="gotoDetail(item)">
<image mode="aspectFit" class="goods-img" :src="item.goods_pic"/>
<view class="goods-info-view">
<text class="app-fc-main fs-32 app-font-bold-500 app-text-ellipse">
{{ item.goods_name || '--' }}
</text>
<text class="app-fc-normal fs-24 goods-num-text">{{ `数量X${item.number}` }}</text>
<text class="app-fc-main fs-28 goods-price-text" >实付款
<text class="fs-28 app-fc-alarm">{{ `¥${item.goods_price}` }}</text>
</text>
</view>
<image src="@/static/images/arrow_right_black.png" mode="aspectFit" class="good-arrow"/>
</view>
</scroll-view>
</view>
<view class="goods-price-container">
<view class="goods-left-view">
<view class="price-view">
<text class="app-fc-main fs-28">¥</text>
<text class="app-fc-main app-font-bold-700 fs-50 price-text">{{ price }}</text>
</view>
<view class="price-tips-view">
<image src="/pageHome/static/tips.png" mode="aspectFit" class="tips-img"/>
<text class="fs-24">商品总价</text>
</view>
</view>
<view class="submit-btn" @click.stop="closeAction">
<text class="app-fc-white fs-30">确定</text>
</view>
</view>
</select-modal>
</template>
<script>
import SelectModal from "@/components/select-modal.vue";
export default {
components: { SelectModal },
props: {
goodsList: {
type: Array,
default: () => []
},
price: {
type: Number,
default: 0
}
},
data() {
return {};
},
options: {
styleIsolation: "shared",
},
methods: {
closeAction() {
this.$emit('close');
},
gotoDetail(item){
this.$emit('gotoDetail', item.goods_id);
},
},
}
</script>
<style lang="scss" scoped>
.order-goods-modal {
::v-deep {
.selected-modal .model-container {
background: #fff0f5;
position: relative;
}
}
.goods-container {
width: 100%;
height: 800rpx;
position: relative;
.goods-scroll-view {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
padding: 0 32rpx;
box-sizing: border-box;
.goods-item {
width: 100%;
height: 200rpx;
display: flex;
align-items: center;
background-color: #fff;
border-radius: 40rpx;
margin-top: 32rpx;
padding: 20rpx;
box-sizing: border-box;
.goods-img {
width: 160rpx;
height: 160rpx;
margin-right: 24rpx;
}
.goods-info-view {
display: flex;
flex-direction: column;
width: calc(100% - 160rpx - 24rpx - 40rpx);
.goods-num-text {
margin-top: 16rpx;
}
.goods-price-text {
margin-top: 40rpx;
}
}
.good-arrow {
width: 20rpx;
height: 20rpx;
margin-left: 20rpx;
}
}
}
}
.goods-price-container {
width: 100%;
background-color: #fff;
padding: 26rpx 32rpx;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
.goods-left-view {
display: flex;
flex-direction: column;
.price-view {
display: flex;
flex-direction: row;
align-items: flex-end;
.price-text {
margin-left: 8rpx;
line-height: 50rpx;
}
}
.price-tips-view {
margin-top: 10rpx;
display: flex;
flex-direction: row;
align-items: center;
.tips-img {
width: 24rpx;
height: 24rpx;
margin-right: 2rpx;
}
}
}
.submit-btn {
width: 260rpx;
height: 92rpx;
display: flex;
align-items: center;
justify-content: center;
background-color: #FE019B;
border-radius: 46rpx;
}
}
}
</style>

View File

@ -0,0 +1,49 @@
<template>
<select-modal @close="closeAction" title="价格说明">
<view class="price-description-container">
<view class="price-info">
<text class="app-fc-main fs-32">{{priceInfo.desc}}</text>
</view>
</view>
</select-modal>
</template>
<script>
import SelectModal from "@/components/select-modal.vue";
export default {
components: { SelectModal },
props: {
priceInfo: {
type: Object,
default: () => {
return {};
}
}
},
data() {
return {};
},
methods: {
closeAction() {
this.$emit('close');
},
},
}
</script>
<style lang="scss" scoped>
.price-description-container {
width: 100%;
padding: 0 40rpx 10rpx;
box-sizing: border-box;
.price-info {
width: 100%;
padding: 36rpx 0;
box-sizing: border-box;
border-bottom: 1px solid #F7F3F7;
}
}
</style>

View File

@ -0,0 +1,230 @@
<template>
<select-modal @close="closeAction" title="选择服务地址">
<view class="select-address-container">
<view v-if="!isLoading && addressList.length" class="address-container">
<scroll-view class="scroll-view" scroll-y @scrolltolower="loadMoreAction"
refresher-background="transparent">
<view class="address-item" v-for="(item, index) in addressList" :key="item.id"
@click.stop="changeAddress(item)">
<view class="address-info-view">
<view class="address-name-view">
<text v-if="item.is_default" class="default-address fs-18">默认</text>
<text class="app-fc-main fs-30 app-font-bold name-text">{{ item.recipient_name }}</text>
<text class="app-fc-main fs-30 app-font-bold">{{ item.phone }}</text>
</view>
<text class="app-fc-normal fs-26">{{ getAddressText(item) }}</text>
</view>
<image @click.stop="goToEditAddress(item)" src="@/static/images/address_edit.png" mode="aspectFit" class="address-edit-icon"/>
</view>
</scroll-view>
</view>
<view class="address-container flex-center" v-if="isLoading">
<uni-load-more status="loading" :show-text="false"/>
</view>
<view v-if="addressList.length === 0 && !isLoading" class="address-container">
<image src="https://activity.wagoo.live/no_address.png" class="no-address-img" mode="widthFix"/>
<!-- <text class="app-fc-normal fs-32 no-text">暂无地址信息</text> -->
</view>
<view class="add-btn" @click.stop="gotoAddAddress">
<image class="add-icon" src="@/static/images/add.png" mode="aspectFit"/>
<text class="app-fc-white fs-30">添加地址</text>
</view>
</view>
</select-modal>
</template>
<script>
import SelectModal from "@/components/select-modal.vue";
import { getAddressList } from "@/api/address";
export default {
components: { SelectModal },
props: {
selectAddress: {
type: Object,
default: () => {
return {};
}
}
},
data() {
return {
addressList: [],
isLoading: true,
pageNumber: 1,
pageSize: 10,
isLoadMore: false,
isNoMore: false,
};
},
computed: {
userInfo() {
return this.$store.state?.user?.userInfo || {};
}
},
mounted() {
this.pageNumber = 1;
this.getData();
},
methods: {
closeAction() {
this.$emit('close');
},
changeAddress(address) {
this.$emit('changeAddress', address);
},
loadMoreAction() {
if (this.isNoMore || this.isLoadMore) {
return;
}
this.pageNumber++;
this.isLoadMore = true;
this.getData()
},
getData() {
getAddressList({ user_id: this.userInfo.userID }).then((res) => {
let list = res?.data || [];
if (this.pageNumber === 1) {
this.addressList = list;
} else {
this.addressList = [...this.addressList, ...list];
}
this.isLoading = false;
this.isLoadMore = false;
this.isNoMore = list.length < this.pageSize;
}).catch(() => {
if (this.pageNumber !== 1) {
this.pageNumber--;
}
this.isLoading = false;
this.isLoadMore = false;
})
},
getAddressText(item) {
// 组合地址信息:省市区 + 详细地址
const region = [item.province, item.city, item.district].filter(Boolean).join('');
return region ? `${region}${item.full_address}` : item.full_address;
},
gotoAddAddress() {
uni.navigateTo({
url: `/pages/client/address/edit?isAdd=1`,
events: {
refreshAddress: () => {
this.isLoading = true;
this.isNoMore = false;
this.isLoadMore = false;
this.pageNumber = 1;
this.getData();
},
},
})
},
goToEditAddress(item){
uni.navigateTo({
url: `/pages/client/address/edit?id=${item?.id || ""}`,
events: {
refreshAddress: () => {
this.isLoading = true;
this.isNoMore = false;
this.isLoadMore = false;
this.pageNumber = 1;
this.getData();
},
},
})
}
}
}
</script>
<style lang="scss" scoped>
.select-address-container {
width: 100%;
padding: 0 56rpx 10rpx;
box-sizing: border-box;
.address-container {
width: 100%;
height: 444rpx;
display: flex;
flex-direction: column;
margin-bottom: 18rpx;
.no-address-img {
margin-top: 30rpx;
width: 348rpx;
align-self: center;
}
.no-text {
margin: 0 0 24rpx;
align-self: center;
}
.scroll-view {
width: 100%;
height: 100%;
}
.address-item {
width: 100%;
display: flex;
align-items: center;
padding: 30rpx 0rpx;
box-sizing: border-box;
border-bottom: 2rpx solid #F7F3F7;
.address-info-view {
display: flex;
flex: 1;
flex-direction: column;
margin-right: 30rpx;
.address-name-view {
display: flex;
flex: 1;
align-items: center;
margin-bottom: 12rpx;
.default-address {
padding: 2rpx 6rpx;
color: #40ae36;
background: rgba(64, 174, 54, 0.2);
border-radius: 6rpx;
margin-right: 16rpx;
}
.name-text {
margin-right: 16rpx;
}
}
}
.address-edit-icon {
width: 48rpx;
height: 48rpx;
}
}
}
.add-btn {
width: 100%;
height: 90rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 45rpx;
background-color: $app_color_main;
.add-icon {
width: 34rpx;
height: 34rpx;
flex-shrink: 0;
margin-right: 18rpx;
}
}
}
</style>

View File

@ -0,0 +1,201 @@
<template>
<select-modal @close="closeAction" title="选择宠物">
<view class="select-pet-container">
<view v-if="!isLoading && petList.length" class="pet-container">
<scroll-view class="scroll-view" scroll-y @scrolltolower="loadMoreAction"
refresher-background="transparent">
<view class="pet-item" v-for="(item, index) in petList" :key="item.chongwu_id" @click.stop="changePet(item)">
<image v-if="item.chongwu_pic_url" class="pet-icon" mode="scaleToFill" :src="item.chongwu_pic_url"/>
<image v-else mode="scaleToFill" class="pet-icon" src="https://activity.wagoo.live/record_avator.png"/>
<view class="pet-name-view">
<text class="app-fc-main fs-32 app-font-bold-500">{{ item.name }}</text>
</view>
<image
v-if="selectPetInfo.chongwu_id === item.chongwu_id"
src="@/static/images/cart_checked.png"
mode="widthFix"
class="select-icon"
/>
<image
v-else
src="@/static/images/unchecked.png"
mode="widthFix"
class="select-icon"
/>
</view>
</scroll-view>
</view>
<view class="pet-container flex-center" v-if="isLoading">
<uni-load-more status="loading" :show-text="false"/>
</view>
<view v-if="petList.length === 0 && !isLoading" class="pet-container" @click.stop="gotoAddPet">
<image src="https://activity.wagoo.live/no_pet.png" class="no-pet-img" mode="heightFix" />
</view>
<view class="add-btn" @click.stop="gotoAddPet">
<image class="add-icon" src="@/static/images/add.png" mode="aspectFit"/>
<text class="app-fc-white fs-30">添加宠物</text>
</view>
</view>
</select-modal>
</template>
<script>
import SelectModal from "@/components/select-modal.vue";
import { getPetList } from "@/api/common";
import appConfig from '../../constants/app.config';
export default {
components: { SelectModal },
props: {
petType: {
type: Number,
default: 0
},
selectPetInfo: {
type: Object,
default: () => {
return {};
}
}
},
data() {
return {
isLoading: true,
pageNumber: 1,
pageSize: 10,
petList: [],
isLoadMore: false,
isNoMore: false,
appConfig,
};
},
mounted() {
this.pageNumber = 1;
this.getData();
},
methods: {
closeAction() {
this.$emit('close');
},
changePet(pet) {
this.$emit('changePet', pet);
},
loadMoreAction() {
if (this.isNoMore || this.isLoadMore) {
return;
}
this.pageNumber++;
this.isLoadMore = true;
this.getData()
},
getData() {
getPetList(null, this.pageNumber, this.pageSize).then((res) => {
let list = res?.info || [];
if (this.pageNumber === 1) {
this.petList = list;
} else {
this.petList = [...this.petList, ...list];
}
this.isLoading = false;
this.isLoadMore = false;
this.isNoMore = list.length < this.pageSize;
}).catch(() => {
if (this.pageNumber !== 1) {
this.pageNumber--;
}
this.isLoading = false;
this.isLoadMore = false;
})
},
gotoAddPet() {
uni.navigateTo({
url: `/pages/client/record/edit?type=${this.petType }&typeId=aaa`,
events: {
addPetSuccess: () => {
this.isLoading = true;
this.isNoMore = false;
this.isLoadMore = false;
this.pageNumber = 1;
this.getData();
},
},
})
}
}
}
</script>
<style lang="scss" scoped>
.select-pet-container {
width: 100%;
padding: 0 60rpx 10rpx;
box-sizing: border-box;
.pet-container {
width: 100%;
height: 444rpx;
display: flex;
flex-direction: column;
margin-bottom: 18rpx;
.no-pet-img {
height: calc(1289 / 1363 * 550rpx);
align-self: center;
}
.no-text {
margin: 44rpx 0 26rpx;
align-self: center;
}
.scroll-view {
width: 100%;
height: 100%;
}
.pet-item {
width: 100%;
display: flex;
align-items: center;
padding: 34rpx 22rpx;
box-sizing: border-box;
border-bottom: 2rpx solid #F7F3F7;
.pet-icon {
width: 80rpx;
height: 80rpx;
border-radius: 40rpx;
overflow: hidden;
}
.pet-name-view {
display: flex;
flex: 1;
margin-left: 24rpx;
}
.select-icon {
width: 27rpx;
}
}
}
.add-btn {
width: 100%;
height: 90rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 45rpx;
background-color: $app_color_main;
.add-icon {
width: 34rpx;
height: 34rpx;
flex-shrink: 0;
margin-right: 18rpx;
}
}
}
</style>

View File

@ -0,0 +1,261 @@
<template>
<select-modal @close="closeAction" title="选择预约时间">
<view class="select-time-container">
<view class="calendar-container">
<uni-calendar
:insert="true"
class="uni-calendar--hook"
:start-date="startDate"
:end-date="endDate"
:date="selectDate"
:showMonth="false"
@change="changeDate"/>
</view>
<view class="tips-view">
<image src="/pageHome/static/tips.png" class="tips-img" mode="aspectFit"></image>
<text class="fs-24 app-fc-normal">
{{isAfternoon? '可以预约的时间范围是后天及之后的 14 天' : '可以预约的时间范围是明天及之后的 14 天' }}
</text>
</view>
<text class="fs-32 app-fc-main time-header-text">可预约时段</text>
<view class="time-list-container">
<scroll-view v-if="!isLoading && dateList.length" class="list-scroll-view" :scroll-y="true">
<view
@click.stop="changeTimeAction(item)"
class="time-item"
:class="{'selected-time-item': selectTimeRange.shiduan_id === item.shiduan_id, 'disabled-time-item': item.num === 0}"
v-for="item in dateList"
:key="item.shiduan_id">
<text class="fs-28 app-fc-main">{{ `${item.start}-${item.end}` }}</text>
<text class="fs-28 app-fc-main">{{ item.num === 0 ? '已约满' : `剩余${item.num}` }}</text>
</view>
</scroll-view>
<view v-if="isLoading" class="loading-view">
<uni-load-more status="loading"/>
</view>
<view v-if="!isLoading && dateList.length === 0" class="loading-view">
<text class="fs-28 app-fc-main">无可预约时间</text>
</view>
</view>
<view class="submit-btn" @click.stop="changeReservationTime">
<text class="app-fc-white fs-30">确定</text>
</view>
</view>
</select-modal>
</template>
<script>
import SelectModal from "@/components/select-modal.vue";
import moment from "moment";
import "moment/locale/zh-cn";
import { getYuYueTimeList } from "@/api/order";
export default {
components: { SelectModal },
props: {
selectTime: {
type: Object,
default: () => {
return {};
}
}
},
data() {
return {
selectTimeRange: {},
isLoading: false,
selectDate: moment().isAfter(moment().format('YYYY-MM-DD 12:00:00')) ? moment().add(2, 'days').format('YYYY-MM-DD') : moment().add(1, 'days').format('YYYY-MM-DD'),
startDate: moment().isAfter(moment().format('YYYY-MM-DD 12:00:00')) ? moment().add(2, 'days').format('YYYY-MM-DD') : moment().add(1, 'days').format('YYYY-MM-DD'),
endDate: moment().isAfter(moment().format('YYYY-MM-DD 12:00:00')) ? moment().add(16, 'days').format('YYYY-MM-DD') : moment().add(15, 'days').format('YYYY-MM-DD'),
dateList: [],
//是下午
isAfternoon: moment().isAfter(moment().format('YYYY-MM-DD 12:00:00'))
};
},
mounted() {
moment.locale('zh-cn'); // 设置中文本地化
if (Object.keys(this.selectTime).length > 0) {
this.selectDate = this.selectTime.date;
this.selectTimeRange = this.selectTime;
this.getTimeArray(this.selectTime.date)
} else {
this.getTimeArray(this.selectDate)
}
},
methods: {
closeAction() {
this.$emit('close');
},
changeDate(e) {
this.selectTimeRange = {};
this.selectDate = e.fulldate;
this.getTimeArray(e.fulldate)
},
changeTimeAction(item) {
if (item.num === 0) {
uni.showToast({
title: '当前时间已约满',
icon: 'none'
})
} else {
this.selectTimeRange = item;
}
},
changeReservationTime() {
if (this.dateList.length === 0) {
uni.showToast({
title: '暂无可预约时间',
icon: 'none'
})
return;
}
if (Object.keys(this.selectTimeRange).length === 0) {
uni.showToast({
title: '请选择一个预约时段',
icon: 'none'
})
return;
}
let dateLabel;
if (moment().format('YYYY-MM-DD') === moment(this.selectDate).format('YYYY-MM-DD')) {
dateLabel = `今天${moment(this.selectDate).format('M月D日')}`
} else if (moment().add(1, "days").format('YYYY-MM-DD') === moment(this.selectDate).format('YYYY-MM-DD')) {
dateLabel = `明天${moment(this.selectDate).format('M月D日')}`
} else if (moment().add(2, "days").format('YYYY-MM-DD') === moment(this.selectDate).format('YYYY-MM-DD')) {
dateLabel = `后天${moment(this.selectDate).format('M月D日')}`
} else {
dateLabel = `${moment(this.selectDate).format('M月D日')}`
}
this.$emit('changeReservationTime', {
dateLabel,
date: this.selectDate,
...this.selectTimeRange,
});
},
getTimeArray(t) {
this.isLoading = true
this.dateList = [];
let isCurrent = moment().format('YYYY-MM-DD') === t;
return getYuYueTimeList(t).then((res) => {
let list = (res?.info || []).map((item) => {
return {
...item,
start: item.start.substring(0, 5),
end: item.end.substring(0, 5),
};
});
this.dateList = list.filter((item) => {
let end = moment(item.end, 'HH:mm');
let now = moment();
if (isCurrent) {
return end.isAfter(now);
} else {
return true;
}
});
this.isLoading = false;
return Promise.resolve();
}).catch(() => {
this.isLoading = false
})
}
},
}
</script>
<style lang="scss" scoped>
.select-time-container {
width: 100%;
height: 85vh;
box-sizing: border-box;
display: flex;
flex-direction: column;
.calendar-container {
width: 100%;
}
.tips-view {
width: 100%;
display: flex;
align-items: center;
padding: 24rpx 30rpx;
box-sizing: border-box;
background-color: #F5F5F5;
margin-bottom: 40rpx;
.tips-img {
width: 24rpx;
height: 24rpx;
margin-right: 6rpx;
}
}
.time-header-text {
margin-left: 28rpx;
}
.time-list-container {
margin-left: 28rpx;
width: calc(100% - 56rpx);
margin-top: 20rpx;
display: flex;
flex: 1;
position: relative;
.loading-view {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.list-scroll-view {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
.time-item {
width: 100%;
height: 102rpx;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
border-radius: 20rpx;
border: 2rpx solid #E5E7EB;
background-color: #fff;
opacity: 1;
margin-bottom: 22rpx;
padding: 0 30rpx;
box-sizing: border-box;
}
.selected-time-item {
border: 2rpx solid $app_color_main;
background-color: $app_color_mark_bg_color;
}
.disabled-time-item {
background-color: #FBF7FC;
opacity: 0.5;
}
}
}
.submit-btn {
width: calc(100% - 120rpx);
margin-left: 60rpx;
margin-bottom: 10rpx;
height: 90rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 45rpx;
background-color: $app_color_main;
}
}
</style>

View File

@ -0,0 +1,111 @@
export const ORDER_STATUS_UNPAY = 1; // 未支付
export const ORDER_STATUS_RESERVED = 2; // 已预约
export const ORDER_STATUS_SEND = 3; // 已派单
export const ORDER_STATUS_SERVICE = 4; //服务中
export const ORDER_STATUS_COMPLETED = 5; // 已完成
export const ORDER_STATUS_CANCELED = 6; // 已取消
export const ORDER_STATUS_REFUND = 7; // 退款中
export const PRICE_DIFF_TYPE_SERVICE = '1'; //差价类型 服务
export const orderStatusList = [{
value: ORDER_STATUS_UNPAY,
label: '未支付',
},
{
value: ORDER_STATUS_RESERVED,
label: '已预约',
},
{
value: ORDER_STATUS_SEND,
label: '已派单',
},
{
value: ORDER_STATUS_SERVICE,
label: '服务中',
},
{
value: ORDER_STATUS_COMPLETED,
label: '已完成',
},
{
value: ORDER_STATUS_CANCELED,
label: '已取消',
},
{
value: ORDER_STATUS_REFUND,
label: '退款中',
},
]
//展示的状态
export const showOrderStatus = [{
value: 0,
label: '全部',
},
{
value: ORDER_STATUS_UNPAY,
label: '未支付',
},
{
value: ORDER_STATUS_RESERVED,
label: '已预约',
},
{
value: ORDER_STATUS_SERVICE,
label: '服务中',
},
{
value: ORDER_STATUS_COMPLETED,
label: '已完成',
},
{
value: ORDER_STATUS_CANCELED,
label: '已取消',
},
]
//充值相关
export const rechargeStatus = [{
value: '1',
label: '充值',
},
{
value: '2',
label: '充值记录',
}
]
//充值赠送金额
export const giftAmount = [{
value: '1',
label: '100',
gift: '10'
},
{
value: '2',
label: '200',
gift: '20'
},
{
value: '3',
label: '500',
gift: '50'
},
{
value: '4',
label: '1000',
gift: '100'
},
{
value: '5',
label: '2000',
gift: '200'
},
{
value: '6',
label: '5000',
gift: '500'
}
]

View File

@ -0,0 +1,267 @@
<template>
<view class="city-select-container">
<view class="list-wrapper">
<uni-indexed-list
:options="cityOptions"
:show-select="true"
@click="onCityClick"
/>
</view>
<!-- 底部确认按钮 -->
<view class="bottom-actions">
<view class="selected-info">
<text class="selected-text">已选择{{ selectedCities.length }}个城市</text>
</view>
<view class="action-buttons">
<view class="btn btn-cancel" @click="handleCancel">
<text class="btn-text">取消</text>
</view>
<view class="btn btn-confirm" @click="handleConfirm">
<text class="btn-text">确定</text>
</view>
</view>
</view>
</view>
</template>
<script>
import { cityData } from '@/api/areas.js';
export default {
data() {
return {
cityOptions: [],
selectedCities: [], // 已选中的城市名称数组
selectedCityNames: [] // 已选中的城市名称(用于显示)
};
},
onLoad(options) {
// 如果有传入已选中的城市,解析并设置
if (options.selectedCities) {
try {
this.selectedCityNames = JSON.parse(decodeURIComponent(options.selectedCities));
this.selectedCities = [...this.selectedCityNames];
} catch (e) {
console.error('解析已选城市失败:', e);
}
}
this.loadCityData();
},
methods: {
// 加载城市数据
loadCityData() {
try {
// 使用 cityData 数据源,已经按首字母分组
this.cityOptions = cityData.map(group => {
// 处理每个字母组的数据
const processedData = group.data.map(city => {
const cityName = city.name;
// 如果城市在已选中列表中,使用对象格式并设置 checked: true
// 否则使用字符串格式(组件会自动转换为 { name: '城市名', checked: false }
if (this.selectedCities.includes(cityName)) {
return {
name: cityName,
checked: true
};
} else {
return cityName;
}
});
return {
letter: group.letter,
data: processedData
};
});
} catch (err) {
console.error('加载城市数据失败:', err);
uni.showToast({
title: '加载城市数据失败',
icon: 'none'
});
}
},
// 获取城市首字母(简单的拼音首字母映射)
getFirstLetter(city) {
if (!city) return 'Z';
// 常用城市拼音首字母映射
const pinyinMap = {
'北京': 'B', '上海': 'S', '天津': 'T', '重庆': 'C',
'河北': 'H', '山西': 'S', '内蒙古': 'N', '辽宁': 'L',
'吉林': 'J', '黑龙江': 'H', '江苏': 'J', '浙江': 'Z',
'安徽': 'A', '福建': 'F', '江西': 'J', '山东': 'S',
'河南': 'H', '湖北': 'H', '湖南': 'H', '广东': 'G',
'广西': 'G', '海南': 'H', '四川': 'S', '贵州': 'G',
'云南': 'Y', '西藏': 'X', '陕西': 'S', '甘肃': 'G',
'青海': 'Q', '宁夏': 'N', '新疆': 'X', '台湾': 'T',
'香港': 'X', '澳门': 'A'
};
// 先检查完整匹配
if (pinyinMap[city]) {
return pinyinMap[city];
}
// 检查是否以某个省份开头
for (let key in pinyinMap) {
if (city.startsWith(key)) {
return pinyinMap[key];
}
}
// 使用首字符的拼音
const char = city.charAt(0);
const charMap = {
'北': 'B', '上': 'S', '天': 'T', '重': 'C',
'河': 'H', '山': 'S', '内': 'N', '辽': 'L',
'吉': 'J', '黑': 'H', '江': 'J', '浙': 'Z',
'安': 'A', '福': 'F', '湖': 'H', '广': 'G',
'海': 'H', '四': 'S', '贵': 'G', '云': 'Y',
'西': 'X', '陕': 'S', '甘': 'G', '青': 'Q',
'宁': 'N', '新': 'X', '台': 'T', '香': 'X', '澳': 'A',
'杭': 'H', '南': 'N', '武': 'W', '成': 'C',
'西': 'X', '郑': 'Z', '长': 'C', '沈': 'S',
'大': 'D', '青': 'Q', '济': 'J', '石': 'S',
'哈': 'H', '合': 'H', '福': 'F', '厦': 'X',
'昆': 'K', '兰': 'L', '呼': 'H', '太': 'T',
'乌': 'W', '银': 'Y', '贵': 'G', '拉': 'L'
};
if (charMap[char]) {
return charMap[char];
}
// 如果是英文字母,直接返回大写
if (/[A-Za-z]/.test(char)) {
return char.toUpperCase();
}
// 默认返回 Z
return 'Z';
},
// 城市点击事件
// e.select 包含所有已选中的项(组件内部维护的选中状态)
onCityClick(e) {
// 从组件返回的 select 数组中提取所有选中项的城市名称
if (e && e.select && Array.isArray(e.select)) {
this.selectedCities = e.select.map(item => item.name || item);
} else {
// 如果没有 select说明可能是单选模式使用 item
if (e && e.item) {
const cityName = e.item.name || e.item;
const index = this.selectedCities.indexOf(cityName);
if (index > -1) {
this.selectedCities.splice(index, 1);
} else {
this.selectedCities.push(cityName);
}
}
}
},
// 取消
handleCancel() {
uni.navigateBack();
},
// 确认
handleConfirm() {
// 通过 eventChannel 返回选中的城市
const eventChannel = this.getOpenerEventChannel && this.getOpenerEventChannel();
if (eventChannel) {
eventChannel.emit('citySelected', {
cities: this.selectedCities,
cityNames: this.selectedCities.join('、')
});
}
uni.navigateBack();
}
}
};
</script>
<style lang="scss" scoped>
.city-select-container {
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
background-color: #fff;
box-sizing: border-box;
}
.list-wrapper {
flex: 1;
overflow: hidden;
position: relative;
box-sizing: border-box;
}
// 覆盖 uni-indexed-list 的 bottom 值,避免被底部按钮遮盖
// 底部按钮区域高度:选中信息(20+28) + 按钮(88) + padding(40) = 176rpx加上 safe-area
::v-deep .uni-indexed-list {
bottom: calc(180rpx + env(safe-area-inset-bottom)) !important;
}
.bottom-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #fff;
border-top: 1rpx solid #eee;
padding: 20rpx 32rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
box-sizing: border-box;
z-index: 100;
.selected-info {
margin-bottom: 20rpx;
.selected-text {
font-size: 28rpx;
color: #666;
}
}
.action-buttons {
display: flex;
gap: 24rpx;
.btn {
flex: 1;
height: 88rpx;
border-radius: 44rpx;
display: flex;
align-items: center;
justify-content: center;
.btn-text {
font-size: 32rpx;
font-weight: 500;
}
&.btn-cancel {
background-color: #f5f5f5;
.btn-text {
color: #333;
}
}
&.btn-confirm {
background-color: #FF19A0;
.btn-text {
color: #fff;
}
}
}
}
}
</style>

View File

@ -0,0 +1,375 @@
<template>
<view class="franchise-container">
<scroll-view class="scroll-content" scroll-y>
<view class="form-wrapper">
<view class="form-card">
<!-- 姓名 -->
<view class="form-item">
<view class="form-label-row">
<text class="required-star">*</text>
<text class="form-label">姓名</text>
</view>
<input class="form-input" v-model="formData.name" placeholder="填写真实姓名" placeholder-style="color: #999;" />
</view>
<!-- 联系电话 -->
<view class="form-item">
<view class="form-label-row">
<text class="required-star">*</text>
<text class="form-label">联系电话</text>
</view>
<input class="form-input" v-model="formData.phone" placeholder="请输入" placeholder-style="color: #999;"
type="number" />
</view>
<!-- 微信号 -->
<view class="form-item">
<text class="form-label">微信号</text>
<input class="form-input" v-model="formData.wechatId" placeholder="请输入" placeholder-style="color: #999;" />
</view>
<!-- 邮箱号 -->
<view class="form-item">
<text class="form-label">邮箱号</text>
<input class="form-input" v-model="formData.email" placeholder="请输入" placeholder-style="color: #999;" />
</view>
<!-- 意向城市 -->
<view class="form-item">
<view class="form-label-row">
<text class="required-star">*</text>
<text class="form-label">意向城市</text>
</view>
<view class="form-selector" @click="selectCities">
<text class="selector-text" :class="{ 'placeholder': !formData.city }">
{{ formData.city || '请选择' }}
</text>
<image v-if="!formData.city" class="arrow-icon" :src="imgPrefix + 'right-arrow.png'" mode="aspectFit" />
</view>
</view>
</view>
<!-- 备注 -->
<view class="form-card">
<text class="section-title">备注</text>
<textarea class="form-textarea" v-model="formData.remarks" placeholder="请输入备注"
placeholder-style="color: #999;" :maxlength="500" />
</view>
<!-- 注意事项 -->
<view class="form-card">
<text class="section-title">注意事项</text>
<view class="notice-content">
<text class="notice-item">(1) 此窗体仅限申请加盟意愿用,谢谢您的配合</text>
<text class="notice-item">(2) 第一阶段书面审核将于5个工作天内回复,请留意邮箱及垃圾信件</text>
<text class="notice-item">(3) 为保护个资,未通过之书面审核数据将定期销毁</text>
</view>
</view>
</view>
</scroll-view>
<!-- 底部按钮 -->
<view class="bottom-buttons">
<view class="btn btn-clear" @click="clearForm">
<text class="btn-text">清除</text>
</view>
<view class="btn btn-submit" @click="submitForm">
<text class="btn-text">送出</text>
</view>
</view>
</view>
</template>
<script>
import { imgPrefix } from '@/utils/common';
import { franchiseApply } from '@/api/franchise';
export default {
name: 'Franchise',
data() {
return {
imgPrefix,
formData: {
name: '',
phone: '',
wechatId: '',
email: '',
city: '',
selectedCityNames: [],
remarks: ''
}
};
},
methods: {
// 选择城市
selectCities() {
const selectedCities = this.formData.selectedCityNames || [];
uni.navigateTo({
url: `/pageHome/franchise/city-select?selectedCities=${encodeURIComponent(JSON.stringify(selectedCities))}`,
events: {
citySelected: (data) => {
this.formData.selectedCityNames = data.cities;
this.formData.city = data.cityNames;
}
}
});
},
clearForm() {
uni.showModal({
title: '提示',
content: '确定要清除所有表单数据吗?',
success: (res) => {
if (res.confirm) {
this.formData = {
name: '',
phone: '',
wechatId: '',
email: '',
city: '',
selectedCityNames: [],
remarks: ''
};
uni.showToast({
title: '已清除',
icon: 'success'
});
}
}
});
},
async submitForm() {
// 验证必填项
if (!this.formData.name.trim()) {
uni.showToast({
title: '请输入姓名',
icon: 'none'
});
return;
}
if (!this.formData.phone.trim()) {
uni.showToast({
title: '请输入联系电话',
icon: 'none'
});
return;
}
if (!this.formData.city) {
uni.showToast({
title: '请选择意向城市',
icon: 'none'
});
return;
}
try {
uni.showLoading({
title: '提交中...'
});
const submitData = {
name: this.formData.name.trim(),
phone: this.formData.phone.trim(),
email: this.formData.email.trim(),
Preferredcity: this.formData.city.replace(/、/g, ','),
weChatID: this.formData.wechatId.trim(),
remarks: this.formData.remarks.trim()
};
await franchiseApply(submitData);
uni.hideLoading();
uni.showToast({
title: '提交成功',
icon: 'success'
});
// 提交成功后返回上一页
setTimeout(() => {
uni.navigateBack();
}, 1500);
} catch (error) {
uni.hideLoading();
uni.showToast({
title: error.message || '提交失败,请重试',
icon: 'none'
});
}
}
}
};
</script>
<style lang="scss" scoped>
.franchise-container {
width: 100%;
height: 100vh;
background-color: #ffecf3;
display: flex;
flex-direction: column;
}
.scroll-content {
flex: 1;
height: 100%;
padding-bottom: 180rpx;
}
.form-wrapper {
padding: 20rpx;
}
.form-card {
background-color: #fff;
border-radius: 16rpx;
padding: 32rpx;
margin-bottom: 20rpx;
&:last-child {
margin-bottom: 0;
}
}
.form-item {
display: flex;
justify-content: space-between;
margin-bottom: 32rpx;
&:last-child {
margin-bottom: 0;
}
}
.form-label-row {
display: flex;
align-items: center;
gap: 4rpx;
margin-bottom: 16rpx;
}
.form-label {
font-size: 28rpx;
color: #333;
}
.required-star {
font-size: 28rpx;
color: #FF19A0;
}
.form-input {
height: 80rpx;
font-size: 28rpx;
color: #333;
box-sizing: border-box;
text-align: right;
}
.form-selector {
height: 80rpx;
display: flex;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
text-align: right;
flex: 1;
}
.selector-text {
font-size: 28rpx;
color: #333;
flex: 1;
}
.selector-text.placeholder {
color: #999;
}
.arrow-icon {
width: 10rpx;
height: 18rpx;
flex-shrink: 0;
margin-left: 8rpx;
}
.section-title {
font-size: 32rpx;
font-weight: 500;
color: #333;
display: block;
margin-bottom: 20rpx;
}
.form-textarea {
width: 100%;
min-height: 200rpx;
background-color: #f5f5f5;
border-radius: 8rpx;
padding: 20rpx 24rpx;
font-size: 28rpx;
color: #333;
box-sizing: border-box;
}
.notice-content {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.notice-item {
font-size: 24rpx;
color: #666;
line-height: 1.6;
}
.bottom-buttons {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
gap: 24rpx;
padding: 20rpx 32rpx;
background: #fff;
box-shadow: 0px -2rpx 10rpx rgba(0, 0, 0, 0.05);
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
z-index: 100;
}
.btn {
flex: 1;
height: 88rpx;
border-radius: 44rpx;
display: flex;
align-items: center;
justify-content: center;
}
.btn-text {
font-size: 32rpx;
font-weight: 500;
}
.btn-clear {
background: #fff;
border: 2rpx solid #FF19A0;
.btn-text {
color: #FF19A0;
}
}
.btn-submit {
background: #FF19A0;
.btn-text {
color: #fff;
}
}
</style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,526 @@
<template>
<view class="reservation-container">
<view class="body-container">
<scroll-view class="scroll-view" :scroll-y="true">
<view class="order-tab-list">
<view class="order-type-item" @click.stop="selectOrderType(ORDER_TYPE_RESERVATION)">
<text class="order-type-text fs-34"
:class="{'order-type-active-text': orderType === ORDER_TYPE_RESERVATION}">
预约单
</text>
<view class="select-line" :class="{'active-line': orderType === ORDER_TYPE_RESERVATION}"/>
</view>
<view class="order-type-item" @click.stop="selectOrderType(ORDER_TYPE_SITE)">
<text class="order-type-text fs-34"
:class="{'order-type-active-text': orderType === ORDER_TYPE_SITE}">
现场单
</text>
<view class="select-line" :class="{'active-line': orderType === ORDER_TYPE_SITE}"/>
</view>
</view>
<view class="pet-type-tab-view">
<view class="pet-type-item-view" @click.stop="selectPetType(PET_TYPE_CAT)">
<view class="pet-type-item"
:class="{'pet-type-active-item': selectedPetType === PET_TYPE_CAT}">
<image :src="selectedPetType === PET_TYPE_CAT ? 'https://activity.wagoo.live/cat_active.png' : 'https://activity.wagoo.live/cat.png' " class="pet-typ-img" mode="aspectFit"/>
<text class="fs-32 app-fc-main"
:class="{'app-fc-white app-font-bold-700': selectedPetType === PET_TYPE_CAT}">
</text>
</view>
<view class="pet-type-triangle" :class="{'pet-type-active-triangle': selectedPetType === PET_TYPE_CAT}"/>
</view>
<view class="pet-type-item-view" @click.stop="selectPetType(PET_TYPE_DOG)">
<view class="pet-type-item"
:class="{'pet-type-active-item': selectedPetType === PET_TYPE_DOG}">
<image :src="selectedPetType === PET_TYPE_DOG ? 'https://activity.wagoo.live/dog_active.png' : 'https://activity.wagoo.live/dog.png'" class="pet-typ-img" mode="aspectFit"/>
<text class="fs-32 app-fc-main"
:class="{'app-fc-white app-font-bold-700': selectedPetType === PET_TYPE_DOG}">
</text>
</view>
<view class="pet-type-triangle" :class="{'pet-type-active-triangle': selectedPetType === PET_TYPE_DOG}"/>
</view>
</view>
<view class="reservation-info-container app-padding-pageHorizontal">
<!-- 其他内容 -->
<view class="content-section">
<info-cell key="pet" cell-type="text" :info-icon="require('@/static/images/dog.png')" title="选择宠物"
:info="petInfo.name || ''" @clickAction="goToSelectPet"/>
<info-cell key="weight" cell-type="text" :info-icon="require('@/pageHome/static/weight.png')"
title="宠物重量区间" :info="petInfo.weight_name" :is-can-click="false"/>
<info-cell v-if="selectedPetType === PET_TYPE_CAT" key="mofa" cell-type="text"
:info-icon="require('@/static/images/dog.png')" title="宠物毛发" :info="hair"
:is-can-click="false"/>
<info-cell v-if="orderType === ORDER_TYPE_RESERVATION" key="time" cell-type="time" :info-icon="require('@/pageHome/static/time.png')"
title="选择预约时间"
:info="Object.keys(reservationTime).length > 0 ? `${reservationTime.dateLabel} ${reservationTime.start}-${reservationTime.end}` :''"
@clickAction="isShowTime = true"/>
<info-cell key="address" cell-type="address" :info-icon="require('@/pageHome/static/address.png')"
title="选择服务地址" :address-info="address" @clickAction="isShowAddress = true"/>
<info-cell key="park" cell-type="park" title="停车状况" :park-state="parkState"
:other-park-state="otherParkState"
@changeParkState="changeParkState" @changeOtherParkState="changeOtherParkState"/>
<view class="price-view">
<image v-if="petWeight.desc" @click.stop="isShowPriceDes=true" src="/pageHome/static/tips.png" mode="aspectFit"
class="tip-img"/>
<text @click.stop="isShowPriceDes=true" class="app-fc-main fs-28">预估</text>
<text class="app-fc-main fs-52 app-font-bold-700 price-text">{{ price || '0.00' }}</text>
<text class="app-fc-main fs-28"></text>
</view>
</view>
<view class="submit-btn" @click.stop="paymentConfirm">
<text class="app-fc-white fs-28 app-font-bold-500">下一步</text>
</view>
</view>
<!-- 广告 -->
<view class="ad-container">
<view class="ad-view" v-if="selectedPetType === PET_TYPE_CAT" v-html="catHtmlData"/>
<view class="ad-view" v-else v-html="dogHtmlData"/>
</view>
</scroll-view>
</view>
<select-address-modal
v-if="isShowAddress"
:select-address="address"
@close="isShowAddress = false"
@changeAddress="changeAddress"
/>
<select-weight-modal
v-if="isShowWeight"
:pet-weight="petWeight"
:weight-list="weightList"
@close="isShowWeight = false"
@changeWeight="changeWeight"
/>
<select-reservation-time-modal
v-if="isShowTime"
:select-time="reservationTime"
@close="isShowTime = false"
@changeReservationTime="changeReservationTime"
/>
<price-description-modal
v-if="isShowPriceDes"
:price-info="petWeight"
@close="isShowPriceDes = false"
/>
</view>
</template>
<script>
import InfoCell from "@/pageHome/components/info-cell.vue";
import SelectAddressModal from "@/pageHome/components/select-address-modal.vue";
import SelectWeightModal from "@/components/petOrder/select-weight-modal.vue";
import SelectReservationTimeModal from "@/pageHome/components/select-reservation-time-modal.vue";
import { getWeightList } from "@/api/common";
import {
ARTICLE_TYPE_RESERVATION_CAT,
ARTICLE_TYPE_RESERVATION_DOG,
ORDER_TYPE_RESERVATION,
ORDER_TYPE_SITE,
PET_HAIR_LONG,
PET_TYPE_CAT,
PET_TYPE_DOG
} from "@/constants/app.business";
import { getArticleDetail } from "@/api/article";
import appConfig from '@/constants/app.config';
import { getCityIsOpen } from '@/api/order';
import PriceDescriptionModal from "@/pageHome/components/price-description-modal.vue";
export default {
components: {
PriceDescriptionModal,
SelectReservationTimeModal,
SelectWeightModal,
SelectAddressModal,
InfoCell
},
data() {
return {
PET_TYPE_CAT,
PET_TYPE_DOG,
ORDER_TYPE_RESERVATION,
ORDER_TYPE_SITE,
orderType: ORDER_TYPE_RESERVATION,
selectedPetType: PET_TYPE_CAT, // 默认选中“猫”
petInfo: {},
parkState: '',
otherParkState: '',
allWeightList: [],
petWeight: {},
price: '',
reservationTime: {},
address: null,
isShowAddress: false,
isShowWeight: false,
isShowTime: false,
catHtmlData: '',
dogHtmlData: '',
isShowPriceDes: false
}
},
mounted() {
this.initData();
},
computed: {
weightList() {
return this.allWeightList.filter((item) => item.type === this.selectedPetType);
},
hair() {
if (Object.keys(this.petInfo).length > 0) {
if (this.selectedPetType === PET_TYPE_CAT) {
return `${this.petInfo.maofa}` === `${PET_HAIR_LONG}` ? '长毛' : '短毛';
} else {
return '';
}
} else {
return '';
}
}
},
methods: {
initData() {
getArticleDetail(ARTICLE_TYPE_RESERVATION_CAT).then((res) => {
this.catHtmlData = this.processHtmlContent(res?.info?.content || '');
})
getArticleDetail(ARTICLE_TYPE_RESERVATION_DOG).then((res) => {
this.dogHtmlData = this.processHtmlContent(res?.info?.content || '');
})
getWeightList().then((res) => {
this.allWeightList = res?.info || [];
})
},
processHtmlContent(html) {
return html.replace(/max-width/g, 'width');
},
selectOrderType(orderType) {
if (this.orderType === orderType) {
return;
}
this.reservationTime = {};
this.parkState = '';
this.address = null;
this.orderType = orderType;
},
selectPetType(petType) {
if (this.selectedPetType === petType) {
return;
}
this.petInfo = {};
this.parkState = '';
this.petWeight = {};
this.price = '';
this.reservationTime = {};
this.address = null;
this.selectedPetType = petType;
},
changeParkState(state) {
this.parkState = state;
this.otherParkState = '';
},
changeOtherParkState(state) {
this.otherParkState = state;
},
goToSelectPet() {
const selectPetInfo = this.petInfo && Object.keys(this.petInfo).length > 0
? encodeURIComponent(JSON.stringify(this.petInfo))
: '';
uni.navigateTo({
url: `/pageHome/selectPet/index?petType=${this.selectedPetType}${selectPetInfo ? `&selectPetInfo=${selectPetInfo}` : ''}`,
events: {
changePet: (pet) => {
this.changePet(pet);
}
}
});
},
changePet(item) {
if (!item || item.chongwu_id === this.petInfo.chongwu_id) {
return
}
this.petInfo = item;
this.selectedPetType = item.type;
if (item.weight_id || item.weight_id === 0) {
this.petWeight = this.allWeightList.find((w) => w.weight_id === item.weight_id) || {}
this.price = this.petWeight.price || '';
}
},
changeAddress(address) {
// 若当前地址未开通服务,则不允许选择
getCityIsOpen(address.qu_id)
.then(() => {
this.address = address;
this.isShowAddress = false;
})
.catch((err) => {
uni.showToast({
title: "暂未开通该区域的预约服务",
icon: "none",
});
});
},
changeWeight(weight) {
this.petWeight = weight;
this.price = this.petWeight.price || '';
this.isShowWeight = false;
},
changeReservationTime(timeData) {
this.reservationTime = timeData;
this.isShowTime = false;
},
paymentConfirm() {
if (!this.petInfo.chongwu_id) {
uni.showToast({
title: '请选择宠物',
icon: 'none'
})
return;
}
if (Object.keys(this.reservationTime).length === 0 && this.orderType === ORDER_TYPE_RESERVATION) {
uni.showToast({
title: '请选择预约时间',
icon: 'none'
})
return;
}
if (!this.address) {
uni.showToast({
title: '请选择服务地址',
icon: 'none'
})
return;
}
if (!this.parkState) {
uni.showToast({
title: '请选择停车状况',
icon: 'none'
})
return;
}
if (this.parkState === '其他' && !this.otherParkState) {
uni.showToast({
title: '请输入停车信息',
icon: 'none'
})
return;
}
uni.navigateTo({
url: '/pageHome/reservation/payment-confirm-page',
success: (res) => {
res.eventChannel.emit('reservationInfo', {
petInfo: this.petInfo,
parkState: this.otherParkState || this.parkState,
petWeight: this.petWeight,
reservationTime: this.reservationTime,
address: this.address,
price: this.price,
orderType: this.orderType,
})
},
events: {
clearData: () => {
this.selectedPetType = PET_TYPE_CAT;
this.petInfo = {};
this.parkState = '';
this.petWeight = {};
this.price = '';
this.reservationTime = {};
this.address = null;
}
}
})
}
},
onShareAppMessage(res) {
return {
title: appConfig.appShareName,
path: '/pages/client/index/index'
}
},
}
</script>
<style lang="scss" scoped>
.reservation-container {
display: flex;
flex: 1;
flex-direction: column;
width: 100%;
height: 100%;
box-sizing: border-box;
background-color: #F7F8FA;
.body-container {
display: flex;
flex-direction: column;
flex: 1;
box-sizing: border-box;
position: relative;
.scroll-view {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
.order-tab-list {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
background-color: #fff;
.order-type-item {
display: flex;
flex: 1;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20rpx 0;
box-sizing: border-box;
.select-line {
margin-top: 14rpx;
width: 24rpx;
height: 10rpx;
background-color: transparent;
border-radius: 5rpx;
}
.active-line {
background-color: #FE019B;
}
}
.order-type-text {
color: #AFA5AE
}
.order-type-active-text {
color: #272427
}
}
.pet-type-tab-view {
display: flex;
flex-direction: row;
align-items: center;
padding: 42rpx 0 24rpx 0;
box-sizing: border-box;
.pet-type-item-view {
display: flex;
flex: 1;
flex-direction: column;
align-items: center;
justify-content: center;
.pet-type-item {
width: 300rpx;
height: 104rpx;
background-color: #fff;
border-radius: 40rpx;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
.pet-typ-img {
width: 80rpx;
height: 80rpx;
margin-right: 32rpx;
}
}
.pet-type-active-item {
background-color: #FE019B;
}
.pet-type-triangle {
width: 0;
height: 0;
border-left: 20rpx solid transparent;
border-right: 20rpx solid transparent;
border-top: 20rpx solid transparent;
}
.pet-type-active-triangle {
border-top: 20rpx solid #FE019B;
}
}
}
.reservation-info-container {
display: flex;
flex-direction: column;
width: 100%;
box-sizing: border-box;
.content-section {
display: flex;
flex-direction: column;
padding: 20rpx 36rpx 36rpx;
box-sizing: border-box;
background-color: #fff;
border-radius: 40rpx;
overflow: hidden;
.price-view {
margin-top: 36rpx;
width: 100%;
height: 164rpx;
display: flex;
align-items: center;
justify-content: center;
background-color: #FFEEF6;
border-radius: 40rpx;
.tip-img {
width: 28rpx;
height: 28rpx;
margin-right: 8rpx;
}
.price-text {
margin-left: 16rpx;
margin-right: 10rpx;
}
}
}
.submit-btn {
margin-top: 40rpx;
margin-bottom: 90rpx;
margin-left: 5%;
width: 90%;
height: 80rpx;
border-radius: 40rpx;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
background-color: $app_color_main;
box-shadow: 12rpx 16rpx 40rpx 0 rgba(240, 135, 228, 0.52);
}
}
.ad-container {
width: 100%;
padding: 0;
box-sizing: border-box;
display: flex;
flex-direction: column;
.ad-view {
width: 100%;
}
}
}
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,434 @@
<template>
<view class="address-container" :class="{ 'empty-background': addressList.length === 0 && !isLoading }">
<!-- 地址列表 -->
<scroll-view class="address-list" scroll-y>
<view v-if="!isLoading && addressList.length">
<view v-for="(item, index) in addressList" :key="item.id" class="address-item"
@click="selectAddress(item)">
<!-- 地址信息 -->
<view class="address-info">
<view class="address-header">
<text class="address-name">{{ item.recipient_name }}</text>
<text class="address-phone">{{ item.phone }}</text>
<view v-if="item.is_default" class="default-tag">默认</view>
</view>
<view class="address-detail">{{ getAddressText(item) }}</view>
</view>
<!-- 底部操作栏 -->
<view class="address-actions">
<!-- 左侧默认地址开关 -->
<view class="action-left" @click.stop="() => { }">
<switch class="custom-switch" v-model="item.is_default"
@change="(val) => handleSwitchChange(val, item, index)" color="#FF19A0" :checked="item.is_default"></switch>
<text class="default-text">设为默认地址</text>
</view>
<!-- 右侧编辑和删除 -->
<view class="action-right">
<view class="action-btn" @click.stop="goToEditAddress(item)">
<image class="img" :src="`${imgPrefix}mall-pen.png`"></image>
<text class="action-text">编辑</text>
</view>
<view class="action-btn" @click.stop="deleteAddress(item, index)">
<image class="img" :src="`${imgPrefix}mall-remove.png`"></image>
<text class="action-text">删除</text>
</view>
</view>
</view>
</view>
</view>
<!-- 加载中 -->
<view v-if="isLoading" class="loading-view">
<uni-load-more status="loading" :show-text="false" />
</view>
<!-- 空状态 -->
<view v-if="addressList.length === 0 && !isLoading" class="empty-state">
<image :src="`${imgPrefix}norAddress.png`" class="empty-img"></image>
<view class="empty-text">
暂无地址信息
</view>
<view class="empty-btn" @click="gotoAddAddress">
去添加地址
</view>
</view>
</scroll-view>
<!-- 底部添加按钮 -->
<view class="footer" v-if="addressList.length > 0">
<view class="footerBtn" @click="gotoAddAddress">新增地址</view>
<view class="bottom-safe-area"></view>
</view>
</view>
</template>
<script>
import {
getAddressList,
deleteAddress,
updateAddress
} from "@/api/address";
import {
imgPrefix
} from "@/utils/common.js";
export default {
data() {
return {
imgPrefix,
addressList: [],
isLoading: true,
typeSelect: false, // 从订单页跳转过来选择地址的标识
selectAddressId: "", // 选中的地址id
};
},
computed: {
userInfo() {
return this.$store.state?.user?.userInfo || {};
}
},
onLoad(options) {
// 如果有传入的选中地址,可以在这里处理
const { typeSelect, addressId } = options;
this.typeSelect = typeSelect;
this.selectAddressId = addressId;
},
onShow() {
// 从编辑页面返回时刷新列表
this.isLoading = true;
this.getData();
},
methods: {
selectAddress(address) {
// 通过 eventChannel 传递数据回原页面
const eventChannel = this.getOpenerEventChannel && this.getOpenerEventChannel();
if (eventChannel) {
eventChannel.emit('changeAddress', address);
}
// 同时使用 uni.$emit 方式,兼容原有监听方式
uni.$emit('selectAddress', address);
uni.navigateBack();
},
handleSwitchChange(val, address) {
// 如果打开开关,设置该地址为默认地址,并取消其他地址的默认状态
this.setDefaultAddressRequest(val.detail.value,address);
},
setDefaultAddressRequest(val,address) {
const userInfo = this.userInfo;
const addressParam = {
user_id: userInfo.userID,
id: address.id || address.address_id,
recipient_name: address.recipient_name || address.name,
phone: address.phone,
is_default: val, // 设置为默认地址
province: address.province || address.sheng,
province_id: address.province_id || address.provinceId || address.sheng_id,
city: address.city || address.shi,
city_id: address.city_id || address.cityId || address.shi_id,
district: address.district || address.area || address.qu,
district_id: address.district_id || address.areaId || address.qu_id,
full_address: address.full_address || address.address,
region_id: 1
};
uni.showLoading({ title: "处理中..." });
updateAddress(addressParam).then(() => {
uni.hideLoading();
uni.showToast({
title: '设置成功',
icon: 'success'
});
// 刷新列表
this.isLoading = true;
this.getData();
}).catch((err) => {
uni.hideLoading();
console.error('设置默认地址失败:', err);
// 恢复开关状态
address.is_default = !val;
uni.showToast({
title: err?.msg || err?.message || '设置失败,请稍后重试',
icon: 'none'
});
});
},
deleteAddress(address, index) {
uni.showModal({
title: '提示',
content: '确定要删除该地址吗?',
success: (res) => {
if (res.confirm) {
// 调用删除接口
this.deleteAddressRequest(address.id, index);
}
}
});
},
deleteAddressRequest(addressId, index) {
uni.showLoading({
title: '删除中...'
});
deleteAddress({
user_id: this.userInfo.userID,
id: addressId
}).then(() => {
uni.hideLoading();
// 删除成功后,从列表中移除
this.addressList.splice(index, 1);
uni.showToast({
title: '删除成功',
icon: 'success'
});
}).catch((err) => {
uni.hideLoading();
uni.showToast({
title: err?.message || '删除失败',
icon: 'none'
});
});
},
goToEditAddress(item) {
uni.navigateTo({
url: `/pages/client/address/edit?id=${item?.id || ""}`,
events: {
refreshAddress: () => {
this.isLoading = true;
this.getData();
},
},
});
},
gotoAddAddress() {
uni.navigateTo({
url: `/pages/client/address/edit?isAdd=1`,
events: {
refreshAddress: () => {
this.isLoading = true;
this.getData();
},
},
});
},
getData() {
getAddressList({
user_id: this.userInfo.userID
}).then((res) => {
let list = (res?.data || []).map(v => ({
...v,
is_default: !!v.is_default, // 确保默认地址开关状态正确
}));
this.addressList = list;
this.isLoading = false;
}).catch(() => {
this.isLoading = false;
});
},
getAddressText(item) {
// 组合地址信息:省市区 + 详细地址
const region = [item.province, item.city, item.district].filter(Boolean).join('');
return region ? `${region}${item.full_address}` : item.full_address;
}
},
};
</script>
<style lang="scss" scoped>
.address-container {
height: 100vh;
overflow: hidden;
background-color: #F7F8FA;
display: flex;
flex-direction: column;
&.empty-background {
background-color: #fff;
}
}
.address-list {
flex: 1;
padding: 20rpx 20rpx 240rpx;
box-sizing: border-box;
height: 100%;
}
.loading-view {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx 0;
}
.address-item {
display: flex;
flex-direction: column;
background-color: #fff;
border-radius: 16rpx;
padding: 20rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.04);
position: relative;
}
.address-info {
flex: 1;
margin-bottom: 20rpx;
.address-header {
display: flex;
align-items: center;
margin-bottom: 10rpx;
flex-wrap: wrap;
.address-name {
font-size: 28rpx;
font-weight: 600;
color: #333333;
margin-right: 16rpx;
}
.address-phone {
font-size: 28rpx;
color: #666262;
margin-right: 16rpx;
}
.default-tag {
display: inline-flex;
align-items: center;
justify-content: center;
background-color: #ffecf3;
color: #FF19A0;
font-size: 20rpx;
padding: 4rpx 8rpx;
border-radius: 20rpx;
line-height: 1;
}
}
.address-detail {
font-size: 28rpx;
color: #666262;
word-break: break-all;
}
}
.address-actions {
display: flex;
align-items: center;
justify-content: space-between;
padding-top: 20rpx;
border-top: 1rpx solid #F0F0F0;
}
.action-left {
display: flex;
align-items: center;
transform: translateX(-10px);
}
.default-text {
font-size: 24rpx;
color: #666262;
margin-left: 0;
}
.action-right {
display: flex;
align-items: center;
gap: 32rpx;
}
.action-btn {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 8rpx;
.action-icon {
width: 24rpx;
height: 24rpx;
}
.action-text {
font-size: 26rpx;
color: #666666;
}
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 200rpx 0;
.empty-img {
width: 320rpx;
height: 320rpx;
}
.empty-text {
margin-top: 24rpx;
font-size: 24rpx;
color: #9B939A;
}
.empty-btn {
background-color: #ff19a0;
color: #fff;
width: calc(100vw - 208rpx);
margin: auto;
margin-top: 40rpx;
padding: 32rpx 0rpx;
text-align: center;
border-radius: 80rpx;
font-size: 32rpx;
}
}
.footer {
background-color: #fff;
border-radius: 32rpx 32rpx 0 0;
padding: 12rpx 24rpx 0;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.05);
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 10;
.footerBtn {
background-color: #ff19a0;
color: #fff;
width: calc(100vw - 48rpx);
margin: auto;
margin-bottom: 24rpx;
padding: 32rpx 0rpx;
text-align: center;
border-radius: 80rpx;
font-size: 32rpx;
}
.bottom-safe-area {
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
}
.img {
width: 24rpx;
height: 24rpx;
margin-right: 8rpx;
}
.custom-switch {
transform: scale(0.7)
}
</style>

View File

@ -0,0 +1,482 @@
<template>
<view :class="petList.length === 0 ? 'chosePetContainer' : 'listContainer'">
<!-- 无宠物占位 -->
<view class="notPetWrapper" v-if="!isLoading && petList.length === 0">
<image :src="`${imgPrefix}record-placeholder.png`" mode="widthFix" class="notPetImg"></image>
<view class="tips">未添加过宠物</view>
<view class="addPetBtn" @click="gotoAddPet">去添加宠物</view>
</view>
<!-- 列表 -->
<view class="petListview" v-else>
<view v-if="isLoading" class="loading-view">
<uni-load-more status="loading" :show-text="false" />
</view>
<scroll-view v-else class="pet-scroll" scroll-y :style="{ height: scrollViewHeight }">
<view v-for="(item, index) in petList" :key="item.id" @click="changePet(item)"
:class="['petItemView', { petItemViewActive: selectPetInfo.id === item.id, itemMarginTop: index > 0 }]">
<view class="leftView">
<image class="petImg" :src="item.avatar || `${imgPrefix}record_avator.png`" mode="aspectFill" />
<view class="petContent">
<view class="petInfoView">
<view class="petName fs-28">{{ item.name }}</view>
<image class="petGenderImg "
:src="`${imgPrefix}${item.gender == 'male' ? 'record-maleImg.png' : 'record-femaleImg.png'}`"></image>
</view>
<view class="petVariety fs-24">{{ item.breed_name || '' }}</view>
</view>
</view>
<view class="rightView">
<view class="editView" @click.stop="toDetails(item)">
<image class="img" :src="`${imgPrefix}mall-pen.png`"></image>
<view class="fs-24" style="color: #3d3d3d;">编辑</view>
</view>
<!-- <view class="removeView" @click.stop="deletePet(item)">
<image class="img" :src="`${imgPrefix}mall-remove.png`"></image>
<view class="fs-24" style="color: #3d3d3d;">删除</view>
</view> -->
</view>
</view>
</scroll-view>
</view>
<!-- 底部按钮 -->
<view class="footer" v-if="petList.length !== 0">
<view class="footerBtn" @click="gotoAddPet">添加宠物</view>
<view class="bottom-safe-area"></view>
</view>
</view>
</template>
<script>
import { getPetList, switchPetArchive } from "@/api/common";
import { deletePet } from "@/api/record";
import appConfig from '@/constants/app.config';
import { imgPrefix } from '@/utils/common.js';
import { PET_TYPE_CAT, PET_TYPE_DOG } from '@/constants/app.business';
export default {
data() {
return {
isLoading: true,
petList: [],
appConfig,
imgPrefix,
petType: 0,
selectPetInfo: {},
from: '', // 来源标记,用于判断是否从宠物档案页面进入
serviceType: '', // 服务类型feeding(上门喂宠) 或 walking(上门遛宠)
scrollViewHeight: 'calc(100vh - 200rpx)' // 默认高度
};
},
onLoad(options) {
// 从页面参数获取 petType 和 selectPetInfo
if (options.petType) {
this.petType = parseInt(options.petType);
}
if (options.selectPetInfo) {
try {
this.selectPetInfo = JSON.parse(decodeURIComponent(options.selectPetInfo));
} catch (e) {
console.error('解析 selectPetInfo 失败:', e);
}
}
// 获取来源标记
if (options.from) {
this.from = options.from;
}
// 获取服务类型
if (options.serviceType) {
this.serviceType = options.serviceType;
}
// 计算 scroll-view 的精确高度
this.calculateScrollViewHeight();
this.getData();
},
onShow() {
// 重新计算 scroll-view 高度(底部按钮区域可能会根据宠物列表显示/隐藏)
this.calculateScrollViewHeight();
// 页面显示时刷新列表(从编辑页面返回时会触发)
// 先移除监听,避免重复注册
uni.$off('refreshPetList');
// 使用 uni.$on 监听全局刷新事件
uni.$on('refreshPetList', () => {
this.getData();
});
},
onUnload() {
// 页面卸载时移除事件监听
uni.$off('refreshPetList');
},
computed: {
userInfo() {
return this.$store.state?.user?.userInfo || {};
}
},
methods: {
calculateScrollViewHeight() {
this.$nextTick(() => {
try {
// 获取系统信息
const systemInfo = uni.getSystemInfoSync();
const windowHeight = systemInfo.windowHeight; // 窗口高度px
// 使用查询选择器获取 footer 的实际高度
const query = uni.createSelectorQuery().in(this);
query.select('.footer').boundingClientRect((rect) => {
if (rect) {
// rect.height 是 px 单位,需要转换为 rpx
// 屏幕宽度通常是 750rpx对应 windowWidth px
const pixelRatio = 750 / systemInfo.windowWidth; // rpx 与 px 的转换比例
const footerHeightRpx = rect.height * pixelRatio;
// 窗口高度转换为 rpx
const windowHeightRpx = windowHeight * pixelRatio;
// 计算 scroll-view 高度rpx
// 减去 footer 高度和 petListview 的 padding-top (20rpx)
const scrollViewHeightRpx = windowHeightRpx - footerHeightRpx - 40;
// 设置高度
this.scrollViewHeight = `${scrollViewHeightRpx}rpx`;
} else {
// 如果底部按钮区域不存在(无宠物时),使用全屏高度减去 padding
const pixelRatio = 750 / systemInfo.windowWidth;
const windowHeightRpx = windowHeight * pixelRatio;
this.scrollViewHeight = `${windowHeightRpx - 40}rpx`;
}
}).exec();
} catch (e) {
console.error('计算 scroll-view 高度失败:', e);
// 失败时使用默认值
const systemInfo = uni.getSystemInfoSync();
const pixelRatio = 750 / systemInfo.windowWidth;
const windowHeightRpx = systemInfo.windowHeight * pixelRatio;
// 默认减去 200rpx底部按钮区域 + 安全区域)
this.scrollViewHeight = `${windowHeightRpx - 200}rpx`;
}
});
},
changePet(pet) {
// 如果是从宠物档案页面进入的,需要调用切换接口
if (this.from === 'petProfile') {
uni.showLoading({
title: '切换中...',
mask: true
});
switchPetArchive({ pet_id: pet.id })
.then((res) => {
uni.hideLoading();
if (res.code === 0 || res.result === 'success') {
uni.showToast({
title: '切换成功',
icon: 'success'
});
// 通过 eventChannel 通知上一页刷新
const eventChannel = this.getOpenerEventChannel && this.getOpenerEventChannel();
if (eventChannel) {
eventChannel.emit('switchPetSuccess');
}
setTimeout(() => {
uni.navigateBack();
}, 200);
} else {
uni.showToast({
title: res.msg || res.message || '切换失败',
icon: 'none'
});
}
})
.catch((err) => {
uni.hideLoading();
uni.showToast({
title: err?.msg || err?.message || err || '切换失败,请稍后重试',
icon: 'none'
});
});
} else {
// 如果有服务类型,先验证宠物类型
if (this.serviceType) {
const petType = pet.type || pet.pet_type;
let isValid = true;
if (this.serviceType === 'feeding') {
// 上门喂宠只能选择猫
if (Number(petType) !== PET_TYPE_CAT) {
uni.showToast({
title: '上门喂宠服务只能选择猫',
icon: 'none',
duration: 2000
});
isValid = false;
}
} else if (this.serviceType === 'walking') {
// 上门遛宠只能选择狗
if (Number(petType) !== PET_TYPE_DOG) {
uni.showToast({
title: '上门遛宠服务只能选择狗',
icon: 'none',
duration: 2000
});
isValid = false;
}
}
// 如果验证失败,不返回
if (!isValid) {
return;
}
}
// 通过 eventChannel 返回数据,避免直接访问上一页实例
const eventChannel = this.getOpenerEventChannel && this.getOpenerEventChannel();
if (eventChannel) {
eventChannel.emit('changePet', pet);
}
// 验证通过后,延迟返回,让提示框有时间显示(如果是成功的情况)
setTimeout(() => {
uni.navigateBack();
}, 100);
}
},
getData() {
getPetList(this.userInfo.userID).then((res) => {
let list = res?.data || [];
this.petList = list;
this.isLoading = false;
// 数据更新后重新计算 scroll-view 高度(底部按钮区域会根据 petList.length 显示/隐藏)
this.calculateScrollViewHeight();
}).catch(() => {
this.isLoading = false;
})
},
gotoAddPet() {
uni.navigateTo({
url: `/pages/client/record/edit?type=${this.petType}&typeId=aaa`,
events: {
addPetSuccess: () => {
// 不需要在这里调用 getData因为 record/edit.vue 已经触发了 refreshPetList 全局事件
// this.isLoading = true;
// this.getData();
},
},
})
},
toDetails(data) {
uni.navigateTo({
url: `/pages/client/record/edit?id=${data?.id || ""
}&typeId=bbb`,
events: {
addPetSuccess: () => {
// 不需要在这里调用 getData因为 record/edit.vue 已经触发了 refreshPetList 全局事件
// this.getData();
},
},
});
},
deletePet(item) {
uni.showModal({
title: '提示',
content: '确定要删除该宠物吗?',
success: (res) => {
if (res.confirm) {
uni.showLoading({
title: '删除中...'
});
deletePet(item.id, this.userInfo.userID)
.then(() => {
uni.hideLoading();
uni.showToast({
title: '删除成功',
icon: 'success'
});
// 刷新列表
this.getData();
})
.catch((err) => {
uni.hideLoading();
uni.showToast({
title: err?.message || '删除失败',
icon: 'none'
});
});
}
}
});
},
}
}
</script>
<style lang="scss" scoped>
.listContainer {
width: 100%;
height: 100vh;
background-color: #ffecf3;
}
.chosePetContainer {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: 100vh;
background-color: #fff;
}
.notPetWrapper {
text-align: center;
.notPetImg {
width: 380rpx;
height: 320rpx;
}
.tips {
margin-top: 22rpx;
margin-bottom: 40rpx;
font-size: 24rpx;
color: #9B939A;
}
.addPetBtn {
padding: 32rpx 0rpx;
width: calc(100vw - 208rpx);
background-color: #ff19a0;
color: #fff;
text-align: center;
border-radius: 80rpx;
font-size: 32rpx;
}
}
.petListview {
padding: 20rpx 0 0;
height: 100vh;
box-sizing: border-box;
.loading-view {
display: flex;
align-items: center;
justify-content: center;
padding-top: 200rpx;
}
.pet-scroll {
width: 100%;
}
.petItemView {
display: flex;
align-items: center;
background-color: #fff;
border-radius: 16rpx;
width: calc(100vw - 40rpx);
margin: auto;
padding: 24rpx 20rpx;
justify-content: space-between;
box-sizing: border-box;
.leftView {
display: flex;
.petImg {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
}
.petContent {
margin-left: 16rpx;
.petInfoView {
display: flex;
align-items: center;
.petName {
font-size: 28rpx;
font-weight: 500;
}
.petGenderImg {
width: 28rpx;
height: 28rpx;
margin-left: 8rpx;
}
}
.petVariety {
font-size: 24rpx;
color: #666262;
}
}
}
.select-icon {
width: 36rpx;
height: 36rpx;
}
.rightView {
display: flex;
align-items: center;
font-size: 24rpx;
color: #666262;
.img {
width: 24rpx;
height: 24rpx;
margin-right: 8rpx;
}
.editView {
// margin-right: 50rpx;
display: flex;
align-items: center;
}
.removeView {
display: flex;
align-items: center;
}
}
}
.petItemViewActive {
border: 1px solid #ff19a0;
}
}
.footer {
background-color: #fff;
border-radius: 32rpx 32rpx 0px 0px;
position: absolute;
bottom: 0;
left: 0;
width: 100%;
box-shadow: 0 -10rpx 30rpx rgba(0, 0, 0, 0.04);
.footerBtn {
color: #fff;
background-color: #FF19A0;
width: calc(100% - 48rpx);
margin: auto;
margin-bottom: 24rpx;
margin-top: 12rpx;
border-radius: 100px;
text-align: center;
padding: 32rpx 0rpx;
}
.bottom-safe-area {
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
}
.itemMarginTop {
margin-top: 16rpx !important;
}
</style>

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