diff --git a/.cursor/rules/md.mdc b/.cursor/rules/md.mdc new file mode 100644 index 0000000..4eab25e --- /dev/null +++ b/.cursor/rules/md.mdc @@ -0,0 +1,6 @@ +--- +description: +globs: +alwaysApply: false +--- +请你写代码之前先查看一下README.md文件,按照这个文件中说的写 \ No newline at end of file diff --git a/README.md b/README.md index 7008566..b4f6709 100644 --- a/README.md +++ b/README.md @@ -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) \ No newline at end of file +# 珠宝商城项目文档 + +## 项目概述 +本项目是一个基于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 + +``` + + +#### 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 + + + +``` + +**注意事项:** +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是否正确配置 + - 确认网络请求是否正常 + - 查看请求参数格式是否正确 + diff --git a/api/api.js b/api/api.js index 871475a..1293351 100644 --- a/api/api.js +++ b/api/api.js @@ -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 \ No newline at end of file diff --git a/api/http.js b/api/http.js index 8f7078e..6c3da63 100644 --- a/api/http.js +++ b/api/http.js @@ -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 } \ No newline at end of file diff --git a/api/model/index.js b/api/model/index.js new file mode 100644 index 0000000..9ec5e2e --- /dev/null +++ b/api/model/index.js @@ -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 \ No newline at end of file diff --git a/api/model/login.js b/api/model/login.js new file mode 100644 index 0000000..3ff9999 --- /dev/null +++ b/api/model/login.js @@ -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 \ No newline at end of file diff --git a/components/base/navbar.vue b/components/base/navbar.vue index 8ada4ad..34d6fee 100644 --- a/components/base/navbar.vue +++ b/components/base/navbar.vue @@ -2,27 +2,33 @@ + :style="{backgroundColor : bgColor,color}"> + + + + :color="color" size="46rpx"> {{ title }} + :color="color" size="58rpx"> - - @@ -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' + }) + } } } @@ -99,7 +116,7 @@ justify-content: center; font-size: 32rpx; align-items: center; - z-index: 99999; + z-index: 999; .left{ position: absolute; left: 40rpx; diff --git a/components/base/tabbar.vue b/components/base/tabbar.vue index d5e7657..6135e05 100644 --- a/components/base/tabbar.vue +++ b/components/base/tabbar.vue @@ -1,19 +1,14 @@ \ No newline at end of file diff --git a/config.js b/config.js index 4175d2b..cf71a65 100644 --- a/config.js +++ b/config.js @@ -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', } } diff --git a/main.js b/main.js index 84751ca..f971a7f 100644 --- a/main.js +++ b/main.js @@ -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' diff --git a/manifest.json b/manifest.json index ea42a21..fe96ebc 100644 --- a/manifest.json +++ b/manifest.json @@ -52,7 +52,7 @@ "quickapp" : {}, /* 小程序特有相关 */ "mp-weixin" : { - "appid" : "wxe7ae8cbe1673834c", + "appid" : "wx328ba180b4a88d49", "setting" : { "urlCheck" : false }, diff --git a/mixins/configList.js b/mixins/configList.js new file mode 100644 index 0000000..6b5fcc1 --- /dev/null +++ b/mixins/configList.js @@ -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: { + } +} \ No newline at end of file diff --git a/mixins/order.js b/mixins/order.js new file mode 100644 index 0000000..b1650dd --- /dev/null +++ b/mixins/order.js @@ -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() + }) + } + } + }) + }, + } +} \ No newline at end of file diff --git a/pages/index/category.vue b/pages/index/category.vue index 5871e0f..98abb07 100644 --- a/pages/index/category.vue +++ b/pages/index/category.vue @@ -29,7 +29,7 @@ - + diff --git a/pages/index/center.vue b/pages/index/center.vue index 1b332a3..8626d4a 100644 --- a/pages/index/center.vue +++ b/pages/index/center.vue @@ -117,7 +117,7 @@ - + diff --git a/pages/index/index.vue b/pages/index/index.vue index e126cdc..4e137cd 100644 --- a/pages/index/index.vue +++ b/pages/index/index.vue @@ -26,7 +26,7 @@ - + @@ -94,16 +94,19 @@ text="立即购买"> + - + diff --git a/pages_order/order/voiceOrder.vue b/pages_order/order/voiceOrder.vue index 8fcfd45..89f0ec7 100644 --- a/pages_order/order/voiceOrder.vue +++ b/pages_order/order/voiceOrder.vue @@ -10,22 +10,35 @@ 录制语音下单 - + - 长按可说话 + {{isRecording ? '松开结束录音' : '长按可说话'}} - + + 正在录音中... + {{recordingTime}}s + + + + + + - - + + -

录音文件01.mp3

-

12MB

+

{{audioName}}

+

{{audioSize}}

- - 100% + + {{uploadProgress}}%
@@ -34,7 +47,7 @@
- + 快捷下单 @@ -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()); + } } } @@ -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; diff --git a/store/store.js b/store/store.js index 7c3e47c..0151305 100644 --- a/store/store.js +++ b/store/store.js @@ -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: {}, }) diff --git a/utils/oss-upload/oss/index.js b/utils/oss-upload/oss/index.js index 024eb80..91b7093 100644 --- a/utils/oss-upload/oss/index.js +++ b/utils/oss-upload/oss/index.js @@ -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 diff --git a/utils/oss-upload/oss/web.js b/utils/oss-upload/oss/web.js deleted file mode 100644 index dfaab6e..0000000 --- a/utils/oss-upload/oss/web.js +++ /dev/null @@ -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) - }) - }) -} \ No newline at end of file diff --git a/utils/utils.js b/utils/utils.js index dabe1e0..b418508 100644 --- a/utils/utils.js +++ b/utils/utils.js @@ -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, '
') + .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, } \ No newline at end of file