Browse Source

feat: 新增图片和语音订单功能,优化API调用和OSS上传

- 新增图片订单功能,支持拍照、上传、识别和下单
- 新增语音订单功能,支持录音、上传、识别和下单
- 优化API调用方式,统一使用this.$api进行接口调用
- 优化OSS上传功能,增加上传进度显示和错误处理
- 更新README文档,详细说明项目结构和核心模块
- 修复导航栏和底部tabbar的样式问题
- 优化登录和用户信息获取逻辑
master
前端-胡立永 5 days ago
parent
commit
034297c31d
24 changed files with 1914 additions and 418 deletions
  1. +6
    -0
      .cursor/rules/md.mdc
  2. +385
    -13
      README.md
  3. +30
    -38
      api/api.js
  4. +21
    -80
      api/http.js
  5. +28
    -0
      api/model/index.js
  6. +34
    -0
      api/model/login.js
  7. +25
    -8
      components/base/navbar.vue
  8. +72
    -80
      components/base/tabbar.vue
  9. +2
    -2
      config.js
  10. +4
    -0
      main.js
  11. +1
    -1
      manifest.json
  12. +61
    -0
      mixins/configList.js
  13. +70
    -0
      mixins/order.js
  14. +1
    -1
      pages/index/category.vue
  15. +1
    -1
      pages/index/center.vue
  16. +18
    -8
      pages/index/index.vue
  17. +13
    -10
      pages_order/auth/wxLogin.vue
  18. +91
    -13
      pages_order/auth/wxUserInfo.vue
  19. +283
    -4
      pages_order/order/pictureOrder.vue
  20. +504
    -11
      pages_order/order/voiceOrder.vue
  21. +150
    -40
      store/store.js
  22. +0
    -10
      utils/oss-upload/oss/index.js
  23. +0
    -63
      utils/oss-upload/oss/web.js
  24. +114
    -35
      utils/utils.js

+ 6
- 0
.cursor/rules/md.mdc View File

@ -0,0 +1,6 @@
---
description:
globs:
alwaysApply: false
---
请你写代码之前先查看一下README.md文件,按照这个文件中说的写

+ 385
- 13
README.md View File

@ -1,13 +1,385 @@
#酒店桌布小程序
![](./doc/home.png)
![](./doc/home-s.png)
![](./doc/cart.png)
![](./doc/order.png)
![](./doc/purse.png)
![](./doc/center.png)
![](./doc/address.png)
![](./doc/editAddress.png)
![](./doc/category.png)
![](./doc/productDetail.png)
![](./doc/productUnit.png)
# 珠宝商城项目文档
## 项目概述
本项目是一个基于uni-app开发的珠宝商城小程序,采用Vue框架开发,集成了完整的商城功能模块。
## 目录结构
```
├── api # API接口目录
│ ├── api.js # API统一出口
│ ├── http.js # HTTP请求封装
│ └── model # 业务模块API
├── components # 公共组件
├── mixins # 混入文件
├── pages # 页面文件
├── static # 静态资源
├── store # Vuex状态管理
├── utils # 工具函数
└── uni_modules # uni-app插件模块
```
## 分包结构说明
### pages_order分包
分包是小程序优化加载性能的重要手段,pages_order作为独立分包,包含以下模块:
```
├── auth # 认证相关页面
├── components # 分包内公共组件
├── mine # 我的模块
├── order # 订单模块
├── product # 商品模块
└── static # 分包静态资源
```
**分包特点:**
- 静态资源就近原则:分包相关的图片等静态资源存放在分包目录下,避免主包体积过大
- 模块化组织:按功能模块划分目录,便于维护和管理
- 组件复用:分包内的通用组件集中管理,提高代码复用性
## 配置文件说明
### config.js
项目核心配置文件,包含以下配置:
**1. 环境配置**
```javascript
// 当前环境
const type = 'prod'
// 环境配置
const config = {
dev: {
baseUrl: 'http://h5.xzaiyp.top/jewelry-admin',
},
prod: {
baseUrl: 'https://jewelry-admin.hhlm1688.com/jewelry-admin',
}
}
```
**2. 默认配置**
```javascript
const defaultConfig = {
// 腾讯地图Key
mapKey: 'XMBBZ-BCPCV-SXPPQ-5Y7MY-PHZXK-YFFVU',
// 阿里云OSS配置
aliOss: {
url: 'https://image.hhlm1688.com/',
config: {
region: 'oss-cn-guangzhou',
accessKeyId: '***',
accessKeySecret: '***',
bucket: 'hanhaiimage',
endpoint: 'oss-cn-shenzhen.aliyuncs.com',
}
}
}
```
**3. UI框架配置**
```javascript
uni.$uv.setConfig({
config: {
unit: 'rpx' // 设置默认单位
},
})
```
## 核心模块详解
### 1. Mixins 混入
#### 1.1 list.js - 列表数据加载混入
提供列表数据的加载、分页、下拉刷新、上拉加载更多等功能。
**主要功能:**
- 统一的分页参数处理
- 下拉刷新和上拉加载更多
- 数据加载状态管理
**使用示例:**
```javascript
// 在页面中使用list混入
import listMixin from '@/mixins/list.js'
export default {
mixins: [listMixin],
data() {
return {
// 指定API接口
mixinsListApi: 'product.list'
}
}
}
```
#### 1.2 configList.js - 全局配置混入
已全局引入的配置管理混入,无需手动引入即可使用。
**主要功能:**
- 统一的分享配置
- 全局配置管理
- 用户信息关联
**配置参数:**
```javascript
// 分享配置示例
this.Gshare.title = '分享标题'
this.Gshare.path = '分享路径'
```
### 2. API 模块
#### 2.1 http.js - 请求封装
统一的HTTP请求处理,包含:
- 请求拦截器
- 响应拦截器
- 统一的错误处理
- Token管理
#### 2.2 api.js - 接口管理
统一管理API接口,支持模块化组织。API模块采用分层结构,便于维护和扩展。
**目录结构:**
```
api/
├── api.js # API统一出口
├── http.js # HTTP请求封装
└── model/ # 业务模块API
├── product.js # 商品相关接口
├── order.js # 订单相关接口
└── user.js # 用户相关接口
```
**接口定义示例:**
```javascript
// api/model/product.js
export default {
// GET请求示例
list: {
url: '/api/product/list',
method: 'GET',
loading: true // 显示加载提示
},
// POST请求示例
create: {
url: '/api/product/create',
method: 'POST',
loading: true // 显示加载提示
auth : true,//效验登录
debounce : 1000,//接口防抖,1s
limit : 500,//接口限流,0.5s
},
}
```
**调用接口示例:**
```javascript
// 第一种写法:callback方式处理响应
this.$api('product.list', {
pageNo: 1,
pageSize: 10,
categoryId: '123'
}, res => {
// 处理列表数据
})
// 第二种写法:Promise方式处理响应
this.$api('product.create', {
name: '商品名称',
price: 99.99,
description: '商品描述'
}).then(res => {
if (res.code === 200) {
// 创建成功
uni.showToast({ title: '创建成功' })
}
})
```
### 3. 公共代码
#### 3.1 工具函数 (utils)
- authorize.js: 授权处理
- pay.js: 微信网页支付相关
- utils.js: 通用工具函数
- timeUtils.js: 时间处理
- position.js: 定位与位置计算
- oss-upload: 阿里云OSS上传模块
**使用示例:**
```javascript
// 授权处理
async preservationImg(img) {
await this.$authorize('scope.writePhotosAlbum')
//在执行$authorize之后,await下面的代码都是确保授权完成的情况下执行
},
// 时间格式化
const formattedTime = this.$timeUtils.formatTime(new Date())
// 微信网页支付调用
import { wxPay } from '@/utils/pay'
wxPay(orderData)
```
#### 3.2 公共组件
- navbar.vue: 自定义导航栏
- tabbar.vue: 底部导航栏
- productItem.vue: 商品列表项
**使用示例:**
```html
<template>
<view>
<navbar title="商品列表" />
<product-item
v-for="item in list"
:key="item.id"
:product="item"
/>
</view>
</template>
```
#### 3.3 OSS上传模块
**配置说明:**
项目使用阿里云OSS进行文件存储,相关配置位于config.js中:
**使用示例:**
1. 单文件上传
```javascript
export default {
methods: {
onUpload(file) {
this.$Oss.ossUpload(file.path).then(url => {
this.filePath = url
})
}
}
}
```
2. 在uv-upload组件中使用
```html
<template>
<uv-upload
:fileList="fileList"
@afterRead="afterRead"
@delete="deleteImage"
name="1"
multiple
:maxCount="maxCount"
></uv-upload>
</template>
<script>
export default {
data() {
return {
fileList: [],
maxCount: 9
}
},
methods: {
// 新增图片
afterRead(e) {
e.file.forEach(file => {
this.$Oss.ossUpload(file.url).then(url => {
this.fileList.push({
url
})
})
})
},
// 删除图片
deleteImage(e) {
this.fileList.splice(e.index, 1)
},
}
}
</script>
```
**注意事项:**
1. 上传前请确保OSS配置正确
2. 建议对上传文件大小进行限制
3. 支持的文件类型:图片、视频、文档等
4. 上传失败时会抛出异常,请做好错误处理
## 最佳实践
### 1. 列表页面开发
```javascript
// pages/product/list.vue
import listMixin from '@/mixins/list.js'
export default {
mixins: [listMixin],
data() {
return {
mixinsListApi: 'product.list',
}
},
methods: {
// 分类切换
onCategoryChange(categoryId) {
this.queryParams.categoryId = categoryId
this.getData()
}
}
}
```
### 2. 详情页面开发
```javascript
// pages/product/detail.vue
import configMixin from '@/mixins/configList.js'
export default {
mixins: [configMixin],
data() {
return {
productId: '',
detail: {}
}
},
onLoad(options) {
this.productId = options.id
this.getDetail()
},
methods: {
getDetail() {
this.$api('product.detail', {
id: this.productId
}, res => {
this.detail = res.result
// 设置分享信息
this.Gshare.title = this.detail.name
this.Gshare.path = `/pages/product/detail?id=${this.productId}`
})
}
}
}
```
## 注意事项
1. 使用mixins时注意命名冲突
2. API调用建议统一使用this.$api方式
3. 页面开发建议继承相应的混入来复用通用功能
## 常见问题
1. 列表加载失败
- 检查mixinsListApi是否正确配置
- 确认网络请求是否正常
- 查看请求参数格式是否正确

