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 @@
-
-
-
-
-
-
-
-
-
-
-
-
- 在线留言
-
-
-
-
-
-
-
-
-
-
-
-
-
- 在线留言
-
-
-
-
-
-
-
-
-
-
- {{item.realname}}
- {{item.position}}
-
- {{item.address}}
-
-
- {{item.mobile}}
- 一键拨打
-
-
-
- {{item.email}}
- 立即导航
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+ 在线留言
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 在线留言
+
+
+
+
+
+
+
+
+
+
+ {{item.realname}}
+ {{item.position}}
+
+ {{item.address}}
+
+
+ {{item.mobile}}
+ 一键拨打
+
+
+
+ {{item.email}}
+ 立即导航
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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);