diff --git a/App.vue b/App.vue index 061c99f..ddc469e 100644 --- a/App.vue +++ b/App.vue @@ -98,6 +98,11 @@ if (uni.getStorageSync('servicerConfig')) { this.$store.commit('setServicerConfig', uni.getStorageSync('servicerConfig')); } + + // 企业微信配置 + if (uni.getStorageSync('wxworkConfig')) { + this.$store.commit('setWxworkConfig', uni.getStorageSync('wxworkConfig')); + } // 版权信息 if (uni.getStorageSync('copyright')) { diff --git a/common/js/customer-service.js b/common/js/customer-service.js new file mode 100644 index 0000000..22a0857 --- /dev/null +++ b/common/js/customer-service.js @@ -0,0 +1,484 @@ +/** + * 客服统一处理服务 + * 整合各种客服方式,提供统一的调用接口 + */ + +export class CustomerService { + constructor(vueInstance) { + this.vm = vueInstance; + } + + /** + * 获取平台配置 + * @returns {Object} 平台对应的客服配置 + */ + getPlatformConfig() { + const servicerConfig = this.vm.$store.state.servicerConfig; + if (!servicerConfig) return null; + + // #ifdef H5 + return servicerConfig.h5; + // #endif + + // #ifdef MP-WEIXIN + return servicerConfig.weapp; + // #endif + + // #ifdef MP-ALIPAY + return servicerConfig.aliapp; + // #endif + + return null; + } + + /** + * 获取企业微信配置 + * @returns {Object} 企业微信配置 + */ + getWxworkConfig() { + return this.vm.$store.state.wxworkConfig; + } + + /** + * 检查客服配置是否可用 + * @returns {boolean} 是否有可用配置 + */ + isConfigAvailable() { + const config = this.getPlatformConfig(); + return config && typeof config === 'object' && config.type; + } + + /** + * 验证客服配置完整性 + * @returns {Object} 验证结果 + */ + validateConfig() { + const config = this.getPlatformConfig(); + const wxworkConfig = this.getWxworkConfig(); + + const result = { + isValid: true, + errors: [], + warnings: [] + }; + + if (!config) { + result.isValid = false; + result.errors.push('客服配置不存在'); + return result; + } + + if (!config.type) { + result.isValid = false; + result.errors.push('客服类型未配置'); + } + + // 验证企业微信配置 + if (config.type === 'wxwork') { + if (!wxworkConfig) { + result.isValid = false; + result.errors.push('企业微信配置不存在'); + } else { + if (!wxworkConfig.enable) { + result.warnings.push('企业微信功能未启用'); + } + if (!wxworkConfig.contact_url) { + result.warnings.push('企业微信活码链接未配置,将使用原有客服方式'); + } + } + } + + return result; + } + + /** + * 获取客服类型 + * @returns {string} 客服类型 + */ + getServiceType() { + const config = this.getPlatformConfig(); + return config?.type || 'none'; + } + + /** + * 处理客服点击事件 + * @param {Object} options 选项参数 + * @param {Object} options.niushop 牛商客服参数 + * @param {string} options.sendMessageTitle 消息标题 + * @param {string} options.sendMessagePath 消息路径 + * @param {string} options.sendMessageImg 消息图片 + */ + handleCustomerClick(options = {}) { + // 验证配置 + const validation = this.validateConfig(); + if (!validation.isValid) { + console.error('客服配置验证失败:', validation.errors); + this.showConfigErrorPopup(validation.errors); + return; + } + + // 显示警告(如果有) + if (validation.warnings.length > 0) { + console.warn('客服配置警告:', validation.warnings); + } + + const config = this.getPlatformConfig(); + const { + niushop = {}, + sendMessageTitle = '', + sendMessagePath = '', + sendMessageImg = '' + } = options; + + // 检查是否为无客服类型 + if (config.type === 'none') { + this.showNoServicePopup(); + return; + } + + // 根据类型处理客服 + switch (config.type) { + case 'wxwork': + this.openWxworkService(false, config, options); + break; + case 'third': + this.openThirdService(config); + break; + case 'niushop': + this.openNiushopService(niushop); + break; + case 'weapp': + this.openWeappService(config, options); + break; + case 'aliapp': + this.openAliappService(config); + break; + default: + this.makePhoneCall(); + } + } + + /** + * 打开企业微信客服 + * @param {boolean} useOriginalService 是否使用原有客服方式 + * @param {Object} servicerConfig 客服配置 + */ + openWxworkService(useOriginalService = false, servicerConfig = null, options = {}) { + const config = servicerConfig || this.getPlatformConfig(); + const wxworkConfig = this.getWxworkConfig(); + + const { + sendMessageTitle = '', + sendMessagePath = '', + sendMessageImg = '' + } = options; + + // #ifdef MP-WEIXIN + if (wxworkConfig?.enable && wxworkConfig?.contact_url && !useOriginalService) { + // 使用活码链接跳转到企业微信 + wx.navigateToMiniProgram({ + appId: 'wxeb490c6f9b154ef9', // 企业微信官方小程序AppID + path: `pages/contacts/externalContactDetail?url=${encodeURIComponent(wxworkConfig.contact_url)}`, + success: () => { + console.log('跳转企业微信成功'); + }, + fail: (err) => { + console.error('跳转企业微信失败:', err); + this.fallbackToOriginalService(); + } + }); + } else { + // 使用原有的客服会话方式 + wx.openCustomerServiceChat({ + extInfo: { url: config.wxwork_url }, + corpId: config.corpid, + showMessageCard: true, + sendMessageTitle, + sendMessagePath, + sendMessageImg + }); + } + // #endif + + // #ifdef H5 + if (wxworkConfig?.enable && wxworkConfig?.contact_url) { + window.location.href = wxworkConfig.contact_url; + } else { + location.href = config.wxwork_url; + } + // #endif + } + + /** + * 打开第三方客服 + * @param {Object} config 客服配置 + */ + openThirdService(config) { + if (config.third_url) { + location.href = config.third_url; + } + } + + /** + * 打开牛商客服 + * @param {Object} niushop 牛商参数 + */ + openNiushopService(niushop) { + if (Object.keys(niushop).length > 0) { + this.vm.$util.redirectTo('/pages_tool/chat/room', niushop); + } else { + this.makePhoneCall(); + } + } + + /** + * 打开微信小程序客服 + * @param {Object} config 客服配置 + * @param {Object} options 选项参数 + */ + openWeappService(config, options = {}) { + // 如果是官方客服,由 open-type="contact" 自动处理 + if (!this.shouldUseCustomService(config)) { + console.log('使用官方微信小程序客服'); + return; + } + + // 自定义客服处理 + console.log('使用自定义微信小程序客服'); + + // 这里可以实现自定义的客服逻辑 + // 例如:跳转到自定义客服页面、显示客服弹窗等 + const { + sendMessageTitle = '', + sendMessagePath = '', + sendMessageImg = '' + } = options; + + // 实现自定义客服逻辑 + this.handleCustomWeappService(config, options); + } + + /** + * 处理自定义微信小程序客服 + * @param {Object} config 客服配置 + * @param {Object} options 选项参数 + */ + handleCustomWeappService(config, options = {}) { + const { + sendMessageTitle = '', + sendMessagePath = '', + sendMessageImg = '' + } = options; + + // 优先级1: 如果有自定义客服页面URL,跳转到自定义页面 + if (config.customServiceUrl) { + // 构建带参数的URL + let url = config.customServiceUrl; + const params = []; + + if (sendMessageTitle) params.push(`title=${encodeURIComponent(sendMessageTitle)}`); + if (sendMessagePath) params.push(`path=${encodeURIComponent(sendMessagePath)}`); + if (sendMessageImg) params.push(`img=${encodeURIComponent(sendMessageImg)}`); + + if (params.length > 0) { + url += (url.includes('?') ? '&' : '?') + params.join('&'); + } + + uni.navigateTo({ + url: url, + fail: (err) => { + console.error('跳转自定义客服页面失败:', err); + this.tryThirdPartyService(config, options); + } + }); + return; + } + + // 优先级2: 尝试第三方客服 + this.tryThirdPartyService(config, options); + } + + /** + * 尝试使用第三方客服 + * @param {Object} config 客服配置 + * @param {Object} options 选项参数 + */ + tryThirdPartyService(config, options = {}) { + // 如果配置了第三方客服URL + if (config.thirdPartyServiceUrl) { + // #ifdef H5 + window.open(config.thirdPartyServiceUrl, '_blank'); + // #endif + + // #ifdef MP-WEIXIN + // 微信小程序可以使用web-view或者跳转到第三方小程序 + if (config.thirdPartyMiniAppId) { + wx.navigateToMiniProgram({ + appId: config.thirdPartyMiniAppId, + path: config.thirdPartyMiniAppPath || '', + fail: (err) => { + console.error('跳转第三方小程序失败:', err); + this.fallbackToPhoneCall(); + } + }); + } else { + // 设置到剪贴板,让用户手动访问 + uni.setClipboardData({ + data: config.thirdPartyServiceUrl, + success: () => { + uni.showModal({ + title: '客服链接已复制', + content: '客服链接已复制到剪贴板,请在浏览器中粘贴访问', + showCancel: false + }); + } + }); + } + // #endif + return; + } + + // 降级到电话客服 + this.fallbackToPhoneCall(); + } + + /** + * 降级到电话客服 + */ + fallbackToPhoneCall() { + uni.showModal({ + title: '联系客服', + content: '在线客服暂时不可用,是否拨打电话联系客服?', + success: (res) => { + if (res.confirm) { + this.makePhoneCall(); + } + } + }); + } + + /** + * 打开支付宝小程序客服 + * @param {Object} config 客服配置 + */ + openAliappService(config) { + // 支付宝小程序客服会由 contact-button 组件自动处理 + console.log('支付宝小程序客服'); + } + + /** + * 拨打电话 + */ + makePhoneCall() { + this.vm.$api.sendRequest({ + url: '/api/site/shopcontact', + success: res => { + if (res.code === 0 && res.data?.mobile) { + uni.makePhoneCall({ + phoneNumber: res.data.mobile + }); + } + } + }); + } + + /** + * 显示无客服弹窗 + */ + showNoServicePopup() { + const siteInfo = this.vm.$store.state.siteInfo; + const message = siteInfo?.site_tel + ? `请联系客服,客服电话是${siteInfo.site_tel}` + : '抱歉,商家暂无客服,请线下联系'; + + uni.showModal({ + title: '联系客服', + content: message, + showCancel: false + }); + } + + /** + * 显示配置错误弹窗 + * @param {Array} errors 错误列表 + */ + showConfigErrorPopup(errors) { + const message = errors.join('\n'); + uni.showModal({ + title: '配置错误', + content: `客服配置有误:\n${message}`, + showCancel: false + }); + } + + /** + * 降级处理:使用原有客服方式 + */ + fallbackToOriginalService() { + uni.showModal({ + title: '提示', + content: '无法直接添加企业微信客服,是否使用其他方式联系客服?', + success: (res) => { + if (res.confirm) { + console.log('降级处理:使用原有客服方式'); + this.openWxworkService(true); + } + } + }); + } + + /** + * 获取客服按钮配置 + * @returns {Object} 按钮配置 + */ + getButtonConfig() { + const config = this.getPlatformConfig(); + if (!config) return { openType: '' }; + + let openType = ''; + + // #ifdef MP-WEIXIN + if (config.type === 'weapp') { + // 检查是否使用官方客服 + if (config.useOfficial !== false) { // 默认为true,使用官方客服 + openType = 'contact'; + } + } + // #endif + + // #ifdef MP-ALIPAY + if (config.type === 'aliapp') openType = 'contact'; + // #endif + + return { + ...config, + openType + }; + } + + /** + * 判断是否应该使用自定义客服处理 + * @param {Object} config 客服配置 + * @returns {boolean} 是否使用自定义客服 + */ + shouldUseCustomService(config) { + // #ifdef MP-WEIXIN + // 如果是微信小程序且type为weapp,检查useOfficial配置 + if (config?.type === 'weapp') { + return config.useOfficial === false; // 明确设置为false才使用自定义 + } + // #endif + + // 其他平台或类型都使用自定义处理 + return true; + } +} + +/** + * 创建客服服务实例 + * @param {Object} vueInstance Vue实例 + * @returns {CustomerService} 客服服务实例 + */ +export function createCustomerService(vueInstance) { + return new CustomerService(vueInstance); +} \ No newline at end of file diff --git a/common/js/golbalConfig.js b/common/js/golbalConfig.js index 73f39c6..d0ddab7 100644 --- a/common/js/golbalConfig.js +++ b/common/js/golbalConfig.js @@ -1,186 +1,190 @@ -export default { - data() { - return { - // 页面样式,动态设置主色调 - themeColor: '' //''--base-color:#fa5d14;--base-help-color:#ff7e00;' - } - }, - onLoad() {}, - onShow() { - // 刷新多语言 - this.$langConfig.refresh(); - let time = setInterval(() => { - let theme = this.themeStyle; - if (theme && theme.main_color) { - this.themeColorSet(); - clearInterval(time); - } - }, 50); - }, - computed: { - themeStyle() { - return this.$store.state.themeStyle; - }, - // 插件是否存在 - addonIsExist() { - return this.$store.state.addonIsExist; - }, - tabBarList() { - return this.$store.state.tabBarList; - }, - siteInfo() { - return this.$store.state.siteInfo; - }, - memberInfo() { - return this.$store.state.memberInfo; - }, - storeToken() { - return this.$store.state.token; - }, - bottomNavHidden() { - return this.$store.state.bottomNavHidden; - }, - globalStoreConfig() { - return this.$store.state.globalStoreConfig; - }, - globalStoreInfo() { - return this.$store.state.globalStoreInfo; - }, - // 定位信息 - location() { - return this.$store.state.location; - }, - // 定位信息(缓存) - locationStorage() { - let data = uni.getStorageSync('location'); - if (data) { - var date = new Date(); - if (this.mapConfig.wap_valid_time > 0) { - data.is_expired = (date.getTime() / 1000) > data.valid_time; // 是否过期 - } else { - data.is_expired = false; - } - } - return data; - }, - // 默认总店(定位失败后使用) - defaultStoreInfo() { - return this.$store.state.defaultStoreInfo; - }, - // 组件刷新计数 - componentRefresh() { - return this.$store.state.componentRefresh; - }, - // 客服配置 - servicerConfig() { - return this.$store.state.servicerConfig; - }, - diySeckillInterval() { - return this.$store.state.diySeckillInterval; - }, - tabBarHeight() { - return this.$store.state.tabBarHeight; - }, - mapConfig() { - return this.$store.state.mapConfig; - }, - copyright() { - let copyright = this.$store.state.copyright; - // 判断是否授权 - if (copyright && !copyright.auth) { - copyright.logo = ''; - copyright.copyright_link = ''; - } - return copyright; - }, - cartList() { - return this.$store.state.cartList; - }, - cartIds() { - return this.$store.state.cartIds; - }, - cartNumber() { - return this.$store.state.cartNumber; - }, - cartMoney() { - return this.$store.state.cartMoney; - } - }, - methods: { - themeColorSet() { - let theme = this.themeStyle; - this.themeColor = `--base-color:${theme.main_color};--base-help-color:${theme.aux_color};`; - if (this.tabBarHeight != '56px') this.themeColor += `--tab-bar-height:${this.tabBarHeight};` - Object.keys(theme).forEach(key => { - let data = theme[key]; - if (typeof(data) == "object") { - Object.keys(data).forEach(k => { - this.themeColor += '--' + k.replace(/_/g, "-") + ':' + data[k] + ';'; - }); - } else if (typeof(key) == "string" && key) { - this.themeColor += '--' + key.replace(/_/g, "-") + ':' + data + ';'; - } - }); - for (let i = 9; i >= 5; i--) { - let color = this.$util.colourBlend(theme.main_color, '#ffffff', (i / 10)); - this.themeColor += `--base-color-light-${i}:${color};`; - } - }, - // 颜色变浅(>0)、变深函数(<0) - lightenDarkenColor(color, amount) { - - var usePound = false; - - if (color[0] == "#") { - color = color.slice(1); - usePound = true; - } - - var num = parseInt(color, 16); - - var r = (num >> 16) + amount; - - if (r > 255) r = 255; - else if (r < 0) r = 0; - - var b = ((num >> 8) & 0x00FF) + amount; - - if (b > 255) b = 255; - else if (b < 0) b = 0; - - var g = (num & 0x0000FF) + amount; - - if (g > 255) g = 255; - else if (g < 0) g = 0; - - return (usePound ? "#" : "") + (g | (b << 8) | (r << 16)).toString(16); - - }, - /** - * 切换门店 - * @param {Object} info 门店信息 - * @param {Object} isJump 是否跳转到首页 - */ - changeStore(info, isJump) { - if (info) { - this.$store.commit('setGlobalStoreInfo', info); - } - let route = this.$util.getCurrRoute(); - if (isJump && route != 'pages/index/index') { - uni.setStorageSync('manual_change_store', true); // 手动切换门店 - this.$store.dispatch('getCartNumber'); //重新获取购物车数据 - this.$util.redirectTo('/pages/index/index'); - } - } - }, - filters: { - /** - * 金额格式化输出 - * @param {Object} money - */ - moneyFormat(money) { - if (isNaN(parseFloat(money))) return money; - return parseFloat(money).toFixed(2); - } - } +export default { + data() { + return { + // 页面样式,动态设置主色调 + themeColor: '' //''--base-color:#fa5d14;--base-help-color:#ff7e00;' + } + }, + onLoad() {}, + onShow() { + // 刷新多语言 + this.$langConfig.refresh(); + let time = setInterval(() => { + let theme = this.themeStyle; + if (theme && theme.main_color) { + this.themeColorSet(); + clearInterval(time); + } + }, 50); + }, + computed: { + themeStyle() { + return this.$store.state.themeStyle; + }, + // 插件是否存在 + addonIsExist() { + return this.$store.state.addonIsExist; + }, + tabBarList() { + return this.$store.state.tabBarList; + }, + siteInfo() { + return this.$store.state.siteInfo; + }, + memberInfo() { + return this.$store.state.memberInfo; + }, + storeToken() { + return this.$store.state.token; + }, + bottomNavHidden() { + return this.$store.state.bottomNavHidden; + }, + globalStoreConfig() { + return this.$store.state.globalStoreConfig; + }, + globalStoreInfo() { + return this.$store.state.globalStoreInfo; + }, + // 定位信息 + location() { + return this.$store.state.location; + }, + // 定位信息(缓存) + locationStorage() { + let data = uni.getStorageSync('location'); + if (data) { + var date = new Date(); + if (this.mapConfig.wap_valid_time > 0) { + data.is_expired = (date.getTime() / 1000) > data.valid_time; // 是否过期 + } else { + data.is_expired = false; + } + } + return data; + }, + // 默认总店(定位失败后使用) + defaultStoreInfo() { + return this.$store.state.defaultStoreInfo; + }, + // 组件刷新计数 + componentRefresh() { + return this.$store.state.componentRefresh; + }, + // 客服配置 + servicerConfig() { + return this.$store.state.servicerConfig; + }, + // 企业微信配置 + wxworkConfig() { + return this.$store.state.wxworkConfig; + }, + diySeckillInterval() { + return this.$store.state.diySeckillInterval; + }, + tabBarHeight() { + return this.$store.state.tabBarHeight; + }, + mapConfig() { + return this.$store.state.mapConfig; + }, + copyright() { + let copyright = this.$store.state.copyright; + // 判断是否授权 + if (copyright && !copyright.auth) { + copyright.logo = ''; + copyright.copyright_link = ''; + } + return copyright; + }, + cartList() { + return this.$store.state.cartList; + }, + cartIds() { + return this.$store.state.cartIds; + }, + cartNumber() { + return this.$store.state.cartNumber; + }, + cartMoney() { + return this.$store.state.cartMoney; + } + }, + methods: { + themeColorSet() { + let theme = this.themeStyle; + this.themeColor = `--base-color:${theme.main_color};--base-help-color:${theme.aux_color};`; + if (this.tabBarHeight != '56px') this.themeColor += `--tab-bar-height:${this.tabBarHeight};` + Object.keys(theme).forEach(key => { + let data = theme[key]; + if (typeof(data) == "object") { + Object.keys(data).forEach(k => { + this.themeColor += '--' + k.replace(/_/g, "-") + ':' + data[k] + ';'; + }); + } else if (typeof(key) == "string" && key) { + this.themeColor += '--' + key.replace(/_/g, "-") + ':' + data + ';'; + } + }); + for (let i = 9; i >= 5; i--) { + let color = this.$util.colourBlend(theme.main_color, '#ffffff', (i / 10)); + this.themeColor += `--base-color-light-${i}:${color};`; + } + }, + // 颜色变浅(>0)、变深函数(<0) + lightenDarkenColor(color, amount) { + + var usePound = false; + + if (color[0] == "#") { + color = color.slice(1); + usePound = true; + } + + var num = parseInt(color, 16); + + var r = (num >> 16) + amount; + + if (r > 255) r = 255; + else if (r < 0) r = 0; + + var b = ((num >> 8) & 0x00FF) + amount; + + if (b > 255) b = 255; + else if (b < 0) b = 0; + + var g = (num & 0x0000FF) + amount; + + if (g > 255) g = 255; + else if (g < 0) g = 0; + + return (usePound ? "#" : "") + (g | (b << 8) | (r << 16)).toString(16); + + }, + /** + * 切换门店 + * @param {Object} info 门店信息 + * @param {Object} isJump 是否跳转到首页 + */ + changeStore(info, isJump) { + if (info) { + this.$store.commit('setGlobalStoreInfo', info); + } + let route = this.$util.getCurrRoute(); + if (isJump && route != 'pages/index/index') { + uni.setStorageSync('manual_change_store', true); // 手动切换门店 + this.$store.dispatch('getCartNumber'); //重新获取购物车数据 + this.$util.redirectTo('/pages/index/index'); + } + } + }, + filters: { + /** + * 金额格式化输出 + * @param {Object} money + */ + moneyFormat(money) { + if (isNaN(parseFloat(money))) return money; + return parseFloat(money).toFixed(2); + } + } } \ No newline at end of file diff --git a/common/js/util.js b/common/js/util.js index cbe0f01..2d73a7a 100644 --- a/common/js/util.js +++ b/common/js/util.js @@ -1,990 +1,1021 @@ -import Config from './config.js' -import store from '@/store/index.js' -import Http from './http.js' -import { - Weixin -} from 'common/js/wx-jssdk.js'; - -export default { - /** - * 页面跳转 - * @param {string} to 跳转链接 /pages/idnex/index - * @param {Object} param 参数 {key : value, ...} - * @param {string} mode 模式 - */ - redirectTo(to, param, mode) { - let url = to; - let tabbarList = ['/pages/index/index', '/pages/goods/category', '/pages/vr/index', '/pages/contact/contact', '/pages/member/index']; - if (param != undefined) { - Object.keys(param).forEach(function (key) { - if (url.indexOf('?') != -1) { - url += "&" + key + "=" + param[key]; - } else { - url += "?" + key + "=" + param[key]; - } - }); - } - for (let i = 0; i < tabbarList.length; i++) { - if (url.indexOf(tabbarList[i]) == 0) { - uni.switchTab({ - url - }); - return; - } - } - switch (mode) { - case 'tabbar': - // 跳转到 tabBar 页面,并关闭其他所有非 tabBar 页面。 - uni.switchTab({ - url - }); - break; - case 'redirectTo': - // 关闭当前页面,跳转到应用内的某个页面。 - uni.redirectTo({ - url - }); - break; - case 'reLaunch': - // 关闭所有页面,打开到应用内的某个页面。 - uni.reLaunch({ - url - }); - break; - default: - // 保留当前页面,跳转到应用内的某个页面 - uni.navigateTo({ - url - }); - } - }, - /** - * 图片路径转换 - * @param {String} img_path 图片地址 - * @param {Object} params 参数,针对商品、相册里面的图片区分大中小,size: big、mid、small - */ - img(img_path, params) { - - - var path = ""; - if (img_path != undefined && img_path != "") { - if (img_path.split(',').length > 1) { - img_path = img_path.split(',')[0]; - } - if (params && img_path != this.getDefaultImage().goods) { - // 过滤默认图 - let arr = img_path.split("."); - let suffix = arr[arr.length - 1]; - arr.pop(); - arr[arr.length - 1] = arr[arr.length - 1] + "_" + params.size.toUpperCase(); - arr.push(suffix); - // if(img_path.indexOf('attachment') == -1){ - // img_path = arr.join("."); - // } - - } - if (img_path.indexOf("http://") == -1 && img_path.indexOf("https://") == -1) { - path = Config.imgDomain + "/" + img_path; - } else { - // console.log(img_path) - path = img_path; - } - } - - // path += '?t=' + parseInt(new Date().getTime() / 1000); - return path; - }, - /** - * 时间戳转日期格式 - * @param {Object} timeStamp - */ - timeStampTurnTime(timeStamp, type = "") { - if (timeStamp != undefined && timeStamp != "" && timeStamp > 0) { - var date = new Date(); - date.setTime(timeStamp * 1000); - var y = date.getFullYear(); - var m = date.getMonth() + 1; - m = m < 10 ? ('0' + m) : m; - var d = date.getDate(); - d = d < 10 ? ('0' + d) : d; - var h = date.getHours(); - h = h < 10 ? ('0' + h) : h; - var minute = date.getMinutes(); - var second = date.getSeconds(); - minute = minute < 10 ? ('0' + minute) : minute; - second = second < 10 ? ('0' + second) : second; - if (type) { - if (type == 'yearMonthDay') { - return y + '年' + m + '月' + d + '日'; - } - return y + '-' + m + '-' + d; - } else { - return y + '-' + m + '-' + d + ' ' + h + ':' + minute + ':' + second; - } - } else { - return ""; - } - }, - /** - * 日期格式转时间戳 - * @param {Object} string - */ - timeTurnTimeStamp(string) { - var f = string.split(' ', 2); - var d = (f[0] ? f[0] : '').split('-', 3); - var t = (f[1] ? f[1] : '').split(':', 3); - return (new Date( - parseInt(d[0], 10) || null, - (parseInt(d[1], 10) || 1) - 1, - parseInt(d[2], 10) || null, - parseInt(t[0], 10) || null, - parseInt(t[1], 10) || null, - parseInt(t[2], 10) || null - )).getTime() / 1000; - }, - /** - * 倒计时 - * @param {Object} seconds 秒 - */ - countDown(seconds) { - let [day, hour, minute, second] = [0, 0, 0, 0] - if (seconds > 0) { - day = Math.floor(seconds / (60 * 60 * 24)) - hour = Math.floor(seconds / (60 * 60)) - (day * 24) - minute = Math.floor(seconds / 60) - (day * 24 * 60) - (hour * 60) - second = Math.floor(seconds) - (day * 24 * 60 * 60) - (hour * 60 * 60) - (minute * 60) - } - if (day < 10) { - day = '0' + day - } - if (hour < 10) { - hour = '0' + hour - } - if (minute < 10) { - minute = '0' + minute - } - if (second < 10) { - second = '0' + second - } - return { - d: day, - h: hour, - i: minute, - s: second - }; - }, - /** - * 数值去重 - * @param {Array} arr 数组 - * @param {string} field 字段 - */ - unique(arr, field) { - const res = new Map(); - return arr.filter((a) => !res.has(a[field]) && res.set(a[field], 1)); - }, - /** - * 判断值是否在数组中 - * @param {Object} elem - * @param {Object} arr - */ - inArray: function (elem, arr) { - return arr == null ? -1 : arr.indexOf(elem); - }, - /** - * 获取某天日期 - * @param {Object} day - */ - getDay: function (day) { - var today = new Date(); - var targetday_milliseconds = today.getTime() + 1000 * 60 * 60 * 24 * day; - today.setTime(targetday_milliseconds); - - const doHandleMonth = function (month) { - var m = month; - if (month.toString().length == 1) { - m = "0" + month; - } - return m - } - - var tYear = today.getFullYear(); - var tMonth = today.getMonth(); - var tDate = today.getDate(); - var tWeek = today.getDay(); - var time = parseInt(today.getTime() / 1000); - tMonth = doHandleMonth(tMonth + 1); - tDate = doHandleMonth(tDate); - - const week = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'] - return { - 't': time, - 'y': tYear, - 'm': tMonth, - 'd': tDate, - 'w': week[tWeek] - }; - }, - /** - * 图片选择加上传 - * @param num - * @param params - * @param callback - * @param url - */ - upload: function (num, params, callback, url) { - // #ifdef H5 - var app_type = this.isWeiXin() ? 'wechat' : 'h5'; - var app_type_name = this.isWeiXin() ? '微信公众号' : 'H5'; - // #endif - - // #ifdef MP-WEIXIN - var app_type = 'weapp'; - var app_type_name = '微信小程序'; - // #endif - - // #ifdef MP-ALIPAY - var app_type = 'aliapp'; - var app_type_name = '支付宝小程序'; - // #endif - - // #ifdef MP-BAIDU - var app_type = 'baiduapp'; - var app_type_name = '百度小程序'; - // #endif - - // #ifdef MP-TOUTIAO - var app_type = 'MP-TOUTIAO'; - var app_type_name = '头条小程序'; - // #endif - - // #ifdef MP-QQ - var app_type = 'MP-QQ'; - var app_type_name = 'QQ小程序'; - // #endif - var data = { - token: store.state.token, - app_type: app_type, - app_type_name: app_type_name - } - data = Object.assign(data, params); - - var imgs_num = num; - var _self = this; - - uni.chooseImage({ - count: imgs_num, - sizeType: ['compressed'], //可以指定是原图还是压缩图,默认二者都有 - sourceType: ['album', 'camera'], //从相册或者拍照 - success: async function (res) { - const tempFilePaths = res.tempFilePaths; - var _data = data; - var imgs = []; - uni.showLoading({ - title: '图片上传中' - }) - for (var i = 0; i < tempFilePaths.length; i++) { - var path = await _self.upload_file_server(tempFilePaths[i], _data, params.path, - url); - imgs.push(path); - if (imgs.length == tempFilePaths.length) { - uni.hideLoading() - uni.showToast({ - title: '上传成功', - icon: 'none' - }) - typeof callback == 'function' && callback(imgs); - } - } - }, - fail: err => { - uni.hideLoading() - uni.showToast({ - title: '上传失败', - icon: 'none' - }) - } - }); - }, - //上传 - upload_file_server(tempFilePath, data, path, url = "", callback) { - if (url) { - var uploadUrl = Config.baseUrl + url - } else { - var uploadUrl = Config.baseUrl + '/api/upload/' + path - } - return new Promise((resolve, reject) => { - uni.uploadFile({ - url: uploadUrl, - filePath: tempFilePath, - name: 'file', - fileType: data.fileType || 'image', - formData: data, - success: function (res) { - var path_str = JSON.parse(res.data); - if (path_str.code >= 0) { - resolve(path_str.data.pic_path); - typeof callback == 'function' && callback(path_str.data.pic_path); - } else { - reject("error"); - } - } - }); - - }); - - }, - /** - * 复制 - * @param {Object} value - * @param {Object} callback - */ - copy(value, callback) { - // #ifdef H5 - var oInput = document.createElement('input'); //创建一个隐藏input(重要!) - oInput.value = value; //赋值 - oInput.setAttribute("readonly", "readonly"); - document.body.appendChild(oInput); - oInput.select(); // 选择对象 - document.execCommand("Copy"); // 执行浏览器复制命令 - oInput.className = 'oInput'; - oInput.style.display = 'none'; - uni.hideKeyboard(); - this.showToast({ - title: '复制成功' - }); - - typeof callback == 'function' && callback(); - // #endif - - // #ifdef MP || APP-PLUS - uni.setClipboardData({ - data: value, - success: () => { - typeof callback == 'function' && callback(); - } - }); - // #endif - }, - /** - * 是否是微信浏览器 - */ - isWeiXin() { - // #ifndef H5 - return false; - // #endif - var ua = navigator.userAgent.toLowerCase(); - if (ua.match(/MicroMessenger/i) == "micromessenger") { - return true; - } else { - return false; - } - }, - /** - * 显示消息提示框 - * @param {Object} params 参数 - */ - showToast(params = {}) { - params.title = params.title || ""; - params.icon = params.icon || "none"; - // params.position = params.position || 'bottom'; - params.duration = params.duration || 1500; - uni.showToast(params); - if (params.success) params.success(); - }, - /** - * 检测苹果X以上的手机 - */ - isIPhoneX() { - let res = uni.getSystemInfoSync(); - if (res.model.search('iPhone X') != -1) { - return true; - } - return false; - }, - //判断安卓还是iOS - isAndroid() { - let platform = uni.getSystemInfoSync().platform - if (platform == 'ios') { - return false; - } else if (platform == 'android') { - return true; - } - }, - /** - * 深度拷贝对象 - * @param {Object} obj - */ - deepClone(obj) { - const isObject = function (obj) { - return typeof obj == 'object'; - } - - if (!isObject(obj)) { - throw new Error('obj 不是一个对象!') - } - //判断传进来的是对象还是数组 - let isArray = Array.isArray(obj) - let cloneObj = isArray ? [] : {} - //通过for...in来拷贝 - for (let key in obj) { - cloneObj[key] = isObject(obj[key]) ? this.deepClone(obj[key]) : obj[key] - } - return cloneObj - }, - /** - * 自定义模板的跳转链接 - * @param {Object} link - */ - diyRedirectTo(link) { - - //if (link == null || Object.keys(link).length == 1) return; - - // 外部链接 - if (link.wap_url && link.wap_url.indexOf('http') != -1 || link.wap_url && link.wap_url.indexOf('http') != -1) { - // #ifdef H5 - window.location.href = link.wap_url; - // #endif - - // #ifdef MP - this.redirectTo('/pages_tool/webview/webview', { - src: encodeURIComponent(link.wap_url) - }); - // #endif - - } else if (link.appid) { - // 跳转其他小程序 - - uni.navigateToMiniProgram({ - appId: link.appid, - path: link.page - }) - - } else if (link.name == 'MOBILE' && !link.wap_url) { - // 拨打电话 - - uni.makePhoneCall({ - phoneNumber: link.mobile, - success: (res) => { - }, - fail: (res) => { - } - }); - - } else if (link.name == 'MEMBER_CONTACT') { - // 客服 - let servicerConfig = store.state.servicerConfig - let config = { - type: 'none' - }; - - // #ifdef H5 - config = servicerConfig.h5; - // #endif - - // #ifdef MP-WEIXIN - config = servicerConfig.weapp; - // #endif - - switch (config.type) { - case 'wxwork': - // 企业微信客服 - - // #ifdef H5 - window.location.href = config.wxwork_url; - // #endif - - // #ifdef MP-WEIXIN - wx.openCustomerServiceChat({ - extInfo: { - url: config.wxwork_url - }, - corpId: config.corpid, - showMessageCard: true, - sendMessageTitle: 'this.sendMessageTitle', - sendMessagePath: 'this.sendMessagePath', - sendMessageImg: 'this.sendMessageImg' - }); - // #endif - break; - case 'third': - // 第三方客服 - window.location.href = config.third_url; - break; - case 'niushop': - // Niushop客服 - this.redirectTo('/pages_tool/chat/room'); - break; - case 'weapp': - // 微信小程序,由于需要手动点击按钮触发,所以要跳转到中间页 - this.redirectTo(link.wap_url); - break; - default: - // 拨打客服电话 - let siteInfo = store.state.siteInfo; - if (siteInfo && siteInfo.site_tel) { - uni.makePhoneCall({ - phoneNumber: siteInfo.site_tel - }); - } else { - this.showToast({ - title: '抱歉,商家暂无客服,请线下联系', - }) - } - } - - } else if (link.wap_url) { - this.redirectTo(link.wap_url); - } - }, - /** - * 获取默认图 - */ - getDefaultImage() { - let defaultImg = store.state.defaultImg; - defaultImg.goods = this.img(defaultImg.goods); - defaultImg.head = this.img(defaultImg.head); - defaultImg.store = this.img(defaultImg.store); - defaultImg.article = this.img(defaultImg.article); - defaultImg.kefu = this.img(defaultImg.kefu); - defaultImg.phone = this.img(defaultImg.phone); - return defaultImg; - }, - /** - * 判断手机是否为iphoneX系列 - */ - uniappIsIPhoneX() { - let isIphoneX = false; - let systemInfo = uni.getSystemInfoSync(); - // #ifdef MP - if (systemInfo.model.search('iPhone X') != -1 || systemInfo.model.search('iPhone 11') != -1 || systemInfo.model.search('iPhone 12') != -1 || systemInfo.model.search('iPhone 13') != -1) { - isIphoneX = true; - } - // #endif - - // #ifdef H5 - var u = navigator.userAgent; - var isIOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/); //ios终端 - if (isIOS) { - if (systemInfo.screenWidth == 375 && systemInfo.screenHeight == 812 && systemInfo.pixelRatio == 3) { - isIphoneX = true; - } else if (systemInfo.screenWidth == 414 && systemInfo.screenHeight == 896 && systemInfo.pixelRatio == 3) { - isIphoneX = true; - } else if (systemInfo.screenWidth == 414 && systemInfo.screenHeight == 896 && systemInfo.pixelRatio == 2) { - isIphoneX = true; - } - } - // #endif - return isIphoneX; - }, - /** - * 判断手机是否为iphone11系列 - */ - uniappIsIPhone11() { - let isIphone11 = false; - let systemInfo = uni.getSystemInfoSync(); - // #ifdef MP - if (systemInfo.model.search('iPhone 11') != -1) { - isIphone11 = true; - } - // #endif - return isIphone11; - }, - // #ifdef H5 - //判断该浏览器是否为safaria浏览器 - isSafari() { - let res = uni.getSystemInfoSync(); - var ua = navigator.userAgent.toLowerCase(); - if (ua.indexOf('applewebkit') > -1 && ua.indexOf('mobile') > -1 && ua.indexOf('safari') > -1 && - ua.indexOf('linux') === -1 && ua.indexOf('android') === -1 && ua.indexOf('chrome') === -1 && - ua.indexOf('ios') === -1 && ua.indexOf('browser') === -1) { - return true; - } else { - return false; - } - }, - // #endif - numberFixed(e, f) { - if (!f) { - f = 0; - } - return Number(e).toFixed(f); - }, - /** - * 获取url参数 - */ - getUrlCode(callback) { - var url = location.search; - var theRequest = new Object(); - if (url.indexOf('?') != -1) { - var str = url.substr(1); - var strs = str.split('&'); - for (var i = 0; i < strs.length; i++) { - theRequest[strs[i].split('=')[0]] = strs[i].split('=')[1]; - } - } - typeof callback == 'function' && callback(theRequest); - }, - /** - * 获取当前页面路由 - */ - getCurrRoute() { - let routes = getCurrentPages(); // 获取当前打开过的页面路由数组 - return routes.length ? routes[routes.length - 1].route : ''; - }, - goBack(backUrl = '/pages/index/index') { - if (getCurrentPages().length == 1) { - this.redirectTo(backUrl); - } else { - uni.navigateBack(); - } - }, - /** - * - * @param val 转化时间字符串 (转化时分秒) - * @returns {string} - */ - getTimeStr(val) { - var h = parseInt(val / 3600).toString(); - var m = parseInt((val % 3600) / 60).toString(); - if (m.length == 1) { - m = '0' + m; - } - if (h.length == 1) { - h = '0' + h; - } - return h + ':' + m; - }, - /** - * 获取定位信息 - */ - getLocation(param = {}) { - /*uni.getLocation({ - type: param.type ?? 'gcj02', - success: res => { - store.commit('setLocation', res); - typeof param.success == 'function' && param.success(res); - }, - fail: res => { - typeof param.fail == 'function' && param.fail(res); - }, - complete: res => { - typeof param.complete == 'function' && param.complete(res); - } - });*/ - }, - // 计算两个经纬度之间的距离 - getDistance(lat1, lng1, lat2, lng2) { - var radLat1 = lat1 * Math.PI / 180.0; - var radLat2 = lat2 * Math.PI / 180.0; - var a = radLat1 - radLat2; - var b = lng1 * Math.PI / 180.0 - lng2 * Math.PI / 180.0; - var s = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 2), 2) + - Math.cos(radLat1) * Math.cos(radLat2) * Math.pow(Math.sin(b / 2), 2))); - s = s * 6378.137; // EARTH_RADIUS; - s = Math.round(s * 10000) / 10000; - return s; - }, - //记录分享人 - onSourceMember(source_member) { - Http.sendRequest({ - url: '/api/Member/alterShareRelation', - data: { - share_member: source_member, - }, - success: res => { - if (res.code >= 0) { - uni.removeStorage({ - key: 'source_member', - success: res => { - console.log('删除成功', res) - } - }) - } - } - }) - }, - /** - * 微信订阅消息 - */ - subscribeMessage(keywords) { - // #ifdef MP-WEIXIN - let url = '/weapp/api/weapp/messagetmplids'; - // #endif - // #ifdef MP-ALIPAY - let url = '/aliapp/api/aliapp/messagetmplids'; - // #endif - Http.sendRequest({ - url, - data: { - keywords: keywords - }, - success: res => { - if (res.data.length) { - // #ifdef MP-WEIXIN - uni.requestSubscribeMessage({ - tmplIds: res.data, - success: (res) => { - console.log("res", res) - }, - fail: (res) => { - console.log('fail', res) - } - }) - // #endif - - // #ifdef MP-ALIPAY - my.requestSubscribeMessage({ - entityIds: res.data, - success: res => { - console.log("res", res) - }, - fail: res => { - console.log('fail', res) - } - }); - // #endif - } - } - }) - }, - /** - * 获取小程序分享内容数据 - */ - getMpShare(path) { - //如果没有特别指定 则获取当前页面的路由 - if (!path) { - let route = this.getCurrentRoute(); - path = route.path; - if (path == '/pages/member/index') { - return new Promise((resolve, reject) => { - resolve({}) - }); - } - } - return new Promise((resolve, reject) => { - Http.sendRequest({ - url: "/weapp/api/weapp/share", - data: { - path: path - }, - success: res => { - if (res.code >= 0) { - let shareConfig = res.data.data; - if (shareConfig) { - //分享给好友 - let appMessageData = { - title: shareConfig.title, - path: shareConfig.path, - imageUrl: shareConfig.imageUrl, - success: res => { - }, - fail: res => { - } - } - //分享到朋友圈 - let query = ''; - if (shareConfig.path.indexOf('?') > 0) { - query = shareConfig.path.split('?')[1]; - } - let timeLineData = { - title: shareConfig.title, - query: shareConfig.path, - imageUrl: shareConfig.imageUrl, - } - resolve({ - appMessage: appMessageData, - timeLine: timeLineData, - }) - } else { - reject(res.data); - } - } else { - reject(res.data) - } - } - }); - }) - }, - /** - * 设置公众号分享 - * @param shareData - * @param callback - */ - setPublicShare(shareData, callback) { - if (!this.isWeiXin()) return; - Http.sendRequest({ - url: '/wechat/api/wechat/jssdkconfig', - data: { - url: uni.getSystemInfoSync().platform == 'ios' ? uni.getStorageSync('initUrl') : window.location - .href - }, - success: res => { - if (res.code == 0) { - var wxJS = new Weixin(); - wxJS.init(res.data); - wxJS.weixin.showOptionMenu(); - - wxJS.setShareData({ - title: shareData.title ?? '', - desc: shareData.desc ?? '', - link: shareData.link ?? location.href, - imgUrl: shareData.imgUrl ? this.img(shareData.imgUrl) : '' - }, (res) => { - typeof callback == 'function' && callback(res); - }) - } - } - }) - }, - //获取当前路由 - getCurrentRoute() { - let currentRoutes = getCurrentPages(); // 获取当前打开过的页面路由数组 - let currentRoute = currentRoutes[currentRoutes.length - 1].route //获取当前页面路由 - let currentParam = currentRoutes[currentRoutes.length - 1].options; //获取路由参数 - // 拼接参数 - let param = []; - for (let key in currentParam) { - param.push(key + '=' + currentParam[key]) - } - let currentPath = '/' + currentRoute; - let currentQuery = param.join('&'); - if (currentQuery) currentPath += '?' + currentQuery; - - return { - path: currentPath, - query: currentQuery, - } - }, - //获取分享路由 - getCurrentShareRoute(member_id) { - let route = this.getCurrentRoute(); - //去掉原来的分享人数据 - route.path = route.path.replace(/[?|&]source_member=\d+/, ''); - if (member_id) { - //路径的处理 - if (route.path.indexOf('?') > 0) { - route.path += '&'; - } else { - route.path += '?'; - } - route.path += 'source_member=' + member_id; - //参数的处理 - if (route.query) { - route.query += '&'; - } - route.query += 'source_member=' + member_id; - } - return route; - }, - /** - * 对象转style字符串 - * @param {Object} obj - */ - objToStyle(obj) { - let s = []; - for (let i in obj) { - s.push(i + ':' + obj[i]); - } - return s.join(';') - }, - /** - * 颜色减值 - * @param {Object} c1 - * @param {Object} c2 - * @param {Object} ratio - */ - colourBlend(c1, c2, ratio) { - ratio = Math.max(Math.min(Number(ratio), 1), 0) - let r1 = parseInt(c1.substring(1, 3), 16) - let g1 = parseInt(c1.substring(3, 5), 16) - let b1 = parseInt(c1.substring(5, 7), 16) - let r2 = parseInt(c2.substring(1, 3), 16) - let g2 = parseInt(c2.substring(3, 5), 16) - let b2 = parseInt(c2.substring(5, 7), 16) - let r = Math.round(r1 * (1 - ratio) + r2 * ratio) - let g = Math.round(g1 * (1 - ratio) + g2 * ratio) - let b = Math.round(b1 * (1 - ratio) + b2 * ratio) - r = ('0' + (r || 0).toString(16)).slice(-2) - g = ('0' + (g || 0).toString(16)).slice(-2) - b = ('0' + (b || 0).toString(16)).slice(-2) - return '#' + r + g + b - }, - /** - * 生成贝塞尔曲线轨迹 - * @param {Object} points - * @param {Object} times - */ - bezier(points, times) { - var bezier_points = []; - var points_D = []; - var points_E = []; - const DIST_AB = Math.sqrt(Math.pow(points[1]['x'] - points[0]['x'], 2) + Math.pow(points[1]['y'] - points[0][ - 'y' - ], 2)); - // 邻控制BC点间距 - const DIST_BC = Math.sqrt(Math.pow(points[2]['x'] - points[1]['x'], 2) + Math.pow(points[2]['y'] - points[1][ - 'y' - ], 2)); - // D每次在AB方向上移动的距离 - if (points[0]['x'] > points[2]['x']) { - var EACH_MOVE_AD = -(DIST_AB / times); - // E每次在BC方向上移动的距离 - var EACH_MOVE_BE = -(DIST_BC / times); - } else { - var EACH_MOVE_AD = +(DIST_AB / times); - // E每次在BC方向上移动的距离 - var EACH_MOVE_BE = +(DIST_BC / times); - } - // 点AB的正切 - const TAN_AB = (points[1]['y'] - points[0]['y']) / (points[1]['x'] - points[0]['x']); - // 点BC的正切 - const TAN_BC = (points[2]['y'] - points[1]['y']) / (points[2]['x'] - points[1]['x']); - // 点AB的弧度值 - const RADIUS_AB = Math.atan(TAN_AB); - // 点BC的弧度值 - const RADIUS_BC = Math.atan(TAN_BC); - // 每次执行 - for (var i = 1; i <= times; i++) { - // AD的距离 - var dist_AD = EACH_MOVE_AD * i; - // BE的距离 - var dist_BE = EACH_MOVE_BE * i; - // D点的坐标 - var point_D = {}; - point_D['x'] = dist_AD * Math.cos(RADIUS_AB) + points[0]['x']; - point_D['y'] = dist_AD * Math.sin(RADIUS_AB) + points[0]['y']; - points_D.push(point_D); - // E点的坐标 - var point_E = {}; - point_E['x'] = dist_BE * Math.cos(RADIUS_BC) + points[1]['x']; - point_E['y'] = dist_BE * Math.sin(RADIUS_BC) + points[1]['y']; - points_E.push(point_E); - // 此时线段DE的正切值 - var tan_DE = (point_E['y'] - point_D['y']) / (point_E['x'] - point_D['x']); - // tan_DE的弧度值 - var radius_DE = Math.atan(tan_DE); - // 地市DE的间距 - var dist_DE = Math.sqrt(Math.pow((point_E['x'] - point_D['x']), 2) + Math.pow((point_E['y'] - point_D['y']), - 2)); - // 此时DF的距离 - var dist_DF = (dist_AD / DIST_AB) * dist_DE; - // 此时DF点的坐标 - var point_F = {}; - point_F['x'] = dist_DF * Math.cos(radius_DE) + point_D['x']; - point_F['y'] = dist_DF * Math.sin(radius_DE) + point_D['y']; - bezier_points.push(point_F); - } - return { - 'bezier_points': bezier_points - }; - }, - /** - * 验证手机号 - * @param {string} mobile 被验证的mobile - * @return {object} 验证后的结果 - **/ - verifyMobile(mobile) { - var parse = /^\d{11}$/.test(mobile); - return parse; - } +import Config from './config.js' +import store from '@/store/index.js' +import Http from './http.js' +import { + Weixin +} from 'common/js/wx-jssdk.js'; + +export default { + /** + * 页面跳转 + * @param {string} to 跳转链接 /pages/idnex/index + * @param {Object} param 参数 {key : value, ...} + * @param {string} mode 模式 + */ + redirectTo(to, param, mode) { + let url = to; + let tabbarList = ['/pages/index/index', '/pages/goods/category', '/pages/vr/index', '/pages/contact/contact', '/pages/member/index']; + if (param != undefined) { + Object.keys(param).forEach(function (key) { + if (url.indexOf('?') != -1) { + url += "&" + key + "=" + param[key]; + } else { + url += "?" + key + "=" + param[key]; + } + }); + } + for (let i = 0; i < tabbarList.length; i++) { + if (url.indexOf(tabbarList[i]) == 0) { + uni.switchTab({ + url + }); + return; + } + } + switch (mode) { + case 'tabbar': + // 跳转到 tabBar 页面,并关闭其他所有非 tabBar 页面。 + uni.switchTab({ + url + }); + break; + case 'redirectTo': + // 关闭当前页面,跳转到应用内的某个页面。 + uni.redirectTo({ + url + }); + break; + case 'reLaunch': + // 关闭所有页面,打开到应用内的某个页面。 + uni.reLaunch({ + url + }); + break; + default: + // 保留当前页面,跳转到应用内的某个页面 + uni.navigateTo({ + url + }); + } + }, + /** + * 图片路径转换 + * @param {String} img_path 图片地址 + * @param {Object} params 参数,针对商品、相册里面的图片区分大中小,size: big、mid、small + */ + img(img_path, params) { + + + var path = ""; + if (img_path != undefined && img_path != "") { + if (img_path.split(',').length > 1) { + img_path = img_path.split(',')[0]; + } + if (params && img_path != this.getDefaultImage().goods) { + // 过滤默认图 + let arr = img_path.split("."); + let suffix = arr[arr.length - 1]; + arr.pop(); + arr[arr.length - 1] = arr[arr.length - 1] + "_" + params.size.toUpperCase(); + arr.push(suffix); + // if(img_path.indexOf('attachment') == -1){ + // img_path = arr.join("."); + // } + + } + if (img_path.indexOf("http://") == -1 && img_path.indexOf("https://") == -1) { + path = Config.imgDomain + "/" + img_path; + } else { + // console.log(img_path) + path = img_path; + } + } + + // path += '?t=' + parseInt(new Date().getTime() / 1000); + return path; + }, + /** + * 时间戳转日期格式 + * @param {Object} timeStamp + */ + timeStampTurnTime(timeStamp, type = "") { + if (timeStamp != undefined && timeStamp != "" && timeStamp > 0) { + var date = new Date(); + date.setTime(timeStamp * 1000); + var y = date.getFullYear(); + var m = date.getMonth() + 1; + m = m < 10 ? ('0' + m) : m; + var d = date.getDate(); + d = d < 10 ? ('0' + d) : d; + var h = date.getHours(); + h = h < 10 ? ('0' + h) : h; + var minute = date.getMinutes(); + var second = date.getSeconds(); + minute = minute < 10 ? ('0' + minute) : minute; + second = second < 10 ? ('0' + second) : second; + if (type) { + if (type == 'yearMonthDay') { + return y + '年' + m + '月' + d + '日'; + } + return y + '-' + m + '-' + d; + } else { + return y + '-' + m + '-' + d + ' ' + h + ':' + minute + ':' + second; + } + } else { + return ""; + } + }, + /** + * 日期格式转时间戳 + * @param {Object} string + */ + timeTurnTimeStamp(string) { + var f = string.split(' ', 2); + var d = (f[0] ? f[0] : '').split('-', 3); + var t = (f[1] ? f[1] : '').split(':', 3); + return (new Date( + parseInt(d[0], 10) || null, + (parseInt(d[1], 10) || 1) - 1, + parseInt(d[2], 10) || null, + parseInt(t[0], 10) || null, + parseInt(t[1], 10) || null, + parseInt(t[2], 10) || null + )).getTime() / 1000; + }, + /** + * 倒计时 + * @param {Object} seconds 秒 + */ + countDown(seconds) { + let [day, hour, minute, second] = [0, 0, 0, 0] + if (seconds > 0) { + day = Math.floor(seconds / (60 * 60 * 24)) + hour = Math.floor(seconds / (60 * 60)) - (day * 24) + minute = Math.floor(seconds / 60) - (day * 24 * 60) - (hour * 60) + second = Math.floor(seconds) - (day * 24 * 60 * 60) - (hour * 60 * 60) - (minute * 60) + } + if (day < 10) { + day = '0' + day + } + if (hour < 10) { + hour = '0' + hour + } + if (minute < 10) { + minute = '0' + minute + } + if (second < 10) { + second = '0' + second + } + return { + d: day, + h: hour, + i: minute, + s: second + }; + }, + /** + * 数值去重 + * @param {Array} arr 数组 + * @param {string} field 字段 + */ + unique(arr, field) { + const res = new Map(); + return arr.filter((a) => !res.has(a[field]) && res.set(a[field], 1)); + }, + /** + * 判断值是否在数组中 + * @param {Object} elem + * @param {Object} arr + */ + inArray: function (elem, arr) { + return arr == null ? -1 : arr.indexOf(elem); + }, + /** + * 获取某天日期 + * @param {Object} day + */ + getDay: function (day) { + var today = new Date(); + var targetday_milliseconds = today.getTime() + 1000 * 60 * 60 * 24 * day; + today.setTime(targetday_milliseconds); + + const doHandleMonth = function (month) { + var m = month; + if (month.toString().length == 1) { + m = "0" + month; + } + return m + } + + var tYear = today.getFullYear(); + var tMonth = today.getMonth(); + var tDate = today.getDate(); + var tWeek = today.getDay(); + var time = parseInt(today.getTime() / 1000); + tMonth = doHandleMonth(tMonth + 1); + tDate = doHandleMonth(tDate); + + const week = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'] + return { + 't': time, + 'y': tYear, + 'm': tMonth, + 'd': tDate, + 'w': week[tWeek] + }; + }, + /** + * 图片选择加上传 + * @param num + * @param params + * @param callback + * @param url + */ + upload: function (num, params, callback, url) { + // #ifdef H5 + var app_type = this.isWeiXin() ? 'wechat' : 'h5'; + var app_type_name = this.isWeiXin() ? '微信公众号' : 'H5'; + // #endif + + // #ifdef MP-WEIXIN + var app_type = 'weapp'; + var app_type_name = '微信小程序'; + // #endif + + // #ifdef MP-ALIPAY + var app_type = 'aliapp'; + var app_type_name = '支付宝小程序'; + // #endif + + // #ifdef MP-BAIDU + var app_type = 'baiduapp'; + var app_type_name = '百度小程序'; + // #endif + + // #ifdef MP-TOUTIAO + var app_type = 'MP-TOUTIAO'; + var app_type_name = '头条小程序'; + // #endif + + // #ifdef MP-QQ + var app_type = 'MP-QQ'; + var app_type_name = 'QQ小程序'; + // #endif + var data = { + token: store.state.token, + app_type: app_type, + app_type_name: app_type_name + } + data = Object.assign(data, params); + + var imgs_num = num; + var _self = this; + + uni.chooseImage({ + count: imgs_num, + sizeType: ['compressed'], //可以指定是原图还是压缩图,默认二者都有 + sourceType: ['album', 'camera'], //从相册或者拍照 + success: async function (res) { + const tempFilePaths = res.tempFilePaths; + var _data = data; + var imgs = []; + uni.showLoading({ + title: '图片上传中' + }) + for (var i = 0; i < tempFilePaths.length; i++) { + var path = await _self.upload_file_server(tempFilePaths[i], _data, params.path, + url); + imgs.push(path); + if (imgs.length == tempFilePaths.length) { + uni.hideLoading() + uni.showToast({ + title: '上传成功', + icon: 'none' + }) + typeof callback == 'function' && callback(imgs); + } + } + }, + fail: err => { + uni.hideLoading() + uni.showToast({ + title: '上传失败', + icon: 'none' + }) + } + }); + }, + //上传 + upload_file_server(tempFilePath, data, path, url = "", callback) { + if (url) { + var uploadUrl = Config.baseUrl + url + } else { + var uploadUrl = Config.baseUrl + '/api/upload/' + path + } + return new Promise((resolve, reject) => { + uni.uploadFile({ + url: uploadUrl, + filePath: tempFilePath, + name: 'file', + fileType: data.fileType || 'image', + formData: data, + success: function (res) { + var path_str = JSON.parse(res.data); + if (path_str.code >= 0) { + resolve(path_str.data.pic_path); + typeof callback == 'function' && callback(path_str.data.pic_path); + } else { + reject("error"); + } + } + }); + + }); + + }, + /** + * 复制 + * @param {Object} value + * @param {Object} callback + */ + copy(value, callback) { + // #ifdef H5 + var oInput = document.createElement('input'); //创建一个隐藏input(重要!) + oInput.value = value; //赋值 + oInput.setAttribute("readonly", "readonly"); + document.body.appendChild(oInput); + oInput.select(); // 选择对象 + document.execCommand("Copy"); // 执行浏览器复制命令 + oInput.className = 'oInput'; + oInput.style.display = 'none'; + uni.hideKeyboard(); + this.showToast({ + title: '复制成功' + }); + + typeof callback == 'function' && callback(); + // #endif + + // #ifdef MP || APP-PLUS + uni.setClipboardData({ + data: value, + success: () => { + typeof callback == 'function' && callback(); + } + }); + // #endif + }, + /** + * 是否是微信浏览器 + */ + isWeiXin() { + // #ifndef H5 + return false; + // #endif + var ua = navigator.userAgent.toLowerCase(); + if (ua.match(/MicroMessenger/i) == "micromessenger") { + return true; + } else { + return false; + } + }, + /** + * 显示消息提示框 + * @param {Object} params 参数 + */ + showToast(params = {}) { + params.title = params.title || ""; + params.icon = params.icon || "none"; + // params.position = params.position || 'bottom'; + params.duration = params.duration || 1500; + uni.showToast(params); + if (params.success) params.success(); + }, + /** + * 检测苹果X以上的手机 + */ + isIPhoneX() { + let res = uni.getSystemInfoSync(); + if (res.model.search('iPhone X') != -1) { + return true; + } + return false; + }, + //判断安卓还是iOS + isAndroid() { + let platform = uni.getSystemInfoSync().platform + if (platform == 'ios') { + return false; + } else if (platform == 'android') { + return true; + } + }, + /** + * 深度拷贝对象 + * @param {Object} obj + */ + deepClone(obj) { + const isObject = function (obj) { + return typeof obj == 'object'; + } + + if (!isObject(obj)) { + throw new Error('obj 不是一个对象!') + } + //判断传进来的是对象还是数组 + let isArray = Array.isArray(obj) + let cloneObj = isArray ? [] : {} + //通过for...in来拷贝 + for (let key in obj) { + cloneObj[key] = isObject(obj[key]) ? this.deepClone(obj[key]) : obj[key] + } + return cloneObj + }, + + /** + * 打开微信企业客服 + * @param {Function} fallbackFunc 降级处理函数 + */ + openWxWorkServiceChat(fallbackFunc) { + // #ifdef MP-WEIXIN + let wxworkConfig = store.state?.wxworkConfig + if (wxworkConfig?.enable && wxworkConfig?.contact_url) { + // 直接使用活码链接跳转 + wx.navigateToMiniProgram({ + appId: 'wxeb490c6f9b154ef9', // 是企业微信官方小程序的AppID(固定值, 由腾讯企业微信团队维护, 不需要修改,用于展示"添加企业微信联系人"的官方页面) + path: `pages/contacts/externalContactDetail?url=${encodeURIComponent(wxworkConfig?.contact_url)}`, + success: () => { + console.log('跳转企业微信成功'); + }, + fail: (err) => { + console.error('跳转企业微信失败:', err); + // 降级处理:使用原有客服方式 + fallbackFunc && fallbackFunc(); + } + }); + } else { + fallbackFunc && fallbackFunc(); + } + // #endif + }, + + /** + * 自定义模板的跳转链接 + * @param {Object} link + */ + diyRedirectTo(link) { + + //if (link == null || Object.keys(link).length == 1) return; + + // 外部链接 + if (link.wap_url && link.wap_url.indexOf('http') != -1 || link.wap_url && link.wap_url.indexOf('http') != -1) { + // #ifdef H5 + window.location.href = link.wap_url; + // #endif + + // #ifdef MP + this.redirectTo('/pages_tool/webview/webview', { + src: encodeURIComponent(link.wap_url) + }); + // #endif + + } else if (link.appid) { + // 跳转其他小程序 + + uni.navigateToMiniProgram({ + appId: link.appid, + path: link.page + }) + + } else if (link.name == 'MOBILE' && !link.wap_url) { + // 拨打电话 + + uni.makePhoneCall({ + phoneNumber: link.mobile, + success: (res) => { + }, + fail: (res) => { + } + }); + + } else if (link.name == 'MEMBER_CONTACT') { + // 客服 + let servicerConfig = store.state.servicerConfig + let config = { + type: 'none' + }; + + // #ifdef H5 + config = servicerConfig.h5; + // #endif + + // #ifdef MP-WEIXIN + config = servicerConfig.weapp; + // #endif + + switch (config.type) { + case 'wxwork': + // 企业微信客服 + + // #ifdef H5 + window.location.href = config.wxwork_url; + // #endif + + // #ifdef MP-WEIXIN + this.openWxWorkServiceChat(() => { + wx.openCustomerServiceChat({ + extInfo: { + url: config.wxwork_url + }, + corpId: config.corpid, + showMessageCard: true, + sendMessageTitle: 'this.sendMessageTitle', + sendMessagePath: 'this.sendMessagePath', + sendMessageImg: 'this.sendMessageImg' + }); + }); + + // #endif + break; + case 'third': + // 第三方客服 + window.location.href = config.third_url; + break; + case 'niushop': + // Niushop客服 + this.redirectTo('/pages_tool/chat/room'); + break; + case 'weapp': + // 微信小程序,由于需要手动点击按钮触发,所以要跳转到中间页 + this.redirectTo(link.wap_url); + break; + default: + // 拨打客服电话 + let siteInfo = store.state.siteInfo; + if (siteInfo && siteInfo.site_tel) { + uni.makePhoneCall({ + phoneNumber: siteInfo.site_tel + }); + } else { + this.showToast({ + title: '抱歉,商家暂无客服,请线下联系', + }) + } + } + + } else if (link.wap_url) { + this.redirectTo(link.wap_url); + } + }, + /** + * 获取默认图 + */ + getDefaultImage() { + let defaultImg = store.state.defaultImg; + defaultImg.goods = this.img(defaultImg.goods); + defaultImg.head = this.img(defaultImg.head); + defaultImg.store = this.img(defaultImg.store); + defaultImg.article = this.img(defaultImg.article); + defaultImg.kefu = this.img(defaultImg.kefu); + defaultImg.phone = this.img(defaultImg.phone); + return defaultImg; + }, + /** + * 判断手机是否为iphoneX系列 + */ + uniappIsIPhoneX() { + let isIphoneX = false; + let systemInfo = uni.getSystemInfoSync(); + // #ifdef MP + if (systemInfo.model.search('iPhone X') != -1 || systemInfo.model.search('iPhone 11') != -1 || systemInfo.model.search('iPhone 12') != -1 || systemInfo.model.search('iPhone 13') != -1) { + isIphoneX = true; + } + // #endif + + // #ifdef H5 + var u = navigator.userAgent; + var isIOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/); //ios终端 + if (isIOS) { + if (systemInfo.screenWidth == 375 && systemInfo.screenHeight == 812 && systemInfo.pixelRatio == 3) { + isIphoneX = true; + } else if (systemInfo.screenWidth == 414 && systemInfo.screenHeight == 896 && systemInfo.pixelRatio == 3) { + isIphoneX = true; + } else if (systemInfo.screenWidth == 414 && systemInfo.screenHeight == 896 && systemInfo.pixelRatio == 2) { + isIphoneX = true; + } + } + // #endif + return isIphoneX; + }, + /** + * 判断手机是否为iphone11系列 + */ + uniappIsIPhone11() { + let isIphone11 = false; + let systemInfo = uni.getSystemInfoSync(); + // #ifdef MP + if (systemInfo.model.search('iPhone 11') != -1) { + isIphone11 = true; + } + // #endif + return isIphone11; + }, + // #ifdef H5 + //判断该浏览器是否为safaria浏览器 + isSafari() { + let res = uni.getSystemInfoSync(); + var ua = navigator.userAgent.toLowerCase(); + if (ua.indexOf('applewebkit') > -1 && ua.indexOf('mobile') > -1 && ua.indexOf('safari') > -1 && + ua.indexOf('linux') === -1 && ua.indexOf('android') === -1 && ua.indexOf('chrome') === -1 && + ua.indexOf('ios') === -1 && ua.indexOf('browser') === -1) { + return true; + } else { + return false; + } + }, + // #endif + numberFixed(e, f) { + if (!f) { + f = 0; + } + return Number(e).toFixed(f); + }, + /** + * 获取url参数 + */ + getUrlCode(callback) { + var url = location.search; + var theRequest = new Object(); + if (url.indexOf('?') != -1) { + var str = url.substr(1); + var strs = str.split('&'); + for (var i = 0; i < strs.length; i++) { + theRequest[strs[i].split('=')[0]] = strs[i].split('=')[1]; + } + } + typeof callback == 'function' && callback(theRequest); + }, + /** + * 获取当前页面路由 + */ + getCurrRoute() { + let routes = getCurrentPages(); // 获取当前打开过的页面路由数组 + return routes.length ? routes[routes.length - 1].route : ''; + }, + goBack(backUrl = '/pages/index/index') { + if (getCurrentPages().length == 1) { + this.redirectTo(backUrl); + } else { + uni.navigateBack(); + } + }, + /** + * + * @param val 转化时间字符串 (转化时分秒) + * @returns {string} + */ + getTimeStr(val) { + var h = parseInt(val / 3600).toString(); + var m = parseInt((val % 3600) / 60).toString(); + if (m.length == 1) { + m = '0' + m; + } + if (h.length == 1) { + h = '0' + h; + } + return h + ':' + m; + }, + /** + * 获取定位信息 + */ + getLocation(param = {}) { + /*uni.getLocation({ + type: param.type ?? 'gcj02', + success: res => { + store.commit('setLocation', res); + typeof param.success == 'function' && param.success(res); + }, + fail: res => { + typeof param.fail == 'function' && param.fail(res); + }, + complete: res => { + typeof param.complete == 'function' && param.complete(res); + } + });*/ + }, + // 计算两个经纬度之间的距离 + getDistance(lat1, lng1, lat2, lng2) { + var radLat1 = lat1 * Math.PI / 180.0; + var radLat2 = lat2 * Math.PI / 180.0; + var a = radLat1 - radLat2; + var b = lng1 * Math.PI / 180.0 - lng2 * Math.PI / 180.0; + var s = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 2), 2) + + Math.cos(radLat1) * Math.cos(radLat2) * Math.pow(Math.sin(b / 2), 2))); + s = s * 6378.137; // EARTH_RADIUS; + s = Math.round(s * 10000) / 10000; + return s; + }, + //记录分享人 + onSourceMember(source_member) { + Http.sendRequest({ + url: '/api/Member/alterShareRelation', + data: { + share_member: source_member, + }, + success: res => { + if (res.code >= 0) { + uni.removeStorage({ + key: 'source_member', + success: res => { + console.log('删除成功', res) + } + }) + } + } + }) + }, + /** + * 微信订阅消息 + */ + subscribeMessage(keywords) { + // #ifdef MP-WEIXIN + let url = '/weapp/api/weapp/messagetmplids'; + // #endif + // #ifdef MP-ALIPAY + let url = '/aliapp/api/aliapp/messagetmplids'; + // #endif + Http.sendRequest({ + url, + data: { + keywords: keywords + }, + success: res => { + if (res.data.length) { + // #ifdef MP-WEIXIN + uni.requestSubscribeMessage({ + tmplIds: res.data, + success: (res) => { + console.log("res", res) + }, + fail: (res) => { + console.log('fail', res) + } + }) + // #endif + + // #ifdef MP-ALIPAY + my.requestSubscribeMessage({ + entityIds: res.data, + success: res => { + console.log("res", res) + }, + fail: res => { + console.log('fail', res) + } + }); + // #endif + } + } + }) + }, + /** + * 获取小程序分享内容数据 + */ + getMpShare(path) { + //如果没有特别指定 则获取当前页面的路由 + if (!path) { + let route = this.getCurrentRoute(); + path = route.path; + if (path == '/pages/member/index') { + return new Promise((resolve, reject) => { + resolve({}) + }); + } + } + return new Promise((resolve, reject) => { + Http.sendRequest({ + url: "/weapp/api/weapp/share", + data: { + path: path + }, + success: res => { + if (res.code >= 0) { + let shareConfig = res.data.data; + if (shareConfig) { + //分享给好友 + let appMessageData = { + title: shareConfig.title, + path: shareConfig.path, + imageUrl: shareConfig.imageUrl, + success: res => { + }, + fail: res => { + } + } + //分享到朋友圈 + let query = ''; + if (shareConfig.path.indexOf('?') > 0) { + query = shareConfig.path.split('?')[1]; + } + let timeLineData = { + title: shareConfig.title, + query: shareConfig.path, + imageUrl: shareConfig.imageUrl, + } + resolve({ + appMessage: appMessageData, + timeLine: timeLineData, + }) + } else { + reject(res.data); + } + } else { + reject(res.data) + } + } + }); + }) + }, + /** + * 设置公众号分享 + * @param shareData + * @param callback + */ + setPublicShare(shareData, callback) { + if (!this.isWeiXin()) return; + Http.sendRequest({ + url: '/wechat/api/wechat/jssdkconfig', + data: { + url: uni.getSystemInfoSync().platform == 'ios' ? uni.getStorageSync('initUrl') : window.location + .href + }, + success: res => { + if (res.code == 0) { + var wxJS = new Weixin(); + wxJS.init(res.data); + wxJS.weixin.showOptionMenu(); + + wxJS.setShareData({ + title: shareData.title ?? '', + desc: shareData.desc ?? '', + link: shareData.link ?? location.href, + imgUrl: shareData.imgUrl ? this.img(shareData.imgUrl) : '' + }, (res) => { + typeof callback == 'function' && callback(res); + }) + } + } + }) + }, + //获取当前路由 + getCurrentRoute() { + let currentRoutes = getCurrentPages(); // 获取当前打开过的页面路由数组 + let currentRoute = currentRoutes[currentRoutes.length - 1].route //获取当前页面路由 + let currentParam = currentRoutes[currentRoutes.length - 1].options; //获取路由参数 + // 拼接参数 + let param = []; + for (let key in currentParam) { + param.push(key + '=' + currentParam[key]) + } + let currentPath = '/' + currentRoute; + let currentQuery = param.join('&'); + if (currentQuery) currentPath += '?' + currentQuery; + + return { + path: currentPath, + query: currentQuery, + } + }, + //获取分享路由 + getCurrentShareRoute(member_id) { + let route = this.getCurrentRoute(); + //去掉原来的分享人数据 + route.path = route.path.replace(/[?|&]source_member=\d+/, ''); + if (member_id) { + //路径的处理 + if (route.path.indexOf('?') > 0) { + route.path += '&'; + } else { + route.path += '?'; + } + route.path += 'source_member=' + member_id; + //参数的处理 + if (route.query) { + route.query += '&'; + } + route.query += 'source_member=' + member_id; + } + return route; + }, + /** + * 对象转style字符串 + * @param {Object} obj + */ + objToStyle(obj) { + let s = []; + for (let i in obj) { + s.push(i + ':' + obj[i]); + } + return s.join(';') + }, + /** + * 颜色减值 + * @param {Object} c1 + * @param {Object} c2 + * @param {Object} ratio + */ + colourBlend(c1, c2, ratio) { + ratio = Math.max(Math.min(Number(ratio), 1), 0) + let r1 = parseInt(c1.substring(1, 3), 16) + let g1 = parseInt(c1.substring(3, 5), 16) + let b1 = parseInt(c1.substring(5, 7), 16) + let r2 = parseInt(c2.substring(1, 3), 16) + let g2 = parseInt(c2.substring(3, 5), 16) + let b2 = parseInt(c2.substring(5, 7), 16) + let r = Math.round(r1 * (1 - ratio) + r2 * ratio) + let g = Math.round(g1 * (1 - ratio) + g2 * ratio) + let b = Math.round(b1 * (1 - ratio) + b2 * ratio) + r = ('0' + (r || 0).toString(16)).slice(-2) + g = ('0' + (g || 0).toString(16)).slice(-2) + b = ('0' + (b || 0).toString(16)).slice(-2) + return '#' + r + g + b + }, + /** + * 生成贝塞尔曲线轨迹 + * @param {Object} points + * @param {Object} times + */ + bezier(points, times) { + var bezier_points = []; + var points_D = []; + var points_E = []; + const DIST_AB = Math.sqrt(Math.pow(points[1]['x'] - points[0]['x'], 2) + Math.pow(points[1]['y'] - points[0][ + 'y' + ], 2)); + // 邻控制BC点间距 + const DIST_BC = Math.sqrt(Math.pow(points[2]['x'] - points[1]['x'], 2) + Math.pow(points[2]['y'] - points[1][ + 'y' + ], 2)); + // D每次在AB方向上移动的距离 + if (points[0]['x'] > points[2]['x']) { + var EACH_MOVE_AD = -(DIST_AB / times); + // E每次在BC方向上移动的距离 + var EACH_MOVE_BE = -(DIST_BC / times); + } else { + var EACH_MOVE_AD = +(DIST_AB / times); + // E每次在BC方向上移动的距离 + var EACH_MOVE_BE = +(DIST_BC / times); + } + // 点AB的正切 + const TAN_AB = (points[1]['y'] - points[0]['y']) / (points[1]['x'] - points[0]['x']); + // 点BC的正切 + const TAN_BC = (points[2]['y'] - points[1]['y']) / (points[2]['x'] - points[1]['x']); + // 点AB的弧度值 + const RADIUS_AB = Math.atan(TAN_AB); + // 点BC的弧度值 + const RADIUS_BC = Math.atan(TAN_BC); + // 每次执行 + for (var i = 1; i <= times; i++) { + // AD的距离 + var dist_AD = EACH_MOVE_AD * i; + // BE的距离 + var dist_BE = EACH_MOVE_BE * i; + // D点的坐标 + var point_D = {}; + point_D['x'] = dist_AD * Math.cos(RADIUS_AB) + points[0]['x']; + point_D['y'] = dist_AD * Math.sin(RADIUS_AB) + points[0]['y']; + points_D.push(point_D); + // E点的坐标 + var point_E = {}; + point_E['x'] = dist_BE * Math.cos(RADIUS_BC) + points[1]['x']; + point_E['y'] = dist_BE * Math.sin(RADIUS_BC) + points[1]['y']; + points_E.push(point_E); + // 此时线段DE的正切值 + var tan_DE = (point_E['y'] - point_D['y']) / (point_E['x'] - point_D['x']); + // tan_DE的弧度值 + var radius_DE = Math.atan(tan_DE); + // 地市DE的间距 + var dist_DE = Math.sqrt(Math.pow((point_E['x'] - point_D['x']), 2) + Math.pow((point_E['y'] - point_D['y']), + 2)); + // 此时DF的距离 + var dist_DF = (dist_AD / DIST_AB) * dist_DE; + // 此时DF点的坐标 + var point_F = {}; + point_F['x'] = dist_DF * Math.cos(radius_DE) + point_D['x']; + point_F['y'] = dist_DF * Math.sin(radius_DE) + point_D['y']; + bezier_points.push(point_F); + } + return { + 'bezier_points': bezier_points + }; + }, + /** + * 验证手机号 + * @param {string} mobile 被验证的mobile + * @return {object} 验证后的结果 + **/ + verifyMobile(mobile) { + var parse = /^\d{11}$/.test(mobile); + return parse; + } } \ No newline at end of file diff --git a/common/js/wxwork-jssdk.js b/common/js/wxwork-jssdk.js new file mode 100644 index 0000000..bd2932b --- /dev/null +++ b/common/js/wxwork-jssdk.js @@ -0,0 +1,187 @@ +/** + * 企业微信JS-SDK调用 + */ +let WxWork = function () { + // 企业微信JS-SDK + this.wxwork = null; + + /** + * 初始化企业微信JS-SDK + * @param {Object} params - 初始化参数 + * @param {string} params.corpId - 企业ID + * @param {string} params.agentId - 应用ID + * @param {string} params.timestamp - 时间戳 + * @param {string} params.nonceStr - 随机字符串 + * @param {string} params.signature - 签名 + * @param {Array} params.jsApiList - 需要使用的JS接口列表 + */ + this.init = function (params) { + if (typeof wx !== 'undefined' && wx.config) { + // 小程序环境下的企业微信 + this.wxwork = wx; + } else if (typeof WWOpenData !== 'undefined') { + // H5环境下的企业微信 + this.wxwork = WWOpenData; + } else { + console.error('企业微信JS-SDK未加载'); + return false; + } + + this.wxwork.config({ + beta: true, // 必须这么写,否则wx.invoke调用形式的jsapi会有问题 + debug: false, // 开启调试模式 + corpId: params.corpId, // 必填,企业号的唯一标识 + agentId: params.agentId, // 必填,企业微信应用ID + timestamp: params.timestamp, // 必填,生成签名的时间戳 + nonceStr: params.nonceStr, // 必填,生成签名的随机串 + signature: params.signature, // 必填,签名 + jsApiList: params.jsApiList || [ + 'openUserProfile', + 'openEnterpriseChat', + 'getContext', + 'getCurExternalContact', + 'openExistedChatWithMsg' + ] // 必填,需要使用的JS接口列表 + }); + + return true; + }; + + /** + * 添加企业微信联系人 + * @param {Object} params - 参数 + * @param {string} params.userId - 用户ID + * @param {Function} success - 成功回调 + * @param {Function} fail - 失败回调 + */ + this.addContact = function (params, success, fail) { + if (!this.wxwork) { + console.error('企业微信JS-SDK未初始化'); + if (fail) fail('企业微信JS-SDK未初始化'); + return; + } + + this.wxwork.ready(() => { + this.wxwork.invoke('openUserProfile', { + type: 'external', // 外部联系人 + userId: params.userId // 用户ID + }, (res) => { + if (res.err_msg === 'openUserProfile:ok') { + if (success) success(res); + } else { + console.error('打开用户资料失败:', res); + if (fail) fail(res.err_msg); + } + }); + }); + }; + + /** + * 打开企业微信客服会话 + * @param {Object} params - 参数 + * @param {string} params.corpId - 企业ID + * @param {string} params.url - 客服URL + * @param {string} params.name - 会话名称 + * @param {Function} success - 成功回调 + * @param {Function} fail - 失败回调 + */ + this.openCustomerService = function (params, success, fail) { + if (!this.wxwork) { + console.error('企业微信JS-SDK未初始化'); + if (fail) fail('企业微信JS-SDK未初始化'); + return; + } + + this.wxwork.ready(() => { + // #ifdef MP-WEIXIN + if (typeof wx !== 'undefined' && wx.openCustomerServiceChat) { + // 微信小程序环境 + wx.openCustomerServiceChat({ + extInfo: { + url: params.url + }, + corpId: params.corpId, + showMessageCard: true, + sendMessageTitle: params.sendMessageTitle || '', + sendMessagePath: params.sendMessagePath || '', + sendMessageImg: params.sendMessageImg || '' + }); + if (success) success(); + } + // #endif + // #ifdef H5 + else if (typeof WWOpenData !== 'undefined') { + // H5环境 + window.location.href = params.url; + if (success) success(); + } + // #endif + else { + // 直接跳转链接 + window.location.href = params.url; + if (success) success(); + } + }); + }; + + /** + * 生成企业微信活码链接 + * @param {Object} params - 参数 + * @param {string} params.configId - 活码配置ID + * @param {string} params.userId - 用户ID + * @returns {string} 活码链接 + */ + this.generateContactUrl = function (params) { + // 企业微信活码链接格式 + const baseUrl = 'https://work.weixin.qq.com/kfid'; + if (params.configId) { + return `${baseUrl}/${params.configId}`; + } + return null; + }; + + /** + * 检查环境是否支持企业微信 + * @returns {boolean} 是否支持 + */ + this.isSupported = function () { + // #ifdef MP-WEIXIN + return typeof wx !== 'undefined' && wx.openCustomerServiceChat; + // #endif + // #ifdef H5 + return typeof WWOpenData !== 'undefined' || /wxwork/i.test(navigator.userAgent); + // #endif + return false; + }; + + /** + * 获取当前环境信息 + * @returns {Object} 环境信息 + */ + this.getEnvironment = function () { + // #ifdef MP-WEIXIN + return { + platform: 'miniprogram', + isWxWork: false, + supportContact: typeof wx !== 'undefined' && wx.openCustomerServiceChat + }; + // #endif + // #ifdef H5 + const isWxWork = /wxwork/i.test(navigator.userAgent); + return { + platform: 'h5', + isWxWork: isWxWork, + supportContact: isWxWork || typeof WWOpenData !== 'undefined' + }; + // #endif + return { + platform: 'unknown', + isWxWork: false, + supportContact: false + }; + }; +} + +export { + WxWork +} \ No newline at end of file diff --git a/components/diy-components/diy-audio.vue b/components/diy-components/diy-audio.vue index c7ddc3e..28b11c6 100644 --- a/components/diy-components/diy-audio.vue +++ b/components/diy-components/diy-audio.vue @@ -35,7 +35,7 @@ \ No newline at end of file diff --git a/components/ns-contact/ns-contact.vue b/components/ns-contact/ns-contact.vue index b69bca8..fc97a74 100644 --- a/components/ns-contact/ns-contact.vue +++ b/components/ns-contact/ns-contact.vue @@ -1,171 +1,170 @@ - - - - - - \ No newline at end of file diff --git a/components/wxwork-contact/CHANGELOG.md b/components/wxwork-contact/CHANGELOG.md new file mode 100644 index 0000000..bbd49e1 --- /dev/null +++ b/components/wxwork-contact/CHANGELOG.md @@ -0,0 +1,66 @@ +# 企业微信联系客服组件更新日志 + +## v2.0.0 - 集成全局Store配置 + +### 新增功能 +- ✅ 企业微信配置集成到全局Store +- ✅ 从 `/api/config/init` 统一获取配置 +- ✅ 支持props覆盖全局配置 +- ✅ 优化配置获取逻辑 + +### 变更内容 +1. **Store集成**: + - 在 `store/index.js` 中添加 `wxworkConfig` 状态 + - 添加 `setWxworkConfig` mutation + - 在 `init` action 中从 `/api/config/init` 获取企业微信配置 + +2. **组件优化**: + - `wxwork-contact.vue` 组件现在优先从全局Store获取配置 + - 支持通过props覆盖全局配置 + - 移除单独的API调用,使用统一配置 + +3. **页面集成**: + - `pages/contact/contact.vue` 页面简化配置获取逻辑 + - 直接使用全局Store中的企业微信配置 + +### 配置格式 +后端 `/api/config/init` 需要返回以下格式的企业微信配置: + +```json +{ + "code": 0, + "data": { + // ... 其他配置 ... + "wxwork": { + "corp_id": "企业ID", + "agent_id": "应用ID", + "contact_id": "客服ID", + "contact_url": "活码链接", + "timestamp": "时间戳", + "nonceStr": "随机字符串", + "signature": "签名", + "enabled": true + } + } +} +``` + +### 使用方式 +```vue + + + + + +``` + +## v1.0.0 - 初始版本 + +### 功能 +- 企业微信JS-SDK封装 +- 基础联系客服组件 +- 支持小程序和H5环境 +- 活码跳转和SDK两种方式 \ No newline at end of file diff --git a/components/wxwork-contact/README.md b/components/wxwork-contact/README.md new file mode 100644 index 0000000..ce09d20 --- /dev/null +++ b/components/wxwork-contact/README.md @@ -0,0 +1,484 @@ +# 企业微信联系客服组件 + +## 功能说明 + +这个组件实现了在小程序中点击"联系客服"后,自动跳转到企业微信添加对应销售的功能。 + +## 关键前提条件 + +### ⚠️ 重要提醒 +微信小程序与企业微信互通有严格的平台限制和权限要求,使用前请确保满足以下所有条件: + +### 1. 微信小程序环境要求 +- **平台限制**:功能仅在微信小程序环境中可用 +- **基础库版本**:需要微信小程序基础库 2.3.0 及以上版本 +- **用户环境**:用户需要在微信中打开小程序 + +### 2. 企业微信配置要求 +- **企业认证**:企业微信账号必须完成企业认证 +- **客户联系功能**:需要开通企业微信"客户联系"功能 +- **应用权限**:企业微信应用需要有客户联系相关权限 + +### 3. 互通配置要求 +- **关联配置**:小程序必须与企业微信进行关联配置 +- **权限申请**:需要在微信开放平台和企业微信后台分别申请相应权限 +- **域名白名单**:相关域名需要在小程序和企业微信后台都配置白名单 + +### 4. 跳转权限要求 +- **小程序AppID**:需要在企业微信中配置允许跳转的小程序AppID +- **企业微信AppID**:需要在微信开放平台配置关联的企业微信AppID +- **业务域授权**:需要配置业务域授权,允许跨平台跳转 + +### 5. 开发调试要求 +- **测试环境**:需要在测试环境中验证跳转功能 +- **权限验证**:确保所有必要的API权限已申请并生效 +- **兼容性测试**:在不同微信版本中进行兼容性测试 + +### 6. 具体配置要求 + +| 条件 | 说明 | +|------|------| +| **小程序与企业微信绑定** | 在企业微信管理后台 → 「应用管理」→「小程序」中关联你的微信小程序AppID | +| **配置可信域名** | 如果涉及网页跳转或回调,需在企业微信后台配置业务域名 | +| **使用企业微信服务商 or 自建应用** | 若需高级功能(如获取客户详情),需有企业微信管理员权限或通过服务商代开发 | + +## 使用方法 + +### 1. 基础用法 + +```vue + + +``` + +### 组件架构 + +### 分层设计 +``` +调用方 (页面组件) + ↓ 传递配置参数 +wxwork-contact 组件 + ↓ 调用SDK +wxwork-jssdk.js + ↓ 调用企业微信API +企业微信服务 +``` + +### 组件设计原则 + +- **独立性**:组件不直接依赖全局Store,所有配置通过props传递 +- **职责分离**:组件只负责UI展示和企业微信SDK调用,配置管理由调用方负责 +- **灵活配置**:支持调用者覆盖任何配置参数 + +## 版本信息 + +### v3.0.0 - 统一客服服务重构版本 +- 创建 `CustomerService` 统一客服处理服务 +- 重构 `ns-contact.vue` 和 `hover-nav.vue` 消除重复代码 +- 提供统一的客服处理接口和平台适配 +- 完善错误处理和降级机制 +- 支持所有客服类型的统一调用 + +### v2.0.0 - Store集成版本 +- 企业微信配置集成到全局Store +- 从 `/api/config/init` 统一获取配置 +- 组件完全独立,通过props接收配置 +- 支持全局配置和局部覆盖 + +### v1.0.0 - 初始版本 +- 基础企业微信联系功能 +- 支持活码跳转和JS-SDK两种方式 + +### 2. 属性说明 + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| btnText | String | '添加企业微信客服' | 按钮文字 | +| corpId | String | - | 企业ID(必需) | +| agentId | String | '' | 应用ID | +| timestamp | String | '' | 时间戳 | +| nonceStr | String | '' | 随机字符串 | +| signature | String | '' | 签名 | +| contactId | String | '' | 客服ID或活码配置ID | +| contactUrl | String | '' | 活码链接 | +| showConfirm | Boolean | true | 是否显示确认弹窗 | + +### 3. 在联系页面中使用 + +在 `pages/contact/contact.vue` 中已集成使用示例: + +```vue + + +``` + +## 配置说明 + +### 后端接口需求 + +企业微信配置已集成到 `/api/config/init` 接口中,返回以下格式的数据: + +```json +{ + "code": 0, + "data": { + // ... 其他配置 ... + "wxwork": { + "corp_id": "企业ID", + "agent_id": "应用ID", + "contact_id": "客服ID", + "contact_url": "活码链接", + "timestamp": "时间戳", + "nonceStr": "随机字符串", + "signature": "签名", + "enabled": true + } + } +} +``` + +### 全局Store集成 + +企业微信配置通过以下方式集成到全局状态管理: + +1. **Store状态**:在 `store/index.js` 中添加 `wxworkConfig` 状态 +2. **配置获取**:在 `init` action 中从 `/api/config/init` 获取企业微信配置 +3. **状态更新**:使用 `setWxworkConfig` mutation 更新配置 +4. **持久化**:配置自动保存到本地存储 + +### 企业微信配置步骤 + +1. **获取企业微信活码**: + - 登录企业微信管理后台 + - 进入"客户联系" -> "配置" -> "联系我" + - 创建活码,获取配置ID或直接获取活码链接 + +2. **配置参数**: + - `corp_id`: 企业ID(在企业微信后台获取) + - `agent_id`: 企业微信应用ID + - `contact_id`: 客服的用户ID + - `contact_url`: 企业微信活码链接(推荐使用活码链接) + - `timestamp`: 生成签名的时间戳 + - `nonceStr`: 生成签名的随机字符串 + - `signature`: JS-SDK签名 + +## 典型业务流程示例 + +### 目标 +用户在小程序中点击"联系客服",自动添加对应的企业微信销售。 + +### 步骤 +1. **后端调用企业微信 API** 创建「联系我」二维码(可带场景值,如 user_id=123)。 +2. **前端在小程序中展示该二维码(或生成跳转链接)**。 +3. **用户长按识别 → 打开企业微信 → 添加客服**。 +4. **企业微信收到添加事件 → 通过 API 获取 external_userid → 关联到原小程序用户**。 + +## 实现原理 + +### 方案1:企业微信活码跳转(推荐) +- 使用企业微信活码链接 +- 通过 `uni.navigateToMiniProgram` 跳转到企业微信小程序 +- 直接添加对应的销售为联系人 + +### 方案2:JS-SDK方式 +- 使用企业微信JS-SDK +- 调用 `openUserProfile` 接口打开用户资料 +- 用户手动添加联系人 + +## 注意事项 + +### 功能限制 +1. **小程序环境**:需要在微信小程序环境中使用 +2. **权限配置**:确保小程序有跳转企业微信的权限 +3. **降级处理**:当企业微信不可用时,会降级到原有客服方式 +4. **用户体验**:建议添加确认弹窗,避免误操作 + +### 前提条件验证 +5. **权限检查**:使用前需要验证所有必需权限是否生效 +6. **配置完整性**:确保所有配置参数都已正确设置 +7. **网络环境**:确保用户网络环境允许访问企业微信服务 +8. **版本兼容**:检查微信版本和企业微信版本兼容性 + +### 调试建议 +9. **错误监控**:添加适当的错误日志和用户反馈机制 +10. **性能优化**:避免频繁的SDK初始化和配置获取 +11. **安全考虑**:敏感配置信息应在服务端处理,前端不暴露 + +## 兼容性 + +- 微信小程序:✅ 支持 +- H5环境:✅ 支持跳转活码链接 +- 其他平台:降级处理 + +## 文件结构 + +``` +components/wxwork-contact/ +├── wxwork-contact.vue # 主组件 +└── README.md # 说明文档 + +components/ns-contact/ +└── ns-contact.vue # 统一客服组件(重构后) + +components/hover-nav/ +└── hover-nav.vue # 悬浮导航组件(重构后) + +common/js/ +├── wxwork-jssdk.js # 企业微信JS-SDK封装 +└── customer-service.js # 客服统一处理服务(新增) + +store/ +└── index.js # 全局Store,包含wxworkConfig状态管理 + +pages/contact/ +└── contact.vue # 联系页面,集成企业微信功能 +``` + +## 系统梳理与优化 (v3.1.0) + +### 🔧 已修复的问题 + +#### 1. **App.vue 配置恢复** +- ✅ 修复了企业微信配置 (`wxworkConfig`) 在应用启动时的恢复 +- ✅ 确保所有配置都能正确从本地存储恢复到Store + +#### 2. **客服服务参数传递** +- ✅ 修复了 `openCustomerServiceChat` 参数传递错误 +- ✅ 正确传递 `sendMessageTitle`、`sendMessagePath`、`sendMessageImg` + +#### 3. **组件配置访问** +- ✅ 在 `ns-contact.vue` 和 `hover-nav.vue` 中添加 computed 属性 +- ✅ 确保能够正确访问 `servicerConfig` + +#### 4. **企业微信服务优化** +- ✅ 改进参数传递,支持自定义消息参数 +- ✅ 统一处理函数调用方式 + +### 🛡️ 新增功能 + +#### 1. **配置验证机制** +```javascript +const validation = this.customerService.validateConfig(); +if (!validation.isValid) { + // 处理配置错误 +} +``` + +#### 2. **错误处理增强** +- ✅ 添加配置错误弹窗 +- ✅ 改进错误日志记录 +- ✅ 警告信息提示 + +#### 3. **类型安全** +- ✅ 完善参数类型检查 +- ✅ 空值和异常情况处理 + +### 📋 当前系统状态 + +| 组件 | 状态 | 功能完整性 | +|------|------|-----------| +| **customer-service.js** | ✅ 完成 | 统一客服处理服务 | +| **ns-contact.vue** | ✅ 修复 | 支持所有客服类型 | +| **hover-nav.vue** | ✅ 修复 | 集成统一客服服务 | +| **App.vue** | ✅ 修复 | 配置完整恢复 | +| **contact.vue** | ⚠️ 待优化 | 仍使用原始方式 | + +### 🔄 待优化项 + +#### 1. **contact.vue 页面重构** +- 建议集成统一客服服务 +- 添加企业微信客服按钮 +- 统一UI交互体验 + +#### 2. **加载状态反馈** +- 客服功能调用时的loading状态 +- 网络请求的进度指示 + +#### 3. **用户体验优化** +- 客服按钮的点击反馈 +- 错误状态的友好提示 + +### 🧪 测试建议 + +1. **配置验证测试**: + - 测试各种配置缺失情况 + - 验证错误提示准确性 + +2. **平台兼容测试**: + - 微信小程序客服功能 + - H5环境跳转 + - 支付宝小程序客服 + +3. **企业微信功能测试**: + - 活码链接跳转 + - 降级处理机制 + - 参数传递正确性 + +### 📖 使用最佳实践 + +```javascript +// 推荐使用方式 +const customerService = createCustomerService(this); + +// 带配置验证的调用 +if (customerService.isConfigAvailable()) { + customerService.handleCustomerClick({ + sendMessageTitle: '商品咨询', + sendMessagePath: '/pages/goods/detail', + sendMessageImg: 'product-image.jpg' + }); +} +``` + +## 重构说明 (v3.0.0) + +### 统一客服处理服务 + +为了解决代码重复问题,我们创建了 `CustomerService` 类来统一处理所有客服逻辑: + +#### 主要改进 +1. **代码复用**:消除 `ns-contact.vue` 和 `hover-nav.vue` 中的重复代码 +2. **统一接口**:提供一致的客服处理API +3. **平台适配**:自动处理不同平台的客服配置 +4. **类型安全**:完善的错误处理和降级机制 + +#### 使用方式 + +**1. 在组件中导入** +```javascript +import { createCustomerService } from '@/common/js/customer-service.js'; +``` + +**2. 初始化服务** +```javascript +created() { + this.customerService = createCustomerService(this); + this.buttonConfig = this.customerService.getButtonConfig(); +} +``` + +**3. 处理客服点击** +```javascript +methods: { + contactServicer() { + this.customerService.handleCustomerClick({ + niushop: this.niushop, + sendMessageTitle: this.sendMessageTitle, + sendMessagePath: this.sendMessagePath, + sendMessageImg: this.sendMessageImg + }); + } +} +``` + +#### 核心方法 + +| 方法名 | 用途 | 参数 | +|--------|------|------| +| `handleCustomerClick(options)` | 统一处理客服点击 | 客服配置选项 | +| `getButtonConfig()` | 获取按钮配置 | - | +| `getServiceType()` | 获取客服类型 | - | +| `openWxworkService()` | 打开企业微信客服 | 是否使用原方式 | + +#### 支持的客服类型 + +- `wxwork` - 企业微信客服 +- `third` - 第三方客服 +- `niushop` - 牛商客服 +- `weapp` - 微信小程序客服 +- `aliapp` - 支付宝小程序客服 +- `none` - 无客服(显示提示) + +## 常见问题 (FAQ) + +### Q: contact_id 是小程序ID吗? +A: 不是的。`contact_id` 是**企业微信中客服人员的用户ID**,具体说明如下: + +- **定义**:企业微信系统内部分配给客服人员的唯一标识符 +- **获取方式**:通过企业微信管理后台的"客户联系" → "配置" → "联系我"功能创建活码时获得 +- **用途**:用于指定具体客服人员,当用户点击"联系客服"时,系统通过这个ID知道应该添加哪个企业微信客服人员 + +### Q: contact_id 和小程序ID的区别? +A: 两者的作用完全不同: + +| 字段 | 用途 | 获取方式 | +|------|------|----------| +| **contact_id** | 指定具体的企业微信客服人员 | 企业微信管理后台获取 | +| **小程序AppID** | 识别整个微信小程序应用 | 微信开放平台获取 | + +在业务流程中: +1. 用户在小程序中点击"联系客服" +2. 系统使用 `contact_id` 打开对应的企业微信客服人员资料 +3. 用户添加该企业微信客服为联系人 + +所以 `contact_id` 是企业微信客服的ID,不是小程序的ID。 + +### Q: wxwork_contact_url 从哪里来? +A: `wxwork_contact_url` 应该来自全局Store中的 `wxworkConfig`,而不是 `servicerConfig`: + +**正确的配置来源**: +- ✅ `this.$store.state.wxworkConfig.contact_url` - 企业微信配置 +- ❌ `this.config.wxwork_contact_url` - 错误的配置路径 + +**配置获取流程**: +1. **后端接口**:`/api/config/init` 返回 `wxwork_config` 数据 +2. **Store存储**:`setWxworkConfig` 将配置保存到 `wxworkConfig` 状态 +3. **组件使用**:通过 `this.$store.state.wxworkConfig.contact_url` 访问 + +**代码修正示例**: +```javascript +// 错误 ❌ +if (this.config.wxwork_contact_url) { ... } + +// 正确 ✅ +const wxworkConfig = this.$store.state?.wxworkConfig; +if (wxworkConfig?.contact_url) { ... } +``` + +**字段对应关系**: +| 后端字段 | Store字段 | 组件使用 | +|---------|----------|----------| +| `contact_url` | `contact_url` | `wxworkConfig.contact_url` | + +### Q: navigateToMiniProgram 中的企业微信小程序AppID 是指我的业务小程序ID吗? +A: 不是的!`appId: 'wxeb490c6f9b154ef9'` 是**企业微信官方小程序的AppID**,不是你的业务小程序ID。 + +**两者的区别**: + +| 小程序类型 | AppID示例 | 用途 | 所有者 | +|-----------|----------|------|--------| +| **企业微信官方小程序** | `wxeb490c6f9b154ef9` | 展示企业联系人详情页面 | 腾讯企业微信团队 | +| **你的业务小程序** | 你的AppID | 你的业务功能(电商、服务等) | 你的企业/组织 | + +**业务流程**: +``` +用户小程序 (你的业务) + ↓ 点击"联系客服" +navigateToMiniProgram 跳转到 +企业微信官方小程序 (wxeb490c6f9b154ef9) + ↓ 展示联系人详情 +用户添加客服人员到企业微信 +``` + +**关键点**: +- 这个AppID是企业微信官方小程序,通常是固定值 +- 用于跳转到企业微信环境展示联系人详情 +- 不需要替换成你自己的小程序AppID | \ No newline at end of file diff --git a/components/wxwork-contact/wxwork-contact.vue b/components/wxwork-contact/wxwork-contact.vue new file mode 100644 index 0000000..65701bf --- /dev/null +++ b/components/wxwork-contact/wxwork-contact.vue @@ -0,0 +1,301 @@ + + + + + \ No newline at end of file diff --git a/pages/contact/contact.vue b/pages/contact/contact.vue index 62984d6..1eebc05 100644 --- a/pages/contact/contact.vue +++ b/pages/contact/contact.vue @@ -1,511 +1,511 @@ - - - - - + + + + + diff --git a/store/index.js b/store/index.js index 046671d..037a268 100644 --- a/store/index.js +++ b/store/index.js @@ -50,8 +50,8 @@ const store = new Vuex.Store({ goods: '', head: '', store: '', - article: '', - aiAgent: '' + article: '', + aiAgent: '' }, cartList: {}, cartIds: [], @@ -67,6 +67,7 @@ const store = new Vuex.Store({ cartPosition: null, // 购物车所在位置 componentRefresh: 0, // 组件刷新 servicerConfig: null, // 客服配置 + wxworkConfig: null, // 企业微信配置 diySeckillInterval: 0, diyGroupPositionObj: {}, diyGroupShowModule: '', @@ -167,6 +168,11 @@ const store = new Vuex.Store({ state.servicerConfig = value; uni.setStorageSync('servicerConfig', value); }, + // 企业微信配置 + setWxworkConfig(state, value) { + state.wxworkConfig = value; + uni.setStorageSync('wxworkConfig', value); + }, setDiySeckillInterval(state, value) { state.diySeckillInterval = value; }, @@ -232,6 +238,11 @@ const store = new Vuex.Store({ this.commit('setSiteInfo', data.site_info); this.commit('setServicerConfig', data.servicer); + + // 企业微信配置 + if (data?.wxwork_config) { + this.commit('setWxworkConfig', data.wxwork_config); + } this.commit('setCopyright', data.copyright);