+ 30
- 38
api/api.js View File

@ -1,9 +1,12 @@
import http from './http.js'
import utils from '../utils/utils.js'
let limit = {}
let debounce = {}
const models = ['login', 'index']
const config = {
// 示例
// wxLogin : {url : '/api/wxLogin', method : 'POST',
@ -11,44 +14,16 @@ const config = {
// limit : 1000
// },
getConfig : {url : '/api/getConfig', method : 'GET', limit : 500},
// 微信登录接口
wxLogin: {
url: '/login/login',
method: 'POST',
limit : 500,
showLoading : true,
},
// 修改个人信息接口
updateInfo: {
url: '/info/updateInfo',
method: 'POST',
auth: true,
limit : 500,
showLoading : true,
},
//隐私政策
getPrivacyPolicy: {
url: '/login/getPrivacyPolicy',
method: 'GET',
},
//用户协议
getUserAgreement: {
url: '/login/getUserAgreement',
method: 'GET',
},
getConfig : {url : '/config_common/getConfig', method : 'GET', limit : 500},
}
export function api(key, data, callback, loadingTitle) {
let req = config[key]
if (!req) {
console.error('无效key' + key);
return
return Promise.reject()
}
if (typeof callback == 'string') {
@ -65,7 +40,7 @@ export function api(key, data, callback, loadingTitle) {
let storageKey = req.url
let storage = limit[storageKey]
if (storage && new Date().getTime() - storage < req.limit) {
return
return Promise.reject()
}
limit[storageKey] = new Date().getTime()
}
@ -73,11 +48,9 @@ export function api(key, data, callback, loadingTitle) {
//必须登录
if (req.auth) {
if (!uni.getStorageSync('token')) {
uni.navigateTo({
url: '/pages_order/auth/wxLogin'
})
console.error('需要登录')
return
utils.toLogin()
console.error('需要登录', req.url)
return Promise.reject()
}
}
@ -101,13 +74,32 @@ export function api(key, data, callback, loadingTitle) {
loadingTitle || req.showLoading, loadingTitle || req.loadingTitle)
}, req.debounce)
return
return Promise.reject()
}
http.http(req.url, data, callback, req.method,
return http.http(req.url, data, callback, req.method,
loadingTitle || req.showLoading, loadingTitle || req.loadingTitle)
}
function addApiModel(model, key){
for(let k in model){
if(config[`${k}`]){
console.error(`重名api------model=${key},key=${k}`);
uni.showModal({
title: `重名api`,
content: `model=${key},key=${k}`
})
continue
}
config[`${k}`] = model[k]
// config[`${key}_${k}`] = model[k]
}
}
models.forEach(key => {
addApiModel(require(`./model/${key}.js`).default, key)
})
export default api

+ 21
- 80
api/http.js View File

@ -1,6 +1,7 @@
import Vue from 'vue'
import utils from '../utils/utils.js'
import store from '../store/store.js'
function http(uri, data, callback, method = 'GET', showLoading, title) {
@ -10,16 +11,23 @@ function http(uri, data, callback, method = 'GET', showLoading, title) {
});
}
let reject, resolve;
let promise = new Promise((res, rej) => {
reject = rej
resolve = res
})
uni.request({
url: Vue.prototype.$config.baseUrl + uri,
data: enhanceData(data),
data,
method: method,
header: {
'X-Access-Token': uni.getStorageSync('token'),
'Content-Type' : method == 'POST' ? 'application/x-www-form-urlencoded' : 'application/json'
'Content-Type' : 'application/x-www-form-urlencoded'
},
success: (res) => {
// console.log(res,'res')
if(showLoading){
uni.hideLoading();
}
@ -27,14 +35,13 @@ function http(uri, data, callback, method = 'GET', showLoading, title) {
if(res.statusCode == 401 ||
res.data.message == '操作失败,token非法无效!' ||
res.data.message == '操作失败,用户不存在!'){
uni.removeStorageSync('token')
store.commit('logout')
console.error('登录过期');
uni.navigateTo({
url: '/pages_order/auth/wxLogin'
})
utils.toLogin()
}
if(res.statusCode == 200 && res.data.code != 200){
if(res.statusCode == 200 && res.data.code != 200
&& res.data.code != 902){
uni.showToast({
mask: true,
duration: 1000,
@ -43,10 +50,12 @@ function http(uri, data, callback, method = 'GET', showLoading, title) {
});
}
callback(res.data)
callback && callback(res.data)
resolve(res.data)
},
fail: () => {
reject('api fail')
uni.showLoading({})
setTimeout(()=>{
uni.hideLoading()
@ -58,79 +67,11 @@ function http(uri, data, callback, method = 'GET', showLoading, title) {
}
}
});
return promise
}
function deleted(uri, data, callback) {
http(uri, data, callback, 'DELETE')
}
function post(uri, data, callback) {
http(uri, data, callback, 'POST')
}
function get(uri, data, callback) {
http(uri, data, callback, 'GET')
}
function enhanceData(data) {
const userid = uni.getStorageSync("userid")
if (!data) {
data = {}
}
if (userid) {
data.userid = userid
}
return data
}
function sync(method, uri, data) {
return new Promise((resolve, reject) => {
uni.request({
url: uri,
data: data,
method: method,
header: {
'auth': '1AS9F1HPC4FBC9EN00J7KX2L5RJ99XHZ'
},
success: (res) => {
resolve(res.data)
},
fail: (err) => {
reject(err);
}
})
})
}
let cache = null
function async (method, uri, data) {
const promise = sync(method, uri, data).then(res => {
cache = res
}).catch(err => {
})
}
function syncHttp(uri, data, method = 'GET') {
async (method, uri, data)
}
export default {
http: http,
delete: deleted,
post: post,
get: get,
syncHttp: syncHttp
}

+ 28
- 0
api/model/index.js View File

@ -0,0 +1,28 @@
// 登录相关接口
const api = {
// 获取banner
getBanner: {
url: '/index/getBanner',
method: 'GET',
},
// 获取菜单
getIcon: {
url: '/index/getIcon',
method: 'GET',
},
// 获取商品详情
getProductDetail: {
url: '/index/getProductDetail',
method: 'GET',
},
// 获取商品列表
getProductList: {
url: '/index/getProductList',
method: 'GET',
},
}
export default api

+ 34
- 0
api/model/login.js View File

@ -0,0 +1,34 @@
// 登录相关接口
const api = {
// 微信登录接口
wxLogin: {
url: '/all_login/appletLogin',
method: 'GET',
limit : 500,
},
// 获取绑定手机号码
bindPhone: {
url: '/login_common/bindPhone',
method: 'GET',
auth: true,
},
// 修改个人信息接口
updateInfo: {
url: '/info_common/updateInfo',
method: 'POST',
auth: true,
limit : 500,
showLoading : true,
},
// 获取个人信息
getInfo: {
url: '/info_common/getInfo',
method: 'GET',
auth: true,
},
}
export default api

+ 25
- 8
components/base/navbar.vue View File

@ -2,27 +2,33 @@
<!-- <view class="navbar"
:style="{backgroundColor : bgColor}"> -->
<view class="title"
:style="{backgroundColor : bgColor, color}">
:style="{backgroundColor : bgColor,color}">
<view class="left">
<uv-icon name="home"
v-if="leftClick && length == 1"
@click="toHome"
:color="color" size="46rpx"></uv-icon>
<uv-icon name="arrow-left"
v-if="leftClick"
v-else-if="leftClick"
@click="$emit('leftClick')"
:color="color" size="46rpx"></uv-icon>
:color="color" size="46rpx"></uv-icon>
</view>
<view>{{ title }}</view>
<view class="icon">
<uv-icon name="search"
v-if="isSearch"
:color="color" size="58rpx"></uv-icon>
:color="color" size="58rpx"></uv-icon>
<uv-icon name="plus-circle" :color="color"
<uv-icon name="plus-circle" :color="color"
v-if="isPlus"
@click="plusCircleShow = true"
size="46rpx" style="margin-left: 30rpx;"></uv-icon>
<view v-if="moreClick" style="margin-left: 30rpx;">
<uv-icon name="more-dot-fill" :color="color"
<uv-icon name="more-dot-fill" :color="color"
v-if="!moreText"
@click="moreClick()"
size="46rpx"></uv-icon>
@ -65,7 +71,9 @@
bgColor : {
default : '#fff'
},
color : '#333',
color : {
default : '#333'
}
},
created() {
},
@ -73,9 +81,18 @@
},
data() {
return {
length : getCurrentPages().length
};
},
methods : {
toHome(){
if(this.length != 1){
return
}
uni.reLaunch({
url: '/pages/index/index'
})
}
}
}
</script>
@ -99,7 +116,7 @@
justify-content: center;
font-size: 32rpx;
align-items: center;
z-index: 99999;
z-index: 999;
.left{
position: absolute;
left: 40rpx;


+ 72
- 80
components/base/tabbar.vue View File

@ -1,19 +1,14 @@
<template>
<view class="">
<view class="tabbar-box"></view>
<view class="tabbar-box">
<view class="tabbar">
<view
:class="{item : true, active : select == index}"
v-for="(item, index) in list"
:key="index"
v-if="!item.isNotShop || !userShop"
@click="toPath(item, index)">
<view class="icon">
<image :src="select == index ?
<view :class="{ 'tabbar-active' : select == item.key}" v-for="(item, index) in list" :key="index"
v-if="!item.isNotShop || !userShop" @click="toPath(item, index)" class="tabbar-item">
<view class="tabbar-icon">
<image :src="select == item.key ?
item.selectedIconPath :
item.iconPath" class="icon-image" mode=""></image>
item.iconPath" class="tabbar-icon-image" mode="aspectFill"></image>
</view>
<view class="title">
<view class="tabbar-title">
{{ item.title }}
</view>
</view>
@ -22,56 +17,47 @@
</template>
<script>
import { mapGetters } from 'vuex'
import {
mapGetters
} from 'vuex'
export default {
name:"tabbar",
props : ['select'],
computed : {
name: "tabbar",
props: ['select'],
computed: {
...mapGetters(['userShop']),
},
data() {
return {
list : [
{
list: [{
"selectedIconPath": "/static/image/tabbar/home-a.png",
"iconPath": "/static/image/tabbar/home.png",
"pagePath": "/pages/index/index",
"title": "首页"
"title": "首页",
key: 'home',
},
{
"selectedIconPath": "/static/image/tabbar/category-a.png",
"iconPath": "/static/image/tabbar/category.png",
"pagePath": "/pages/index/category",
"title": "分类",
"title": "商品列表",
key: 'category',
},
// {
// "selectedIconPath": "/static/image/tabbar/order-a.png",
// "iconPath": "/static/image/tabbar/order.png",
// "pagePath": "/pages/index/order",
// "title": ""
// },
// {
// "selectedIconPath": "/static/image/tabbar/cart-a.png",
// "iconPath": "/static/image/tabbar/cart.png",
// "pagePath": "/pages/index/cart",
// "title": "",
// isNotShop : true,
// },
{
"selectedIconPath": "/static/image/tabbar/center-a.png",
"iconPath": "/static/image/tabbar/center.png",
"pagePath": "/pages/index/center",
"title": "个人中心"
"title": "我的",
key: 'center',
}
]
};
},
methods : {
toPath(item, index){
if(index == this.select){
methods: {
toPath(item, index) {
if (item.key == this.select) {
return
}
uni.redirectTo({
uni.reLaunch({
url: item.pagePath
})
},
@ -80,49 +66,55 @@
</script>
<style scoped lang="scss">
.tabbar-box{
height: 120rpx;
padding-bottom: env(safe-area-inset-bottom);
}
.tabbar{
position: fixed;
width: 750rpx;
background-color: #fff;
display: flex;
justify-content: center;
align-items: center;
flex-direction: row;
height: 120rpx;
padding-bottom: env(safe-area-inset-bottom);
z-index: 999999;
bottom: 0;
left: 0;
color: #BCBCBC;
.item{
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.icon{
width: 54rpx;
height: 54rpx;
.icon-image{
width: 54rpx;
height: 54rpx;
.tabbar-box {
height: 120rpx;
padding-bottom: env(safe-area-inset-bottom);
.tabbar {
position: fixed;
width: 750rpx;
background-color: #fff;
display: flex;
justify-content: center;
align-items: center;
flex-direction: row;
height: 120rpx;
padding-bottom: env(safe-area-inset-bottom);
z-index: 999999;
bottom: 0;
left: 0;
color: #BCBCBC;
.tabbar-item {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.tabbar-icon {
width: 54rpx;
height: 54rpx;
.tabbar-icon-image {
width: 54rpx;
height: 54rpx;
}
}
.tabbar-title {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
-o-text-overflow: ellipsis;
font-size: 23rpx;
line-height: 35rpx;
}
}
.tabbar-active {
color: $uni-color !important;
}
}
.title{
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
-o-text-overflow: ellipsis;
font-size: 23rpx;
line-height: 35rpx;
}
}
}
.active{
color: $uni-color !important;
}
</style>

+ 2
- 2
config.js View File

@ -13,10 +13,10 @@ const type = 'dev'
// 环境配置
const config = {
dev : {
baseUrl : 'http://www.gcosc.fun:82',
baseUrl : 'http://h5.xzaiyp.top/building-admin',
},
prod : {
baseUrl : 'http://xxx.xxx.xxx/xxx',
baseUrl : 'http://h5.xzaiyp.top/building-admin',
}
}


+ 4
- 0
main.js View File

@ -14,6 +14,10 @@ import store from '@/store/store'
import './config'
import './utils/index.js'
import mixinConfigList from '@/mixins/configList.js'
Vue.mixin(mixinConfigList)
//组件注册
import configPopup from '@/components/config/configPopup.vue'
import navbar from '@/components/base/navbar.vue'


+ 1
- 1
manifest.json View File

@ -52,7 +52,7 @@
"quickapp" : {},
/* */
"mp-weixin" : {
"appid" : "wxe7ae8cbe1673834c",
"appid" : "wx328ba180b4a88d49",
"setting" : {
"urlCheck" : false
},


+ 61
- 0
mixins/configList.js View File

@ -0,0 +1,61 @@
import { mapState } from 'vuex'
export default {
data() {
return {
// 默认的全局分享内容
Gshare: {
// title: '三只青蛙',
path: '/pages_order/auth/wxLogin', // 全局分享的路径,比如 首页
// imageUrl: '/static/image/login/logo.png', // 全局分享的图片(可本地可网络)
}
}
},
computed: {
...mapState(['configList', 'userInfo', 'riceInfo']),
currentPagePath() {
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];
let path = `/${currentPage.route}`;
// 获取当前页面的参数
const options = currentPage.options;
if (options && Object.keys(options).length > 0) {
const params = this.$utils.objectToUrlParams(options);
path += `?${params}`;
}
return path;
},
},
// 定义全局分享
// 1.发送给朋友
onShareAppMessage(res) {
let o = {
title : this.configList.logo_name,
...this.Gshare,
}
if(this.userInfo.id){
if(this.Gshare.path.includes('?')){
o.path += '&shareId=' + this.userInfo.id
}else{
o.path += '?shareId=' + this.userInfo.id
}
}
return o
},
//2.分享到朋友圈
onShareTimeline(res) {
let o = {
...this.Gshare,
title : this.configList.logo_name,
}
if(this.userInfo.id){
o.path = this.Gshare.path + '?shareId=' + this.userInfo.id
}
return o
},
methods: {
}
}

+ 70
- 0
mixins/order.js View File

@ -0,0 +1,70 @@
export default {
data() {
return {
}
},
computed: {
},
methods: {
// 立即支付
toPayOrder(item){
let api = ''
// if([0, 1].includes(item.shopState)){
// api = 'createOrderTwo'
// }else{
api = 'createSumOrderAgain'
// }
this.$api(api, {
orderId : item.id,
addressId : item.addressId
}, res => {
if(res.code == 200){
uni.requestPaymentWxPay(res)
.then(res => {
uni.showToast({
title: '支付成功',
icon: 'none'
})
this.getData()
}).catch(n => {
this.getData()
})
}
})
},
// 确认收货
confirmOrder(item){
uni.showModal({
title: '您收到货了吗?',
success : e => {
if(e.confirm){
this.$api('confirmOrder', {
orderId : item.id,
}, res => {
this.getData()
})
}
}
})
},
// 取消订单
cancelOrder(item){
uni.showModal({
title: '确认取消订单吗?',
success : e => {
if(e.confirm){
this.$api('cancelOrder', {
orderId : item.id,
}, res => {
this.getData()
})
}
}
})
},
}
}

+ 1
- 1
pages/index/category.vue View File

@ -29,7 +29,7 @@
</uv-vtabs>
</view>
<tabber select="1" />
<tabber select="category" />
</view>
</template>


+ 1
- 1
pages/index/center.vue View File

@ -117,7 +117,7 @@
</view>
</view>
<tabber select="4" />
<tabber select="center" />
</view>
</template>


+ 18
- 8
pages/index/index.vue View File

@ -26,7 +26,7 @@
</view>
<view class="image-home">
<uv-swiper class="uv-swaip" height="350rpx" :list="list" circular bgColor="#ffffff">
<uv-swiper class="uv-swaip" height="350rpx" :list="bannerList" circular bgColor="#ffffff">
</uv-swiper>
</view>
@ -94,16 +94,19 @@
text="立即购买"></uv-button>
</view>
</view>
<PrivacyAgreementPoup />
<tabber select="0" />
<tabber select="home" />
</view>
</template>
<script>
import PrivacyAgreementPoup from '@/components/config/PrivacyAgreementPoup.vue'
import tabber from '@/components/base/tabbar.vue'
import mixinsList from '@/mixins/list.js'
export default {
mixins: [mixinsList],
components: {
tabber,
PrivacyAgreementPoup,
@ -111,17 +114,24 @@ export default {
data() {
return {
keyword: '',
list: [
'https://cdn.uviewui.com/uview/swiper/swiper3.png',
'https://cdn.uviewui.com/uview/swiper/swiper2.png',
'https://cdn.uviewui.com/uview/swiper/swiper1.png',
]
bannerList : [],
mixinsListApi: 'getClassShopPageList',
}
},
computed: {
},
onLoad() {
this.getBanner()
},
methods: {
//
getBanner() {
this.$api('getBanner', res => {
if (res.code == 200) {
this.bannerList = res.result
}
})
},
toUrl() {
uni.navigateTo({
url: '/pages_order/order/fastCreateOrder'


+ 13
- 10
pages_order/auth/wxLogin.vue View File

@ -1,7 +1,7 @@
<template>
<view class="login">
<view class="logo">
<!-- <image src="/static/image/login/logo.png" mode=""></image> -->
<image src="/static/image/login/logo.png" mode=""></image>
</view>
<view class="title">
欢迎使用酒店桌布租赁平台
@ -12,12 +12,12 @@
<image src="../static/auth/wx.png" mode=""></image>
</view>
<view class="">
微信授权登录
授权登录
</view>
</view>
<!-- <view class="btn b2">
使用短信验证登录
</view> -->
<view class="btn b2" @click="qux">
取消登录
</view>
@ -59,7 +59,11 @@
}
},
methods: {
qux(){
uni.reLaunch({
url: '/pages/index/index'
})
},
wxLogin(){
if(!this.checkboxValue.length){
return uni.showToast({
@ -90,11 +94,10 @@
.logo{
height: 140rpx;
width: 140rpx;
background-color: #ddd;
border-radius: 30rpx;
image{
width: 80rpx;
height: 80rpx;
width: 140rpx;
height: 140rpx;
border-radius: 50%;
}
margin-bottom: 20rpx;
}


+ 91
- 13
pages_order/auth/wxUserInfo.vue View File

@ -1,7 +1,10 @@
<template>
<view class="login">
<!-- <view class="logo">
<image :src="configList.logo_image" mode=""></image>
</view> -->
<view class="title">
酒店桌布租赁平台
{{ configList.logo_name }}
</view>
<view class="title">
申请获取你的头像昵称
@ -13,7 +16,7 @@
头像
</view>
<view class="">
<image :src="userInfo.headImage" v-if="userInfo.headImage" style="width: 60rpx;height: 60rpx;"
<image :src="userInfoForm.headImage" v-if="userInfoForm.headImage" style="width: 60rpx;height: 60rpx;"
mode=""></image>
<image src="../static/auth/headImage.png" v-else style="width: 50rpx;height: 50rpx;" mode=""></image>
@ -26,9 +29,34 @@
</view>
<view class="">
<input type="nickname" placeholder="请输入昵称" style="text-align: right;" id="nickName"
v-model="userInfo.nickName" />
v-model="userInfoForm.nickName" />
</view>
</view>
<!-- <view class="line">
<view class="">
手机号
</view>
<view class=""
v-if="userInfoForm.phone">
<input placeholder="请输入手机号" style="text-align: right;"
disabled
v-model="userInfoForm.phone" />
</view>
<view class=""
v-else>
<button
class="getPhoneNumber"
open-type="getPhoneNumber"
@getphonenumber="getPhone">
获取电话号码
</button>
</view>
</view> -->
<view class="btn" @click="submit">
确认
</view>
@ -39,21 +67,43 @@
export default {
data() {
return {
userInfo: {
userInfoForm: {
headImage: '',
nickName: '',
phone : '',
}
};
},
onShow() {},
onLoad() {
// this.userInfoForm.phone = this.userInfo.phone || ''
this.userInfoForm.nickName = this.userInfo.nickName || ''
this.userInfoForm.headImage = this.userInfo.headImage || ''
},
computed: {},
methods: {
onChooseAvatar(res) {
let self = this
self.$Oss.ossUpload(res.target.avatarUrl)
.then(url => {
self.userInfo.headImage = url
})
this.$Oss.ossUpload(res.target.avatarUrl)
.then(url => {
this.userInfoForm.headImage = url
})
},
getPhone(e){
this.$api('bindPhone', {
phoneCode : e.detail.code
}, res => {
if(res.code == 200){
let phoneObj = JSON.parse(res.result)
if(phoneObj.errmsg == 'ok'){
this.userInfoForm.phone = phoneObj.phone_info.phoneNumber
}else{
uni.showModal({
title: phoneObj.errmsg
})
}
console.log(phoneObj);
}
})
},
submit() {
let self = this
@ -65,18 +115,23 @@
})
.exec((res) => {
const nickName = res?.[0]?.value
self.userInfo.nickName = nickName
self.userInfoForm.nickName = nickName
if (self.$utils.verificationAll(self.userInfo, {
if (self.$utils.verificationAll(self.userInfoForm, {
headImage: '请选择头像',
nickName: '请填写昵称',
// phone: '',
})) {
return
}
self.$api('infoUpdateInfo', self.userInfo, res => {
self.$api('updateInfo', {
avatarUrl : self.userInfoForm.headImage,
nickName : self.userInfoForm.nickName,
// phone : self.userInfoForm.phone,
}, res => {
if (res.code == 200) {
uni.switchTab({
uni.reLaunch({
url:'/pages/index/index'
})
}
@ -95,6 +150,16 @@
justify-content: center;
align-items: center;
height: 80vh;
.logo{
height: 140rpx;
width: 140rpx;
image{
height: 140rpx;
width: 140rpx;
border-radius: 30rpx;
}
margin-bottom: 20rpx;
}
.title {
line-height: 45rpx;
@ -129,5 +194,18 @@
border-radius: 15rpx;
margin-top: 10vh;
}
.getPhoneNumber{
// all: unset;
display: flex;
justify-content: center;
align-items: center;
// background: $uni-linear-gradient-btn-color;
background: $uni-color;
color: #fff;
width: 200rpx;
height: 60rpx;
border-radius: 30rpx;
font-size: 24rpx;
}
}
</style>

+ 283
- 4
pages_order/order/pictureOrder.vue View File

@ -10,18 +10,39 @@
<text>拍照下单</text>
</view>
<view class="picture-upload">
<view class="upload-content">
<view class="upload-content" @click="chooseImage" v-if="!imageUrl">
<uv-icon name="camera-fill" color="#D03F25" size="200"></uv-icon>
</view>
<view class="image-preview" v-else>
<image :src="imageUrl" mode="aspectFit" class="preview-img"></image>
<view class="preview-actions">
<view class="action-btn delete" @click="deleteImage">
<uv-icon name="trash" size="40rpx" color="#D03F25"></uv-icon>
<text>删除</text>
</view>
<view class="action-btn retake" @click="chooseImage">
<uv-icon name="camera" size="40rpx" color="#D03F25"></uv-icon>
<text>重拍</text>
</view>
</view>
</view>
<view class="text-upload">
<text>(拍照上传你所需要识别的产品图片)</text>
</view>
</view>
<view class="fast-order">
<view class="picture-button" @click="$utils.redirectTo('/pages_order/order/firmOrder')">
<view class="picture-button" @click="submitPictureOrder" :style="imageUrl ? '' : 'opacity: 0.5;'">
<text>快捷下单</text>
</view>
</view>
<!-- 上传进度指示器 -->
<view class="upload-progress" v-if="isUploading">
<view class="progress-bg">
<view class="progress-bar" :style="{width: uploadProgress + '%'}"></view>
</view>
<text class="progress-text">{{uploadProgress}}%</text>
</view>
</view>
</template>
@ -29,15 +50,197 @@
export default {
data() {
return {
imageUrl: '', //
imageOssUrl: '', // OSS
isUploading: false, //
uploadProgress: 0, //
recognitionResult: null, //
};
}
},
methods: {
//
chooseImage() {
uni.chooseImage({
count: 1, //
sizeType: ['compressed'], //
sourceType: ['camera', 'album'], //
success: (res) => {
//
this.imageUrl = res.tempFilePaths[0];
console.log('已选择图片', this.imageUrl);
},
fail: (err) => {
console.error('选择图片失败', err);
}
});
},
//
deleteImage() {
uni.showModal({
title: '提示',
content: '确定要删除这张图片吗?',
success: (res) => {
if (res.confirm) {
this.imageUrl = '';
this.imageOssUrl = '';
this.recognitionResult = null;
}
}
});
},
//
submitPictureOrder() {
if (!this.imageUrl) {
uni.showToast({
title: '请先拍照上传图片',
icon: 'none'
});
return;
}
if (this.isUploading) {
uni.showToast({
title: '正在上传中,请稍候',
icon: 'none'
});
return;
}
//
uni.showLoading({
title: '图片识别中...'
});
this.isUploading = true;
this.uploadProgress = 0;
//
this.uploadImage();
},
//
uploadImage() {
//
const simulateProgress = () => {
this.uploadProgress = 0;
const interval = setInterval(() => {
this.uploadProgress += 5;
if (this.uploadProgress >= 90) {
clearInterval(interval);
}
}, 100);
return interval;
};
const progressInterval = simulateProgress();
// 使OSS
this.$Oss.ossUpload(this.imageUrl).then(url => {
//
clearInterval(progressInterval);
this.uploadProgress = 100;
this.imageOssUrl = url;
console.log('图片上传成功', url);
//
this.recognizeImage(url);
}).catch(err => {
//
clearInterval(progressInterval);
console.error('图片上传失败', err);
this.handleUploadFailed('图片上传失败,请重试');
});
},
//
recognizeImage(imageUrl) {
// 使API
this.$api('order.recognizeImage', {
imageUrl: imageUrl,
userId: uni.getStorageSync('userId') || ''
}, res => {
//
if (res.code === 0) {
this.recognitionResult = res.data.result;
console.log('图片识别成功', this.recognitionResult);
//
this.processOrder();
} else {
this.handleUploadFailed(res.msg || '图片识别失败');
}
}, err => {
//
console.error('图片识别请求失败', err);
this.handleUploadFailed('网络请求失败,请检查网络连接');
});
},
//
processOrder() {
// 使API
this.$api('order.createFromImage', {
userId: uni.getStorageSync('userId') || '',
imageUrl: this.imageOssUrl,
recognitionResult: this.recognitionResult
}).then(res => {
// Promise
uni.hideLoading();
this.isUploading = false;
if (res.code === 0) {
//
const orderId = res.data.orderId;
//
uni.showToast({
title: '图片下单成功',
icon: 'success',
duration: 1500,
success: () => {
setTimeout(() => {
this.$utils.redirectTo('/pages_order/order/firmOrder?orderId=' + orderId);
}, 1500);
}
});
} else {
uni.showModal({
title: '提示',
content: res.msg || '创建订单失败',
showCancel: false
});
}
}).catch(err => {
//
uni.hideLoading();
this.isUploading = false;
console.error('创建订单请求失败', err);
this.handleUploadFailed('网络请求失败,请检查网络连接');
});
},
//
handleUploadFailed(message) {
uni.hideLoading();
this.isUploading = false;
this.uploadProgress = 0;
uni.showModal({
title: '上传失败',
content: message,
showCancel: false
});
}
}
}
</script>
<style scoped lang="scss">
.hand-top{
background-color: #ffffff;
position: relative;
.picture-top{
color: #333333;
height: 100rpx;
@ -71,6 +274,48 @@
justify-content: center;
margin-top: 60rpx;
}
.image-preview {
width: 680rpx;
height: 400rpx;
background-color: #F4F4F4;
margin: auto;
margin-top: 60rpx;
border-radius: 20rpx;
position: relative;
overflow: hidden;
.preview-img {
width: 100%;
height: 100%;
object-fit: contain;
}
.preview-actions {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 80rpx;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: space-around;
align-items: center;
.action-btn {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 10rpx 30rpx;
text {
color: #fff;
font-size: 24rpx;
margin-top: 6rpx;
}
}
}
}
.text-upload{
height: 100rpx;
text-align: center;
@ -92,5 +337,39 @@
border-radius: 100rpx;
}
}
.upload-progress {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 600rpx;
background-color: rgba(0, 0, 0, 0.7);
border-radius: 20rpx;
padding: 30rpx;
text-align: center;
z-index: 999;
.progress-bg {
width: 100%;
height: 20rpx;
background-color: #eee;
border-radius: 10rpx;
overflow: hidden;
margin-bottom: 20rpx;
.progress-bar {
height: 100%;
background-color: #D03F25;
border-radius: 10rpx;
transition: width 0.2s;
}
}
.progress-text {
color: #fff;
font-size: 28rpx;
}
}
}
</style>

+ 504
- 11
pages_order/order/voiceOrder.vue View File

@ -10,22 +10,35 @@
<text>录制语音下单</text>
</view>
<view class="voice-upload">
<view class="long-speak">
<view class="long-speak"
@touchstart="startRecord"
@touchend="stopRecord"
@touchcancel="cancelRecord"
:class="{'recording': isRecording}">
<uv-icon name="mic" size="45rpx" color="#DC2828"></uv-icon>
<text>长按可说话</text>
<text>{{isRecording ? '松开结束录音' : '长按可说话'}}</text>
</view>
<view class="recording-file">
<view class="recording-status" v-if="isRecording">
<text>正在录音中...</text>
<view class="recording-time">{{recordingTime}}s</view>
</view>
<!-- 录音波形效果 -->
<view class="voice-wave" v-if="isRecording">
<view class="wave-item" v-for="(item, index) in waveItems" :key="index"
:style="{height: item + 'rpx'}"></view>
</view>
<view class="recording-file" v-if="audioPath">
<view class="file">
<image src="../static/order/1.png" mode="" class="record"></image>
<image src="../static/order/2.png" mode="" class="file-start"></image>
<image src="../static/order/3.png" mode="" class="file-delete"></image>
<image src="../static/order/2.png" mode="" class="file-start" @click="playVoice"></image>
<image src="../static/order/3.png" mode="" class="file-delete" @click="deleteVoice"></image>
<view class="file-top">
<p>录音文件01.mp3</p>
<p style="color: #A6ADBA;">12MB</p>
<p>{{audioName}}</p>
<p style="color: #A6ADBA;">{{audioSize}}</p>
</view>
<view class="file-bottom">
<view class="schedule"></view>
<text>100%</text>
<view class="schedule" :style="{width: uploadProgress + '%'}"></view>
<text>{{uploadProgress}}%</text>
</view>
</view>
</view>
@ -34,7 +47,7 @@
</view>
</view>
<view class="fast-order">
<view class="voice-button" @click="$utils.redirectTo('/pages_order/order/firmOrder')">
<view class="voice-button" @click="submitVoiceOrder" :style="audioPath ? '' : 'opacity: 0.5;'">
<text>快捷下单</text>
</view>
</view>
@ -45,8 +58,452 @@
export default {
data() {
return {
recorderManager: null, //
innerAudioContext: null, //
isRecording: false, //
isPlaying: false, //
audioPath: '', //
audioName: '录音文件01.mp3', //
audioSize: '0KB', //
uploadProgress: 100, //
recordStartTime: 0, //
recordTimeout: null, //
recordMaxDuration: 60000, //
recordingTime: 0, //
recordTimer: null, //
isUploading: false, //
recognitionResult: null, //
waveTimer: null, //
waveItems: [], //
audioUrl: '', // URL
};
},
onLoad() {
//
this.recorderManager = uni.getRecorderManager();
//
this.initWaveItems();
//
this.recorderManager.onStart(() => {
console.log('录音开始');
this.isRecording = true;
this.recordStartTime = Date.now();
this.recordingTime = 0;
//
this.recordTimer = setInterval(() => {
this.recordingTime = Math.floor((Date.now() - this.recordStartTime) / 1000);
}, 1000);
//
this.startWaveAnimation();
//
this.recordTimeout = setTimeout(() => {
if (this.isRecording) {
this.stopRecord();
}
}, this.recordMaxDuration);
});
//
this.recorderManager.onStop((res) => {
console.log('录音结束', res);
this.isRecording = false;
if (this.recordTimeout) {
clearTimeout(this.recordTimeout);
this.recordTimeout = null;
}
if (this.recordTimer) {
clearInterval(this.recordTimer);
this.recordTimer = null;
}
//
this.stopWaveAnimation();
if (res.tempFilePath) {
this.audioPath = res.tempFilePath;
//
this.formatFileSize(res.fileSize || 0);
//
this.audioName = '录音文件' + this.formatDate(new Date()) + '.mp3';
//
if (this.recordingTime < 1) {
uni.showToast({
title: '录音时间太短,请重新录制',
icon: 'none'
});
this.audioPath = '';
}
}
});
//
this.recorderManager.onError((err) => {
console.error('录音错误', err);
uni.showToast({
title: '录音失败: ' + err.errMsg,
icon: 'none'
});
this.isRecording = false;
if (this.recordTimeout) {
clearTimeout(this.recordTimeout);
this.recordTimeout = null;
}
if (this.recordTimer) {
clearInterval(this.recordTimer);
this.recordTimer = null;
}
//
this.stopWaveAnimation();
});
//
this.innerAudioContext = uni.createInnerAudioContext();
//
this.innerAudioContext.onEnded(() => {
console.log('播放结束');
this.isPlaying = false;
});
//
this.innerAudioContext.onError((err) => {
console.error('播放错误', err);
uni.showToast({
title: '播放失败',
icon: 'none'
});
this.isPlaying = false;
});
},
onUnload() {
//
if (this.innerAudioContext) {
this.innerAudioContext.destroy();
}
if (this.recordTimeout) {
clearTimeout(this.recordTimeout);
this.recordTimeout = null;
}
if (this.recordTimer) {
clearInterval(this.recordTimer);
this.recordTimer = null;
}
//
this.stopWaveAnimation();
},
methods: {
//
initWaveItems() {
const itemCount = 16; //
this.waveItems = Array(itemCount).fill(10); // 10rpx
},
//
startWaveAnimation() {
//
this.stopWaveAnimation();
//
this.waveTimer = setInterval(() => {
const newWaveItems = [];
for (let i = 0; i < this.waveItems.length; i++) {
// 10-60
newWaveItems.push(Math.floor(Math.random() * 50) + 10);
}
this.waveItems = newWaveItems;
}, 100);
},
//
stopWaveAnimation() {
if (this.waveTimer) {
clearInterval(this.waveTimer);
this.waveTimer = null;
}
this.initWaveItems(); //
},
//
startRecord() {
if (this.isRecording) return;
const options = {
duration: this.recordMaxDuration, //
sampleRate: 44100, //
numberOfChannels: 1, //
encodeBitRate: 192000, //
format: 'mp3', //
frameSize: 50 //
};
//
this.recorderManager.start(options);
//
uni.vibrateShort({
success: function () {
console.log('震动成功');
}
});
},
//
stopRecord() {
if (!this.isRecording) return;
this.recorderManager.stop();
//
uni.vibrateShort({
success: function () {
console.log('震动成功');
}
});
},
//
cancelRecord() {
if (!this.isRecording) return;
this.recorderManager.stop();
this.audioPath = ''; //
uni.showToast({
title: '录音已取消',
icon: 'none'
});
},
//
playVoice() {
if (!this.audioPath) {
uni.showToast({
title: '没有可播放的录音',
icon: 'none'
});
return;
}
if (this.isPlaying) {
//
this.innerAudioContext.stop();
this.isPlaying = false;
return;
}
//
this.innerAudioContext.src = this.audioPath;
this.innerAudioContext.play();
this.isPlaying = true;
//
setTimeout(() => {
if (this.isPlaying) {
this.isPlaying = false;
}
}, this.recordingTime * 1000 + 500); // 500ms
},
//
deleteVoice() {
if (!this.audioPath) return;
uni.showModal({
title: '提示',
content: '确定要删除这段录音吗?',
success: (res) => {
if (res.confirm) {
//
if (this.isPlaying) {
this.innerAudioContext.stop();
this.isPlaying = false;
}
this.audioPath = '';
this.audioName = '录音文件01.mp3';
this.audioSize = '0KB';
this.recordingTime = 0;
this.audioUrl = '';
uni.showToast({
title: '录音已删除',
icon: 'none'
});
}
}
});
},
//
submitVoiceOrder() {
if (!this.audioPath) {
uni.showToast({
title: '请先录制语音',
icon: 'none'
});
return;
}
if (this.isUploading) {
uni.showToast({
title: '正在上传中,请稍候',
icon: 'none'
});
return;
}
//
uni.showLoading({
title: '语音识别中...'
});
this.isUploading = true;
this.uploadProgress = 0;
//
this.uploadAudioFile();
},
//
uploadAudioFile() {
//
const simulateProgress = () => {
this.uploadProgress = 0;
const interval = setInterval(() => {
this.uploadProgress += 5;
if (this.uploadProgress >= 90) {
clearInterval(interval);
}
}, 100);
return interval;
};
const progressInterval = simulateProgress();
// 使OSS
this.$Oss.ossUpload(this.audioPath).then(url => {
//
clearInterval(progressInterval);
this.uploadProgress = 100;
this.audioUrl = url;
console.log('音频上传成功', url);
//
this.recognizeVoice(url);
}).catch(err => {
//
clearInterval(progressInterval);
console.error('音频上传失败', err);
this.handleUploadFailed('音频上传失败,请重试');
});
},
//
recognizeVoice(fileUrl) {
// 使API
this.$api('order.recognizeVoice', {
fileUrl: fileUrl,
userId: uni.getStorageSync('userId') || ''
}, res => {
//
if (res.code === 0) {
this.recognitionResult = res.data.result;
console.log('语音识别成功', this.recognitionResult);
//
this.processOrder();
} else {
this.handleUploadFailed(res.msg || '语音识别失败');
}
}, err => {
//
console.error('语音识别请求失败', err);
this.handleUploadFailed('网络请求失败,请检查网络连接');
});
},
//
processOrder() {
// 使API
this.$api('order.createFromVoice', {
userId: uni.getStorageSync('userId') || '',
audioUrl: this.audioUrl,
recognitionResult: this.recognitionResult
}).then(res => {
// Promise
uni.hideLoading();
this.isUploading = false;
if (res.code === 0) {
//
const orderId = res.data.orderId;
//
uni.showToast({
title: '语音下单成功',
icon: 'success',
duration: 1500,
success: () => {
setTimeout(() => {
this.$utils.redirectTo('/pages_order/order/firmOrder?orderId=' + orderId);
}, 1500);
}
});
} else {
uni.showModal({
title: '提示',
content: res.msg || '创建订单失败',
showCancel: false
});
}
}).catch(err => {
//
uni.hideLoading();
this.isUploading = false;
console.error('创建订单请求失败', err);
this.handleUploadFailed('网络请求失败,请检查网络连接');
});
},
//
handleUploadFailed(message) {
uni.hideLoading();
this.isUploading = false;
this.uploadProgress = 0;
uni.showModal({
title: '上传失败',
content: message,
showCancel: false
});
},
//
formatFileSize(size) {
if (size < 1024) {
this.audioSize = size + 'B';
} else if (size < 1024 * 1024) {
this.audioSize = (size / 1024).toFixed(2) + 'KB';
} else {
this.audioSize = (size / (1024 * 1024)).toFixed(2) + 'MB';
}
},
//
formatDate(date) {
const pad = (n) => n < 10 ? '0' + n : n;
return pad(date.getMonth() + 1) + pad(date.getDate());
}
}
}
</script>
@ -83,7 +540,43 @@
align-items: center;
justify-content: center;
border-radius: 100rpx;
&.recording {
background-color: rgba(220, 40, 40, 0.1);
border: 1rpx solid #DC2828;
}
}
.recording-status {
width: 85%;
margin: auto;
margin-top: 20rpx;
text-align: center;
color: #DC2828;
font-size: 28rpx;
display: flex;
justify-content: center;
align-items: center;
.recording-time {
margin-left: 10rpx;
font-weight: bold;
}
}
.voice-wave {
width: 85%;
height: 120rpx;
margin: 20rpx auto;
display: flex;
justify-content: space-between;
align-items: flex-end;
.wave-item {
width: 10rpx;
background-color: #DC2828;
border-radius: 10rpx;
transition: height 0.1s ease-in-out;
}
}
.recording-file{
height: 250rpx;
display: flex;


+ 150
- 40
store/store.js View File

@ -1,5 +1,6 @@
import Vue from 'vue'
import Vuex from 'vuex'
import utils from '../utils/utils.js'
Vue.use(Vuex); //vue的插件机制
@ -8,75 +9,184 @@ import api from '@/api/api.js'
//Vuex.Store 构造器选项
const store = new Vuex.Store({
state: {
configList: [], //配置列表
shop : false,
userInfo : {}, //用户信息
},
getters: {
// 角色 true为水洗店 false为酒店
userShop(state){
return state.shop
}
configList: {}, //配置列表
userInfo: {}, //用户信息
riceInfo: {}, //用户相关信息
category: [], //分类信息
payOrderProduct: [], //支付订单中的商品
promotionUrl : '',//分享二维码
},
getters: {},
mutations: {
// 初始化配置
initConfig(state){
// api('getConfig', res => {
// if(res.code == 200){
// state.configList = res.result
// }
// })
let config = ['getPrivacyPolicy', 'getUserAgreement']
config.forEach(k => {
api(k, res => {
if (res.code == 200) {
state.configList[k] = res.result
}
})
initConfig(state) {
api('getConfig', res => {
const configList = {
...state.configList,
}
if (res.code == 200) {
res.result.forEach(n => {
configList[n.keyName] = n.keyContent;
configList[n.keyName + '_keyValue'] = n.keyValue;
});
}
state.configList = configList
uni.$emit('initConfig', state.configList)
})
// let config = ['getPrivacyPolicy', 'getUserAgreement']
// config.forEach(k => {
// api(k, res => {
// if (res.code == 200) {
// state.configList[k] = res.result
// }
// })
// })
},
login(state){
login(state, config = {}) {
uni.showLoading({
title: '登录中...'
})
uni.login({
success(res) {
if(res.errMsg != "login:ok"){
success : res => {
if (res.errMsg != "login:ok") {
return
}
api('wxLogin', {
code : res.code
}, res => {
let data = {
code: res.code,
}
if (uni.getStorageSync('shareId')) {
data.shareId = uni.getStorageSync('shareId')
}
api('wxLogin', data, res => {
uni.hideLoading()
if(res.code != 200){
if (res.code != 200) {
return
}
state.userInfo = res.result.userInfo
uni.setStorageSync('token', res.result.token)
if(!state.userInfo.nickName || !state.userInfo.headImage){
if(config.path){
let path = config.path
delete config.path
delete config.shareId
let para = utils.objectToUrlParams(config)
uni.reLaunch({
url: `${path}?${para}`,
})
return
}
if (!state.userInfo.nickName ||
!state.userInfo.headImage ||
!state.userInfo.phone
) {
uni.navigateTo({
url: '/pages_order/auth/wxUserInfo'
})
}else{
uni.navigateBack(-1)
} else {
utils.navigateBack(-1)
}
})
}
})
},
getUserInfo(state){
api('infoGetInfo', res => {
if(res.code == 200){
getUserInfo(state) {
api('getInfo', res => {
if (res.code == 200) {
state.userInfo = res.result
if (!state.userInfo.nickName ||
!state.userInfo.headImage ||
!state.userInfo.phone
) {
uni.showModal({
title: '申请获取您的信息!',
cancelText: '稍后补全',
confirmText: '现在补全',
success(e) {
if (e.confirm) {
uni.navigateTo({
url: '/pages_order/auth/wxUserInfo'
})
}
}
})
}
}
})
},
getRiceInfo(state) {
api('getRiceInfo', {
token: uni.getStorageSync('token') || ''
}, res => {
if (res.code == 200) {
state.riceInfo = res.result
}
})
},
// 退出登录
logout(state, reLaunch = false) {
// uni.showModal({
// title: '确认退出登录吗',
// success(r) {
// if (r.confirm) {
// state.userInfo = {}
// uni.removeStorageSync('token')
// uni.reLaunch({
// url: '/pages/index/index'
// })
// }
// }
// })
state.userInfo = {}
uni.removeStorageSync('token')
if(reLaunch){
uni.reLaunch({
url: '/pages/index/index'
})
}
},
getQrCode(state) {
let that = this;
if(!uni.getStorageSync('token')){
return
}
uni.getImageInfo({
src: `${Vue.prototype.$config.baseUrl}/info_common/getInviteCode?token=${uni.getStorageSync('token')}`,
success : res => {
that.commit('setPromotionUrl', res.path)
},
fail : err => {
}
})
},
// 查询分类接口
getCategoryList(state) {
api('getCategoryPidList', res => {
if (res.code == 200) {
state.category = res.result
}
})
},
// 设置支付订单中的商品
setPayOrderProduct(state, data) {
state.payOrderProduct = data
},
setPromotionUrl(state, data){
state.promotionUrl = data
},
},
actions: {},
})


+ 0
- 10
utils/oss-upload/oss/index.js View File

@ -2,10 +2,6 @@
* 阿里云OSS工具类
*/
import OSSConfig from "@/utils/oss-upload/oss/OSSConfig.js"
//支持web端
import {
uploadFileToOSS
} from '@/utils/oss-upload/oss/web.js'
import ossConfig from '@/config.js'
/**
@ -93,9 +89,6 @@ export function ossUploadImage({
count: 1,
sizeType,
success(res) {
// #ifdef H5
return uploadFileToOSS(res.tempFiles[0]).then(success).catch(fail)
// #endif
ossUpload(res.tempFilePaths[0], key, folder).then(success).catch(fail)
},
fail
@ -123,9 +116,6 @@ export function ossUploadVideo({
maxDuration,
camera,
success(res) {
// #ifdef H5
return uploadFileToOSS(res.tempFile).then(success).catch(fail)
// #endif
ossUpload(res.tempFilePath, key, folder).then(success).catch(fail)
},
fail


+ 0
- 63
utils/oss-upload/oss/web.js View File

@ -1,63 +0,0 @@
// 此方法适用于web
import OSS from "ali-oss"
import config from '@/config.js'
/**
* 生成一个随机的Key
*/
function storeKey() {
let s = [];
let hexDigits = "0123456789abcdef";
for (let i = 0; i < 36; i++) {
s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
}
s[14] = "4";
s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1);
s[8] = s[13] = s[18] = s[23] = "-";
return s.join("");
}
/**
* 根据当天日期在OSS端生成文件夹
*/
function storeFolder() {
const date = new Date();
const formatNumber = n => {
n = n.toString()
return n[1] ? n : '0' + n
}
return [date.getFullYear(), date.getMonth() + 1, date.getDate()].map(formatNumber).join('-')
}
export function uploadFileToOSS(file) {
uni.showLoading({
title: '上传中...'
});
return new Promise((resolve,reject) => {
// 创建OSS实例
const client = new OSS(config.aliOss.config);
// 设置文件名和文件目录
const suffix = '.' + file.name.split('.').pop();
let key = storeFolder()
if(key[key.length - 1] != '/') key += '/'
const fileName = key + storeKey() + suffix; // 注意:文件名需要是唯一的
// 使用put接口上传文件
client.multipartUpload(fileName, file, {
headers: {
'Content-Disposition': 'inline',
'Content-Type': file.type
}
}).then(res => {
uni.hideLoading();
resolve(config.aliOss.url + res.name);
}).catch(err => {
uni.hideLoading();
reject(err)
})
})
}

+ 114
- 35
utils/utils.js View File

@ -1,3 +1,8 @@
/**
* 将数据转换为数组格式
* @param {any} data - 需要转换的数据
* @returns {Array} 转换后的数组
*/
function toArray(data) {
if (!data) return []
if (data instanceof Array){
@ -7,6 +12,10 @@ function toArray(data) {
}
}
/**
* 生成UUID
* @returns {string} 生成的UUID字符串
*/
function generateUUID() {
return 'xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random() * 16 | 0,
@ -15,6 +24,10 @@ function generateUUID() {
});
}
/**
* 生成随机颜色
* @returns {string} 生成的十六进制颜色值
*/
function generateRandomColor() {
const letters = '0123456789ABCDEF';
let color = '#';
@ -24,6 +37,10 @@ function generateRandomColor() {
return color;
}
/**
* 生成浅色系的随机颜色
* @returns {string} 生成的RGB格式颜色值
*/
function generateLightRandomColor() {
const min = 150;
const range = 105;
@ -34,6 +51,12 @@ function generateLightRandomColor() {
return color;
}
/**
* 表单数据验证
* @param {Object} data - 需要验证的表单数据
* @param {Object} msg - 验证失败时的提示信息
* @returns {boolean} 验证结果true表示验证失败false表示验证通过
*/
function verificationAll(data, msg){
if (!msg){
@ -57,39 +80,14 @@ function verificationAll(data, msg){
return true
}
}
// let Msgs = {
// default : msg || '表单数据未填写'
// }
// if(typeof msg == 'object'){
// Msgs = {
// default : '表单数据未填写',
// ...msg,
// }
// }
// if (!data){
// uni.showToast({
// title: Msgs.default,
// icon: "none"
// })
// return true
// }
// for (let key in data) {
// if (!data[key] || data[key] === "") {
// uni.showToast({
// title: (Msgs[key] || Msgs.default),
// icon: "none"
// })
// return true
// }
// }
return false
}
//验证手机号是否合法
/**
* 验证手机号是否合法
* @param {string} phone - 需要验证的手机号
* @returns {boolean} 验证结果true表示合法false表示不合法
*/
function verificationPhone(phone){
if(!/^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$/.test(phone)){
return false
@ -98,6 +96,11 @@ function verificationPhone(phone){
}
//获取url中参数的方法
/**
* 获取URL中指定参数的值
* @param {string} name - 参数名称
* @returns {string} 参数值如果不存在则返回空字符串
*/
export function getHrefParams(name) {
var url = window.location.href;
try {
@ -114,8 +117,12 @@ export function getHrefParams(name) {
}
}
//深度对比合并两个对象,相同属性b会覆盖a
/**
* 深度合并两个对象相同属性b会覆盖a
* @param {Object} a - 目标对象
* @param {Object} b - 源对象
* @returns {Object} 合并后的新对象
*/
export function deepMergeObject(a, b){
let data = JSON.parse(JSON.stringify(a))
function mergeObject(obj1, obj2){
@ -131,7 +138,10 @@ export function deepMergeObject(a, b){
return mergeObject(data, b)
}
//复制内容
/**
* 复制文本到剪贴板
* @param {string} content - 要复制的内容
*/
export function copyText(content) {
uni.setClipboardData({
data: content,
@ -144,6 +154,21 @@ export function copyText(content) {
})
}
/**
* 将字符串中的文本格式化为HTML
* @param {string} str - 需要格式化的字符串
* @returns {string} 格式化后的HTML字符串
*/
export function stringFormatHtml(str){
return str && str.replace(/\n/gi, '<br>')
.replace(/ /gi, ' ')
}
/**
* 处理页面导航参数
* @param {string|Object} url - 页面路径或导航参数对象
* @returns {Object} 处理后的导航参数对象
*/
function params(url){
if(typeof url == 'object'){
return url
@ -160,19 +185,70 @@ function params(url){
return data
}
/**
* 页面导航方法
* @param {...any} args - 导航参数
*/
export function navigateTo(...args){
uni.navigateTo(params(...args))
}
/**
* 返回上一页
* @param {number} num - 返回的页面数默认为-1
*/
export function navigateBack(num = -1){
if(getCurrentPages().length == 1){
uni.reLaunch({
url: '/pages/index/index'
})
return
}
uni.navigateBack(num)
}
/**
* 重定向到指定页面
* @param {...any} args - 导航参数
*/
export function redirectTo(...args){
uni.redirectTo(params(...args))
}
/**
* 登录跳转函数防止短时间内多次调用
* @returns {Function} 节流处理后的登录跳转函数
*/
export const toLogin = function(){
let time = 0
return () => {
if(new Date().getTime() - time < 1000){
return
}
time = new Date().getTime()
uni.navigateTo({
url: '/pages_order/auth/wxLogin'
})
}
}()
// 将对象转换为URL参数
function objectToUrlParams(obj) {
if (!obj || typeof obj !== 'object') {
return '';
}
return Object.keys(obj)
.filter(key => obj[key] !== undefined && obj[key] !== null)
.map(key => {
const value = typeof obj[key] === 'object'
? JSON.stringify(obj[key])
: obj[key];
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
})
.join('&');
}
export default {
toArray,
generateUUID,
@ -185,5 +261,8 @@ export default {
navigateTo,
navigateBack,
redirectTo,
copyText
copyText,
stringFormatHtml,
toLogin,
objectToUrlParams,
}

Loading…
Cancel
Save