armg 4 weken geleden
commit
fbe38b3d51
100 gewijzigde bestanden met toevoegingen van 13256 en 0 verwijderingen
  1. 25 0
      .gitignore
  2. 32 0
      App.vue
  3. 44 0
      api/abacus.js
  4. 8 0
      api/api.js
  5. 31 0
      api/user.js
  6. 42 0
      config/app.js
  7. 16 0
      config/cache.js
  8. 8 0
      config/socket.js
  9. 10 0
      hooks/useCheckAdmin.js
  10. 87 0
      hooks/useDebounceThrottle.js
  11. 94 0
      hooks/useImageUpload.js
  12. 251 0
      hooks/usePayment.js
  13. 481 0
      hooks/useRealGoldPrice.js
  14. 39 0
      hooks/useSafeNavigate.js
  15. 38 0
      hooks/useSendCode.js
  16. 329 0
      hooks/useSocket.js
  17. 112 0
      hooks/useToast.js
  18. 20 0
      index.html
  19. 24 0
      libs/EmojiDecoder.js
  20. 122 0
      libs/RecorderManager.js
  21. 24 0
      libs/apps.js
  22. 62 0
      libs/chat.js
  23. 63 0
      libs/login.js
  24. 138 0
      libs/routine.js
  25. 322 0
      libs/wechat.js
  26. 29 0
      main.js
  27. 80 0
      manifest.json
  28. 465 0
      package-lock.json
  29. 11 0
      package.json
  30. 71 0
      pages.json
  31. 27 0
      pages/index/index.vue
  32. 6 0
      pages/order_addcart/order_addcart.vue
  33. 6 0
      pages/user/index.vue
  34. 6 0
      pages/users/order_list/index.vue
  35. 25 0
      project.config.json
  36. 14 0
      project.private.config.json
  37. 470 0
      static/css/base.css
  38. 692 0
      static/css/style.scss
  39. BIN
      static/images/tabbar/1-001.png
  40. BIN
      static/images/tabbar/1-002.png
  41. BIN
      static/images/tabbar/2-001.png
  42. BIN
      static/images/tabbar/2-002.png
  43. BIN
      static/images/tabbar/3-001.png
  44. BIN
      static/images/tabbar/3-002.png
  45. BIN
      static/images/tabbar/4-001.png
  46. BIN
      static/images/tabbar/4-002.png
  47. 140 0
      stores/app.js
  48. 3 0
      stores/pinia.js
  49. 25 0
      stores/rights.js
  50. 13 0
      uni.promisify.adaptor.js
  51. 76 0
      uni.scss
  52. 21 0
      uni_modules/uview-plus/LICENSE
  53. 74 0
      uni_modules/uview-plus/README.md
  54. 1348 0
      uni_modules/uview-plus/changelog.md
  55. 109 0
      uni_modules/uview-plus/components/u-action-sheet-data/u-action-sheet-data.vue
  56. 26 0
      uni_modules/uview-plus/components/u-action-sheet/actionSheet.js
  57. 70 0
      uni_modules/uview-plus/components/u-action-sheet/props.js
  58. 302 0
      uni_modules/uview-plus/components/u-action-sheet/u-action-sheet.vue
  59. 76 0
      uni_modules/uview-plus/components/u-agreement/u-agreement.vue
  60. 28 0
      uni_modules/uview-plus/components/u-album/album.js
  61. 95 0
      uni_modules/uview-plus/components/u-album/props.js
  62. 344 0
      uni_modules/uview-plus/components/u-album/u-album.vue
  63. 26 0
      uni_modules/uview-plus/components/u-alert/alert.js
  64. 75 0
      uni_modules/uview-plus/components/u-alert/props.js
  65. 293 0
      uni_modules/uview-plus/components/u-alert/u-alert.vue
  66. 23 0
      uni_modules/uview-plus/components/u-avatar-group/avatarGroup.js
  67. 54 0
      uni_modules/uview-plus/components/u-avatar-group/props.js
  68. 109 0
      uni_modules/uview-plus/components/u-avatar-group/u-avatar-group.vue
  69. 28 0
      uni_modules/uview-plus/components/u-avatar/avatar.js
  70. 81 0
      uni_modules/uview-plus/components/u-avatar/props.js
  71. 179 0
      uni_modules/uview-plus/components/u-avatar/u-avatar.vue
  72. 27 0
      uni_modules/uview-plus/components/u-back-top/backtop.js
  73. 56 0
      uni_modules/uview-plus/components/u-back-top/props.js
  74. 132 0
      uni_modules/uview-plus/components/u-back-top/u-back-top.vue
  75. 27 0
      uni_modules/uview-plus/components/u-badge/badge.js
  76. 79 0
      uni_modules/uview-plus/components/u-badge/props.js
  77. 176 0
      uni_modules/uview-plus/components/u-badge/u-badge.vue
  78. 1000 0
      uni_modules/uview-plus/components/u-barcode/u-barcode.vue
  79. 27 0
      uni_modules/uview-plus/components/u-box/props.js
  80. 91 0
      uni_modules/uview-plus/components/u-box/u-box.vue
  81. 43 0
      uni_modules/uview-plus/components/u-button/button.js
  82. 46 0
      uni_modules/uview-plus/components/u-button/nvue.scss
  83. 159 0
      uni_modules/uview-plus/components/u-button/props.js
  84. 503 0
      uni_modules/uview-plus/components/u-button/u-button.vue
  85. 81 0
      uni_modules/uview-plus/components/u-button/vue.scss
  86. 48 0
      uni_modules/uview-plus/components/u-calendar/calendar.js
  87. 109 0
      uni_modules/uview-plus/components/u-calendar/header.vue
  88. 616 0
      uni_modules/uview-plus/components/u-calendar/month.vue
  89. 169 0
      uni_modules/uview-plus/components/u-calendar/props.js
  90. 421 0
      uni_modules/uview-plus/components/u-calendar/u-calendar.vue
  91. 86 0
      uni_modules/uview-plus/components/u-calendar/util.js
  92. 15 0
      uni_modules/uview-plus/components/u-car-keyboard/carKeyboard.js
  93. 17 0
      uni_modules/uview-plus/components/u-car-keyboard/props.js
  94. 314 0
      uni_modules/uview-plus/components/u-car-keyboard/u-car-keyboard.vue
  95. 40 0
      uni_modules/uview-plus/components/u-card/card.js
  96. 134 0
      uni_modules/uview-plus/components/u-card/props.js
  97. 184 0
      uni_modules/uview-plus/components/u-card/u-card.vue
  98. 333 0
      uni_modules/uview-plus/components/u-cascader/u-cascader.vue
  99. 381 0
      uni_modules/uview-plus/components/u-cate-tab/u-cate-tab.vue
  100. 0 0
      uni_modules/uview-plus/components/u-cell-group/cellGroup.js

+ 25 - 0
.gitignore

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

+ 32 - 0
App.vue

@@ -0,0 +1,32 @@
+<script setup>
+import { ref, computed, watch, nextTick } from "vue";
+import { onLoad, onShow, onLaunch } from "@dcloudio/uni-app";
+// onLoad 接受 A 页面传递的参数
+onLoad((option) => {
+});
+
+onShow(() => {
+  // console.log("app 页面 onShow");
+});
+
+onLaunch(async () => {
+  // console.log("app 页面 onLaunch");
+
+});
+</script>
+
+<style lang="scss">
+@import "static/css/style.scss";
+@import "static/css/base.css";
+// @import "@/uni_modules/uview-plus/index.scss";
+// @import "static/iconfont/iconfont.css";
+  /* 隐藏滚动条,但依旧具备可以滚动的功能 */
+.uni-scroll-view::-webkit-scrollbar {
+  display: none;
+}
+::-webkit-scrollbar {
+  width: 0;
+  height: 0;
+  color: transparent;
+}
+</style>

+ 44 - 0
api/abacus.js

@@ -0,0 +1,44 @@
+import request from "@/utils/request.js";
+
+/**
+ * 获取小算盘列表
+ * @param userId 用户id
+ * @param status 1为买入,0为已卖出
+ */
+export function getAbacusList(data) {
+	return request.get('abacus/list', data);
+}
+
+/**
+ * 删除一条记录
+ * @param id 记录id
+ */
+export function deleteAbacusItem(id) {
+	return request.get(`abacus/delete?id=${id}`);
+}
+
+/**
+ * 获取当前金价和调整价
+ */
+export function getCurrentPrice(data) {
+	return request.get('rtj/price/adjustnow', data);
+}
+
+/**
+ * 添加黄金记录
+ * @param purchasePrice 购入时每克金价
+ * @param status 1为买入,0为已卖出
+ */
+export function addAbacusRecord(data) {
+	return request.post('abacus/create', data);
+}
+
+/**
+ * 更新记录
+ * @param id
+ * @param sellPrice 卖出时每克金价
+ * @param status 1为买入,0为已卖出
+ */
+export function updateAbacusRecord(data) {
+	return request.post('abacus/update', data);
+}

+ 8 - 0
api/api.js

@@ -0,0 +1,8 @@
+import request from "@/utils/request.js";
+/**
+ * 获取小程序配置
+ *
+ */
+export function getMiniProgramData() {
+  return request.get("mini-program/config/info", {}, { noAuth: true });
+}

+ 31 - 0
api/user.js

@@ -0,0 +1,31 @@
+import request from "@/utils/request.js";
+import Cache from "@/utils/cache.js"
+/**
+ * 获取用户信息
+ * 
+*/
+export function getUserInfo(){
+  return request.get('user');
+}
+
+/**
+ * h5用户手机号登录
+ * @param data object 用户手机号 也只能
+ */
+export function loginMobile(data) {
+  return request.post("login/mobile", data, { noAuth : true });
+}
+
+
+// 根据用户ID查询用户等级详细信息
+export function getUserLevelInfo(userId) {
+  return request.get(`user/level/detail/${userId}`);
+}
+
+/**
+ * 静默绑定推广人
+ * @param {Object} puid
+ */
+export function spread(puid) {
+  return request.get("user/bindSpread?spreadPid=" + puid);
+}

+ 42 - 0
config/app.js

@@ -0,0 +1,42 @@
+// let domain = "https://www.shuibeibyg.com/front-api"; // 正式环境IP
+let domain = "https://test.shuibeibyg.com/front-api"; // 测试环境IP
+// let domain = 'http://192.168.100.199:8081' // 晋守桦IP
+
+
+export const HTTP_REQUEST_URL = domain;
+
+// 请求头
+export const HEADER = {
+  "content-type": "application/json",
+};
+
+// 表单提交头
+export const HEADERPARAMS = {
+  "content-type": "application/x-www-form-urlencoded",
+};
+
+// token 名称
+export const TOKENNAME = "Authori-zation";
+
+// 缓存时间,0 为永久
+export const EXPIRE = 0;
+
+// 分页默认限制条数
+export const LIMIT = 10;
+
+// 401白名单
+export const WHITELIST = [
+  "/pages/index/index",
+  "/pages/login/login",
+  "/pages/user/index",
+  "/pages/users/login/index",
+  "/pages/users/about/index",
+  "/pages/users/utils/dapan",
+  "/pages/change_password/change_password",
+  "/pages/users/vault/index",
+  "/pages/users/ranking_list/index",
+  "/pages/VIP/VIP",
+];
+
+// oss对象存储地址
+export const BASE_OSS_URL = "https://sb-admin.oss-cn-shenzhen.aliyuncs.com/";

+ 16 - 0
config/cache.js

@@ -0,0 +1,16 @@
+export const LOGIN_STATUS = "LOGIN_STATUS_TOKEN"
+export const UID = "UID"
+export const USER_INFO = "USER_INFO"
+export const EXPIRES_TIME = "EXPIRES_TIME"
+export const WX_AUTH = "WX_AUTH"
+export const STATE_KEY = "wx_authorize_state"
+export const LOGINTYPE = "loginType"
+export const BACK_URL = "login_back_url"
+export const STATE_R_KEY = "roution_authorize_state"
+export const LOGO_URL = "LOGO_URL"
+export const TIPS_KEY = "TIPS_KEY"
+export const SPREAD = "spread"
+export const CACHE_LONGITUDE = "LONGITUDE"
+export const CACHE_LATITUDE = "LATITUDE"
+export const PLATFORM = "systemPlatform"
+// export const SUBSCRIBE_MESSAGE = 'SUBSCRIBE_MESSAGE' // 如需启用可取消注释

+ 8 - 0
config/socket.js

@@ -0,0 +1,8 @@
+// Socket链接 暂不做配置
+export const SOCKET_URL = '';
+
+// Socket调试模式
+export const SOCKET_URL_DEBUG = true;
+
+// 心跳间隔
+export const PING_INTERVAL = 3000;

+ 10 - 0
hooks/useCheckAdmin.js

@@ -0,0 +1,10 @@
+import { checkLogin } from "@/libs/login";
+import { useAppStore } from "@/stores/app";
+export function useCkeckAdmin() {
+  const appStore = useAppStore();
+  if (!appStore.tokenComputed && !checkLogin()) {
+    return false;
+  } else {
+    return true;
+  }
+}

+ 87 - 0
hooks/useDebounceThrottle.js

@@ -0,0 +1,87 @@
+import { ref, onUnmounted } from 'vue'
+import { onHide } from "@dcloudio/uni-app";
+
+/**
+ * 防抖 Hook
+ * @param {Function} fn - 需要防抖的函数
+ * @param {number} [delay=300] - 防抖延迟时间(毫秒)
+ * @param {boolean} [immediate=false] - 是否立即执行
+ * @returns {Ref<Function>} 防抖后的函数
+ */
+export function useDebounce(fn, delay = 300, immediate = false) {
+  const debounceRef = ref()
+  let timer = null
+
+  debounceRef.value = (...args) => {
+    if (timer) {
+      // 清除现有的定时器
+      clearTimeout(timer)
+    }
+
+    if (immediate && !timer) {
+      // 如果设置了立即执行且没有定时器,则立即调用函数
+      fn(...args)
+    } else {
+      // 设置定时器,在延迟时间后执行函数
+      timer = setTimeout(() => {
+        fn(...args)
+        timer = null
+      }, delay)
+    }
+  }
+
+  // 页面隐藏时清理定时器
+  onHide(() => {
+    if (timer) {
+      clearTimeout(timer)
+    }
+  })
+
+  return debounceRef
+}
+
+/**
+ * 节流 Hook
+ * @param {Function} fn - 需要节流的函数
+ * @param {number} [delay=300] - 节流间隔时间(毫秒)
+ * @param {boolean} [immediate=false] - 是否立即执行
+ * @returns {Ref<Function>} 节流后的函数
+ */
+export function useThrottle(fn, delay = 300, immediate = false) {
+  const throttleRef = ref()
+  let lastTime = 0
+  let timer = null
+
+  throttleRef.value = (...args) => {
+    const now = Date.now()
+
+    if (immediate && !lastTime) {
+      // 如果设置了立即执行且是第一次调用,则立即执行
+      fn(...args)
+      lastTime = now
+      return
+    }
+
+    if (now - lastTime >= delay) {
+      // 如果距离上次执行已超过间隔时间,直接执行
+      fn(...args)
+      lastTime = now
+    } else if (!timer) {
+      // 如果在间隔时间内,使用定时器确保在间隔结束后执行
+      timer = setTimeout(() => {
+        fn(...args)
+        lastTime = Date.now()
+        timer = null
+      }, delay)
+    }
+  }
+
+  // 页面隐藏时清理定时器
+  onHide(() => {
+    if (timer) {
+      clearTimeout(timer)
+    }
+  })
+
+  return throttleRef
+}

+ 94 - 0
hooks/useImageUpload.js

@@ -0,0 +1,94 @@
+import { ref } from "vue";
+import { HTTP_REQUEST_URL, TOKENNAME } from "@/config/app.js";
+import { toLogin, checkLogin } from "@/libs/login";
+import { useAppStore } from "@/stores/app";
+import { useToast } from "@/hooks/useToast";
+
+export function useImageUpload(formData) {
+  const imageList = ref([]);
+  const appStore = useAppStore();
+  const uploadLoading = ref(false);
+  const { Toast } = useToast();
+
+  // 上传单个文件
+  const uploadFilePromise = (url) => {
+    return new Promise((resolve, reject) => {
+      uni.uploadFile({
+        url: `${HTTP_REQUEST_URL}/api/front/user/upload/image`,
+        filePath: url,
+        name: "multipart",
+        header: {
+          [TOKENNAME]: appStore.tokenComputed,
+        },
+        formData,
+        success: (res) => {
+          // 兼容后端返回格式
+          let data = res.data;
+          if (typeof data === "string") {
+            try {
+              data = JSON.parse(data);
+            } catch (e) {}
+          }
+          console.log("data", data);
+          if (data?.code === 200) {
+            resolve(data.data);
+          } else {
+            reject(data.data);
+          }
+        },
+        fail: reject,
+      });
+    });
+  };
+
+  // 处理上传
+  const afterRead = async (event) => {
+    if (!appStore.tokenComputed && !checkLogin()) {
+      toLogin();
+      return Promise.reject({ msg: "未登录" });
+    }
+    uploadLoading.value = true;
+    let lists = [].concat(event.file);
+    let fileListLen = imageList.value.length;
+    lists.forEach((item) => {
+      imageList.value.push({
+        ...item,
+        status: "uploading",
+        message: "上传中",
+      });
+    });
+
+    // 逐个上传,每个都 try-catch,保证每个图片都能更新状态
+    for (let i = 0; i < lists.length; i++) {
+      const curIndex = fileListLen + i;
+      try {
+        const result = await uploadFilePromise(lists[i].url);
+        imageList.value.splice(curIndex, 1, {
+          ...imageList.value[curIndex],
+          status: "success",
+          message: "",
+          info: result,
+        });
+      } catch (error) {
+        imageList.value.splice(curIndex, 1, {
+          ...imageList.value[curIndex],
+          status: "failed",
+          message: "上传失败",
+        });
+      }
+    }
+    uploadLoading.value = false;
+  };
+
+  // 删除图片
+  const deletePic = (event) => {
+    imageList.value.splice(event.index, 1);
+  };
+
+  return {
+    uploadLoading,
+    imageList,
+    afterRead,
+    deletePic,
+  };
+}

+ 251 - 0
hooks/usePayment.js

@@ -0,0 +1,251 @@
+import { useToast } from './useToast'
+import { ref, reactive } from 'vue';
+export function usePayment() {
+  const { Toast } = useToast()
+
+  // 支付状态
+  const paymentLoading = ref(false);
+  const paymentResult = ref(null);
+
+  // 支付配置
+  const paymentConfig = reactive({
+    // 支付类型
+    PAYMENT_TYPES: {
+      ALIPAY: "alipay",
+      WECHAT: "weixin",
+      BALANCE: "balance",
+    },
+
+    // 支付状态
+    PAYMENT_STATUS: {
+      SUCCESS: "支付成功",
+      FAIL: "支付失败",
+      CANCEL: "用户取消支付",
+    },
+  });
+
+  /**
+   * 获取可用的支付渠道
+   */
+  const getAvailableChannels = () => {
+    return new Promise((resolve, reject) => {
+      // #ifdef APP
+      uni.getProvider({
+        service: "payment",
+        success: (res) => {
+          console.log("可用支付渠道:", res.provider);
+          resolve(res.provider);
+        },
+        fail: (err) => {
+          console.error("获取支付渠道失败:", err);
+          reject(err);
+        },
+      });
+      // #endif
+
+      // #ifndef APP
+      // H5端默认返回所有渠道(实际支付需要调用后端接口)
+      // resolve(["alipay", "wxpay", "balance"]);
+      resolve(["balance"]);
+      // #endif
+    });
+  };
+
+  /**
+   * 支付宝支付
+   * @param {String} orderInfo - 服务器返回的支付宝订单信息字符串
+   */
+  const alipayPayment = (orderInfo) => {
+    return new Promise((resolve, reject) => {
+      // #ifdef APP
+      uni.requestPayment({
+        provider: "alipay",
+        orderInfo: orderInfo,
+        success: (res) => {
+          console.log("支付宝支付成功:", res);
+          try {
+            const rawdata = JSON.parse(res.rawdata);
+            resolve({
+              status: paymentConfig.PAYMENT_STATUS.SUCCESS,
+              data: rawdata,
+              message: "支付成功",
+            });
+          } catch (e) {
+            resolve({
+              status: paymentConfig.PAYMENT_STATUS.SUCCESS,
+              data: res,
+              message: "支付成功",
+            });
+          }
+        },
+        fail: (err) => {
+          console.error("支付宝支付失败:", err);
+          const errorMsg = err.errMsg || "支付失败";
+
+          // 判断是否为用户取消
+          if (errorMsg.includes("cancel") || errorMsg.includes("取消")) {
+            resolve({
+              status: paymentConfig.PAYMENT_STATUS.CANCEL,
+              data: err,
+              message: "用户取消支付",
+            });
+          } else {
+            reject({
+              status: paymentConfig.PAYMENT_STATUS.FAIL,
+              data: err,
+              message: errorMsg,
+            });
+          }
+        },
+      });
+      // #endif
+
+      // #ifndef APP
+      // H5端需要调用后端接口进行支付
+      console.warn("H5端支付宝支付需要后端配合实现");
+      reject({
+        status: paymentConfig.PAYMENT_STATUS.FAIL,
+        message: "H5端暂不支持支付宝支付",
+      });
+      // #endif
+    });
+  };
+
+  /**
+   * 微信支付
+   * @param {Object} orderInfo - 服务器返回的微信支付订单对象
+   */
+  const wechatPayment = (orderInfo) => {
+    return new Promise((resolve, reject) => {
+      // 验证订单信息
+      if (!orderInfo || !orderInfo.appid || !orderInfo.partnerid) {
+        reject({
+          status: paymentConfig.PAYMENT_STATUS.FAIL,
+          message: "微信支付订单信息不完整",
+        });
+        return;
+      }
+
+      // #ifdef APP-PLUS
+      uni.requestPayment({
+        provider: "wxpay",
+        orderInfo: orderInfo,
+        success: (res) => {
+          console.log("微信支付成功:", res);
+          try {
+            const rawdata = JSON.parse(res.rawdata || "{}");
+            resolve({
+              status: paymentConfig.PAYMENT_STATUS.SUCCESS,
+              data: rawdata,
+              message: "支付成功",
+            });
+          } catch (e) {
+            resolve({
+              status: paymentConfig.PAYMENT_STATUS.SUCCESS,
+              data: res,
+              message: "支付成功",
+            });
+          }
+        },
+        fail: (err) => {
+          console.error("微信支付失败:", err);
+          const errorMsg = err.errMsg || "支付失败";
+
+          // 判断是否为用户取消
+          if (errorMsg.includes("cancel") || errorMsg.includes("取消")) {
+            resolve({
+              status: paymentConfig.PAYMENT_STATUS.CANCEL,
+              data: err,
+              message: "用户取消支付",
+            });
+          } else {
+            reject({
+              status: paymentConfig.PAYMENT_STATUS.FAIL,
+              data: err,
+              message: errorMsg,
+            });
+          }
+        },
+      });
+      // #endif
+
+      // #ifndef APP
+      // H5端需要调用后端接口进行支付
+      console.warn("H5端微信支付需要后端配合实现");
+      reject({
+        status: paymentConfig.PAYMENT_STATUS.FAIL,
+        message: "H5端暂不支持微信支付",
+      });
+      // #endif
+    });
+  };
+
+  /**
+   * 统一支付方法
+   * @param {Object} options - 支付配置
+   * @param {String} options.type - 支付类型 ('alipay' | 'wxpay' | 'balance')
+   * @param {Object|String} options.orderInfo - 订单信息
+   * @param {Object} options.params - 其他参数
+   */
+  const submitPayment = async (options) => {
+    const { type, orderInfo, params = {} } = options;
+
+    if (paymentLoading.value) {
+      throw new Error("支付正在进行中,请勿重复提交");
+    }
+
+    paymentLoading.value = true;
+    paymentResult.value = null;
+
+    try {
+      // 检查支付渠道是否可用
+      const channels = await getAvailableChannels();
+      console.log("channels:", channels);
+
+      let result = null;
+
+      switch (type) {
+        case paymentConfig.PAYMENT_TYPES.ALIPAY:
+          if (channels.indexOf('alipay') === -1) {
+            Toast({title: "设备不支持支付宝支付"})
+            throw new Error("设备不支持支付宝支付");
+          }
+          result = await alipayPayment(orderInfo);
+          break;
+
+        case paymentConfig.PAYMENT_TYPES.WECHAT:
+          if (channels.indexOf('wxpay') === -1) {
+            Toast({title: "设备不支持微信支付"})
+            throw new Error("设备不支持微信支付");
+          }
+          result = await wechatPayment(orderInfo);
+          break;
+
+        case paymentConfig.PAYMENT_TYPES.BALANCE:
+          result = await balancePayment({ ...params, orderInfo });
+          break;
+
+        default:
+          throw new Error("不支持的支付类型");
+      }
+
+      paymentResult.value = result;
+      return result;
+    } catch (error) {
+      paymentResult.value = error;
+      throw error;
+    } finally {
+      paymentLoading.value = false;
+    }
+  };
+
+  return {
+    paymentLoading,
+    paymentResult,
+    paymentConfig,
+    getAvailableChannels,
+    alipayPayment,
+    wechatPayment,
+    submitPayment,
+  }
+}

+ 481 - 0
hooks/useRealGoldPrice.js

@@ -0,0 +1,481 @@
+/**
+ * 实时金价 hooks(UniApp 单例模式实现)
+ * @description 封装实时金价、黄金9999、铂金、白银、K金的价格获取与实时更新逻辑,通过单例WebSocket避免多页面连接超限,结合缓存减少接口请求
+ *
+ * 核心特性:
+ * 1. 单例WebSocket:全局唯一连接,通过引用计数管理生命周期(页面全部隐藏时断开,任一页面显示时重连)
+ * 2. 多金属支持:基础黄金(RTJ_Au)、黄金9999(AU9999)、铂金(RTJ_Pt)、白银(RTJ_Ag)、K金(自动计算,基础黄金75%)
+ * 3. 数据自动处理:实时更新销售价/回收价/调整价,自动维护当日最高/最低价
+ * 4. 缓存优化:接口请求结果缓存30秒,避免重复请求减轻服务器压力
+ * 5. 错误容错:WebSocket断连自动重连(3秒延迟),数据解析异常捕获日志
+ *
+ * 依赖项:
+ * - vue:ref/watch 响应式能力
+ * - @dcloudio/uni-app:页面生命周期(onShow/onHide/onUnload)
+ * - @/api/abacus:getCurrentPrice 接口(获取初始金价)
+ * - pako:WebSocket消息解压(gzip格式)
+ * - @/hooks/useSocket:WebSocket基础封装(需支持on/send/connect/disconnect方法)
+ * - @/utils/weAtob:base64解码工具(处理WebSocket消息)
+ *
+ * 注意事项:
+ * - WebSocket地址(wss://litews.jjh9999.com/wsv2/)、token(junit:test)
+ * - 缓存时长(CACHE_DURATION)默认30秒,可根据需求调整
+ * - K金价格依赖基础黄金自动计算(75%比例),无需额外接口请求
+ */
+import { ref, watch } from "vue";
+import { onShow, onHide, onUnload } from "@dcloudio/uni-app";
+import { getCurrentPrice } from "@/api/abacus";
+import pako from "pako/dist/pako.min.js";
+import useWebSocket from "@/hooks/useSocket";
+import { weAtob } from "@/utils";
+
+// WebSocket单例核心变量
+let wsInstance = null;
+let connectCount = 0;
+let isConnected = false;
+const dataCallbacks = new Set();
+
+// 初始化WebSocket单例
+const initWSSingleton = () => {
+  if (wsInstance) return wsInstance;
+
+  // 创建WebSocket实例
+  const ws = useWebSocket("wss://litews.jjh9999.com/wsv2/", {
+    heartbeat: false,
+    heartbeatInterval: 20000,
+  });
+
+  // 初始化参数
+  ws.on("open", () => {
+    console.log("WS单例连接成功");
+    isConnected = true;
+    ws.send({
+      company_id: "2289041670615554",
+      id: "2189041670615552",
+      code: 1,
+      token: "junit:test",
+      type: 1,
+    });
+  });
+
+  // 接收消息:统一解析并分发给所有注册的回调
+  ws.on("message", (msg) => {
+    try {
+      const strData = weAtob(msg.data);
+      const charData = strData.split("").map((x) => x.charCodeAt(0));
+      const binData = new Uint8Array(charData);
+      const tempData = pako.inflate(binData, { to: "string" });
+      const { data } = JSON.parse(tempData);
+
+      if (data && data?.code !== 0) {
+        const itemData = JSON.parse(data);
+        if (itemData?.priceDataNew) {
+          const priceData = {
+            itemAu: itemData.priceDataNew.find((v) => v.gmCode === "RTJ_Au"),
+            itemAu9999: itemData.priceDataNew.find(
+              (v) => v.gmCode === "AU9999"
+            ),
+            itemPt: itemData.priceDataNew.find((v) => v.gmCode === "RTJ_Pt"),
+            itemAg: itemData.priceDataNew.find((v) => v.gmCode === "RTJ_Ag"),
+          };
+          dataCallbacks.forEach((callback) => callback(priceData));
+        }
+      }
+    } catch (error) {
+      console.error("WS单例消息处理错误:", error);
+    }
+  });
+
+  // 连接关闭:若仍有页面使用,3秒后自动重连
+  ws.on("close", () => {
+    isConnected = false;
+    if (connectCount > 0) {
+      setTimeout(() => ws.connect(), 3000);
+    }
+  });
+
+  ws.on("error", (error) => {
+    console.error("WS单例连接错误:", error);
+    isConnected = false;
+  });
+
+  wsInstance = ws;
+  return ws;
+};
+
+const CACHE_DURATION = 30 * 1000; // 缓存有效期:30秒
+const fetchCache = new Map();
+
+export default function useRealGoldPrice(options) {
+  const { realCode = "All" } = options || {};
+
+  // 基础黄金(RTJ_Au)
+  const realGoldprice = ref(0); // 实时销售价
+  const realGoldRecyclePrice = ref(0); // 实时回收价
+  const goldAdjustPrice = ref(0); // 调整价
+  const goldTodayHigh = ref(0); // 今日最高价
+  const goldTodayLow = ref(0); // 今日最低价
+
+  // 黄金9999(AU9999)
+  const realAu9999price = ref(0);
+  const realAu9999RecyclePrice = ref(0);
+  const Au9999AdjustPrice = ref(0);
+  const au9999TodayHigh = ref(0);
+  const au9999TodayLow = ref(0);
+
+  // 铂金(RTJ_Pt)
+  const realPtprice = ref(0);
+  const realPtRecyclePrice = ref(0);
+  const PtAdjustPrice = ref(0);
+  const ptTodayHigh = ref(0);
+  const ptTodayLow = ref(0);
+
+  // 白银(RTJ_Ag)
+  const realAgprice = ref(0);
+  const realAgRecyclePrice = ref(0);
+  const AgAdjustPrice = ref(0);
+  const agTodayHigh = ref(0);
+  const agTodayLow = ref(0);
+
+  // K金(基础黄金75%自动计算)
+  const realKGoldprice = ref(0);
+  const realKGoldRecyclePrice = ref(0);
+  const KGoldAdjustPrice = ref(0);
+  const kGoldTodayHigh = ref(0);
+  const kGoldTodayLow = ref(0);
+
+  // 当前价(简化快捷访问)
+  const currentGoldPrice = ref(0);
+  const currentAu9999Price = ref(0);
+  const currentPtPrice = ref(0);
+  const currentAgPrice = ref(0);
+  const currentKGoldPrice = ref(0);
+
+  // 更新当日最高/最低价
+  const updateHighLowPrices = (currentPrice, highRef, lowRef) => {
+    const price = Number(currentPrice);
+    if (isNaN(price)) return;
+
+    // 首次设置价格时,同步初始化最高/最低价
+    if (highRef.value === 0 && lowRef.value === 0) {
+      highRef.value = price;
+      lowRef.value = price;
+      return;
+    }
+
+    highRef.value = Math.max(highRef.value, price);
+    lowRef.value = Math.min(lowRef.value, price);
+  };
+
+  // 统一更新金属价格(销售价/回收价)
+  const updatePrice = (
+    item,
+    sellPriceRef,
+    recyclePriceRef,
+    adjustPriceRef,
+    highRef,
+    lowRef
+  ) => {
+    if (item?.sellPrice?.price) {
+      const sellPrice = (
+        Number(item.sellPrice.price) + Number(adjustPriceRef.value)
+      ).toFixed(2);
+      sellPriceRef.value = sellPrice;
+      updateHighLowPrices(sellPrice, highRef, lowRef);
+    }
+    if (item?.buyPrice?.price) {
+      recyclePriceRef.value = (
+        Number(item.buyPrice.price) + Number(adjustPriceRef.value)
+      ).toFixed(2);
+    }
+  };
+
+  watch(
+    [realGoldprice, realGoldRecyclePrice, goldAdjustPrice],
+    ([newSell, newRecycle, newAdjust]) => {
+      const kSell = (Number(newSell) * 0.75).toFixed(2);
+      realKGoldprice.value = kSell;
+      realKGoldRecyclePrice.value = (Number(newRecycle) * 0.75).toFixed(2);
+      KGoldAdjustPrice.value = (Number(newAdjust) * 0.75).toFixed(2);
+      currentKGoldPrice.value = kSell;
+      updateHighLowPrices(kSell, kGoldTodayHigh, kGoldTodayLow);
+    },
+    { immediate: true }
+  );
+
+  // WebSocket单例集成
+  const ws = initWSSingleton();
+
+  // 当前页面的价格更新回调(接收单例分发的数据)
+  const handlePriceUpdate = (priceData) => {
+    const { itemAu, itemAu9999, itemPt, itemAg } = priceData;
+
+    // 根据realCode筛选需要更新的金属类型
+    switch (realCode) {
+      case "RTJ_Au":
+        updatePrice(
+          itemAu,
+          realGoldprice,
+          realGoldRecyclePrice,
+          goldAdjustPrice,
+          goldTodayHigh,
+          goldTodayLow
+        );
+        currentGoldPrice.value = realGoldprice.value;
+        break;
+      case "AU9999":
+        updatePrice(
+          itemAu9999,
+          realAu9999price,
+          realAu9999RecyclePrice,
+          Au9999AdjustPrice,
+          au9999TodayHigh,
+          au9999TodayLow
+        );
+        currentAu9999Price.value = realAu9999price.value;
+        break;
+      case "RTJ_Pt":
+        updatePrice(
+          itemPt,
+          realPtprice,
+          realPtRecyclePrice,
+          PtAdjustPrice,
+          ptTodayHigh,
+          ptTodayLow
+        );
+        currentPtPrice.value = realPtprice.value;
+        break;
+      case "RTJ_Ag":
+        updatePrice(
+          itemAg,
+          realAgprice,
+          realAgRecyclePrice,
+          AgAdjustPrice,
+          agTodayHigh,
+          agTodayLow
+        );
+        currentAgPrice.value = realAgprice.value;
+        break;
+      case "RTJ_KGold":
+        break; // K金依赖基础黄金,无需额外处理
+      case "All":
+        updatePrice(
+          itemAu,
+          realGoldprice,
+          realGoldRecyclePrice,
+          goldAdjustPrice,
+          goldTodayHigh,
+          goldTodayLow
+        );
+        updatePrice(
+          itemAu9999,
+          realAu9999price,
+          realAu9999RecyclePrice,
+          Au9999AdjustPrice,
+          au9999TodayHigh,
+          au9999TodayLow
+        );
+        updatePrice(
+          itemPt,
+          realPtprice,
+          realPtRecyclePrice,
+          PtAdjustPrice,
+          ptTodayHigh,
+          ptTodayLow
+        );
+        updatePrice(
+          itemAg,
+          realAgprice,
+          realAgRecyclePrice,
+          AgAdjustPrice,
+          agTodayHigh,
+          agTodayLow
+        );
+        currentGoldPrice.value = realGoldprice.value;
+        currentAu9999Price.value = realAu9999price.value;
+        currentPtPrice.value = realPtprice.value;
+        currentAgPrice.value = realAgprice.value;
+        break;
+    }
+  };
+
+  onShow(() => {
+    connectCount++;
+    if (connectCount === 1 && !isConnected) {
+      ws.connect();
+    }
+    initPriceData();
+  });
+
+  onHide(() => {
+    connectCount = Math.max(connectCount - 1, 0);
+    if (connectCount === 0 && isConnected) {
+      ws.disconnect();
+    }
+  });
+
+  onUnload(() => {
+    // 页面卸载时移除回调,避免内存泄漏
+    dataCallbacks.delete(handlePriceUpdate);
+    connectCount = Math.max(connectCount - 1, 0);
+    if (connectCount === 0 && isConnected) {
+      ws.disconnect();
+    }
+  });
+
+  // 注册当前页面的价格更新回调
+  dataCallbacks.add(handlePriceUpdate);
+
+  // 主动获取金属价格(支持缓存)
+  async function fetchGoldPrice(code = "RTJ_Au", tradeType = 3) {
+    const cacheKey = `${code}_${tradeType}`;
+    const now = Date.now();
+
+    // 优先使用缓存(30秒内有效)
+    const cached = fetchCache.get(cacheKey);
+    if (cached && now - cached.lastFetchTime < CACHE_DURATION) {
+      handleFetchData(cached.data, code, tradeType);
+      return cached.data;
+    }
+
+    // 缓存失效,发起接口请求
+    const params = { tradeType };
+    if (code === "AU9999" || code === "RTJ_Pt" || code === "RTJ_Ag") {
+      params.code = code;
+    }
+
+    try {
+      const { data } = await getCurrentPrice(params);
+      // 缓存请求结果
+      fetchCache.set(cacheKey, { lastFetchTime: now, data });
+      // 处理并更新价格
+      handleFetchData(data, code, tradeType);
+      return data;
+    } catch (error) {
+      console.error(`获取${code}价格失败:`, error);
+      throw error;
+    }
+  }
+
+  // 根据realCode初始化对应金属价格
+  const initPriceData = () => {
+    const tasks = [];
+    if (realCode === "All" || realCode === "RTJ_Au")
+      tasks.push(fetchGoldPrice("RTJ_Au"));
+    if (realCode === "All" || realCode === "AU9999")
+      tasks.push(fetchGoldPrice("AU9999"));
+    if (realCode === "All" || realCode === "RTJ_Pt")
+      tasks.push(fetchGoldPrice("RTJ_Pt"));
+    if (realCode === "All" || realCode === "RTJ_Ag")
+      tasks.push(fetchGoldPrice("RTJ_Ag"));
+    Promise.all(tasks).catch((err) => console.error("初始化价格失败:", err));
+  };
+
+  // 处理接口返回的价格数据
+  const handleFetchData = (data, code, tradeType) => {
+    let sellRef, recycleRef, adjustRef, highRef, lowRef;
+
+    // 匹配对应金属的响应式变量
+    switch (code) {
+      case "AU9999":
+        [sellRef, recycleRef, adjustRef, highRef, lowRef] = [
+          realAu9999price,
+          realAu9999RecyclePrice,
+          Au9999AdjustPrice,
+          au9999TodayHigh,
+          au9999TodayLow,
+        ];
+        break;
+      case "RTJ_Pt":
+        [sellRef, recycleRef, adjustRef, highRef, lowRef] = [
+          realPtprice,
+          realPtRecyclePrice,
+          PtAdjustPrice,
+          ptTodayHigh,
+          ptTodayLow,
+        ];
+        break;
+      case "RTJ_Ag":
+        [sellRef, recycleRef, adjustRef, highRef, lowRef] = [
+          realAgprice,
+          realAgRecyclePrice,
+          AgAdjustPrice,
+          agTodayHigh,
+          agTodayLow,
+        ];
+        break;
+      default: // RTJ_Au(基础黄金)
+        [sellRef, recycleRef, adjustRef, highRef, lowRef] = [
+          realGoldprice,
+          realGoldRecyclePrice,
+          goldAdjustPrice,
+          goldTodayHigh,
+          goldTodayLow,
+        ];
+        break;
+    }
+
+    // 计算最终价格(调整价+基础价)
+    const adjustPrice = tradeType === 3 ? 0 : data.adjustPrice;
+    adjustRef.value = adjustPrice;
+    const sellPrice = (Number(adjustPrice) + Number(data.sellPrice)).toFixed(2);
+    sellRef.value = sellPrice;
+    recycleRef.value = (
+      Number(adjustPrice) + Number(data.buyPrice || 0)
+    ).toFixed(2);
+    updateHighLowPrices(sellPrice, highRef, lowRef);
+
+    // 更新当前价快捷访问变量
+    if (code === "RTJ_Au") currentGoldPrice.value = sellPrice;
+    if (code === "AU9999") currentAu9999Price.value = sellPrice;
+    if (code === "RTJ_Pt") currentPtPrice.value = sellPrice;
+    if (code === "RTJ_Ag") currentAgPrice.value = sellPrice;
+  };
+
+  return {
+    // 基础黄金相关
+    realGoldprice,
+    realGoldRecyclePrice,
+    goldAdjustPrice,
+    goldTodayHigh,
+    goldTodayLow,
+
+    // 黄金9999相关
+    realAu9999price,
+    realAu9999RecyclePrice,
+    Au9999AdjustPrice,
+    currentAu9999Price,
+    au9999TodayHigh,
+    au9999TodayLow,
+
+    // 铂金相关
+    realPtprice,
+    realPtRecyclePrice,
+    PtAdjustPrice,
+    ptTodayHigh,
+    ptTodayLow,
+
+    // 白银相关
+    realAgprice,
+    realAgRecyclePrice,
+    AgAdjustPrice,
+    agTodayHigh,
+    agTodayLow,
+
+    // K金相关
+    realKGoldprice,
+    realKGoldRecyclePrice,
+    KGoldAdjustPrice,
+    kGoldTodayHigh,
+    kGoldTodayLow,
+
+    // 当前价快捷访问
+    currentGoldPrice,
+    currentPtPrice,
+    currentAgPrice,
+    currentKGoldPrice,
+
+    // 主动获取价格方法
+    fetchGoldPrice,
+  };
+}

+ 39 - 0
hooks/useSafeNavigate.js

@@ -0,0 +1,39 @@
+// hooks/useSafeNavigate.js
+import { ref } from 'vue';
+
+// 模块顶层的全局状态锁
+const globalIsNavigating = ref(false);
+
+export function useSafeNavigate(options = {}) {
+  const defaultOptions = {
+    wait: 300, // 导航冷却时间(ms)
+  };
+  const config = { ...defaultOptions, ...options };
+
+  const safeNavigateTo = (url, navigateOptions = {}) => {
+    if (globalIsNavigating.value) {
+      console.log('正在跳转...');
+      return;
+    }
+
+    globalIsNavigating.value = true;
+    uni.navigateTo({
+      url,
+      ...navigateOptions,
+      success: () => {
+        setTimeout(() => {
+          globalIsNavigating.value = false;
+        }, config.wait);
+      },
+      fail: (err) => {
+        console.error('Navigation failed:', err);
+        globalIsNavigating.value = false;
+      },
+    });
+  };
+
+  return {
+    isNavigating: globalIsNavigating, // 共享的全局状态
+    safeNavigateTo,
+  };
+}

+ 38 - 0
hooks/useSendCode.js

@@ -0,0 +1,38 @@
+// composables/useSendCode.js
+import { ref, onUnmounted } from 'vue'
+
+export function useSendCode() {
+  const disabled = ref(false)
+  const text = ref('获取验证码')
+  let timer = null
+  let count = 60
+
+  const sendCode = () => {
+    if (disabled.value) return
+
+    disabled.value = true
+    count = 60
+    text.value = `剩余 ${count}s`
+
+    timer = setInterval(() => {
+      count--
+      if (count < 0) {
+        clearInterval(timer)
+        disabled.value = false
+        text.value = '重新获取'
+        return
+      }
+      text.value = `剩余 ${count}s`
+    }, 1000)
+  }
+
+  onUnmounted(() => {
+    if (timer) clearInterval(timer)
+  })
+
+  return {
+    disabled,
+    text,
+    sendCode
+  }
+}

+ 329 - 0
hooks/useSocket.js

@@ -0,0 +1,329 @@
+import { ref, reactive, onUnmounted, nextTick } from "vue";
+import { onHide } from "@dcloudio/uni-app";
+
+/**
+ * useWebSocket - 封装适用于 uni-app App 平台的 WebSocket Hook
+ * 支持自动重连、心跳检测、事件监听、消息记录等
+ *
+ * @param {string} url WebSocket 服务器地址(如 wss://example.com/ws)
+ * @param {object} options 可选配置项
+ * @returns {object} 提供连接状态、消息、发送、断开等方法
+ */
+export default function useWebSocket(url, options = {}) {
+  // 默认配置项
+  const defaultOptions = {
+    protocols: [], // 子协议
+    autoConnect: true, // 是否自动连接
+    reconnect: true, // 是否断线重连
+    reconnectInterval: 3000, // 重连间隔(ms)
+    maxReconnectAttempts: 3, // 最大重连次数
+    heartbeat: true, // 是否启用心跳
+    heartbeatInterval: 30000, // 心跳间隔(ms)
+    heartbeatMessage: "ping", // 心跳消息内容
+    ...options,
+  };
+
+  // 响应式状态
+  const socketTask = ref(null); // WebSocket 实例
+  const isConnected = ref(false); // 当前是否已连接
+  const isConnecting = ref(false); // 当前是否正在连接
+  const reconnectCount = ref(0); // 当前重连次数
+  const lastMessage = ref(null); // 最后一条接收到的消息
+  const messageHistory = ref([]); // 所有接收到的历史消息(上限100)
+
+  // 事件监听器
+  const listeners = reactive({
+    open: [],
+    message: [],
+    error: [],
+    close: [],
+  });
+
+  // 控制标志与定时器
+  let reconnectTimer = null;
+  let heartbeatTimer = null;
+  let heartbeatTimeoutTimer = null;
+  let isManuallyClosed = false; // 是否为手动断开(用于判断是否执行自动重连)
+
+  /**
+   * 创建 WebSocket 连接
+   */
+  const connect = () => {
+    if (isConnected.value || isConnecting.value) {
+      console.warn("WebSocket 已连接或正在连接中");
+      return;
+    }
+
+    isConnecting.value = true;
+    isManuallyClosed = false;
+
+    // 发起连接
+    socketTask.value = uni.connectSocket({
+      url,
+      protocols: defaultOptions.protocols,
+      success: () => {
+        console.log("WebSocket 连接请求已发出");
+        nextTick(() => {
+          setupListeners();
+        });
+      },
+      fail: (err) => {
+        isConnecting.value = false;
+        console.error("WebSocket 连接请求失败:", err);
+        handleError(err);
+      },
+    });
+  };
+
+  /**
+   * 绑定 socket 事件监听器
+   */
+  const setupListeners = () => {
+    console.log("socketTask.value", socketTask.value);
+    if (!socketTask.value) return;
+
+    socketTask.value.onOpen((res) => {
+      console.log("WebSocket 已连接");
+      isConnected.value = true;
+      isConnecting.value = false;
+      reconnectCount.value = 0;
+
+      // 启动心跳机制
+      if (defaultOptions.heartbeat) {
+        startHeartbeat();
+      }
+
+      listeners.open.forEach((cb) => cb(res));
+    });
+
+    socketTask.value.onMessage((res) => {
+      const message = {
+        data: res.data,
+        timestamp: Date.now(),
+      };
+
+      lastMessage.value = message;
+      messageHistory.value.push(message);
+
+      // 限制消息历史上限为100条
+      if (messageHistory.value.length > 100) {
+        messageHistory.value.shift();
+      }
+
+      listeners.message.forEach((cb) => cb(message));
+    });
+
+    socketTask.value.onError((err) => {
+      console.error("WebSocket 发生错误:", err);
+      handleError(err);
+    });
+
+    socketTask.value.onClose((res) => {
+      console.log("WebSocket 已关闭", res);
+      isConnected.value = false;
+      isConnecting.value = false;
+
+      stopHeartbeat();
+      listeners.close.forEach((cb) => cb(res));
+
+      // 自动重连(仅当未手动断开时)
+      if (
+        !isManuallyClosed &&
+        defaultOptions.reconnect &&
+        reconnectCount.value < defaultOptions.maxReconnectAttempts
+      ) {
+        attemptReconnect();
+      }
+    });
+  };
+
+  /**
+   * 主动断开连接
+   */
+  const disconnect = () => {
+    isManuallyClosed = true;
+    clearReconnectTimer();
+    stopHeartbeat();
+
+    if (socketTask.value) {
+      socketTask.value.close({
+        code: 1000,
+        reason: "客户端主动断开",
+      });
+    }
+
+    isConnected.value = false;
+    isConnecting.value = false;
+    reconnectCount.value = 0;
+    socketTask.value = null;
+  };
+
+  /**
+   * 发送消息
+   * @param {string|object} data 发送的数据(支持对象自动转 JSON)
+   * @returns {Promise}
+   */
+  const send = (data) => {
+    // console.log('isConnected', isConnected.value)
+    if (!isConnected.value || !socketTask.value) {
+      return Promise.reject(new Error("WebSocket 未连接"));
+    }
+
+    return new Promise((resolve, reject) => {
+      try {
+        const msg = typeof data === "object" ? JSON.stringify(data) : data;
+        socketTask.value.send({
+          data: msg,
+          success: resolve,
+          fail: reject,
+        });
+      } catch (err) {
+        console.error("发送数据格式错误:", err);
+        reject(err);
+      }
+    });
+  };
+
+  /**
+   * 尝试自动重连
+   */
+  const attemptReconnect = () => {
+    if (reconnectCount.value >= defaultOptions.maxReconnectAttempts) {
+      console.warn("已达到最大重连次数,停止重连");
+      return;
+    }
+
+    reconnectCount.value++;
+    console.log(`尝试第 ${reconnectCount.value} 次重连...`);
+
+    reconnectTimer = setTimeout(() => {
+      connect();
+    }, defaultOptions.reconnectInterval);
+  };
+
+  /**
+   * 清除重连定时器
+   */
+  const clearReconnectTimer = () => {
+    if (reconnectTimer) {
+      clearTimeout(reconnectTimer);
+      reconnectTimer = null;
+    }
+  };
+
+  /**
+   * 启动心跳检测
+   */
+  const startHeartbeat = () => {
+    stopHeartbeat();
+
+    heartbeatTimer = setInterval(() => {
+      if (!isConnected.value) return;
+
+      send(defaultOptions.heartbeatMessage)
+        .then(() => {
+          clearTimeout(heartbeatTimeoutTimer);
+          heartbeatTimeoutTimer = setTimeout(() => {
+            console.warn("心跳无响应,断开连接");
+            socketTask.value?.close();
+          }, defaultOptions.heartbeatInterval + 2000);
+        })
+        .catch((err) => {
+          console.error("心跳发送失败:", err);
+        });
+    }, defaultOptions.heartbeatInterval);
+  };
+
+  /**
+   * 停止心跳定时器
+   */
+  const stopHeartbeat = () => {
+    if (heartbeatTimer) {
+      clearInterval(heartbeatTimer);
+      heartbeatTimer = null;
+    }
+    if (heartbeatTimeoutTimer) {
+      clearTimeout(heartbeatTimeoutTimer);
+      heartbeatTimeoutTimer = null;
+    }
+  };
+
+  /**
+   * 分发错误事件
+   * @param {any} error
+   */
+  const handleError = (error) => {
+    listeners.error.forEach((cb) => cb(error));
+  };
+
+  /**
+   * 注册事件监听器
+   * @param {string} event 事件名 open/message/error/close
+   * @param {Function} callback
+   */
+  const on = (event, callback) => {
+    if (listeners[event]) {
+      listeners[event].push(callback);
+    }
+  };
+
+  /**
+   * 移除事件监听器
+   * @param {string} event
+   * @param {Function} callback
+   */
+  const off = (event, callback) => {
+    if (listeners[event]) {
+      const index = listeners[event].indexOf(callback);
+      if (index > -1) {
+        listeners[event].splice(index, 1);
+      }
+    }
+  };
+
+  /**
+   * 清除消息历史
+   */
+  const clearHistory = () => {
+    messageHistory.value = [];
+    lastMessage.value = null;
+  };
+
+  /**
+   * 手动执行重连(带清理)
+   */
+  const reconnect = () => {
+    disconnect();
+    nextTick(() => {
+      reconnectCount.value = 0;
+      isManuallyClosed = false;
+      connect();
+    });
+  };
+
+  // 页面加载时自动连接
+  if (defaultOptions.autoConnect) {
+    connect();
+  }
+
+  // 页面离开时时断开连接
+  // onHide(() => {
+  //   disconnect()
+  // })
+
+  // 对外暴露状态与操作方法
+  return {
+    isConnected,
+    isConnecting,
+    reconnectCount,
+    lastMessage,
+    messageHistory,
+    connect,
+    disconnect,
+    send,
+    reconnect,
+    clearHistory,
+    on,
+    off,
+  };
+}

+ 112 - 0
hooks/useToast.js

@@ -0,0 +1,112 @@
+import { ref } from "vue";
+
+	/**
+	 * opt  object | string
+	 * to_url object | string
+	 * 例:
+	 * this.Tips('/pages/test/test'); 跳转不提示
+	 * this.Tips({title:'提示'},'/pages/test/test'); 提示并跳转
+	 * this.Tips({title:'提示'},{tab:1,url:'/pages/index/index'}); 提示并跳转值table上
+	 * tab=1 一定时间后跳转至 table上
+	 * tab=2 一定时间后跳转至非 table上
+	 * tab=3 一定时间后返回上页面
+	 * tab=4 关闭所有页面跳转至非table上
+	 * tab=5 关闭当前页面跳转至table上
+	 */
+
+export function useToast() {
+  // 提示标题
+  const tipTitle = ref("");
+  // 提示图标
+  const tipIcon = ref("none");
+  // 提示持续时间
+  const tipEndtime = ref(2000);
+
+  /**
+   * Tips - 显示提示并可按需跳转页面
+   * @param {Object|string} opt 提示配置或直接为跳转路径
+   * @param {Object|string|Function} to_url 跳转配置、路径或回调
+   */
+  function Toast(opt, to_url) {
+    console.log("Toast", opt, to_url);
+    // 如果第一个参数是字符串,视为跳转路径
+    if (typeof opt == "string") {
+      to_url = opt;
+      opt = {};
+    }
+    // 提示内容
+    tipTitle.value = opt.title || "";
+    // 图标
+    tipIcon.value = opt.icon || "none";
+    // 持续时间
+    tipEndtime.value = opt.endtime || 1000;
+    // 接口调用成功的回调函数
+    let success = opt.success;
+    // 显示提示
+    if (tipTitle.value)
+      uni.showToast({
+        title: tipTitle.value,
+        icon: tipIcon.value,
+        duration: tipEndtime.value,
+        success,
+      });
+    // 跳转逻辑
+    if (to_url != undefined) {
+      if (typeof to_url == "object") {
+        // 对象方式配置跳转
+        let tab = to_url.tab || 1,
+          url = to_url.url || "";
+        console.log('tab', tab)
+        switch (tab) {
+          case 1:
+            // 一定时间后跳转至 table
+            setTimeout(() => uni.switchTab({ url }), tipEndtime.value);
+            break;
+          case 2:
+            // 跳转至非table页面
+            setTimeout(() => uni.navigateTo({ url }), tipEndtime.value);
+            break;
+          case 3:
+            // 返回上一个页面 
+            setTimeout(() => {
+
+              // #ifndef H5
+              uni.navigateBack({ delta: 1 });
+              // #endif
+
+              // #ifdef H5
+              history.back();
+              // #endif
+
+            }, tipEndtime.value);
+            break;
+          case 4:
+            // 关闭所有页面跳转到非 tab 页面
+            setTimeout(() => uni.reLaunch({ url }), tipEndtime.value);
+            break;
+          case 5:
+            // 关闭当前页面跳转到非 tab 页面
+            setTimeout(() => uni.redirectTo({ url }), tipEndtime.value);
+            break;
+        }
+      } else if (typeof to_url == "function") {
+        // 回调函数
+        setTimeout(() => to_url && to_url(), tipEndtime.value);
+      } else {
+        // 普通路径跳转
+        setTimeout(
+          () => uni.navigateTo({ url: to_url }),
+          tipTitle.value ? tipEndtime.value : 0
+        );
+      }
+    }
+  }
+
+  // 导出响应式数据和方法
+  return {
+    tipTitle,
+    tipIcon,
+    tipEndtime,
+    Toast
+  };
+}

+ 20 - 0
index.html

@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+  <head>
+    <meta charset="UTF-8" />
+    <script>
+      var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
+        CSS.supports('top: constant(a)'))
+      document.write(
+        '<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
+        (coverSupport ? ', viewport-fit=cover' : '') + '" />')
+    </script>
+    <title></title>
+    <!--preload-links-->
+    <!--app-context-->
+  </head>
+  <body>
+    <div id="app"><!--app-html--></div>
+    <script type="module" src="/main.js"></script>
+  </body>
+</html>

+ 24 - 0
libs/EmojiDecoder.js

@@ -0,0 +1,24 @@
+class EmojiDecoder {
+    emojiMap = null;
+    url = "";
+    patterns = [];
+    metaChars = /[[\]{}()*+?.\\|^$\-,&#\s]/g;
+
+    constructor(url,emojiMap) {
+        this.url = url || '';
+        this.emojiMap = emojiMap || {};
+        for (let i in this.emojiMap) {
+            if (this.emojiMap.hasOwnProperty(i)){
+                this.patterns.push('('+i.replace(this.metaChars, "\\$&")+')');
+            }
+        }
+    }
+
+    decode (text) {
+        return text.replace(new RegExp(this.patterns.join('|'),'g'),  (match) => {
+            return typeof this.emojiMap[match] != 'undefined' ? '<img height="20rpx" width="20rpx" src="'+this.url+this.emojiMap[match]+'" />' : match;
+        });
+    }
+}
+
+export default EmojiDecoder

+ 122 - 0
libs/RecorderManager.js

@@ -0,0 +1,122 @@
+// #ifdef H5
+import Recorder from 'recorder-core';
+import 'recorder-core/src/engine/mp3';
+import 'recorder-core/src/engine/mp3-engine';
+// #endif
+
+export default class RecorderManager {
+    recorder = null;
+    //录音开始时间
+    uniStartTime = null;
+    //语音录音中
+    recording = false;
+    onRecordingComplete = null;
+
+    constructor() {
+        // #ifdef H5
+        this.recorder = Recorder({
+            type : 'mp3',
+            sampleRate:16000,//录音的采样率,越大细节越丰富越细腻
+            bitRate:16,//录音的比特率,越大音质越好
+            onProcess : function () {}
+        });
+        // #endif
+
+        // #ifndef H5
+        this.recorder = uni.getRecorderManager();
+        // #endif
+    }
+
+    onRecordComplete(callBack) {
+      this.onRecordingComplete = callBack;
+      // #ifndef H5
+      this.initUniRecorder();
+      // #endif
+    }
+
+    initUniRecorder() {
+        //录音结束后,发送
+        this.recorder.onStop((res) => {
+            const duration = Date.now() - this.uniStartTime;
+            res.duration = duration;
+            this.onRecordingComplete(res, duration);
+        });
+        // 监听录音报错
+        this.recorder.onError((e) => {
+            this.recording = false;
+            this.recorder.stop();
+            console.log('录音失败:', e);
+        })
+    }
+
+    authorize() {
+        return new Promise((resolve,reject) => {
+            // #ifdef H5
+            this.recorder.open(() => {
+                resolve();
+            }, (e) => {
+                this.recorder.close();
+                reject(e);
+            });
+            // #endif
+
+            // #ifndef H5
+            if (uni.authorize) {
+                uni.authorize({
+                    scope: 'scope.record',
+                    success() {
+                        resolve()
+                    },
+                    fail: (e) => {
+                        reject(e)
+                    }
+                });
+            }
+            // #endif
+        });
+    }
+
+    start() {
+        this.recording = true;
+
+        // #ifdef H5
+        this.recorder.start();
+        // #endif
+
+        // #ifndef H5
+        try {
+            // 更多配置参考uniapp:https://uniapp.dcloud.net.cn/api/media/record-manager.html#getrecordermanager
+            this.recorder.start({
+                duration: 600000 // 指定录音的时长,单位 ms
+            });
+            this.uniStartTime = Date.now();
+        } catch (e) {
+            console.log(e);
+        }
+        // #endif
+    }
+
+    stop() {
+        this.recording = false;
+
+        // #ifdef H5
+        this.recorder.stop((blob, duration) => {
+            const file = new File([blob], 'audio.mp3', {type: blob.type});
+            file.tempFilePath = URL.createObjectURL(blob);
+            file.duration = duration;
+            this.onRecordingComplete(file, duration);
+        }, (msg) => {
+            console.log('录音失败:',msg)
+        })
+        // #endif
+
+        // #ifndef H5
+        try {
+            this.recorder.stop();
+        } catch (e) {
+            console.log(e);
+        }
+        // #endif
+    }
+
+}

+ 24 - 0
libs/apps.js

@@ -0,0 +1,24 @@
+import { appAuth } from '../api/public';
+
+class Apps{
+	/**
+	 * 授权登录获取token
+	 * @param {Object} code
+	 */
+	authApp(code) {
+		return new Promise((resolve, reject) => {
+			appAuth(code,{'spread_spid': 0})
+				.then(({
+					data
+				}) => {
+					resolve(data);
+					Cache.set(WX_AUTH, code);
+					Cache.clear(STATE_KEY);
+					loginType && Cache.clear(LOGINTYPE);
+					
+				})
+				.catch(reject);
+		});
+	}
+}
+export default new Apps();

+ 62 - 0
libs/chat.js

@@ -0,0 +1,62 @@
+import $store from "@/store";
+import { VUE_APP_WS_URL } from "@/utils/index.js";
+
+const Socket = function() {
+  this.ws = new WebSocket(wss(VUE_APP_WS_URL));
+  this.ws.onopen = this.onOpen.bind(this);
+  this.ws.onerror = this.onError.bind(this);
+  this.ws.onmessage = this.onMessage.bind(this);
+  this.ws.onclose = this.onClose.bind(this);
+};
+
+function wss(wsSocketUrl) {
+    let ishttps = document.location.protocol == 'https:';
+    if (ishttps) {
+        return wsSocketUrl.replace('ws:', 'wss:');
+    } else {
+        return wsSocketUrl.replace('wss:', 'ws:');
+    }
+}
+
+Socket.prototype = {
+  vm(vm) {
+    this.vm = vm;
+  },
+  close() {
+    clearInterval(this.timer);
+    this.ws.close();
+  },
+  onOpen: function() {
+    console.log("ws open");
+    this.init();
+    this.send({
+      type: "login",
+      data: $store.state.app.token
+    });
+    this.vm.$emit("socket_open");
+  },
+  init: function() {
+    var that = this;
+    this.timer = setInterval(function() {
+      that.send({ type: "ping" });
+    }, 10000);
+  },
+  send: function(data) {
+    return this.ws.send(JSON.stringify(data));
+  },
+  onMessage: function(res) {
+    const { type, data = {} } = JSON.parse(res.data);
+    this.vm.$emit(type, data);
+  },
+  onClose: function() {
+    clearInterval(this.timer);
+  },
+  onError: function(e) {
+    console.log(e);
+    this.vm.$emit("socket_error", e);
+  }
+};
+
+Socket.prototype.constructor = Socket;
+
+export default Socket;

+ 63 - 0
libs/login.js

@@ -0,0 +1,63 @@
+import { useAppStore } from "@/stores/app.js";
+import Cache from "../utils/cache";
+import { Debounce } from "@/utils/validate.js";
+
+import {
+  LOGIN_STATUS,
+  USER_INFO,
+  EXPIRES_TIME,
+  STATE_R_KEY,
+  BACK_URL,
+} from "@/config/cache";
+
+function prePage() {
+  let pages = getCurrentPages();
+  let prePage = pages[pages.length - 1];
+  return prePage.route;
+}
+
+export const toLogin = Debounce(_toLogin, 800);
+
+export function _toLogin(push, pathLogin) {
+  const appStore = useAppStore();
+  appStore.LOGOUT();
+  let path = prePage();
+  let login_back_url = Cache.get(BACK_URL);
+
+  uni.navigateTo({
+    url: "/pages/users/login/index",
+  });
+}
+
+export function checkLogin() {
+  let token = Cache.get(LOGIN_STATUS);
+  let expiresTime = Cache.get(EXPIRES_TIME);
+  let newTime = Math.round(new Date() / 1000);
+  const appStore = useAppStore();
+
+  // 如果没有token,清除所有登录信息
+  if (!token) {
+    Cache.clear(LOGIN_STATUS);
+    Cache.clear(EXPIRES_TIME);
+    Cache.clear(USER_INFO);
+    Cache.clear(STATE_R_KEY);
+    return false;
+  }
+
+  // 如果有过期时间且已过期,清除所有登录信息
+  if (expiresTime && expiresTime < newTime) {
+    Cache.clear(LOGIN_STATUS);
+    Cache.clear(EXPIRES_TIME);
+    Cache.clear(USER_INFO);
+    Cache.clear(STATE_R_KEY);
+    return false;
+  }
+
+  // 恢复登录状态
+  appStore.UPDATE_LOGIN(token);
+  let userInfo = Cache.get(USER_INFO, true);
+  if (userInfo) {
+    appStore.UPDATE_USERINFO(userInfo);
+  }
+  return true;
+}

+ 138 - 0
libs/routine.js

@@ -0,0 +1,138 @@
+import { useAppStore } from "@/stores/app";
+import { checkLogin } from "./login";
+import { login } from "../api/public";
+import Cache from "../utils/cache";
+import { STATE_R_KEY, USER_INFO, EXPIRES_TIME } from "@/config/cache";
+// const appStore = useAppStore() // 在这里使用会报错,Pinia在main.js还未挂载
+class Routine {
+  constructor() {
+    this.scopeUserInfo = "scope.userInfo";
+  }
+
+  async getUserCode() {
+    let isAuth = await this.isAuth(),
+      code = "";
+    if (isAuth) code = await this.getCode();
+    return code;
+  }
+
+  /**
+   * 获取用户信息
+   */
+  getUserProfile() {
+    let code = this.getUserCode();
+    return new Promise((resolve, reject) => {
+      uni.getUserProfile({
+        lang: "zh_CN",
+        desc: "用于完善会员资料", // 声明获取用户个人信息后的用途,后续会展示在弹窗中,请谨慎填写
+        success(user) {
+          if (code) user.code = code;
+          resolve({ userInfo: user, islogin: false });
+        },
+        fail(res) {
+          reject(res);
+        },
+      });
+    });
+  }
+
+  /**
+   * 获取用户信息
+   */
+  authorize() {
+    return new Promise((resolve, reject) => {
+      if (checkLogin())
+        return resolve({
+          userInfo: Cache.get(USER_INFO, true),
+          islogin: true,
+        });
+      uni.authorize({
+        scope: this.scopeUserInfo,
+        success() {
+          resolve({ islogin: false });
+        },
+        fail(res) {
+          reject(res);
+        },
+      });
+    });
+  }
+
+  async getCode() {
+    let provider = await this.getProvider();
+    return new Promise((resolve, reject) => {
+      uni.login({
+        provider: provider,
+        success(res) {
+          if (res.code) Cache.set(STATE_R_KEY, res.code, 10800);
+          return resolve(res.code);
+        },
+        fail() {
+          return reject(null);
+        },
+      });
+    });
+  }
+
+  /**
+   * 获取服务供应商
+   */
+  getProvider() {
+    return new Promise((resolve, reject) => {
+      uni.getProvider({
+        service: "oauth",
+        success(res) {
+          resolve(res.provider);
+        },
+        fail() {
+          resolve(false);
+        },
+      });
+    });
+  }
+
+  /**
+   * 是否授权
+   */
+  isAuth() {
+    return new Promise((resolve, reject) => {
+      uni.getSetting({
+        success(res) {
+          if (!res.authSetting[this.scopeUserInfo]) {
+            resolve(true);
+          } else {
+            resolve(true);
+          }
+        },
+        fail() {
+          resolve(false);
+        },
+      });
+    });
+  }
+  /**
+   * 小程序登录
+   */
+  authUserInfo(code, data) {
+    return new Promise((resolve, reject) => {
+      login(code, data)
+        .then((res) => {
+          if (res.data.type === "login") {
+            const appStore = useAppStore();
+            appStore.LOGIN({ token: res.data.token });
+            appStore.SETUID(res.data.uid);
+            // 保存过期时间(7天后过期)
+            const expiresTime =
+              Math.round(new Date() / 1000) + 7 * 24 * 60 * 60;
+            Cache.set(EXPIRES_TIME, expiresTime, 0);
+          }
+          return resolve(res);
+        })
+        .catch((res) => {
+          return reject(res);
+        });
+    });
+  }
+}
+
+export default new Routine();

+ 322 - 0
libs/wechat.js

@@ -0,0 +1,322 @@
+// #ifdef H5
+import WechatJSSDK from 'weixin-js-sdk'
+
+import {
+	getWechatConfig,
+	wechatAuth
+} from "@/api/public";
+import {
+	WX_AUTH,
+	STATE_KEY,
+	LOGINTYPE,
+	BACK_URL
+} from '@/config/cache';
+import {
+	parseQuery
+} from '@/utils';
+import { useAppStore } from '@/stores/app.js';
+import Cache from '@/utils/cache';
+
+class AuthWechat {
+
+	constructor() {
+		//微信实例化对象
+		this.instance = WechatJSSDK;
+		//是否实例化
+		this.status = false;
+
+		this.initConfig = {};
+
+	}
+	
+	isAndroid(){
+		let u = navigator.userAgent;
+		return u.indexOf('Android') > -1 || u.indexOf('Adr') > -1;
+	}
+	
+	signLink() {
+		if (typeof window.entryUrl === 'undefined' || window.entryUrl === '') {
+			window.entryUrl = location.href.split('#')[0]
+		}
+		return  /(Android)/i.test(navigator.userAgent) ? location.href.split('#')[0] : window.entryUrl;
+	}
+
+
+	/**
+	 * 初始化wechat(分享配置)
+	 */
+	wechat() {
+		return new Promise((resolve, reject) => {
+			// if (this.status && !this.isAndroid()) return resolve(this.instance);
+			getWechatConfig()
+				.then(res => {
+					this.instance.config(res.data);
+					this.initConfig = res.data;
+					this.status = true;
+					this.instance.ready(() => {
+						resolve(this.instance);
+					})
+				}).catch(err => {
+					console.log('微信分享配置失败',err);
+					this.status = false;
+					reject(err);
+				});
+		});
+	}
+
+	/**
+	 * 验证是否初始化
+	 */
+	verifyInstance() {
+		let that = this;
+		return new Promise((resolve, reject) => {
+			if (that.instance === null && !that.status) {
+				that.wechat().then(res => {
+					resolve(that.instance);
+				}).catch(() => {
+					return reject();
+				})
+			} else {
+				return resolve(that.instance);
+			}
+		})
+	}
+	// 微信公众号的共享地址
+	openAddress() {
+		return new Promise((resolve, reject) => {
+			this.wechat().then(wx => {
+				this.toPromise(wx.openAddress).then(res => {
+					resolve(res);
+				}).catch(err => {
+					reject(err);
+				});
+			}).catch(err => {
+				reject(err);
+			})
+		});
+	}
+
+    // 获取经纬度;
+	location(){
+		return new Promise((resolve, reject) => {
+			this.wechat().then(wx => {
+				this.toPromise(wx.getLocation,{type: 'wgs84'}).then(res => {
+					resolve(res);
+				}).catch(err => {
+					reject(err);
+				});
+			}).catch(err => {
+				reject(err);
+			})
+		});
+	} 
+	
+	// 使用微信内置地图查看位置接口;
+	seeLocation(config){
+		return new Promise((resolve, reject) => {
+			this.wechat().then(wx => {
+				this.toPromise(wx.openLocation, config).then(res => {
+					resolve(res);
+				}).catch(err => {
+					reject(err);
+				});
+			}).catch(err => {
+				reject(err);
+			})
+		});
+	}
+	
+	/**
+	 * 微信支付
+	 * @param {Object} config
+	 */
+	pay(config) {
+		return new Promise((resolve, reject) => {
+			this.wechat().then((wx) => {
+				this.toPromise(wx.chooseWXPay, config).then(res => {
+					resolve(res);
+				}).catch(res => {
+					resolve(res);
+				});
+			}).catch(res => {
+				reject(res);
+			});
+		});
+	}
+	
+	toPromise(fn, config = {}) {
+		return new Promise((resolve, reject) => {
+			fn({
+				...config,
+				success(res) {
+					resolve(res);
+				},
+				fail(err) {
+					reject(err);
+				},
+				complete(err) {
+					reject(err);
+				},
+				cancel(err) {
+					reject(err);
+				}
+			});
+		});
+	}
+
+	/**
+	 * 自动去授权
+	 */
+	oAuth(snsapiBase,url) {
+		const TOKEN = useAppStore.tokenComputed
+		if (uni.getStorageSync(WX_AUTH) && TOKEN && snsapiBase == 'snsapi_base') return;
+		const {
+			code
+		} = parseQuery();
+		if (!code || code == uni.getStorageSync('snsapiCode')){
+			return this.toAuth(snsapiBase,url);
+		}else{
+			if(Cache.has('snsapiKey'))
+				return this.auth(code).catch(error=>{
+					uni.showToast({
+						title:error,
+						icon:'none'
+					})
+				})
+		}
+		// if (uni.getStorageSync(WX_AUTH) && store.state.app.token) return;
+		// const {
+		// 	code
+		// } = parseQuery();
+		// if (!code){
+		// 	return this.toAuth(snsapiBase,url);
+		// }else{
+		// 	if(Cache.has('snsapiKey'))
+		// 		return this.auth(code).catch(error=>{
+		// 			uni.showToast({
+		// 				title:error,
+		// 				icon:'none'
+		// 			})
+		// 		})
+		// }
+	}
+
+	clearAuthStatus() {
+
+	}
+
+	/**
+	 * 授权登录获取token
+	 * @param {Object} code
+	 */
+	auth(code) {
+		return new Promise((resolve, reject) => {
+			wechatAuth(code, Cache.get("spread"))
+				.then(({
+					data
+				}) => {
+					resolve(data);
+					Cache.set(WX_AUTH, code);
+					Cache.clear(STATE_KEY);
+					// Cache.clear('spread');
+					loginType && Cache.clear(LOGINTYPE);
+					
+				})
+				.catch(reject);
+		});
+	}
+
+	/**
+	 * 获取跳转授权后的地址
+	 * @param {Object} appId
+	 */
+	getAuthUrl(appId,snsapiBase,backUrl) {
+		let url = `${location.origin}${backUrl}`
+				if(url.indexOf('?') == -1){
+							url = url+'?'
+						}else{
+							url = url+'&'
+						}
+				const redirect_uri = encodeURIComponent(
+					`${url}scope=${snsapiBase}&back_url=` +
+					encodeURIComponent(
+						encodeURIComponent(
+							uni.getStorageSync(BACK_URL) ?
+							uni.getStorageSync(BACK_URL) :
+							location.pathname + location.search
+						)
+					)
+				);
+				uni.removeStorageSync(BACK_URL);
+				const state = encodeURIComponent(
+					("" + Math.random()).split(".")[1] + "authorizestate"
+				);
+				uni.setStorageSync(STATE_KEY, state);
+				return `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appId}&redirect_uri=${redirect_uri}&response_type=code&scope=snsapi_userinfo&state=${state}#wechat_redirect`;
+				// if(snsapiBase==='snsapi_base'){
+				// 	return `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appId}&redirect_uri=${redirect_uri}&response_type=code&scope=snsapi_base&state=${state}#wechat_redirect`;
+				// }else{
+				// 	return `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appId}&redirect_uri=${redirect_uri}&response_type=code&scope=snsapi_userinfo&state=${state}#wechat_redirect`;
+				// }
+    }
+	
+	/**
+	 * 跳转自动登录
+	 */
+	toAuth(snsapiBase,backUrl) {
+		let that = this;
+		this.wechat().then(wx => {
+			location.href = this.getAuthUrl(that.initConfig.appId,snsapiBase,backUrl);
+		})
+	}
+
+	/**
+	 * 绑定事件
+	 * @param {Object} name 事件名
+	 * @param {Object} config 参数
+	 */
+	wechatEvevt(name, config) {
+		let that = this;
+		return new Promise((resolve, reject) => {
+			let configDefault = {
+				fail(res) {
+					if (that.instance) return reject({
+						is_ready: true,
+						wx: that.instance
+					});
+					that.verifyInstance().then(wx => {
+						return reject({
+							is_ready: true,
+							wx: wx
+						});
+					})
+				},
+				success(res) {
+					return resolve(res,2222);
+				}
+			};
+			Object.assign(configDefault, config);
+			that.wechat().then(wx => {
+				if (typeof name === 'object') {
+					name.forEach(item => {
+						wx[item] && wx[item](configDefault)
+					})
+				} else {
+					wx[name] && wx[name](configDefault)
+				}
+			})
+		});
+	}
+
+	isWeixin() {
+		return navigator.userAgent.toLowerCase().indexOf("micromessenger") !== -1;
+	}
+
+}
+
+export default new AuthWechat();
+// #endif
+
+// #ifndef H5
+export default {};
+// #endif

+ 29 - 0
main.js

@@ -0,0 +1,29 @@
+import App from "./App";
+import * as Pinia from "pinia";
+import uviewPlus from "@/uni_modules/uview-plus";
+
+import { createSSRApp } from "vue";
+export function createApp() {
+  const app = createSSRApp(App);
+  app.use(Pinia.createPinia());
+
+  app.use(uviewPlus, () => {
+    return {
+      options: {
+        // 修改$u.config对象的属性
+        config: {
+          // 修改默认单位为rpx,相当于执行 uni.$u.config.unit = 'rpx'
+          unit: "rpx",
+          // customIcon: {
+          // family: 'iconfont',
+          // url: '//at.alicdn.com/t/c/font_4946742_e8oa3t01rkk.css'
+          // }
+        },
+      },
+    };
+  });
+
+  return {
+    app,
+  };
+}

+ 80 - 0
manifest.json

@@ -0,0 +1,80 @@
+{
+    "name" : "wxapp-shuibei",
+    "appid" : "__UNI__5EF0BA0",
+    "description" : "",
+    "versionName" : "1.0.0",
+    "versionCode" : "100",
+    "transformPx" : false,
+    /* 5+App特有相关 */
+    "app-plus" : {
+        "usingComponents" : true,
+        "nvueStyleCompiler" : "uni-app",
+        "compilerVersion" : 3,
+        "splashscreen" : {
+            "alwaysShowBeforeRender" : true,
+            "waiting" : true,
+            "autoclose" : true,
+            "delay" : 0
+        },
+        /* 模块配置 */
+        "modules" : {},
+        /* 应用发布信息 */
+        "distribute" : {
+            /* android打包配置 */
+            "android" : {
+                "permissions" : [
+                    "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
+                    "<uses-permission android:name=\"android.permission.VIBRATE\"/>",
+                    "<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
+                    "<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.CAMERA\"/>",
+                    "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
+                    "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
+                    "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
+                    "<uses-feature android:name=\"android.hardware.camera\"/>",
+                    "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
+                ]
+            },
+            /* ios打包配置 */
+            "ios" : {},
+            /* SDK配置 */
+            "sdkConfigs" : {}
+        }
+    },
+    /* 快应用特有相关 */
+    "quickapp" : {},
+    /* 小程序特有相关 */
+    "mp-weixin" : {
+        "appid" : "wxafbc646c10f9bd15",
+        "setting" : {
+            "urlCheck" : false,
+            "es6" : true,
+            "postcss" : true,
+            "minified" : true
+        },
+        "usingComponents" : true,
+        "permission" : {
+            "scope.userLocation" : {
+                "desc" : "用于获取您的位置信息,以便提供附近服务"
+            }
+        }
+    },
+    "mp-alipay" : {
+        "usingComponents" : true
+    },
+    "mp-baidu" : {
+        "usingComponents" : true
+    },
+    "mp-toutiao" : {
+        "usingComponents" : true
+    },
+    "uniStatistics" : {
+        "enable" : false
+    },
+    "vueVersion" : "3"
+}

+ 465 - 0
package-lock.json

@@ -0,0 +1,465 @@
+{
+  "name": "wxapp-shuibei",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "dependencies": {
+        "clipboard": "^2.0.11",
+        "dayjs": "^1.11.13",
+        "pako": "^2.1.0",
+        "pinia": "^3.0.3",
+        "recorder-core": "^1.3.25011100",
+        "uqrcodejs": "^4.0.7",
+        "weixin-js-sdk": "^1.6.5"
+      }
+    },
+    "node_modules/@babel/helper-string-parser": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+      "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+      "peer": true,
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-identifier": {
+      "version": "7.28.5",
+      "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+      "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+      "peer": true,
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/parser": {
+      "version": "7.28.5",
+      "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.28.5.tgz",
+      "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
+      "peer": true,
+      "dependencies": {
+        "@babel/types": "^7.28.5"
+      },
+      "bin": {
+        "parser": "bin/babel-parser.js"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@babel/types": {
+      "version": "7.28.5",
+      "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.28.5.tgz",
+      "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
+      "peer": true,
+      "dependencies": {
+        "@babel/helper-string-parser": "^7.27.1",
+        "@babel/helper-validator-identifier": "^7.28.5"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.5.5",
+      "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+      "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+      "peer": true
+    },
+    "node_modules/@vue/compiler-core": {
+      "version": "3.5.22",
+      "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.22.tgz",
+      "integrity": "sha512-jQ0pFPmZwTEiRNSb+i9Ow/I/cHv2tXYqsnHKKyCQ08irI2kdF5qmYedmF8si8mA7zepUFmJ2hqzS8CQmNOWOkQ==",
+      "peer": true,
+      "dependencies": {
+        "@babel/parser": "^7.28.4",
+        "@vue/shared": "3.5.22",
+        "entities": "^4.5.0",
+        "estree-walker": "^2.0.2",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "node_modules/@vue/compiler-dom": {
+      "version": "3.5.22",
+      "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.22.tgz",
+      "integrity": "sha512-W8RknzUM1BLkypvdz10OVsGxnMAuSIZs9Wdx1vzA3mL5fNMN15rhrSCLiTm6blWeACwUwizzPVqGJgOGBEN/hA==",
+      "peer": true,
+      "dependencies": {
+        "@vue/compiler-core": "3.5.22",
+        "@vue/shared": "3.5.22"
+      }
+    },
+    "node_modules/@vue/compiler-sfc": {
+      "version": "3.5.22",
+      "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.22.tgz",
+      "integrity": "sha512-tbTR1zKGce4Lj+JLzFXDq36K4vcSZbJ1RBu8FxcDv1IGRz//Dh2EBqksyGVypz3kXpshIfWKGOCcqpSbyGWRJQ==",
+      "peer": true,
+      "dependencies": {
+        "@babel/parser": "^7.28.4",
+        "@vue/compiler-core": "3.5.22",
+        "@vue/compiler-dom": "3.5.22",
+        "@vue/compiler-ssr": "3.5.22",
+        "@vue/shared": "3.5.22",
+        "estree-walker": "^2.0.2",
+        "magic-string": "^0.30.19",
+        "postcss": "^8.5.6",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "node_modules/@vue/compiler-ssr": {
+      "version": "3.5.22",
+      "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.22.tgz",
+      "integrity": "sha512-GdgyLvg4R+7T8Nk2Mlighx7XGxq/fJf9jaVofc3IL0EPesTE86cP/8DD1lT3h1JeZr2ySBvyqKQJgbS54IX1Ww==",
+      "peer": true,
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.22",
+        "@vue/shared": "3.5.22"
+      }
+    },
+    "node_modules/@vue/devtools-api": {
+      "version": "7.7.7",
+      "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-7.7.7.tgz",
+      "integrity": "sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg==",
+      "dependencies": {
+        "@vue/devtools-kit": "^7.7.7"
+      }
+    },
+    "node_modules/@vue/devtools-kit": {
+      "version": "7.7.7",
+      "resolved": "https://registry.npmmirror.com/@vue/devtools-kit/-/devtools-kit-7.7.7.tgz",
+      "integrity": "sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA==",
+      "dependencies": {
+        "@vue/devtools-shared": "^7.7.7",
+        "birpc": "^2.3.0",
+        "hookable": "^5.5.3",
+        "mitt": "^3.0.1",
+        "perfect-debounce": "^1.0.0",
+        "speakingurl": "^14.0.1",
+        "superjson": "^2.2.2"
+      }
+    },
+    "node_modules/@vue/devtools-shared": {
+      "version": "7.7.7",
+      "resolved": "https://registry.npmmirror.com/@vue/devtools-shared/-/devtools-shared-7.7.7.tgz",
+      "integrity": "sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==",
+      "dependencies": {
+        "rfdc": "^1.4.1"
+      }
+    },
+    "node_modules/@vue/reactivity": {
+      "version": "3.5.22",
+      "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.22.tgz",
+      "integrity": "sha512-f2Wux4v/Z2pqc9+4SmgZC1p73Z53fyD90NFWXiX9AKVnVBEvLFOWCEgJD3GdGnlxPZt01PSlfmLqbLYzY/Fw4A==",
+      "peer": true,
+      "dependencies": {
+        "@vue/shared": "3.5.22"
+      }
+    },
+    "node_modules/@vue/runtime-core": {
+      "version": "3.5.22",
+      "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.22.tgz",
+      "integrity": "sha512-EHo4W/eiYeAzRTN5PCextDUZ0dMs9I8mQ2Fy+OkzvRPUYQEyK9yAjbasrMCXbLNhF7P0OUyivLjIy0yc6VrLJQ==",
+      "peer": true,
+      "dependencies": {
+        "@vue/reactivity": "3.5.22",
+        "@vue/shared": "3.5.22"
+      }
+    },
+    "node_modules/@vue/runtime-dom": {
+      "version": "3.5.22",
+      "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.22.tgz",
+      "integrity": "sha512-Av60jsryAkI023PlN7LsqrfPvwfxOd2yAwtReCjeuugTJTkgrksYJJstg1e12qle0NarkfhfFu1ox2D+cQotww==",
+      "peer": true,
+      "dependencies": {
+        "@vue/reactivity": "3.5.22",
+        "@vue/runtime-core": "3.5.22",
+        "@vue/shared": "3.5.22",
+        "csstype": "^3.1.3"
+      }
+    },
+    "node_modules/@vue/server-renderer": {
+      "version": "3.5.22",
+      "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.22.tgz",
+      "integrity": "sha512-gXjo+ao0oHYTSswF+a3KRHZ1WszxIqO7u6XwNHqcqb9JfyIL/pbWrrh/xLv7jeDqla9u+LK7yfZKHih1e1RKAQ==",
+      "peer": true,
+      "dependencies": {
+        "@vue/compiler-ssr": "3.5.22",
+        "@vue/shared": "3.5.22"
+      },
+      "peerDependencies": {
+        "vue": "3.5.22"
+      }
+    },
+    "node_modules/@vue/shared": {
+      "version": "3.5.22",
+      "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.22.tgz",
+      "integrity": "sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w==",
+      "peer": true
+    },
+    "node_modules/birpc": {
+      "version": "2.7.0",
+      "resolved": "https://registry.npmmirror.com/birpc/-/birpc-2.7.0.tgz",
+      "integrity": "sha512-tub/wFGH49vNCm0xraykcY3TcRgX/3JsALYq/Lwrtti+bTyFHkCUAWF5wgYoie8P41wYwig2mIKiqoocr1EkEQ==",
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/clipboard": {
+      "version": "2.0.11",
+      "resolved": "https://registry.npmmirror.com/clipboard/-/clipboard-2.0.11.tgz",
+      "integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==",
+      "dependencies": {
+        "good-listener": "^1.2.2",
+        "select": "^1.1.2",
+        "tiny-emitter": "^2.0.0"
+      }
+    },
+    "node_modules/copy-anything": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmmirror.com/copy-anything/-/copy-anything-4.0.5.tgz",
+      "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==",
+      "dependencies": {
+        "is-what": "^5.2.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/mesqueeb"
+      }
+    },
+    "node_modules/csstype": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz",
+      "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
+      "peer": true
+    },
+    "node_modules/dayjs": {
+      "version": "1.11.19",
+      "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.19.tgz",
+      "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw=="
+    },
+    "node_modules/delegate": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmmirror.com/delegate/-/delegate-3.2.0.tgz",
+      "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw=="
+    },
+    "node_modules/entities": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz",
+      "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+      "peer": true,
+      "engines": {
+        "node": ">=0.12"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
+    "node_modules/estree-walker": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz",
+      "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+      "peer": true
+    },
+    "node_modules/good-listener": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmmirror.com/good-listener/-/good-listener-1.2.2.tgz",
+      "integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==",
+      "dependencies": {
+        "delegate": "^3.1.2"
+      }
+    },
+    "node_modules/hookable": {
+      "version": "5.5.3",
+      "resolved": "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz",
+      "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="
+    },
+    "node_modules/is-what": {
+      "version": "5.5.0",
+      "resolved": "https://registry.npmmirror.com/is-what/-/is-what-5.5.0.tgz",
+      "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/mesqueeb"
+      }
+    },
+    "node_modules/magic-string": {
+      "version": "0.30.21",
+      "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz",
+      "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+      "peer": true,
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.5"
+      }
+    },
+    "node_modules/mitt": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz",
+      "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.11",
+      "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz",
+      "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "peer": true,
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/pako": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/pako/-/pako-2.1.0.tgz",
+      "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug=="
+    },
+    "node_modules/perfect-debounce": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
+      "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+      "peer": true
+    },
+    "node_modules/pinia": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmmirror.com/pinia/-/pinia-3.0.3.tgz",
+      "integrity": "sha512-ttXO/InUULUXkMHpTdp9Fj4hLpD/2AoJdmAbAeW2yu1iy1k+pkFekQXw5VpC0/5p51IOR/jDaDRfRWRnMMsGOA==",
+      "dependencies": {
+        "@vue/devtools-api": "^7.7.2"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/posva"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.4.4",
+        "vue": "^2.7.0 || ^3.5.11"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/postcss": {
+      "version": "8.5.6",
+      "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz",
+      "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "peer": true,
+      "dependencies": {
+        "nanoid": "^3.3.11",
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.1"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/recorder-core": {
+      "version": "1.3.25011100",
+      "resolved": "https://registry.npmmirror.com/recorder-core/-/recorder-core-1.3.25011100.tgz",
+      "integrity": "sha512-trXsCH0zurhoizT4Z22C0OsM0SDOW+2OvtgRxeLQFwxoFeqFjDjYZsbZEZUiKMJLhBvamI4K7Ic+qZ2LBo74TA=="
+    },
+    "node_modules/rfdc": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz",
+      "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="
+    },
+    "node_modules/select": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmmirror.com/select/-/select-1.1.2.tgz",
+      "integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA=="
+    },
+    "node_modules/source-map-js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+      "peer": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/speakingurl": {
+      "version": "14.0.1",
+      "resolved": "https://registry.npmmirror.com/speakingurl/-/speakingurl-14.0.1.tgz",
+      "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/superjson": {
+      "version": "2.2.5",
+      "resolved": "https://registry.npmmirror.com/superjson/-/superjson-2.2.5.tgz",
+      "integrity": "sha512-zWPTX96LVsA/eVYnqOM2+ofcdPqdS1dAF1LN4TS2/MWuUpfitd9ctTa87wt4xrYnZnkLtS69xpBdSxVBP5Rm6w==",
+      "dependencies": {
+        "copy-anything": "^4"
+      },
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/tiny-emitter": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
+      "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="
+    },
+    "node_modules/uqrcodejs": {
+      "version": "4.0.7",
+      "resolved": "https://registry.npmmirror.com/uqrcodejs/-/uqrcodejs-4.0.7.tgz",
+      "integrity": "sha512-84+aZmD2godCVI+93lxE3YUAPNY8zAJvNA7xRS7R7U+q57KzMDepBSfNCwoRUhWOfR6eHFoAOcHRPwsP6ka1cA=="
+    },
+    "node_modules/vue": {
+      "version": "3.5.22",
+      "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.22.tgz",
+      "integrity": "sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==",
+      "peer": true,
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.22",
+        "@vue/compiler-sfc": "3.5.22",
+        "@vue/runtime-dom": "3.5.22",
+        "@vue/server-renderer": "3.5.22",
+        "@vue/shared": "3.5.22"
+      },
+      "peerDependencies": {
+        "typescript": "*"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/weixin-js-sdk": {
+      "version": "1.6.5",
+      "resolved": "https://registry.npmmirror.com/weixin-js-sdk/-/weixin-js-sdk-1.6.5.tgz",
+      "integrity": "sha512-Gph1WAWB2YN/lMOFB/ymb+hbU/wYazzJgu6PMMktCy9cSCeW5wA6Zwt0dpahJbJ+RJEwtTv2x9iIu0U4enuVSQ=="
+    }
+  }
+}

+ 11 - 0
package.json

@@ -0,0 +1,11 @@
+{
+  "dependencies": {
+    "clipboard": "^2.0.11",
+    "dayjs": "^1.11.13",
+    "pako": "^2.1.0",
+    "pinia": "^3.0.3",
+    "recorder-core": "^1.3.25011100",
+    "uqrcodejs": "^4.0.7",
+    "weixin-js-sdk": "^1.6.5"
+  }
+}

+ 71 - 0
pages.json

@@ -0,0 +1,71 @@
+{
+	"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
+		{
+			"path": "pages/index/index",
+			"style": {
+				"navigationBarTitleText": "首页"
+			}
+		},
+		{
+			"path": "pages/order_addcart/order_addcart",
+			"style": {
+				"navigationBarTitleText": "购物车"
+			}
+		},
+		{
+			"path": "pages/user/index",
+			"style": {
+				"navigationBarTitleText": "个人中心"
+			}
+		}
+	],
+	"subPackages": [
+		{
+			"root": "pages/users",
+			"name": "users",
+			"pages": [
+				{
+					"path": "order_list/index",
+					"style": {
+						"navigationBarTitleText": "我的订单",
+						"navigationBarBackgroundColor": "#e93323",
+						"navigationBarTextStyle": "black"
+					}
+				}
+			]
+		}
+	],
+	"globalStyle": {
+		"navigationBarBackgroundColor": "#F8F8F8",
+		"navigationBarTextStyle": "black",
+		"navigationBarTitleText": "水贝商城",
+		"backgroundColor": "#ffe079"
+	},
+	"tabBar": {
+		"color": "#282828",
+		"selectedColor": "#CD9933",
+		"borderStyle": "black",
+		"backgroundColor": "#ffffff",
+		"list": [
+			{
+				"pagePath": "pages/index/index",
+				"iconPath": "static/images/tabbar/1-001.png",
+				"selectedIconPath": "static/images/tabbar/1-002.png",
+				"text": "首页"
+			},
+			{
+				"pagePath": "pages/order_addcart/order_addcart",
+				"iconPath": "static/images/tabbar/3-001.png",
+				"selectedIconPath": "static/images/tabbar/3-002.png",
+				"text": "购物车"
+			},
+			{
+				"pagePath": "pages/user/index",
+				"iconPath": "static/images/tabbar/4-001.png",
+				"selectedIconPath": "static/images/tabbar/4-002.png",
+				"text": "我的"
+			}
+		]
+	},
+	"uniIdRouter": {}
+}

+ 27 - 0
pages/index/index.vue

@@ -0,0 +1,27 @@
+<template>
+	<view class="homeWrapper">
+		首页
+		<text>{{ shopInfo.name }}</text>
+	</view>
+</template>
+
+<script setup>
+import { onLoad, onShow, onLaunch, onShareAppMessage } from "@dcloudio/uni-app";
+import { ref, computed, watch, Text } from "vue";
+import { useAppStore } from "@/stores/app";
+import { useStoreRights } from "@/stores/rights";
+import useRealGoldPrice from "@/hooks/useRealGoldPrice";
+// 获取实时金价
+// const { realGoldprice } = useRealGoldPrice("RTJ_Au");
+const appStore = useAppStore()
+const shopInfo = appStore.shopInfoGetter;
+console.log("shopInfo=",appStore.shopInfoGetter)
+</script>
+<style lang="scss" setup>
+.homeWrapper{
+	background-color: #f5f5f5;
+	text{
+		color:#ff0000;
+	}
+}
+</style>

+ 6 - 0
pages/order_addcart/order_addcart.vue

@@ -0,0 +1,6 @@
+<template>
+    <view>购物车</view>
+</template>
+<script setup>
+    
+</script>

+ 6 - 0
pages/user/index.vue

@@ -0,0 +1,6 @@
+<template>
+    <view>我的</view>
+</template>
+<script setup>
+    
+</script>

+ 6 - 0
pages/users/order_list/index.vue

@@ -0,0 +1,6 @@
+<template>
+    <view></view>
+</template>
+<script setup>
+    
+</script>

+ 25 - 0
project.config.json

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

+ 14 - 0
project.private.config.json

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

File diff suppressed because it is too large
+ 470 - 0
static/css/base.css


File diff suppressed because it is too large
+ 692 - 0
static/css/style.scss


BIN
static/images/tabbar/1-001.png


BIN
static/images/tabbar/1-002.png


BIN
static/images/tabbar/2-001.png


BIN
static/images/tabbar/2-002.png


BIN
static/images/tabbar/3-001.png


BIN
static/images/tabbar/3-002.png


BIN
static/images/tabbar/4-001.png


BIN
static/images/tabbar/4-002.png


+ 140 - 0
stores/app.js

@@ -0,0 +1,140 @@
+import { defineStore } from "pinia";
+import { getUserInfo } from "@/api/user.js";
+import { LOGIN_STATUS, UID, PLATFORM, EXPIRES_TIME } from "@/config/cache.js";
+import Cache from "@/utils/cache.js";
+import { USER_INFO } from "@/config/cache.js";
+import { getMiniProgramData } from "@/api/api";
+
+export const useAppStore = defineStore("app", {
+  state: () => {
+    return {
+      token: Cache.get(LOGIN_STATUS) || "",
+      backgroundColor: "#fff",
+      userInfo: Cache.get(USER_INFO) ? JSON.parse(Cache.get(USER_INFO)) : null,
+      uid: Cache.get(UID) || null,
+      homeActive: false,
+      chatUrl: Cache.get("chatUrl") || "",
+      systemPlatform: Cache.get(PLATFORM) ? Cache.get(PLATFORM) : "",
+      productType: Cache.get("productType") || "",
+      navbarHeight: 0,
+      userPanelInfo: {},
+      refreshArticles: false,
+      indexRefreshArticles: false,
+      wxConfig: {
+        auditModeEnabled: false,
+        carouselImages: [
+          {
+            id: 0,
+            imageUrl: "",
+            jumpUrl: "",
+            sort: 0,
+          },
+        ],
+        homePopupImage: "",
+        inviteImage: "",
+        isOpen: true,
+        logoImage: "",
+        mailingAddress: "",
+        signCoinValue: 0,
+        signGrowthValue: 0,
+      },
+      // 店铺信息
+      shopInfo:{
+        name:"水贝001号店铺",
+        shopId:"001"
+      }
+    };
+  },
+  getters: {
+    uidComputed: (state) => state.uid,
+    tokenComputed: (state) => state.token,
+    isLogin: (state) => !!state.token,
+    homeActiveComputed: (state) => state.homeActive,
+    productTypeComputed: (state) => state.productType,
+    chatUrlComputed: (state) => state.chatUrl,
+    $userInfo: (state) => state.userInfo,
+    navbarHeightGetter: (state) => state.navbarHeight,
+    userPanelInfoGetter: (state) => state.userPanelInfo,
+    $wxConfig: (state) => state.wxConfig,
+    shopInfoGetter: (state) => state.shopInfo,
+  },
+  actions: {
+    SET_REFRESH(val) {
+      this.refreshArticles = val;
+    },
+    setIndexRefersh(val) {
+      this.indexRefreshArticles = val;
+    },
+    SET_NAVBAR_HEIGHT(val) {
+      this.navbarHeight = val;
+    },
+    LOGIN(opt) {
+      this.token = opt.token;
+      Cache.set(LOGIN_STATUS, opt.token);
+    },
+    SETUID(val) {
+      this.uid = val;
+      Cache.set(UID, val);
+    },
+    UPDATE_LOGIN(token) {
+      this.token = token;
+    },
+    LOGOUT() {
+      this.token = undefined;
+      this.uid = undefined;
+      Cache.clear(LOGIN_STATUS);
+      Cache.clear(UID);
+      Cache.clear(USER_INFO);
+      Cache.clear(EXPIRES_TIME);
+    },
+    BACKGROUND_COLOR(color) {
+      this.color = color;
+      document.body.style.backgroundColor = color;
+    },
+    UPDATE_USERINFO(userInfo) {
+      this.userInfo = userInfo;
+      Cache.set(USER_INFO, userInfo);
+    },
+    OPEN_HOME() {
+      this.homeActive = true;
+    },
+    CLOSE_HOME() {
+      this.homeActive = false;
+    },
+    SET_CHATURL(chatUrl) {
+      this.chatUrl = chatUrl;
+    },
+    SYSTEM_PLATFORM(systemPlatform) {
+      this.systemPlatform = systemPlatform;
+      Cache.set(PLATFORM, systemPlatform);
+    },
+    changInfo(payload) {
+      this.userInfo[payload.amount1] = payload.amount2;
+      Cache.set(USER_INFO, this.userInfo);
+    },
+    PRODUCT_TYPE(productType) {
+      this.productType = productType;
+      Cache.set("productType", productType);
+    },
+    async USERINFO(force) {
+      try {
+        const res = await getUserInfo();
+        this.UPDATE_USERINFO(res.data);
+        return res.data;
+      } catch (e) {}
+    },
+    // 获取小程序基本配置
+    async GET_WX_CONFIG() {
+      try {
+        const res = await getMiniProgramData();
+        this.wxConfig = { ...res.data };
+      } catch (e) {
+        console.log("GET_WX_CONFIG-stores-res", e);
+        throw e;
+      }
+    },
+    UPDATE_userPanelInfo(userPanelInfo) {
+      this.userPanelInfo = userPanelInfo;
+    },
+  },
+});

+ 3 - 0
stores/pinia.js

@@ -0,0 +1,3 @@
+import { createPinia } from 'pinia';
+const pinia = createPinia();
+export default pinia;

+ 25 - 0
stores/rights.js

@@ -0,0 +1,25 @@
+import { defineStore } from "pinia";
+import { ref } from "vue";
+import { getUserLevelInfo } from "@/api/user";
+
+// 定义并导出 store
+export const useStoreRights = defineStore("rights", () => {
+  // 用户权益
+  const userBenefits = ref({});
+
+  const getUserBenefits = async (id) => {
+    try {
+      const res = await getUserLevelInfo(id);
+      userBenefits.value = res.data || { sold: 0, buy: 0 }; // 默认权益为0,避免NaN
+    } catch (error) {
+      console.error("获取用户权益失败:", error);
+      userBenefits.value = { sold: 0, buy: 0 }; // 出错时默认权益为0
+    }
+  };
+
+  // 4. 返回需要暴露的状态、计算属性、方法
+  return {
+    userBenefits,
+    getUserBenefits,
+  };
+});

+ 13 - 0
uni.promisify.adaptor.js

@@ -0,0 +1,13 @@
+uni.addInterceptor({
+  returnValue (res) {
+    if (!(!!res && (typeof res === "object" || typeof res === "function") && typeof res.then === "function")) {
+      return res;
+    }
+    return new Promise((resolve, reject) => {
+      res.then((res) => {
+        if (!res) return resolve(res) 
+        return res[0] ? reject(res[0]) : resolve(res[1])
+      });
+    });
+  },
+});

+ 76 - 0
uni.scss

@@ -0,0 +1,76 @@
+/**
+ * 这里是uni-app内置的常用样式变量
+ *
+ * uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
+ * 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
+ *
+ */
+
+/**
+ * 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
+ *
+ * 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
+ */
+
+/* 颜色变量 */
+
+/* 行为相关颜色 */
+$uni-color-primary: #007aff;
+$uni-color-success: #4cd964;
+$uni-color-warning: #f0ad4e;
+$uni-color-error: #dd524d;
+
+/* 文字基本颜色 */
+$uni-text-color:#333;//基本色
+$uni-text-color-inverse:#fff;//反色
+$uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息
+$uni-text-color-placeholder: #808080;
+$uni-text-color-disable:#c0c0c0;
+
+/* 背景颜色 */
+$uni-bg-color:#ffffff;
+$uni-bg-color-grey:#f8f8f8;
+$uni-bg-color-hover:#f1f1f1;//点击状态颜色
+$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色
+
+/* 边框颜色 */
+$uni-border-color:#c8c7cc;
+
+/* 尺寸变量 */
+
+/* 文字尺寸 */
+$uni-font-size-sm:12px;
+$uni-font-size-base:14px;
+$uni-font-size-lg:16px;
+
+/* 图片尺寸 */
+$uni-img-size-sm:20px;
+$uni-img-size-base:26px;
+$uni-img-size-lg:40px;
+
+/* Border Radius */
+$uni-border-radius-sm: 2px;
+$uni-border-radius-base: 3px;
+$uni-border-radius-lg: 6px;
+$uni-border-radius-circle: 50%;
+
+/* 水平间距 */
+$uni-spacing-row-sm: 5px;
+$uni-spacing-row-base: 10px;
+$uni-spacing-row-lg: 15px;
+
+/* 垂直间距 */
+$uni-spacing-col-sm: 4px;
+$uni-spacing-col-base: 8px;
+$uni-spacing-col-lg: 12px;
+
+/* 透明度 */
+$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
+
+/* 文章场景相关 */
+$uni-color-title: #2C405A; // 文章标题颜色
+$uni-font-size-title:20px;
+$uni-color-subtitle: #555555; // 二级标题颜色
+$uni-font-size-subtitle:26px;
+$uni-color-paragraph: #3F536E; // 文章段落颜色
+$uni-font-size-paragraph:15px;

+ 21 - 0
uni_modules/uview-plus/LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 https://uiadmin.net/uview-plus
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 74 - 0
uni_modules/uview-plus/README.md

@@ -0,0 +1,74 @@
+<p align="center">
+    <img alt="logo" src="https://uiadmin.net/uview-plus/common/logo.png" width="120" height="120" style="margin-bottom: 10px;">
+</p>
+<h3 align="center" style="margin: 30px 0 30px;font-weight: bold;font-size:40px;">uview-plus 3.0</h3>
+<h3 align="center">多平台快速开发的UI框架</h3>
+
+[![stars](https://img.shields.io/github/stars/ijry/uview-plus?style=flat-square&logo=GitHub)](https://github.com/ijry/uview-plus)
+[![forks](https://img.shields.io/github/forks/ijry/uview-plus?style=flat-square&logo=GitHub)](https://github.com/ijry/uview-plus)
+[![issues](https://img.shields.io/github/issues/ijry/uview-plus?style=flat-square&logo=GitHub)](https://github.com/ijry/uview-plus/issues)
+[![release](https://img.shields.io/github/v/release/ijry/uview-plus?style=flat-square)](https://gitee.com/jry/uview-plus/releases)
+[![license](https://img.shields.io/github/license/ijry/uview-plus?style=flat-square)](https://en.wikipedia.org/wiki/MIT_License)
+
+## 说明
+
+uview-plus,是uni-app全面兼容vue3/nvue/鸿蒙/uni-app-x(即将发布)的uni-app生态框架,全面的组件和便捷的工具会让您信手拈来,如鱼得水。uview-plus是基于uView2.x移植的支持vue3的版本,感谢uView。
+
+## 可视化设计
+
+uview-plus现已推出免费可视化设计,可以方便的进行页面可视化设计,导出源码即可使用。极大提高前端页面开发效率;如产品经理设计师直接使用更可作为高保真高可用原型制作工具,让设计稿即代码,无需传统的设计稿开发还原步骤。
+
+<img src="https://s3.bmp.ovh/imgs/2024/11/24/fd58d00071e6e5df.png" width="900" height="auto" >
+<img src="https://s3.bmp.ovh/imgs/2024/11/24/8e85a519fe627fb1.png" width="900" height="auto" >
+
+
+## 文档
+[官方文档:https://uview-plus.jiangruyi.com](https://uview-plus.jiangruyi.com)
+[备用文档:https://uiadmin.net/uview-plus](https://uiadmin.net/uview-plus)
+
+
+## 预览
+
+您可以通过**微信**扫码,查看最佳的演示效果。
+<br>
+<br>
+<img src="https://uview-plus.jiangruyi.com/common/h5_qrcode.png" width="220" height="220" >
+
+## 链接
+
+- [官方文档](https://uview-plus.jiangruyi.com)
+- [更新日志](https://uview-plus.jiangruyi.com/components/changelog.html)
+- [升级指南](https://uview-plus.jiangruyi.com/components/changeGuide.html)
+- [关于我们](https://uview-plus.jiangruyi.com/cooperation/about.html)
+
+## 交流反馈
+
+欢迎加入我们的QQ群交流反馈:[点此跳转](https://uview-plus.jiangruyi.com/components/addQQGroup.html)
+
+## 关于PR
+
+> 我们非常乐意接受各位的优质PR,但在此之前我希望您了解uview-plus是一个需要兼容多个平台的(小程序、h5、ios app、android app)包括nvue页面、vue页面。
+> 所以希望在您修复bug并提交之前尽可能的去这些平台测试一下兼容性。最好能携带测试截图以方便审核。非常感谢!
+
+## 安装
+
+#### **uni-app插件市场链接** —— [https://ext.dcloud.net.cn/plugin?name=uview-plus](https://ext.dcloud.net.cn/plugin?name=uview-plus)
+
+请通过[官网安装文档](https://uview-plus.jiangruyi.com/components/install.html)了解更详细的内容
+
+## 快速上手
+
+请通过[快速上手](https://uview-plus.jiangruyi.com/components/quickstart.html)了解更详细的内容
+
+## 使用方法
+配置easycom规则后,自动按需引入,无需`import`组件,直接引用即可。
+
+```html
+<template>
+	<u-button text="按钮"></u-button>
+</template>
+```
+
+## 版权信息
+uview-plus遵循[MIT](https://en.wikipedia.org/wiki/MIT_License)开源协议,意味着您无需支付任何费用,也无需授权,即可将uview-plus应用到您的产品中。
+

File diff suppressed because it is too large
+ 1348 - 0
uni_modules/uview-plus/changelog.md


+ 109 - 0
uni_modules/uview-plus/components/u-action-sheet-data/u-action-sheet-data.vue

@@ -0,0 +1,109 @@
+<template>
+	<view class="u-action-sheet-data">
+		<view class="u-action-sheet-data__trigger">
+			<slot name="trigger"></slot>
+			<up-input
+				v-if="!$slots['trigger']"
+				:modelValue="current"
+				disabled
+				disabledColor="#ffffff"
+				:placeholder="title"
+				border="none"
+			></up-input>
+			<view @click="show = true"
+				class="u-action-sheet-data__trigger__cover"></view>
+		</view>
+		<up-action-sheet
+			:show="show"
+			:actions="options"
+			:title="title"
+			safeAreaInsetBottom
+			:description="description"
+			@close="show = false"
+			@select="select"
+		>
+		</up-action-sheet>
+	</view>
+</template>
+
+<script>
+export default {
+    props: {
+		modelValue: {
+			type: [String, Number],
+			default: ''
+		},
+		title: {
+			type: String,
+			default: ''
+		},
+		description: {
+			type: String,
+			default: ''
+		},
+		options: {
+			type: Array,
+			default: () => {
+				return []
+			}
+		},
+		valueKey: {
+			type: String,
+			default: 'value'
+		},
+		labelKey: {
+			type: String,
+			default: 'name'
+		}
+    },
+    data() {
+        return {
+			show: false,
+			current: '',
+        }
+    },
+    created() {
+		if (this.modelValue) {
+			this.options.forEach((ele) => {
+				if (ele[this.valueKey] == this.modelValue) {
+					this.current = ele[this.labelKey]
+				}
+			})
+		}
+    },
+    emits: ['update:modelValue'],
+	watch: {
+		modelValue() {
+			this.options.forEach((ele) => {
+				if (ele[this.valueKey] == this.modelValue) {
+					this.current = ele[this.labelKey]
+				}
+			})
+		}
+	},
+    methods: {
+        hideKeyboard() {
+            uni.hideKeyboard()
+        },
+        select(e) {
+            this.$emit('update:modelValue', e[this.valueKey])
+			this.current = e[this.labelKey]
+        },
+    }
+}
+</script>
+
+<style lang="scss" scoped>
+	.u-action-sheet-data {
+		&__trigger {
+			position: relative;
+			&__cover {
+				position: absolute;
+				top: 0;
+				left: 0;
+				right: 0;
+				bottom: 0;
+			}
+		}
+	}
+</style>

+ 26 - 0
uni_modules/uview-plus/components/u-action-sheet/actionSheet.js

@@ -0,0 +1,26 @@
+/*
+ * @Author       : LQ
+ * @Description  :
+ * @version      : 3.0
+ * @Date         : 2021-08-20 16:44:21
+ * @LastAuthor   : jry
+ * @lastTime     : 2025-08-16 10:52:35
+ * @FilePath     : /uview-plus/libs/config/props/actionSheet.js
+ */
+export default {
+    // action-sheet组件
+    actionSheet: {
+        show: false,
+        title: '',
+        description: '',
+        actions: [],
+        index: '',
+        cancelText: '',
+        closeOnClickAction: true,
+        safeAreaInsetBottom: true,
+        openType: '',
+        closeOnClickOverlay: true,
+        round: 0,
+        wrapMaxHeight: '600px'
+    }
+}

+ 70 - 0
uni_modules/uview-plus/components/u-action-sheet/props.js

@@ -0,0 +1,70 @@
+/*
+ * @Author       : LQ
+ * @Description  :
+ * @version      : 3.0
+ * @LastAuthor   : jry
+ * @lastTime     : 2025-08-16 10:52:35
+ * @FilePath     : /uview-plus/libs/config/props/props.js
+ */
+import { defineMixin } from '../../libs/vue'
+import defProps from '../../libs/config/props.js'
+
+export const props = defineMixin({
+    props: {
+        // 操作菜单是否展示 (默认false)
+        show: {
+            type: Boolean,
+            default: () => defProps.actionSheet.show
+        },
+        // 标题
+        title: {
+            type: String,
+            default: () => defProps.actionSheet.title
+        },
+        // 选项上方的描述信息
+        description: {
+            type: String,
+            default: () => defProps.actionSheet.description
+        },
+        // 数据
+        actions: {
+            type: Array,
+            default: () => defProps.actionSheet.actions
+        },
+        // 取消按钮的文字,不为空时显示按钮
+        cancelText: {
+            type: String,
+            default: () => defProps.actionSheet.cancelText
+        },
+        // 点击某个菜单项时是否关闭弹窗
+        closeOnClickAction: {
+            type: Boolean,
+            default: () => defProps.actionSheet.closeOnClickAction
+        },
+        // 处理底部安全区(默认true)
+        safeAreaInsetBottom: {
+            type: Boolean,
+            default: () => defProps.actionSheet.safeAreaInsetBottom
+        },
+        // 小程序的打开方式
+        openType: {
+            type: String,
+            default: () => defProps.actionSheet.openType
+        },
+        // 点击遮罩是否允许关闭 (默认true)
+        closeOnClickOverlay: {
+            type: Boolean,
+            default: () => defProps.actionSheet.closeOnClickOverlay
+        },
+        // 圆角值
+        round: {
+            type: [Boolean, String, Number],
+            default: () => defProps.actionSheet.round
+        },
+        // 选项区域最大高度
+        wrapMaxHeight: {
+            type: [String],
+            default: () => defProps.actionSheet.wrapMaxHeight
+        },
+    }
+})

+ 302 - 0
uni_modules/uview-plus/components/u-action-sheet/u-action-sheet.vue

@@ -0,0 +1,302 @@
+<template>
+	<u-popup
+	    :show="show"
+	    mode="bottom"
+	    @close="closeHandler"
+	    :safeAreaInsetBottom="safeAreaInsetBottom"
+	    :round="round"
+	>
+		<view class="u-action-sheet">
+			<!-- 顶部标题区域 -->
+			<view
+			    class="u-action-sheet__header"
+			    v-if="title"
+			>
+				<text class="u-action-sheet__header__title u-line-1">{{title}}</text>
+				<view
+				    class="u-action-sheet__header__icon-wrap"
+				    @tap.stop="cancel"
+				>
+					<up-icon
+					    name="close"
+					    size="17"
+					    color="#c8c9cc"
+					    bold
+					></up-icon>
+				</view>
+			</view>
+			<!-- 描述信息 -->
+			<text
+			    class="u-action-sheet__description"
+				:style="[{
+					marginTop: `${title && description ? 0 : '18px'}`
+				}]"
+			    v-if="description"
+			>{{description}}</text>
+			<slot>
+				<!-- 分割线 -->
+				<u-line v-if="description"></u-line>
+				<!-- 操作项列表 -->
+				<scroll-view scroll-y class="u-action-sheet__item-wrap" :style="{maxHeight: wrapMaxHeight}">
+					<view :key="index" v-for="(item, index) in actions">
+						<!-- #ifdef MP -->
+						<button
+						    class="u-reset-button"
+						    :openType="item.openType"
+						    @getuserinfo="onGetUserInfo"
+						    @contact="onContact"
+						    @getphonenumber="onGetPhoneNumber"
+						    @error="onError"
+						    @launchapp="onLaunchApp"
+						    @opensetting="onOpenSetting"
+						    :lang="lang"
+						    :session-from="sessionFrom"
+						    :send-message-title="sendMessageTitle"
+						    :send-message-path="sendMessagePath"
+						    :send-message-img="sendMessageImg"
+						    :show-message-card="showMessageCard"
+						    :app-parameter="appParameter"
+						    @tap="selectHandler(index)"
+						    :hover-class="!item.disabled && !item.loading ? 'u-action-sheet--hover' : ''"
+						>
+							<!-- #endif -->
+							<view
+							    class="u-action-sheet__item-wrap__item"
+							    @tap.stop="selectHandler(index)"
+							    :hover-class="!item.disabled && !item.loading ? 'u-action-sheet--hover' : ''"
+							    :hover-stay-time="150"
+							    :style="getItemHoverStyle(index)"
+							>
+								<template v-if="!item.loading">
+									<text
+									    class="u-action-sheet__item-wrap__item__name"
+									    :style="[itemStyle(index)]"
+									>{{ item.name }}</text>
+									<text
+									    v-if="item.subname"
+									    class="u-action-sheet__item-wrap__item__subname"
+									>{{ item.subname }}</text>
+								</template>
+								<!-- 加载状态图标 -->
+								<u-loading-icon
+								    v-else
+								    custom-class="van-action-sheet__loading"
+								    size="18"
+								    mode="circle"
+								/>
+							</view>
+							<!-- #ifdef MP -->
+						</button>
+						<!-- #endif -->
+						<!-- 选项间分割线 -->
+						<u-line v-if="index !== actions.length - 1"></u-line>
+					</view>
+				</scroll-view>
+			</slot>
+			<!-- 取消按钮前的分割区域 -->
+			<u-gap
+			    bgColor="#eaeaec"
+			    height="6"
+			    v-if="cancelText"
+			></u-gap>
+			<!-- 取消按钮 -->
+			<view class="u-action-sheet__item-wrap__item u-action-sheet__cancel"
+				hover-class="u-action-sheet--hover" @tap="cancel" v-if="cancelText">
+				<text
+				    @touchmove.stop.prevent
+				    :hover-stay-time="150"
+				    class="u-action-sheet__cancel-text"
+				>{{cancelText}}</text>
+			</view>
+		</view>
+	</u-popup>
+</template>
+
+<script>
+	import { openType } from '../../libs/mixin/openType'
+	import { buttonMixin } from '../../libs/mixin/button'
+	import { props } from './props';
+	import { mpMixin } from '../../libs/mixin/mpMixin';
+	import { mixin } from '../../libs/mixin/mixin';
+	import { addUnit } from '../../libs/function/index';
+	/**
+	 * ActionSheet 操作菜单
+	 * @description 本组件用于从底部弹出一个操作菜单,供用户选择并返回结果。本组件功能类似于uni的uni.showActionSheetAPI,配置更加灵活,所有平台都表现一致。
+	 * @tutorial https://ijry.github.io/uview-plus/components/actionSheet.html
+	 * 
+	 * @property {Boolean}			show				操作菜单是否展示 (默认 false )
+	 * @property {String}			title				操作菜单标题
+	 * @property {String}			description			选项上方的描述信息
+	 * @property {Array<Object>}	actions				按钮的文字数组,见官方文档示例
+	 * @property {String}			cancelText			取消按钮的提示文字,不为空时显示按钮
+	 * @property {Boolean}			closeOnClickAction	点击某个菜单项时是否关闭弹窗 (默认 true )
+	 * @property {Boolean}			safeAreaInsetBottom	处理底部安全区 (默认 true )
+	 * @property {String}			openType			小程序的打开方式 (contact | launchApp | getUserInfo | openSetting |getPhoneNumber |error )
+	 * @property {Boolean}			closeOnClickOverlay	点击遮罩是否允许关闭  (默认 true )
+	 * @property {Number|String}	round				圆角值,默认无圆角  (默认 0 )
+	 * @property {String}			lang				指定返回用户信息的语言,zh_CN 简体中文,zh_TW 繁体中文,en 英文
+	 * @property {String}			sessionFrom			会话来源,openType="contact"时有效
+	 * @property {String}			sendMessageTitle	会话内消息卡片标题,openType="contact"时有效
+	 * @property {String}			sendMessagePath		会话内消息卡片点击跳转小程序路径,openType="contact"时有效
+	 * @property {String}			sendMessageImg		会话内消息卡片图片,openType="contact"时有效
+	 * @property {Boolean}			showMessageCard		是否显示会话内消息卡片,设置此参数为 true,用户进入客服会话会在右下角显示"可能要发送的小程序"提示,用户点击后可以快速发送小程序消息,openType="contact"时有效 (默认 false )
+	 * @property {String}			appParameter		打开 APP 时,向 APP 传递的参数,openType=launchApp 时有效
+	 * 
+	 * @event {Function} select			点击ActionSheet列表项时触发 
+	 * @event {Function} close			点击取消按钮时触发
+	 * @event {Function} getuserinfo	用户点击该按钮时,会返回获取到的用户信息,回调的 detail 数据与 wx.getUserInfo 返回的一致,openType="getUserInfo"时有效
+	 * @event {Function} contact		客服消息回调,openType="contact"时有效
+	 * @event {Function} getphonenumber	获取用户手机号回调,openType="getPhoneNumber"时有效
+	 * @event {Function} error			当使用开放能力时,发生错误的回调,openType="error"时有效
+	 * @event {Function} launchapp		打开 APP 成功的回调,openType="launchApp"时有效
+	 * @event {Function} opensetting	在打开授权设置页后回调,openType="openSetting"时有效
+	 * @example <u-action-sheet :actions="list" :title="title" :show="show"></u-action-sheet>
+	 */
+	export default {
+		name: "u-action-sheet",
+		// 一些props参数和methods方法,通过mixin混入,因为其他文件也会用到
+		mixins: [openType, buttonMixin, mixin, props],
+		data() {
+			return {
+
+			}
+		},
+		computed: {
+			// 操作项目的样式
+			itemStyle() {
+				return (index) => {
+					let style = {};
+					if (this.actions[index].color) style.color = this.actions[index].color
+					if (this.actions[index].fontSize) style.fontSize = addUnit(this.actions[index].fontSize)
+					// 选项被禁用的样式
+					if (this.actions[index].disabled) style.color = '#c0c4cc'
+					return style;
+				}
+			},
+		},
+		emits: ["close", "select", "update:show"],
+		methods: {
+			// 关闭操作菜单事件处理
+			closeHandler() {
+				// 允许点击遮罩关闭时,才发出close事件
+				if(this.closeOnClickOverlay) {
+					this.$emit('update:show', false)
+					this.$emit('close')
+				}
+			},
+			// 点击取消按钮
+			cancel() {
+				this.$emit('update:show', false)
+				this.$emit('close')
+			},
+			// 选择操作项处理
+			selectHandler(index) {
+				const item = this.actions[index]
+				if (item && !item.disabled && !item.loading) {
+					this.$emit('select', item)
+					if (this.closeOnClickAction) {
+						this.$emit('update:show', false)
+						this.$emit('close')
+					}
+				}
+			},
+			// 动态处理Hover时候第一个item的圆角
+			getItemHoverStyle(index) {
+				if (index === 0 && this.round && !this.title && !this.description) {
+					return {
+						borderTopLeftRadius: `${this.round}px`,
+						borderTopRightRadius: `${this.round}px`,
+					}
+				}
+				return {}
+			},
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	$u-action-sheet-reset-button-width:100% !default;
+	$u-action-sheet-title-font-size: 16px !default;
+	$u-action-sheet-title-padding: 12px 30px !default;
+	$u-action-sheet-title-color: $u-main-color !default;
+	$u-action-sheet-header-icon-wrap-right:15px !default;
+	$u-action-sheet-header-icon-wrap-top:15px !default;
+	$u-action-sheet-description-font-size:13px !default;
+	$u-action-sheet-description-color:14px !default;
+	$u-action-sheet-description-margin: 18px 15px !default;
+	$u-action-sheet-item-wrap-item-padding:17px !default;
+	$u-action-sheet-item-wrap-name-font-size:16px !default;
+	$u-action-sheet-item-wrap-subname-font-size:13px !default;
+	$u-action-sheet-item-wrap-subname-color: #c0c4cc !default;
+	$u-action-sheet-item-wrap-subname-margin-top:10px !default;
+	$u-action-sheet-cancel-text-font-size:16px !default;
+	$u-action-sheet-cancel-text-color:$u-content-color !default;
+	$u-action-sheet-cancel-text-font-size:15px !default;
+	$u-action-sheet-cancel-text-hover-background-color:rgb(242, 243, 245) !default;
+
+	.u-reset-button {
+		width: $u-action-sheet-reset-button-width;
+	}
+
+	.u-action-sheet {
+		text-align: center;
+		&__header {
+			position: relative;
+			padding: $u-action-sheet-title-padding;
+			&__title {
+				font-size: $u-action-sheet-title-font-size;
+				color: $u-action-sheet-title-color;
+				font-weight: bold;
+				text-align: center;
+			}
+
+			&__icon-wrap {
+				position: absolute;
+				right: $u-action-sheet-header-icon-wrap-right;
+				top: $u-action-sheet-header-icon-wrap-top;
+			}
+		}
+
+		&__description {
+			font-size: $u-action-sheet-description-font-size;
+			color: $u-tips-color;
+			margin: $u-action-sheet-description-margin;
+			text-align: center;
+		}
+
+		&__item-wrap {
+
+			&__item {
+				padding: $u-action-sheet-item-wrap-item-padding;
+				@include flex;
+				align-items: center;
+				justify-content: center;
+				flex-direction: column;
+
+				&__name {
+					font-size: $u-action-sheet-item-wrap-name-font-size;
+					color: $u-main-color;
+					text-align: center;
+				}
+
+				&__subname {
+					font-size: $u-action-sheet-item-wrap-subname-font-size;
+					color: $u-action-sheet-item-wrap-subname-color;
+					margin-top: $u-action-sheet-item-wrap-subname-margin-top;
+					text-align: center;
+				}
+			}
+		}
+
+		&__cancel-text {
+			font-size: $u-action-sheet-cancel-text-font-size;
+			color: $u-action-sheet-cancel-text-color;
+			text-align: center;
+			// padding: $u-action-sheet-cancel-text-font-size;
+		}
+
+		&--hover {
+			background-color: $u-action-sheet-cancel-text-hover-background-color;
+		}
+	}
+</style>

+ 76 - 0
uni_modules/uview-plus/components/u-agreement/u-agreement.vue

@@ -0,0 +1,76 @@
+<style scoped lang="scss">
+    .agreement-content {
+        width: 100%;;
+        display: inline-block;
+        flex-direction: column;
+        .agreement-url {
+            display: inline-block;
+            color: blue;
+            // #ifdef H5
+            cursor: pointer;
+            // #endif
+        }
+    }
+</style>
+
+<template>
+    <view class="up-agreement">
+        <up-modal v-model:show="show" showCancelButton @confirm="confirm" @cancel="close" confirmText="阅读并同意">
+            <view class="agreement-content">
+                <slot>
+                    我们非常重视您的个人信息和隐私保护。为了更好地保障您的个人权益,在您使用我们的产品前,
+                    请务必审慎阅读《<text class="agreement-url" @click="urlClick('urlProtocol')">用户协议</text>》
+                    和《<text class="agreement-url" @click="urlClick('urlPrivacy')">隐私政策</text>》内的所有条款,
+                    尤其是:1.我们对您的个人信息的收集/保存/使用/对外提供/保护等规则条款,以及您的用户权利等条款;2. 约定我们的限制责任、免责
+                    条款;3.其他以颜色或加粗进行标识的重要条款。如您对以上协议有任何疑问,请先不要同意,您点击“同意并继续”的行为即表示您已阅读
+                    完毕并同意以上协议的全部内容。
+                </slot>
+            </view>
+        </up-modal>
+    </view>
+</template>
+
+<script>
+    export default {
+        name: 'up-agreement',
+        props: {
+            urlProtocol: {
+                type: String,
+                default: '/pages/user_agreement/agreement/info?title=用户协议'
+            },
+            urlPrivacy: {
+                type: String,
+                default: '/pages/user_agreement/agreement/info?title=隐私政策'
+            },
+        },
+        emits: ['confirm'],
+        data() {
+            return {
+                show: false
+            }
+        },
+        methods: {
+            close() {
+                // #ifdef H5
+                window.opener = null;
+                window.close();
+                // #endif
+                // #ifdef APP-PLUS
+                plus.runtime.quit();
+                // #endif
+            },
+            confirm() {
+                this.show = false;
+                this.$emit('confirm', 1);
+            },
+            showModal() {
+                this.show = true;
+            },
+            urlClick(type) {
+                uni.navigateTo({
+                    url: this[type]
+                });
+            }
+        }
+    }
+</script>

+ 28 - 0
uni_modules/uview-plus/components/u-album/album.js

@@ -0,0 +1,28 @@
+/*
+ * @Author       : LQ
+ * @Description  :
+ * @version      : 1.0
+ * @Date         : 2021-08-20 16:44:21
+ * @LastAuthor   : jry
+ * @lastTime     : 2025-08-16 16:32:24
+ * @FilePath     : /uview-plus/libs/config/props/album.js
+ */
+export default {
+    // album 组件
+    album: {
+        urls: [],
+        keyName: '',
+        singleSize: 180,
+        multipleSize: 70,
+        space: 6,
+        singleMode: 'scaleToFill',
+        multipleMode: 'aspectFill',
+        maxCount: 9,
+        previewFullImage: true,
+        rowCount: 3,
+        showMore: true,
+        autoWrap: false,
+        unit: 'px',
+        stop: true,
+    }
+}

+ 95 - 0
uni_modules/uview-plus/components/u-album/props.js

@@ -0,0 +1,95 @@
+/*
+ * @Author       : jry
+ * @Description  :
+ * @version      : 3.0
+ * @LastAuthor   : jry
+ * @lastTime     : 2025-08-16 16:35:24
+ * @FilePath     : /uview-plus/components/u-album/props.js
+ */
+import { defineMixin } from '../../libs/vue'
+import defProps from '../../libs/config/props.js'
+
+export const props = defineMixin({
+    props: {
+        // 图片地址,Array<String>|Array<Object>形式
+        urls: {
+            type: Array,
+            default: () => defProps.album.urls
+        },
+        // 指定从数组的对象元素中读取哪个属性作为图片地址
+        keyName: {
+            type: String,
+            default: () => defProps.album.keyName
+        },
+        // 单图时,图片长边的长度
+        singleSize: {
+            type: [String, Number],
+            default: () => defProps.album.singleSize
+        },
+        // 多图时,图片边长
+        multipleSize: {
+            type: [String, Number],
+            default: () => defProps.album.multipleSize
+        },
+        // 多图时,图片水平和垂直之间的间隔
+        space: {
+            type: [String, Number],
+            default: () => defProps.album.space
+        },
+        // 单图时,图片缩放裁剪的模式
+        singleMode: {
+            type: String,
+            default: () => defProps.album.singleMode
+        },
+        // 多图时,图片缩放裁剪的模式
+        multipleMode: {
+            type: String,
+            default: () => defProps.album.multipleMode
+        },
+        // 最多展示的图片数量,超出时最后一个位置将会显示剩余图片数量
+        maxCount: {
+            type: [String, Number],
+            default: () => defProps.album.maxCount
+        },
+        // 是否可以预览图片
+        previewFullImage: {
+            type: Boolean,
+            default: () => defProps.album.previewFullImage
+        },
+        // 每行展示图片数量,如设置,singleSize和multipleSize将会无效
+        rowCount: {
+            type: [String, Number],
+            default: () => defProps.album.rowCount
+        },
+        // 超出maxCount时是否显示查看更多的提示
+        showMore: {
+            type: Boolean,
+            default: () => defProps.album.showMore
+        },
+        // 图片形状,circle-圆形,square-方形
+        shape: {
+            type: String,
+            default: () => defProps.image.shape
+        },
+        // 圆角,单位任意
+        radius: {
+            type: [String, Number],
+            default: () => defProps.image.radius
+        },
+        // 自适应换行
+        autoWrap: {
+            type: Boolean,
+            default: () => defProps.album.autoWrap
+        },
+        // 单位
+        unit: {
+            type: [String],
+            default: () => defProps.album.unit
+        },
+        // 阻止点击冒泡
+        stop: {
+            type: Boolean,
+            default: () => defProps.album.stop
+        }
+    }
+})

+ 344 - 0
uni_modules/uview-plus/components/u-album/u-album.vue

@@ -0,0 +1,344 @@
+<template>
+    <view class="u-album">
+        <!-- 相册行容器,每行显示 rowCount 个图片 -->
+        <view
+            class="u-album__row"
+            ref="u-album__row"
+            v-for="(arr, index) in showUrls"
+            :forComputedUse="albumWidth"
+            :key="index"
+            :style="{flexWrap: autoWrap ? 'wrap' : 'nowrap'}"
+        >
+            <!-- 图片包装容器 -->
+            <view
+                class="u-album__row__wrapper"
+                v-for="(item, index1) in arr"
+                :key="index1"
+                :style="[imageStyle(index + 1, index1 + 1)]"
+                @tap="onPreviewTap($event, getSrc(item))"
+            >
+                <!-- 图片显示 -->
+                <image
+                    :src="getSrc(item)"
+                    :mode="
+                        urls.length === 1
+                            ? imageHeight > 0
+                                ? singleMode
+                                : 'widthFix'
+                            : multipleMode
+                    "
+                    :style="[
+                        {
+                            width: imageWidth,
+                            height: imageHeight,
+                            borderRadius: shape == 'circle' ? '10000px' : addUnit(radius)
+                        }
+                    ]"
+                ></image>
+                <!-- 超出最大显示数量时的更多提示 -->
+                <view
+                    v-if="
+                        showMore &&
+                        urls.length > rowCount * showUrls.length &&
+                        index === showUrls.length - 1 &&
+                        index1 === showUrls[showUrls.length - 1].length - 1
+                    "
+                    class="u-album__row__wrapper__text"
+                    :style="{
+					    borderRadius: shape == 'circle' ? '50%' : addUnit(radius),
+				    }"
+                >
+                    <up-text
+                        :text="`+${urls.length - maxCount}`"
+                        color="#fff"
+                        :size="multipleSize * 0.3"
+                        align="center"
+                        customStyle="justify-content: center"
+                    ></up-text>
+                </view>
+            </view>
+        </view>
+    </view>
+</template>
+
+<script>
+import { props } from './props';
+import { mpMixin } from '../../libs/mixin/mpMixin';
+import { mixin } from '../../libs/mixin/mixin';
+import { addUnit, sleep } from '../../libs/function/index';
+import test from '../../libs/function/test';
+// #ifdef APP-NVUE
+// 不支持百分比单位,这里需要通过dom查询组件的宽度
+const dom = uni.requireNativePlugin('dom')
+// #endif
+
+/**
+ * Album 相册
+ * @description 本组件提供一个类似相册的功能,让开发者开发起来更加得心应手。减少重复的模板代码
+ * @tutorial https://ijry.github.io/uview-plus/components/album.html
+ *
+ * @property {Array}           urls             图片地址列表 Array<String>|Array<Object>形式
+ * @property {String}          keyName          指定从数组的对象元素中读取哪个属性作为图片地址
+ * @property {String | Number} singleSize       单图时,图片长边的长度  (默认 180 )
+ * @property {String | Number} multipleSize     多图时,图片边长 (默认 70 )
+ * @property {String | Number} space            多图时,图片水平和垂直之间的间隔 (默认 6 )
+ * @property {String}          singleMode       单图时,图片缩放裁剪的模式 (默认 'scaleToFill' )
+ * @property {String}          multipleMode     多图时,图片缩放裁剪的模式 (默认 'aspectFill' )
+ * @property {String | Number} maxCount         取消按钮的提示文字 (默认 9 )
+ * @property {Boolean}         previewFullImage 是否可以预览图片 (默认 true )
+ * @property {String | Number} rowCount         每行展示图片数量,如设置,singleSize和multipleSize将会无效	(默认 3 )
+ * @property {Boolean}         showMore         超出maxCount时是否显示查看更多的提示 (默认 true )
+ * @property {String}          shape            图片形状,circle-圆形,square-方形 (默认 'square' )
+ * @property {String | Number} radius           圆角值,单位任意,如果为数值,则为px单位 (默认 0 )
+ * @property {Boolean}         autoWrap         自适应换行模式,不受rowCount限制,图片会自动换行 (默认 false )
+ * @property {String}          unit             图片单位 (默认 px )
+ * @event    {Function}        albumWidth       某些特殊的情况下,需要让文字与相册的宽度相等,这里事件的形式对外发送  (回调参数 width )
+ * @example <u-album :urls="urls2" @albumWidth="width => albumWidth = width" multipleSize="68" ></u-album>
+ */
+export default {
+    name: 'u-album',
+    mixins: [mpMixin, mixin, props],
+    data() {
+        return {
+            // 单图的宽度
+            singleWidth: 0,
+            // 单图的高度
+            singleHeight: 0,
+            // 单图时,如果无法获取图片的尺寸信息,让图片宽度默认为容器的一定百分比
+            singlePercent: 0.6
+        }
+    },
+    watch: {
+        urls: {
+            immediate: true,
+            handler(newVal) {
+                // 当只有一张图片时,获取图片尺寸信息
+                if (newVal.length === 1) {
+                    this.getImageRect()
+                }
+            }
+        }
+    },
+    computed: {
+        /**
+         * 计算图片样式
+         * @param {Number} index1 - 行索引
+         * @param {Number} index2 - 列索引
+         * @returns {Object} 图片样式对象
+         */
+        imageStyle() {
+            return (index1, index2) => {
+                const { space, rowCount, multipleSize, urls } = this,
+                    rowLen = this.showUrls.length,
+                    allLen = this.urls.length
+                const style = {
+                    marginRight: addUnit(space),
+                    marginBottom: addUnit(space)
+                }
+                // 如果为最后一行,则每个图片都无需下边框
+                if (index1 === rowLen && !this.autoWrap) style.marginBottom = 0
+                // 每行的最右边一张和总长度的最后一张无需右边框
+                if (!this.autoWrap) {
+                    if (
+                        index2 === rowCount ||
+                        (index1 === rowLen &&
+                            index2 === this.showUrls[index1 - 1].length)
+                    )
+                        style.marginRight = 0
+                }
+                return style
+            }
+        },
+        /**
+         * 将图片地址数组划分为二维数组,用于按行显示
+         * @returns {Array} 二维数组,每个子数组代表一行图片
+         */
+        showUrls() {
+            if (this.autoWrap) {
+                // 自动换行模式下,所有图片放在一行中显示
+                return [ this.urls.slice(0, this.maxCount) ];
+            } else {
+                // 固定行数模式下,按 rowCount 分割图片
+                const arr = []
+                this.urls.map((item, index) => {
+                    // 限制最大展示数量
+                    if (index + 1 <= this.maxCount) {
+                        // 计算该元素为第几个素组内
+                        const itemIndex = Math.floor(index / this.rowCount)
+                        // 判断对应的索引是否存在
+                        if (!arr[itemIndex]) {
+                            arr[itemIndex] = []
+                        }
+                        arr[itemIndex].push(item)
+                    }
+                })
+                return arr
+            }
+        },
+        /**
+         * 计算图片宽度
+         * @returns {String} 图片宽度样式值
+         */
+        imageWidth() {
+            return addUnit(
+                this.urls.length === 1 ? this.singleWidth : this.multipleSize, this.unit
+            )
+        },
+        /**
+         * 计算图片高度
+         * @returns {String} 图片高度样式值
+         */
+        imageHeight() {
+            return addUnit(
+                this.urls.length === 1 ? this.singleHeight : this.multipleSize, this.unit
+            )
+        },
+        /**
+         * 计算相册总宽度,用于外部组件对齐
+         * 此变量无实际用途,仅仅是为了利用computed特性,让其在urls长度等变化时,重新计算图片的宽度
+         * @returns {Number} 相册宽度
+         */
+        albumWidth() {
+            let width = 0
+            if (this.urls.length === 1) {
+                width = this.singleWidth
+            } else {
+                width =
+                    this.showUrls[0].length * this.multipleSize +
+                    this.space * (this.showUrls[0].length - 1)
+            }
+            this.$emit('albumWidth', width)
+            return width
+        }
+    },
+    emits: ['preview', 'albumWidth'],
+    methods: {
+        addUnit,
+        /**
+         * 点击图片预览
+         * @param {Event} e - 点击事件对象
+         * @param {String} url - 当前点击图片的地址
+         */
+        onPreviewTap(e, url) {
+            // 获取所有图片地址
+            const urls = this.urls.map((item) => {
+                return this.getSrc(item)
+            })
+            if (this.previewFullImage) {
+                // 使用系统默认预览图片功能
+                uni.previewImage({
+                    current: url,
+                    urls
+                })
+                // 是否阻止事件传播
+                this.stop && this.preventEvent(e)
+            } else {
+                // 发送自定义预览事件
+                this.$emit('preview', {
+                    urls,
+                    currentIndex: urls.indexOf(url)
+                })
+            }
+        },
+        /**
+         * 获取图片地址
+         * @param {String|Object} item - 图片项,可以是字符串或对象
+         * @returns {String} 图片地址
+         */
+        getSrc(item) {
+            return test.object(item)
+                ? (this.keyName && item[this.keyName]) || item.src
+                : item
+        },
+        /**
+         * 单图时,获取图片的尺寸
+         * 在小程序中,需要将网络图片的的域名添加到小程序的download域名才可能获取尺寸
+         * 在没有添加的情况下,让单图宽度默认为盒子的一定宽度(singlePercent)
+         */
+        getImageRect() {
+            const src = this.getSrc(this.urls[0])
+            uni.getImageInfo({
+                src,
+                success: (res) => {
+                    let singleSize = this.singleSize;
+                    // 单位
+                    let unit = '';
+                    if (Number.isNaN(Number(this.singleSize))) {
+                        // 大小中有字符 则记录字符
+                        unit = this.singleSize.replace(/\d+/g, ''); // 单位
+                        singleSize = Number(this.singleSize.replace(/\D+/g, ''), 10); // 具体值
+                    }
+
+                    // 判断图片横向还是竖向展示方式
+                    const isHorizotal = res.width >= res.height
+                    this.singleWidth = isHorizotal
+                        ? singleSize
+                        : (res.width / res.height) * singleSize
+                    this.singleHeight = !isHorizotal
+                        ? singleSize
+                        : (res.height / res.width) * this.singleWidth
+
+                    // 如果有单位统一设置单位
+                    if(unit != null && unit !== ''){
+                        this.singleWidth = this.singleWidth + unit
+                        this.singleHeight = this.singleHeight + unit
+                    }
+                },
+                fail: () => {
+                    // 获取图片信息失败时,通过组件宽度计算
+                    this.getComponentWidth()
+                }
+            })
+        },
+        /**
+         * 获取组件的宽度,用于计算单图显示尺寸
+         */
+        async getComponentWidth() {
+            // 延时一定时间,以获取dom尺寸
+            await sleep(30)
+            // #ifndef APP-NVUE
+            // H5、小程序等平台通过 $uGetRect 获取组件宽度
+            this.$uGetRect('.u-album__row').then((size) => {
+                this.singleWidth = size.width * this.singlePercent
+            })
+            // #endif
+
+            // #ifdef APP-NVUE
+            // NVUE 平台通过 dom 插件获取组件宽度
+            // 这里ref="u-album__row"所在的标签为通过for循环出来,导致this.$refs['u-album__row']是一个数组
+            const ref = this.$refs['u-album__row'][0]
+            ref &&
+                dom.getComponentRect(ref, (res) => {
+                    this.singleWidth = res.size.width * this.singlePercent
+                })
+            // #endif
+        }
+    }
+}
+</script>
+
+<style lang="scss" scoped>
+.u-album {
+    @include flex(column);
+
+    &__row {
+        @include flex(row);
+
+        &__wrapper {
+            position: relative;
+
+            &__text {
+                position: absolute;
+                top: 0;
+                left: 0;
+                right: 0;
+                bottom: 0;
+                background-color: rgba(0, 0, 0, 0.3);
+                @include flex(row);
+                justify-content: center;
+                align-items: center;
+            }
+        }
+    }
+}
+</style>

+ 26 - 0
uni_modules/uview-plus/components/u-alert/alert.js

@@ -0,0 +1,26 @@
+/*
+ * @Author       : LQ
+ * @Description  :
+ * @version      : 3.0
+ * @Date         : 2021-08-20 16:44:21
+ * @LastAuthor   : jry
+ * @lastTime     : 2025-08-17 17:23:53
+ * @FilePath     : /uview-plus/libs/config/props/alert.js
+ */
+export default {
+    // alert警告组件
+    alert: {
+        title: '',
+        type: 'warning',
+        description: '',
+        closable: false,
+        showIcon: false,
+        effect: 'light',
+        center: false,
+        fontSize: 14,
+        transitionMode: 'fade',
+        duration: 0,
+        icon: '',
+        value: true
+    }
+}

+ 75 - 0
uni_modules/uview-plus/components/u-alert/props.js

@@ -0,0 +1,75 @@
+/*
+ * @Author       : jry
+ * @Description  :
+ * @version      : 3.0
+ * @LastAuthor   : jry
+ * @lastTime     : 2025-08-17 17:23:53
+ * @FilePath     : /uview-plus/libs/config/props/props.js
+ */
+import { defineMixin } from '../../libs/vue'
+import defProps from '../../libs/config/props.js'
+
+export const props = defineMixin({
+    props: {
+        // 显示文字
+        title: {
+            type: String,
+            default: () => defProps.alert.title
+        },
+        // 主题,success/warning/info/error
+        type: {
+            type: String,
+            default: () => defProps.alert.type
+        },
+        // 辅助性文字
+        description: {
+            type: String,
+            default: () => defProps.alert.description
+        },
+        // 是否可关闭
+        closable: {
+            type: Boolean,
+            default: () => defProps.alert.closable
+        },
+        // 是否显示图标
+        showIcon: {
+            type: Boolean,
+            default: () => defProps.alert.showIcon
+        },
+        // 浅或深色调,light-浅色,dark-深色
+        effect: {
+            type: String,
+            default: () => defProps.alert.effect
+        },
+        // 文字是否居中
+        center: {
+            type: Boolean,
+            default: () => defProps.alert.center
+        },
+        // 字体大小
+        fontSize: {
+            type: [String, Number],
+            default: () => defProps.alert.fontSize
+        },
+        // 动画类型
+        transitionMode: {
+            type: [String],
+            default: () => defProps.alert.transitionMode
+        },
+        // 自动定时关闭毫秒
+        duration: {
+            type: [Number],
+            default: () => defProps.alert.duration
+        },
+        // 自定义图标
+        icon: {
+            type: [String],
+            default: () => defProps.alert.icon
+        },
+        // 是否显示
+        modelValue: {
+            type: [Boolean],
+            default: () => defProps.alert.value
+        }
+    }
+})

+ 293 - 0
uni_modules/uview-plus/components/u-alert/u-alert.vue

@@ -0,0 +1,293 @@
+<template>
+	<up-transition
+	    :mode="transitionMode"
+	    :show="show"
+	>
+		<view
+		    class="u-alert"
+		    :class="[`u-alert--${type}--${effect}`]"
+		    @tap.stop="clickHandler"
+		    :style="[addStyle(customStyle)]"
+		>
+			<!-- 左侧图标 -->
+			<view
+			    class="u-alert__icon"
+			    v-if="showIcon"
+			>
+				<up-icon
+				    :name="iconName"
+				    size="18"
+				    :color="iconColor"
+				></up-icon>
+			</view>
+			<!-- 内容区域 -->
+			<view
+			    class="u-alert__content"
+			    :style="[{
+					paddingRight: closable ? '20px' : 0
+				}]"
+			>
+				<!-- 标题 -->
+				<text
+				    class="u-alert__content__title"
+				    v-if="title"
+					:style="[{
+						fontSize: addUnit(fontSize),
+						textAlign: center ? 'center' : 'left'
+					}]"
+				    :class="[effect === 'dark' ? 'u-alert__text--dark' : `u-alert__text--${type}--light`]"
+				>{{ title }}</text>
+				<!-- 描述信息 -->
+				<text
+				    class="u-alert__content__desc"
+					v-if="description"
+					:style="[{
+						fontSize: addUnit(fontSize),
+						textAlign: center ? 'center' : 'left'
+					}]"
+				    :class="[effect === 'dark' ? 'u-alert__text--dark' : `u-alert__text--${type}--light`]"
+				>{{ description }}</text>
+			</view>
+			<!-- 关闭按钮 -->
+			<view
+			    class="u-alert__close"
+			    v-if="closable"
+			    @tap.stop="closeHandler"
+			>
+				<slot name="close">
+					<up-icon
+					    name="close"
+					    :color="iconColor"
+					    size="15"
+					></up-icon>
+				</slot>
+			</view>
+		</view>
+	</up-transition>
+</template>
+
+<script>
+	import { props } from './props';
+	import { mpMixin } from '../../libs/mixin/mpMixin';
+	import { mixin } from '../../libs/mixin/mixin';
+	import { addUnit, addStyle } from '../../libs/function/index';
+	/**
+	 * Alert  警告提示
+	 * @description 警告提示,展现需要关注的信息。
+	 * @tutorial https://ijry.github.io/uview-plus/components/alertTips.html
+	 * 
+	 * @property {String}			title       显示的文字 
+	 * @property {String}			type        使用预设的颜色 (默认 'warning' )
+	 * @property {String}			description 辅助性文字,颜色比title浅一点,字号也小一点,可选  
+	 * @property {Boolean}			closable    关闭按钮(默认为叉号icon图标)  (默认 false )
+	 * @property {Boolean}			showIcon    是否显示左边的辅助图标   ( 默认 false )
+	 * @property {String}			effect      多图时,图片缩放裁剪的模式  (默认 'light' )
+	 * @property {Boolean}			center		文字是否居中  (默认 false )
+	 * @property {String | Number}	fontSize    字体大小  (默认 14 )
+	 * @property {Object}			customStyle	定义需要用到的外部样式
+	 * @property {String}			transitionMode 过渡动画模式 (默认 'fade' )
+	 * @property {String | Number}	duration	自动关闭延时(毫秒),设置为0或负数则不自动关闭 (默认 0 )
+	 * @property {String}			icon		自定义图标名称,优先级高于type默认图标
+	 * @property {Boolean}			modelValue/v-model	绑定值,控制是否显示 (默认 true )
+	 * @event    {Function}        click       点击组件时触发
+	 * @event    {Function}        close       点击关闭按钮时触发
+	 * @event    {Function}        closed      关闭动画结束时触发
+	 * @example  <up-alert :title="title"  type = "warning" :closable="closable" :description = "description"></up-alert>
+	 */
+	export default {
+		name: 'u-alert',
+		mixins: [mpMixin, mixin, props],
+		data() {
+			return {
+				// 控制组件显示隐藏
+				show: true
+			}
+		},
+		computed: {
+			// 根据不同的主题类型返回对应的图标颜色
+			iconColor() {
+				return this.effect === 'light' ? this.type : '#fff'
+			},
+			// 不同主题对应不同的图标
+			iconName() {
+				// 如果用户自定义了图标,则优先使用自定义图标
+				if (this.icon) return this.icon;
+				
+				switch (this.type) {
+					case 'success':
+						return 'checkmark-circle-fill';
+						break;
+					case 'error':
+						return 'close-circle-fill';
+						break;
+					case 'warning':
+						return 'error-circle-fill';
+						break;
+					case 'info':
+						return 'info-circle-fill';
+						break;
+					case 'primary':
+						return 'more-circle-fill';
+						break;
+					default: 
+						return 'error-circle-fill';
+				}
+			}
+		},
+		emits: ["click","close", "closed", "update:modelValue"],
+		watch: {
+			modelValue: {
+				handler(newVal) {
+					this.show = newVal;
+				},
+				immediate: true
+			},
+			show: {
+				handler(newVal) {
+					this.$emit('update:modelValue', newVal);
+					
+					// 如果是从显示到隐藏,且启用了自动关闭功能
+					if (!newVal && this.duration > 0) {
+						this.$emit('closed');
+					}
+				}
+			}
+		},
+		mounted() {
+			// 如果设置了自动关闭时间,则在指定时间后自动关闭
+			if (this.duration > 0) {
+				setTimeout(() => {
+					this.closeHandler();
+				}, this.duration);
+			}
+		},
+		methods: {
+			addUnit,
+			addStyle,
+			// 点击内容区域触发click事件
+			clickHandler() {
+				this.$emit('click')
+			},
+			// 点击关闭按钮触发close事件并隐藏组件
+			closeHandler() {
+				this.show = false   
+				this.$emit('close');
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+
+	.u-alert {
+		position: relative;
+		background-color: $u-primary;
+		padding: 8px 10px;
+		@include flex(row);
+		align-items: center;
+		border-top-left-radius: 4px;
+		border-top-right-radius: 4px;
+		border-bottom-left-radius: 4px;
+		border-bottom-right-radius: 4px;
+
+		&--primary--dark {
+			background-color: $u-primary;
+		}
+
+		&--primary--light {
+			background-color: #ecf5ff;
+		}
+
+		&--error--dark {
+			background-color: $u-error;
+		}
+
+		&--error--light {
+			background-color: #FEF0F0;
+		}
+
+		&--success--dark {
+			background-color: $u-success;
+		}
+
+		&--success--light {
+			background-color: #f5fff0;
+		}
+
+		&--warning--dark {
+			background-color: $u-warning;
+		}
+
+		&--warning--light {
+			background-color: #FDF6EC;
+		}
+
+		&--info--dark {
+			background-color: $u-info;
+		}
+
+		&--info--light {
+			background-color: #f4f4f5;
+		}
+
+		&__icon {
+			margin-right: 5px;
+		}
+
+		&__content {
+			@include flex(column);
+			flex: 1;
+
+			&__title {
+				color: $u-main-color;
+				font-size: 14px;
+				font-weight: bold;
+				color: #fff;
+				margin-bottom: 2px;
+			}
+
+			&__desc {
+				color: $u-main-color;
+				font-size: 14px;
+				flex-wrap: wrap;
+				color: #fff;
+			}
+		}
+
+		&__title--dark,
+		&__desc--dark {
+			color: #FFFFFF;
+		}
+
+		&__text--primary--light,
+		&__text--primary--light {
+			color: $u-primary;
+		}
+
+		&__text--success--light,
+		&__text--success--light {
+			color: $u-success;
+		}
+
+		&__text--warning--light,
+		&__text--warning--light {
+			color: $u-warning;
+		}
+
+		&__text--error--light,
+		&__text--error--light {
+			color: $u-error;
+		}
+
+		&__text--info--light,
+		&__text--info--light {
+			color: $u-info;
+		}
+
+		&__close {
+			position: absolute;
+			top: 11px;
+			right: 10px;
+		}
+	}
+</style>

+ 23 - 0
uni_modules/uview-plus/components/u-avatar-group/avatarGroup.js

@@ -0,0 +1,23 @@
+/*
+ * @Author       : LQ
+ * @Description  :
+ * @version      : 1.0
+ * @Date         : 2021-08-20 16:44:21
+ * @LastAuthor   : LQ
+ * @lastTime     : 2021-08-20 16:49:55
+ * @FilePath     : /u-view2.0/uview-ui/libs/config/props/avatarGroup.js
+ */
+export default {
+    // avatarGroup 组件
+    avatarGroup: {
+        urls: [],
+        maxCount: 5,
+        shape: 'circle',
+        mode: 'scaleToFill',
+        showMore: true,
+        size: 40,
+        keyName: '',
+        gap: 0.5,
+		extraValue: 0
+    }
+}

+ 54 - 0
uni_modules/uview-plus/components/u-avatar-group/props.js

@@ -0,0 +1,54 @@
+import { defineMixin } from '../../libs/vue'
+import defProps from '../../libs/config/props.js'
+export const props = defineMixin({
+    props: {
+        // 头像图片组
+        urls: {
+            type: Array,
+            default: () => defProps.avatarGroup.urls
+        },
+        // 最多展示的头像数量
+        maxCount: {
+            type: [String, Number],
+            default: () => defProps.avatarGroup.maxCount
+        },
+        // 头像形状
+        shape: {
+            type: String,
+            default: () => defProps.avatarGroup.shape
+        },
+        // 图片裁剪模式
+        mode: {
+            type: String,
+            default: () => defProps.avatarGroup.mode
+        },
+        // 超出maxCount时是否显示查看更多的提示
+        showMore: {
+            type: Boolean,
+            default: () => defProps.avatarGroup.showMore
+        },
+        // 头像大小
+        size: {
+            type: [String, Number],
+            default: () => defProps.avatarGroup.size
+        },
+        // 指定从数组的对象元素中读取哪个属性作为图片地址
+        keyName: {
+            type: String,
+            default: () => defProps.avatarGroup.keyName
+        },
+		// 头像之间的遮挡比例
+        gap: {
+            type: [String, Number],
+            validator(value) {
+                return value >= 0 && value <= 1
+            },
+            default: () => defProps.avatarGroup.gap
+        },
+		// 需额外显示的值
+		extraValue: {
+			type: [Number, String],
+			default: () => defProps.avatarGroup.extraValue
+		}
+    }
+})

+ 109 - 0
uni_modules/uview-plus/components/u-avatar-group/u-avatar-group.vue

@@ -0,0 +1,109 @@
+<template>
+	<view class="u-avatar-group">
+		<view
+		    class="u-avatar-group__item"
+		    v-for="(item, index) in showUrl"
+		    :key="index"
+		    :style="{
+				marginLeft: index === 0 ? 0 : addUnit(-size * gap)
+			}"
+		>
+			<u-avatar
+			    :size="size"
+			    :shape="shape"
+			    :mode="mode"
+			    :src="testObject(item) ? keyName && item[keyName] || item.url : item"
+			></u-avatar>
+			<view
+			    class="u-avatar-group__item__show-more"
+			    v-if="showMore && index === showUrl.length - 1 && (urls.length > maxCount || extraValue > 0)"
+				@tap="clickHandler"
+			>
+				<up-text
+				    color="#ffffff"
+				    :size="size * 0.4"
+				    :text="`+${extraValue || urls.length - showUrl.length}`"
+					align="center"
+					customStyle="justify-content: center"
+				></up-text>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	import { props } from './props';
+	import { mpMixin } from '../../libs/mixin/mpMixin';
+	import { mixin } from '../../libs/mixin/mixin';
+	import { addUnit } from '../../libs/function/index';
+	import test from '../../libs/function/test';
+	/**
+	 * AvatarGroup  头像组
+	 * @description 本组件一般用于展示头像的地方,如个人中心,或者评论列表页的用户头像展示等场所。
+	 * @tutorial https://ijry.github.io/uview-plus/components/avatar.html
+	 * 
+	 * @property {Array}           urls     头像图片组 (默认 [] )
+	 * @property {String | Number} maxCount 最多展示的头像数量 ( 默认 5 )
+	 * @property {String}          shape    头像形状( 'circle' (默认) | 'square' )
+	 * @property {String}          mode     图片裁剪模式(默认 'scaleToFill' )
+	 * @property {Boolean}         showMore 超出maxCount时是否显示查看更多的提示 (默认 true )
+	 * @property {String | Number} size      头像大小 (默认 40 )
+	 * @property {String}          keyName  指定从数组的对象元素中读取哪个属性作为图片地址 
+	 * @property {String | Number} gap      头像之间的遮挡比例(0.4代表遮挡40%)  (默认 0.5 )
+	 * @property {String | Number} extraValue  需额外显示的值
+	 * @event    {Function}        showMore 头像组更多点击
+	 * @example  <u-avatar-group:urls="urls" size="35" gap="0.4" ></u-avatar-group:urls=>
+	 */
+	export default {
+		name: 'u-avatar-group',
+		mixins: [mpMixin, mixin, props],
+		data() {
+			return {
+
+			}
+		},
+		computed: {
+			showUrl() {
+				return this.urls.slice(0, this.maxCount)
+			}
+		},
+		emits: ["showMore"],
+		methods: {
+			addUnit,
+			testObject: test.object,
+			clickHandler() {
+				this.$emit('showMore')
+			}
+		},
+	}
+</script>
+
+<style lang="scss" scoped>
+
+	.u-avatar-group {
+		@include flex;
+
+		&__item {
+			margin-left: -10px;
+			position: relative;
+
+			&--no-indent {
+				// 如果你想质疑作者不会使用:first-child,说明你太年轻,因为nvue不支持
+				margin-left: 0;
+			}
+
+			&__show-more {
+				position: absolute;
+				top: 0;
+				bottom: 0;
+				left: 0;
+				right: 0;
+				background-color: rgba(0, 0, 0, 0.3);
+				@include flex;
+				align-items: center;
+				justify-content: center;
+				border-radius: 100px;
+			}
+		}
+	}
+</style>

+ 28 - 0
uni_modules/uview-plus/components/u-avatar/avatar.js

@@ -0,0 +1,28 @@
+/*
+ * @Author       : LQ
+ * @Description  :
+ * @version      : 1.0
+ * @Date         : 2021-08-20 16:44:21
+ * @LastAuthor   : LQ
+ * @lastTime     : 2021-08-20 16:49:22
+ * @FilePath     : /u-view2.0/uview-ui/libs/config/props/avatar.js
+ */
+export default {
+    // avatar 组件
+    avatar: {
+        src: '',
+        shape: 'circle',
+        size: 40,
+        mode: 'scaleToFill',
+        text: '',
+        bgColor: '#c0c4cc',
+        color: '#ffffff',
+        fontSize: 18,
+        icon: '',
+        mpAvatar: false,
+        randomBgColor: false,
+        defaultUrl: '',
+        colorIndex: '',
+        name: ''
+    }
+}

+ 81 - 0
uni_modules/uview-plus/components/u-avatar/props.js

@@ -0,0 +1,81 @@
+import { defineMixin } from '../../libs/vue'
+import defProps from '../../libs/config/props.js'
+import test from '../../libs/function/test';
+export const props = defineMixin({
+    props: {
+        // 头像图片路径(不能为相对路径)
+        src: {
+            type: String,
+            default: () => defProps.avatar.src
+        },
+        // 头像形状,circle-圆形,square-方形
+        shape: {
+            type: String,
+            default: () => defProps.avatar.shape
+        },
+        // 头像尺寸
+        size: {
+            type: [String, Number],
+            default: () => defProps.avatar.size
+        },
+        // 裁剪模式
+        mode: {
+            type: String,
+            default: () => defProps.avatar.mode
+        },
+        // 显示的文字
+        text: {
+            type: String,
+            default: () => defProps.avatar.text
+        },
+        // 背景色
+        bgColor: {
+            type: String,
+            default: () => defProps.avatar.bgColor
+        },
+        // 文字颜色
+        color: {
+            type: String,
+            default: () => defProps.avatar.color
+        },
+        // 文字大小
+        fontSize: {
+            type: [String, Number],
+            default: () => defProps.avatar.fontSize
+        },
+        // 显示的图标
+        icon: {
+            type: String,
+            default: () => defProps.avatar.icon
+        },
+        // 显示小程序头像,只对百度,微信,QQ小程序有效
+        mpAvatar: {
+            type: Boolean,
+            default: () => defProps.avatar.mpAvatar
+        },
+        // 是否使用随机背景色
+        randomBgColor: {
+            type: Boolean,
+            default: () => defProps.avatar.randomBgColor
+        },
+        // 加载失败的默认头像(组件有内置默认图片)
+        defaultUrl: {
+            type: String,
+            default: () => defProps.avatar.defaultUrl
+        },
+        // 如果配置了randomBgColor为true,且配置了此值,则从默认的背景色数组中取出对应索引的颜色值,取值0-19之间
+        colorIndex: {
+            type: [String, Number],
+            // 校验参数规则,索引在0-19之间
+            validator(n) {
+                return test.range(n, [0, 19]) || n === ''
+            },
+            default: () => defProps.avatar.colorIndex
+        },
+        // 组件标识符
+        name: {
+            type: String,
+            default: () => defProps.avatar.name
+        }
+    }
+})

File diff suppressed because it is too large
+ 179 - 0
uni_modules/uview-plus/components/u-avatar/u-avatar.vue


+ 27 - 0
uni_modules/uview-plus/components/u-back-top/backtop.js

@@ -0,0 +1,27 @@
+/*
+ * @Author       : LQ
+ * @Description  :
+ * @version      : 1.0
+ * @Date         : 2021-08-20 16:44:21
+ * @LastAuthor   : LQ
+ * @lastTime     : 2021-08-20 16:50:18
+ * @FilePath     : /u-view2.0/uview-ui/libs/config/props/backtop.js
+ */
+export default {
+    // backtop组件
+    backtop: {
+        mode: 'circle',
+        icon: 'arrow-upward',
+        text: '',
+        duration: 100,
+        scrollTop: 0,
+        top: 400,
+        bottom: 100,
+        right: 20,
+        zIndex: 9,
+        iconStyle: {
+            color: '#909399',
+            fontSize: '19px'
+        }
+    }
+}

+ 56 - 0
uni_modules/uview-plus/components/u-back-top/props.js

@@ -0,0 +1,56 @@
+import { defineMixin } from '../../libs/vue'
+import defProps from '../../libs/config/props.js'
+export const props = defineMixin({
+    props: {
+        // 返回顶部的形状,circle-圆形,square-方形
+        mode: {
+            type: String,
+            default: () => defProps.backtop.mode
+        },
+        // 自定义图标
+        icon: {
+            type: String,
+            default: () => defProps.backtop.icon
+        },
+        // 提示文字
+        text: {
+            type: String,
+            default: () => defProps.backtop.text
+        },
+        // 返回顶部滚动时间
+        duration: {
+            type: [String, Number],
+            default: () => defProps.backtop.duration
+        },
+        // 滚动距离
+        scrollTop: {
+            type: [String, Number],
+            default: () => defProps.backtop.scrollTop
+        },
+        // 距离顶部多少距离显示,单位px
+        top: {
+            type: [String, Number],
+            default: () => defProps.backtop.top
+        },
+        // 返回顶部按钮到底部的距离,单位px
+        bottom: {
+            type: [String, Number],
+            default: () => defProps.backtop.bottom
+        },
+        // 返回顶部按钮到右边的距离,单位px
+        right: {
+            type: [String, Number],
+            default: () => defProps.backtop.right
+        },
+        // 层级
+        zIndex: {
+            type: [String, Number],
+            default: () => defProps.backtop.zIndex
+        },
+        // 图标的样式,对象形式
+        iconStyle: {
+            type: Object,
+            default: () => defProps.backtop.iconStyle
+        }
+    }
+})

+ 132 - 0
uni_modules/uview-plus/components/u-back-top/u-back-top.vue

@@ -0,0 +1,132 @@
+<template>
+	<u-transition
+	    mode="fade"
+	    :customStyle="backTopStyle"
+	    :show="show"
+	>
+		<view
+		    class="u-back-top"
+			:style="[contentStyle]"
+		    v-if="!$slots.default && !$slots.$default"
+			@click="backToTop"
+		>
+			<up-icon
+			    :name="icon"
+			    :custom-style="iconStyle"
+			></up-icon>
+			<text
+			    v-if="text"
+			    class="u-back-top__text"
+			>{{text}}</text>
+		</view>
+		<slot v-else />
+	</u-transition>
+</template>
+
+<script>
+	import { props } from './props';
+	import { mpMixin } from '../../libs/mixin/mpMixin';
+	import { mixin } from '../../libs/mixin/mixin';
+	import { addUnit, addStyle, getPx, deepMerge, error } from '../../libs/function/index';
+	// #ifdef APP-NVUE
+	const dom = weex.requireModule('dom')
+	// #endif
+	/**
+	 * backTop 返回顶部
+	 * @description 本组件一个用于长页面,滑动一定距离后,出现返回顶部按钮,方便快速返回顶部的场景。
+	 * @tutorial https://uview-plus.jiangruyi.com/components/backTop.html
+	 * 
+	 * @property {String}			mode  		返回顶部的形状,circle-圆形,square-方形 (默认 'circle' )
+	 * @property {String} 			icon 		自定义图标 (默认 'arrow-upward' ) 见官方文档示例
+	 * @property {String} 			text 		提示文字 
+	 * @property {String | Number}  duration	返回顶部滚动时间 (默认 100)
+	 * @property {String | Number}  scrollTop	滚动距离 (默认 0 )
+	 * @property {String | Number}  top  		距离顶部多少距离显示,单位px (默认 400 )
+	 * @property {String | Number}  bottom  	返回顶部按钮到底部的距离,单位px (默认 100 )
+	 * @property {String | Number}  right  		返回顶部按钮到右边的距离,单位px (默认 20 )
+	 * @property {String | Number}  zIndex 		层级   (默认 9 )
+	 * @property {Object<Object>}  	iconStyle 	图标的样式,对象形式   (默认 {color: '#909399',fontSize: '19px'})
+	 * @property {Object}			customStyle	定义需要用到的外部样式
+	 * 
+	 * @example <u-back-top :scrollTop="scrollTop"></u-back-top>
+	 */
+	export default {
+		name: 'u-back-top',
+		mixins: [mpMixin, mixin, props],
+		computed: {
+			backTopStyle() {
+				// 动画组件样式
+				const style = {
+					bottom: addUnit(this.bottom),
+					right: addUnit(this.right),
+					width: '40px',
+					height: '40px',
+					position: 'fixed',
+					zIndex: 10,
+				}
+				return style
+			},
+			show() {
+				return getPx(this.scrollTop) > getPx(this.top)
+			},
+			contentStyle() {
+				const style = {}
+				let radius = 0
+				// 是否圆形
+				if(this.mode === 'circle') {
+					radius = '100px'
+				} else {
+					radius = '4px'
+				}
+				// 为了兼容安卓nvue,只能这么分开写
+				style.borderTopLeftRadius = radius
+				style.borderTopRightRadius = radius
+				style.borderBottomLeftRadius = radius
+				style.borderBottomRightRadius = radius
+				return deepMerge(style, addStyle(this.customStyle))
+			}
+		},
+		emits: ["click"],
+		methods: {
+			backToTop() {
+				// #ifdef APP-NVUE
+				if (!this.$parent.$refs['u-back-top']) {
+					error(`nvue页面需要给页面最外层元素设置"ref='u-back-top'`)
+				}
+				dom.scrollToElement(this.$parent.$refs['u-back-top'], {
+					offset: 0
+				})
+				// #endif
+				
+				// #ifndef APP-NVUE
+				uni.pageScrollTo({
+					scrollTop: 0,
+					duration: this.duration
+				});
+				// #endif
+				this.$emit('click')
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+     $u-back-top-flex:1 !default;
+     $u-back-top-height:100% !default;
+     $u-back-top-background-color:#E1E1E1 !default;
+     $u-back-top-tips-font-size:12px !default;
+	.u-back-top {
+		@include flex;
+		flex-direction: column;
+		align-items: center;
+		flex:$u-back-top-flex;
+		height: $u-back-top-height;
+		justify-content: center;
+		background-color: $u-back-top-background-color;
+
+		&__tips {
+			font-size:$u-back-top-tips-font-size;
+			transform: scale(0.8);
+		}
+	}
+</style>

+ 27 - 0
uni_modules/uview-plus/components/u-badge/badge.js

@@ -0,0 +1,27 @@
+/*
+ * @Author       : LQ
+ * @Description  :
+ * @version      : 1.0
+ * @Date         : 2021-08-20 16:44:21
+ * @LastAuthor   : LQ
+ * @lastTime     : 2021-08-23 19:51:50
+ * @FilePath     : /u-view2.0/uview-ui/libs/config/props/badge.js
+ */
+export default {
+    // 徽标数组件
+    badge: {
+        isDot: false,
+        value: '',
+        show: true,
+        max: 999,
+        type: 'error',
+        showZero: false,
+        bgColor: null,
+        color: null,
+        shape: 'circle',
+        numberType: 'overflow',
+        offset: [],
+        inverted: false,
+        absolute: false
+    }
+}

+ 79 - 0
uni_modules/uview-plus/components/u-badge/props.js

@@ -0,0 +1,79 @@
+import { defineMixin } from '../../libs/vue'
+import defProps from '../../libs/config/props.js'
+export const props = defineMixin({
+    props: {
+        // 是否显示圆点
+        isDot: {
+            type: Boolean,
+            default: () => defProps.badge.isDot
+        },
+        // 显示的内容
+        value: {
+            type: [Number, String],
+            default: () => defProps.badge.value
+        },
+        // 显示的内容
+        modelValue: {
+            type: [Number, String],
+            default: () => defProps.badge.modelValue
+        },
+        // 是否显示
+        show: {
+            type: Boolean,
+            default: () => defProps.badge.show
+        },
+        // 最大值,超过最大值会显示 '{max}+'
+        max: {
+            type: [Number, String],
+            default: () => defProps.badge.max
+        },
+        // 主题类型,error|warning|success|primary
+        type: {
+            type: String,
+            default: () => defProps.badge.type
+        },
+        // 当数值为 0 时,是否展示 Badge
+        showZero: {
+            type: Boolean,
+            default: () => defProps.badge.showZero
+        },
+        // 背景颜色,优先级比type高,如设置,type参数会失效
+        bgColor: {
+            type: [String, null],
+            default: () => defProps.badge.bgColor
+        },
+        // 字体颜色
+        color: {
+            type: [String, null],
+            default: () => defProps.badge.color
+        },
+        // 徽标形状,circle-四角均为圆角,horn-左下角为直角
+        shape: {
+            type: String,
+            default: () => defProps.badge.shape
+        },
+        // 设置数字的显示方式,overflow|ellipsis|limit
+        // overflow会根据max字段判断,超出显示`${max}+`
+        // ellipsis会根据max判断,超出显示`${max}...`
+        // limit会依据1000作为判断条件,超出1000,显示`${value/1000}K`,比如2.2k、3.34w,最多保留2位小数
+        numberType: {
+            type: String,
+            default: () => defProps.badge.numberType
+        },
+        // 设置badge的位置偏移,格式为 [x, y],也即设置的为top和right的值,absolute为true时有效
+        offset: {
+            type: Array,
+            default: () => defProps.badge.offset
+        },
+        // 是否反转背景和字体颜色
+        inverted: {
+            type: Boolean,
+            default: () => defProps.badge.inverted
+        },
+        // 是否绝对定位
+        absolute: {
+            type: Boolean,
+            default: () => defProps.badge.absolute
+        }
+    }
+})

+ 176 - 0
uni_modules/uview-plus/components/u-badge/u-badge.vue

@@ -0,0 +1,176 @@
+<template>
+	<text
+		v-if="show && ((Number(value) === 0 ? showZero : true) || isDot)"
+		:class="[isDot ? 'u-badge--dot' : 'u-badge--not-dot', inverted && 'u-badge--inverted', shape === 'horn' && 'u-badge--horn', `u-badge--${type}${inverted ? '--inverted' : ''}`]"
+		:style="[addStyle(customStyle), badgeStyle]"
+		class="u-badge"
+	>{{ isDot ? '' :showValue }}</text>
+</template>
+
+<script>
+	import { props } from './props';
+	import { mpMixin } from '../../libs/mixin/mpMixin';
+	import { mixin } from '../../libs/mixin/mixin';
+	import { addStyle, addUnit } from '../../libs/function/index';
+	/**
+	 * badge 徽标数
+	 * @description 该组件一般用于图标右上角显示未读的消息数量,提示用户点击,有圆点和圆包含文字两种形式。
+	 * @tutorial https://uview-plus.jiangruyi.com/components/badge.html
+	 * 
+	 * @property {Boolean} 			isDot 		是否显示圆点 (默认 false )
+	 * @property {String | Number} 	value 		显示的内容
+	 * @property {Boolean} 			show 		是否显示 (默认 true )
+	 * @property {String | Number} 	max 		最大值,超过最大值会显示 '{max}+'  (默认999)
+	 * @property {String} 			type 		主题类型,error|warning|success|primary (默认 'error' )
+	 * @property {Boolean} 			showZero	当数值为 0 时,是否展示 Badge (默认 false )
+	 * @property {String} 			bgColor 	背景颜色,优先级比type高,如设置,type参数会失效
+	 * @property {String} 			color 		字体颜色 (默认 '#ffffff' )
+	 * @property {String} 			shape 		徽标形状,circle-四角均为圆角,horn-左下角为直角 (默认 'circle' )
+	 * @property {String} 			numberType	设置数字的显示方式,overflow|ellipsis|limit  (默认 'overflow' )
+	 * @property {Array}} 			offset		设置badge的位置偏移,格式为 [x, y],也即设置的为top和right的值,absolute为true时有效
+	 * @property {Boolean} 			inverted	是否反转背景和字体颜色(默认 false )
+	 * @property {Boolean} 			absolute	是否绝对定位(默认 false )
+	 * @property {Object}			customStyle	定义需要用到的外部样式
+	 * @example <u-badge :type="type" :count="count"></u-badge>
+	 */
+	export default {
+		name: 'u-badge',
+		mixins: [mpMixin, props, mixin],
+		computed: {
+			// 是否将badge中心与父组件右上角重合
+			boxStyle() {
+				let style = {};
+				return style;
+			},
+			// 整个组件的样式
+			badgeStyle() {
+				const style = {}
+				if(this.color) {
+					style.color = this.color
+				}
+				if (this.bgColor && !this.inverted) {
+					style.backgroundColor = this.bgColor
+				}
+				if (this.absolute) {
+					style.position = 'absolute'
+					// 如果有设置offset参数
+					if(this.offset.length) {
+						// top和right分为为offset的第一个和第二个值,如果没有第二个值,则right等于top
+						const top = this.offset[0]
+						const right = this.offset[1] || top
+						style.top = addUnit(top)
+						style.right = addUnit(right)
+					}
+				}
+				return style
+			},
+			showValue() {
+				switch (this.numberType) {
+					case "overflow":
+						return Number(this.value) > Number(this.max) ? this.max + "+" : this.value
+						break;
+					case "ellipsis":
+						return Number(this.value) > Number(this.max) ? "..." : this.value
+						break;
+					case "limit":
+						return Number(this.value) > 999 ? Number(this.value) >= 9999 ?
+							Math.floor(this.value / 1e4 * 100) / 100 + "w" : Math.floor(this.value /
+								1e3 * 100) / 100 + "k" : this.value
+						break;
+					default:
+						return Number(this.value)
+				}
+			},
+		},
+		methods: {
+			addStyle
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+
+	$u-badge-primary: $u-primary !default;
+	$u-badge-error: $u-error !default;
+	$u-badge-success: $u-success !default;
+	$u-badge-info: $u-info !default;
+	$u-badge-warning: $u-warning !default;
+	$u-badge-dot-radius: 100px !default;
+	$u-badge-dot-size: 8px !default;
+	$u-badge-dot-right: 4px !default;
+	$u-badge-dot-top: 0 !default;
+	$u-badge-text-font-size: 11px !default;
+	$u-badge-text-right: 10px !default;
+	$u-badge-text-padding: 2px 5px !default;
+	$u-badge-text-align: center !default;
+	$u-badge-text-color: #FFFFFF !default;
+
+	.u-badge {
+		border-top-right-radius: $u-badge-dot-radius;
+		border-top-left-radius: $u-badge-dot-radius;
+		border-bottom-left-radius: $u-badge-dot-radius;
+		border-bottom-right-radius: $u-badge-dot-radius;
+		@include flex;
+		line-height: $u-badge-text-font-size;
+		text-align: $u-badge-text-align;
+		font-size: $u-badge-text-font-size;
+		color: $u-badge-text-color;
+
+		&--dot {
+			height: $u-badge-dot-size;
+			width: $u-badge-dot-size;
+		}
+		
+		&--inverted {
+			font-size: 13px;
+		}
+		
+		&--not-dot {
+			padding: $u-badge-text-padding;
+		}
+
+		&--horn {
+			border-bottom-left-radius: 0;
+		}
+
+		&--primary {
+			background-color: $u-badge-primary;
+		}
+		
+		&--primary--inverted {
+			color: $u-badge-primary;
+		}
+
+		&--error {
+			background-color: $u-badge-error;
+		}
+		
+		&--error--inverted {
+			color: $u-badge-error;
+		}
+
+		&--success {
+			background-color: $u-badge-success;
+		}
+		
+		&--success--inverted {
+			color: $u-badge-success;
+		}
+
+		&--info {
+			background-color: $u-badge-info;
+		}
+		
+		&--info--inverted {
+			color: $u-badge-info;
+		}
+
+		&--warning {
+			background-color: $u-badge-warning;
+		}
+		
+		&--warning--inverted {
+			color: $u-badge-warning;
+		}
+	}
+</style>

File diff suppressed because it is too large
+ 1000 - 0
uni_modules/uview-plus/components/u-barcode/u-barcode.vue


+ 27 - 0
uni_modules/uview-plus/components/u-box/props.js

@@ -0,0 +1,27 @@
+import { defineMixin } from '../../libs/vue'
+import defProps from '../../libs/config/props.js'
+
+export const propsBox = defineMixin({
+    props: {
+        // 背景色
+        bgColors: {
+            type: [Array],
+            default: ['#EEFCFF', '#FCF8FF', '#FDF8F2']
+        },
+        // 高度
+        height: {
+            type: [String],
+            default: "160px"
+        },
+        // 圆角
+        borderRadius: {
+            type: [String],
+            default: "6px"
+        },
+        // 间隔
+        gap: {
+            type: [String],
+            default: "15px"
+        },
+    }
+})

+ 91 - 0
uni_modules/uview-plus/components/u-box/u-box.vue

@@ -0,0 +1,91 @@
+<template>
+	<view class="u-box" :style="[{height: height}, addStyle(customStyle)]">
+        <view class="u-box__left" :style="{borderRadius: borderRadius, backgroundColor: bgColors[0]}">
+            <slot name="left">左</slot>
+        </view>
+        <view class="u-box__gap" :style="{width: gap, height: height}"></view>
+        <view class="u-box__right">
+            <view class="u-box__right-top" :style="{borderRadius: borderRadius, backgroundColor: bgColors[1]}">
+                <slot name="rightTop">右上</slot>
+            </view>
+            <view class="u-box__right-gap" :style="{height: gap}"></view>
+            <view class="u-box__right-bottom" :style="{borderRadius: borderRadius, backgroundColor: bgColors[2]}">
+                <slot name="rightBottom">右下</slot>
+            </view>
+        </view>
+	</view>
+</template>
+
+<script>
+	import { propsBox } from './props';
+	import { mpMixin } from '../../libs/mixin/mpMixin';
+	import { mixin } from '../../libs/mixin/mixin';
+	import { addStyle } from '../../libs/function/index';
+	import test from '../../libs/function/test';
+	/**
+	 * box 盒子
+	 * @description box盒子一般为左边一个盒子,右侧两个等高的半盒组成,常用于App首页座位重点突出。
+	 * @tutorial https://uview-plus.jiangruyi.com/components/box.html
+	 * @property {Array}	bgColors			背景色
+	 * @property {String}	height			    高度
+     * @property {String}	borderRadius		圆角
+	 * @property {Object}   customStyle		    定义需要用到的外部样式
+	 * 
+	 * @event {Function}			click			点击cell列表时触发
+	 * @example <up-box colors=['blue', 'red', 'yellow'] height="200px"></up-box>
+	 */
+	export default {
+		name: 'up-box',
+		data() {
+			return {
+			}
+		},
+		mixins: [mpMixin, mixin, propsBox],
+		computed: {
+		},
+		emits: [],
+		methods: {
+			addStyle,
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+
+	.u-box {
+        /* #ifndef APP-NVUE */
+        /* #endif */
+        @include flex();
+        flex: 1;
+
+		&__left {
+            @include flex();
+            justify-content: center;
+            align-items: center;
+            flex: 1;
+        }
+        &__gap {
+            @include flex();
+            flex-direction: column;
+        }
+        &__right {
+            @include flex();
+            flex-direction: column;
+            flex: 1;
+        }
+
+        &__right-top {
+            @include flex();
+            flex: 1;
+            justify-content: center;
+            align-items: center;
+        }
+
+        &__right-bottom {
+            @include flex();
+            flex: 1;
+            justify-content: center;
+            align-items: center;
+        }
+	}
+</style>

+ 43 - 0
uni_modules/uview-plus/components/u-button/button.js

@@ -0,0 +1,43 @@
+/*
+ * @Author       : LQ
+ * @Description  :
+ * @version      : 1.0
+ * @Date         : 2021-08-20 16:44:21
+ * @LastAuthor   : LQ
+ * @lastTime     : 2021-08-20 16:51:27
+ * @FilePath     : /u-view2.0/uview-ui/libs/config/props/button.js
+ */
+export default {
+    // button组件
+    button: {
+        hairline: false,
+        type: 'info',
+        size: 'normal',
+        shape: 'square',
+        plain: false,
+        disabled: false,
+        loading: false,
+        loadingText: '',
+        loadingMode: 'spinner',
+        loadingSize: 15,
+        openType: '',
+        formType: '',
+        appParameter: '',
+        hoverStopPropagation: true,
+        lang: 'en',
+        sessionFrom: '',
+        sendMessageTitle: '',
+        sendMessagePath: '',
+        sendMessageImg: '',
+        showMessageCard: false,
+        dataName: '',
+        throttleTime: 0,
+        hoverStartTime: 0,
+        hoverStayTime: 200,
+        text: '',
+        icon: '',
+        iconColor: '',
+        color: '',
+        stop: true,
+    }
+}

+ 46 - 0
uni_modules/uview-plus/components/u-button/nvue.scss

@@ -0,0 +1,46 @@
+$u-button-active-opacity:0.75 !default;
+$u-button-loading-text-margin-left:4px !default;
+$u-button-text-color: #FFFFFF !default;
+$u-button-text-plain-error-color:$u-error !default;
+$u-button-text-plain-warning-color:$u-warning !default;
+$u-button-text-plain-success-color:$u-success !default;
+$u-button-text-plain-info-color:$u-info !default;
+$u-button-text-plain-primary-color:$u-primary !default;
+.u-button {
+	&--active {
+		opacity: $u-button-active-opacity;
+	}
+	
+	&--active--plain {
+		background-color: rgb(217, 217, 217);
+	}
+	
+	&__loading-text {
+		margin-left:$u-button-loading-text-margin-left;
+	}
+	
+	&__text,
+	&__loading-text {
+		color:$u-button-text-color;
+	}
+	
+	&__text--plain--error {
+		color:$u-button-text-plain-error-color;
+	}
+	
+	&__text--plain--warning {
+		color:$u-button-text-plain-warning-color;
+	}
+	
+	&__text--plain--success{
+		color:$u-button-text-plain-success-color;
+	}
+	
+	&__text--plain--info {
+		color:$u-button-text-plain-info-color;
+	}
+	
+	&__text--plain--primary {
+		color:$u-button-text-plain-primary-color;
+	}
+}

+ 159 - 0
uni_modules/uview-plus/components/u-button/props.js

@@ -0,0 +1,159 @@
+import { defineMixin } from '../../libs/vue'
+import defProps from '../../libs/config/props.js'
+export const props = defineMixin({
+    props: {
+        // 是否细边框
+        hairline: {
+            type: Boolean,
+            default: () => defProps.button.hairline
+        },
+        // 按钮的预置样式,info,primary,error,warning,success
+        type: {
+            type: String,
+            default: () => defProps.button.type
+        },
+        // 按钮尺寸,large,normal,small,mini
+        size: {
+            type: String,
+            default: () => defProps.button.size
+        },
+        // 按钮形状,circle(两边为半圆),square(带圆角)
+        shape: {
+            type: String,
+            default: () => defProps.button.shape
+        },
+        // 按钮是否镂空
+        plain: {
+            type: Boolean,
+            default: () => defProps.button.plain
+        },
+        // 是否禁止状态
+        disabled: {
+            type: Boolean,
+            default: () => defProps.button.disabled
+        },
+        // 是否加载中
+        loading: {
+            type: Boolean,
+            default: () => defProps.button.loading
+        },
+        // 加载中提示文字
+        loadingText: {
+            type: [String, Number],
+            default: () => defProps.button.loadingText
+        },
+        // 加载状态图标类型
+        loadingMode: {
+            type: String,
+            default: () => defProps.button.loadingMode
+        },
+        // 加载图标大小
+        loadingSize: {
+            type: [String, Number],
+            default: () => defProps.button.loadingSize
+        },
+        // 开放能力,具体请看uniapp稳定关于button组件部分说明
+        // https://uniapp.dcloud.io/component/button
+        openType: {
+            type: String,
+            default: () => defProps.button.openType
+        },
+        // 用于 <form> 组件,点击分别会触发 <form> 组件的 submit/reset 事件
+        // 取值为submit(提交表单),reset(重置表单)
+        formType: {
+            type: String,
+            default: () => defProps.button.formType
+        },
+        // 打开 APP 时,向 APP 传递的参数,open-type=launchApp时有效
+        // 只微信小程序、QQ小程序有效
+        appParameter: {
+            type: String,
+            default: () => defProps.button.appParameter
+        },
+        // 指定是否阻止本节点的祖先节点出现点击态,微信小程序有效
+        hoverStopPropagation: {
+            type: Boolean,
+            default: () => defProps.button.hoverStopPropagation
+        },
+        // 指定返回用户信息的语言,zh_CN 简体中文,zh_TW 繁体中文,en 英文。只微信小程序有效
+        lang: {
+            type: String,
+            default: () => defProps.button.lang
+        },
+        // 会话来源,open-type="contact"时有效。只微信小程序有效
+        sessionFrom: {
+            type: String,
+            default: () => defProps.button.sessionFrom
+        },
+        // 会话内消息卡片标题,open-type="contact"时有效
+        // 默认当前标题,只微信小程序有效
+        sendMessageTitle: {
+            type: String,
+            default: () => defProps.button.sendMessageTitle
+        },
+        // 会话内消息卡片点击跳转小程序路径,open-type="contact"时有效
+        // 默认当前分享路径,只微信小程序有效
+        sendMessagePath: {
+            type: String,
+            default: () => defProps.button.sendMessagePath
+        },
+        // 会话内消息卡片图片,open-type="contact"时有效
+        // 默认当前页面截图,只微信小程序有效
+        sendMessageImg: {
+            type: String,
+            default: () => defProps.button.sendMessageImg
+        },
+        // 是否显示会话内消息卡片,设置此参数为 true,用户进入客服会话会在右下角显示"可能要发送的小程序"提示,
+        // 用户点击后可以快速发送小程序消息,open-type="contact"时有效
+        showMessageCard: {
+            type: Boolean,
+            default: () => defProps.button.showMessageCard
+        },
+        // 额外传参参数,用于小程序的data-xxx属性,通过target.dataset.name获取
+        dataName: {
+            type: String,
+            default: () => defProps.button.dataName
+        },
+        // 节流,一定时间内只能触发一次
+        throttleTime: {
+            type: [String, Number],
+            default: () => defProps.button.throttleTime
+        },
+        // 按住后多久出现点击态,单位毫秒
+        hoverStartTime: {
+            type: [String, Number],
+            default: () => defProps.button.hoverStartTime
+        },
+        // 手指松开后点击态保留时间,单位毫秒
+        hoverStayTime: {
+            type: [String, Number],
+            default: () => defProps.button.hoverStayTime
+        },
+        // 按钮文字,之所以通过props传入,是因为slot传入的话
+        // nvue中无法控制文字的样式
+        text: {
+            type: [String, Number],
+            default: () => defProps.button.text
+        },
+        // 按钮图标
+        icon: {
+            type: String,
+            default: () => defProps.button.icon
+        },
+        // 按钮图标
+        iconColor: {
+            type: String,
+            default: () => defProps.button.icon
+        },
+        // 按钮颜色,支持传入linear-gradient渐变色
+        color: {
+            type: String,
+            default: () => defProps.button.color
+        },
+        // 停止冒泡
+        stop: {
+            type: Boolean,
+            default: () => defProps.button.stop
+        },
+    }
+})

+ 503 - 0
uni_modules/uview-plus/components/u-button/u-button.vue

@@ -0,0 +1,503 @@
+<template>
+    <!-- #ifndef APP-NVUE -->
+    <button
+        :hover-start-time="Number(hoverStartTime)"
+        :hover-stay-time="Number(hoverStayTime)"
+        :form-type="formType"
+        :open-type="openType"
+        :app-parameter="appParameter"
+        :hover-stop-propagation="hoverStopPropagation"
+        :send-message-title="sendMessageTitle"
+        :send-message-path="sendMessagePath"
+        :lang="lang"
+        :data-name="dataName"
+        :session-from="sessionFrom"
+        :send-message-img="sendMessageImg"
+        :show-message-card="showMessageCard"
+        @getphonenumber="getphonenumber"
+        @getuserinfo="getuserinfo"
+        @error="error"
+        @opensetting="opensetting"
+        @launchapp="launchapp"
+        @agreeprivacyauthorization="agreeprivacyauthorization"
+        :hover-class="!disabled && !loading ? 'u-button--active' : ''"
+        class="u-button u-reset-button"
+        :style="[baseColor, addStyle(customStyle)]"
+        @tap="clickHandler"
+        :class="bemClass"
+    >
+        <template v-if="loading">
+            <u-loading-icon
+                :mode="loadingMode"
+                :size="loadingSize * 1.15"
+                :color="loadingColor"
+            ></u-loading-icon>
+            <text
+                class="u-button__loading-text"
+                :style="[{ fontSize: textSize + 'px' }]"
+                >{{ loadingText || text }}</text
+            >
+        </template>
+        <template v-else>
+            <up-icon
+                v-if="icon"
+                :name="icon"
+                :color="iconColorCom"
+                :size="textSize * 1.35"
+                :customStyle="{ marginRight: '2px' }"
+            ></up-icon>
+            <slot>
+                <text
+                    class="u-button__text"
+                    :style="[{ fontSize: textSize + 'px' }]"
+                    >{{ text }}</text
+                >
+            </slot>
+        </template>
+    </button>
+    <!-- #endif -->
+
+    <!-- #ifdef APP-NVUE -->
+    <view
+        :hover-start-time="Number(hoverStartTime)"
+        :hover-stay-time="Number(hoverStayTime)"
+        class="u-button"
+        :hover-class="
+            !disabled && !loading && !color && (plain || type === 'info')
+                ? 'u-button--active--plain'
+                : !disabled && !loading && !plain
+                ? 'u-button--active'
+                : ''
+        "
+        @tap="clickHandler"
+        :class="bemClass"
+        :style="[baseColor, addStyle(customStyle)]"
+    >
+        <template v-if="loading">
+            <u-loading-icon
+                :mode="loadingMode"
+                :size="loadingSize * 1.15"
+                :color="loadingColor"
+            ></u-loading-icon>
+            <text
+                class="u-button__loading-text"
+                :style="[nvueTextStyle]"
+                :class="[plain && `u-button__text--plain--${type}`]"
+                >{{ loadingText || text }}</text
+            >
+        </template>
+        <template v-else>
+            <up-icon
+                v-if="icon"
+                :name="icon"
+                :color="iconColorCom"
+                :size="textSize * 1.35"
+            ></up-icon>
+            <text
+                class="u-button__text"
+                :style="[
+                    {
+                        marginLeft: icon ? '2px' : 0,
+                    },
+                    nvueTextStyle,
+                ]"
+                :class="[plain && `u-button__text--plain--${type}`]"
+                >{{ text }}</text
+            >
+        </template>
+    </view>
+    <!-- #endif -->
+</template>
+
+<script lang="ts">
+import { buttonMixin } from "../../libs/mixin/button";
+import { openType } from "../../libs/mixin/openType";
+import { mpMixin } from '../../libs/mixin/mpMixin';
+import { mixin } from '../../libs/mixin/mixin';
+import { props } from "./props";
+import { addStyle } from '../../libs/function/index';
+import { throttle } from '../../libs/function/throttle';
+import color from '../../libs/config/color';
+/**
+ * button 按钮
+ * @description Button 按钮
+ * @tutorial https://ijry.github.io/uview-plus/components/button.html
+ *
+ * @property {Boolean}			hairline				是否显示按钮的细边框 (默认 true )
+ * @property {String}			type					按钮的预置样式,info,primary,error,warning,success (默认 'info' )
+ * @property {String}			size					按钮尺寸,large,normal,mini (默认 normal)
+ * @property {String}			shape					按钮形状,circle(两边为半圆),square(带圆角) (默认 'square' )
+ * @property {Boolean}			plain					按钮是否镂空,背景色透明 (默认 false)
+ * @property {Boolean}			disabled				是否禁用 (默认 false)
+ * @property {Boolean}			loading					按钮名称前是否带 loading 图标(App-nvue 平台,在 ios 上为雪花,Android上为圆圈) (默认 false)
+ * @property {String | Number}	loadingText				加载中提示文字
+ * @property {String}			loadingMode				加载状态图标类型 (默认 'spinner' )
+ * @property {String | Number}	loadingSize				加载图标大小 (默认 15 )
+ * @property {String}			openType				开放能力,具体请看uniapp稳定关于button组件部分说明
+ * @property {String}			formType				用于 <form> 组件,点击分别会触发 <form> 组件的 submit/reset 事件
+ * @property {String}			appParameter			打开 APP 时,向 APP 传递的参数,open-type=launchApp时有效 (注:只微信小程序、QQ小程序有效)
+ * @property {Boolean}			hoverStopPropagation	指定是否阻止本节点的祖先节点出现点击态,微信小程序有效(默认 true )
+ * @property {String}			lang					指定返回用户信息的语言,zh_CN 简体中文,zh_TW 繁体中文,en 英文(默认 en )
+ * @property {String}			sessionFrom				会话来源,openType="contact"时有效
+ * @property {String}			sendMessageTitle		会话内消息卡片标题,openType="contact"时有效
+ * @property {String}			sendMessagePath			会话内消息卡片点击跳转小程序路径,openType="contact"时有效
+ * @property {String}			sendMessageImg			会话内消息卡片图片,openType="contact"时有效
+ * @property {Boolean}			showMessageCard			是否显示会话内消息卡片,设置此参数为 true,用户进入客服会话会在右下角显示"可能要发送的小程序"提示,用户点击后可以快速发送小程序消息,openType="contact"时有效(默认false)
+ * @property {String}			dataName				额外传参参数,用于小程序的data-xxx属性,通过target.dataset.name获取
+ * @property {String | Number}	throttleTime			节流,一定时间内只能触发一次 (默认 0 )
+ * @property {String | Number}	hoverStartTime			按住后多久出现点击态,单位毫秒 (默认 0 )
+ * @property {String | Number}	hoverStayTime			手指松开后点击态保留时间,单位毫秒 (默认 200 )
+ * @property {String | Number}	text					按钮文字,之所以通过props传入,是因为slot传入的话(注:nvue中无法控制文字的样式)
+ * @property {String}			icon					按钮图标
+ * @property {String}			iconColor				按钮图标颜色
+ * @property {String}			color					按钮颜色,支持传入linear-gradient渐变色
+ * @property {Object}			customStyle				定义需要用到的外部样式
+ *
+ * @event {Function}	click			非禁止并且非加载中,才能点击
+ * @event {Function}	getphonenumber	open-type="getPhoneNumber"时有效
+ * @event {Function}	getuserinfo		用户点击该按钮时,会返回获取到的用户信息,从返回参数的detail中获取到的值同uni.getUserInfo
+ * @event {Function}	error			当使用开放能力时,发生错误的回调
+ * @event {Function}	opensetting		在打开授权设置页并关闭后回调
+ * @event {Function}	launchapp		打开 APP 成功的回调
+ * @event {Function}	agreeprivacyauthorization	用户同意隐私协议事件回调
+ * @example <u-button>月落</u-button>
+ */
+export default {
+    name: "u-button",
+    // #ifdef MP
+    mixins: [mpMixin, mixin, buttonMixin, openType, props],
+    // #endif
+    // #ifndef MP
+    mixins: [mpMixin, mixin, props],
+    // #endif
+    data() {
+        return {};
+    },
+    computed: {
+        // 生成bem风格的类名
+        bemClass() {
+            // this.bem为一个computed变量,在mixin中
+            if (!this.color) {
+                return this.bem(
+                    "button",
+                    ["type", "shape", "size"],
+                    ["disabled", "plain", "hairline"]
+                );
+            } else {
+                // 由于nvue的原因,在有color参数时,不需要传入type,否则会生成type相关的类型,影响最终的样式
+                return this.bem(
+                    "button",
+                    ["shape", "size"],
+                    ["disabled", "plain", "hairline"]
+                );
+            }
+        },
+        loadingColor() {
+            if (this.plain) {
+                // 如果有设置color值,则用color值,否则使用type主题颜色
+                return this.color
+                    ? this.color
+                    : color[`u-${this.type}`];
+            }
+            if (this.type === "info") {
+                return "#c9c9c9";
+            }
+            return "rgb(200, 200, 200)";
+        },
+        iconColorCom() {
+            // 如果是镂空状态,设置了color就用color值,否则使用主题颜色,
+            // up-icon的color能接受一个主题颜色的值
+			if (this.iconColor) return this.iconColor;
+			if (this.plain) {
+                return this.color ? this.color : this.type;
+            } else {
+                return this.type === "info" ? "#000000" : "#ffffff";
+            }
+        },
+        baseColor() {
+            let style = {};
+            if (this.color) {
+                // 针对自定义了color颜色的情况,镂空状态下,就是用自定义的颜色
+                style.color = this.plain ? this.color : "white";
+                if (!this.plain) {
+                    // 非镂空,背景色使用自定义的颜色
+                    style["background-color"] = this.color;
+                }
+                if (this.color.indexOf("gradient") !== -1) {
+                    // 如果自定义的颜色为渐变色,不显示边框,以及通过backgroundImage设置渐变色
+                    // weex文档说明可以写borderWidth的形式,为什么这里需要分开写?
+                    // 因为weex是阿里巴巴为了部门业绩考核而做的你懂的东西,所以需要这么写才有效
+                    style.borderTopWidth = 0;
+                    style.borderRightWidth = 0;
+                    style.borderBottomWidth = 0;
+                    style.borderLeftWidth = 0;
+                    if (!this.plain) {
+                        style.backgroundImage = this.color;
+                    }
+                } else {
+                    // 非渐变色,则设置边框相关的属性
+                    style.borderColor = this.color;
+                    style.borderWidth = "1px";
+                    style.borderStyle = "solid";
+                }
+            }
+            return style;
+        },
+        // nvue版本按钮的字体不会继承父组件的颜色,需要对每一个text组件进行单独的设置
+        nvueTextStyle() {
+            let style = {};
+            // 针对自定义了color颜色的情况,镂空状态下,就是用自定义的颜色
+            if (this.type === "info") {
+                style.color = "#323233";
+            }
+            if (this.color) {
+                style.color = this.plain ? this.color : "white";
+            }
+            style.fontSize = this.textSize + "px";
+            return style;
+        },
+        // 字体大小
+        textSize() {
+            let fontSize = 14,
+                { size } = this;
+            if (size === "large") fontSize = 16;
+            if (size === "normal") fontSize = 14;
+            if (size === "small") fontSize = 12;
+            if (size === "mini") fontSize = 10;
+            return fontSize;
+        },
+    },
+	emits: ['click', 'getphonenumber', 'getuserinfo',
+		'error', 'opensetting', 'launchapp', 'agreeprivacyauthorization'],
+    methods: {
+        addStyle,
+        clickHandler(e: any) {
+            // 非禁止并且非加载中,才能点击
+            if (!this.disabled && !this.loading) {
+				// 进行节流控制,每this.throttle毫秒内,只在开始处执行
+				throttle(() => {
+					this.$emit("click", e);
+				}, this.throttleTime);
+            }
+            // 是否阻止事件传播
+            this.stop && this.preventEvent(e)
+        },
+        // 下面为对接uniapp官方按钮开放能力事件回调的对接
+        getphonenumber(res: any) {
+            this.$emit("getphonenumber", res);
+        },
+        getuserinfo(res: any) {
+            this.$emit("getuserinfo", res);
+        },
+        error(res: any) {
+            this.$emit("error", res);
+        },
+        opensetting(res: any) {
+            this.$emit("opensetting", res);
+        },
+        launchapp(res: any) {
+            this.$emit("launchapp", res);
+        },
+        agreeprivacyauthorization(res) {
+            this.$emit("agreeprivacyauthorization", res);
+        },
+    },
+};
+</script>
+
+<style lang="scss" scoped>
+/* #ifndef APP-NVUE */
+@import "./vue.scss";
+/* #endif */
+
+/* #ifdef APP-NVUE */
+@import "./nvue.scss";
+/* #endif */
+
+$u-button-u-button-height: 40px !default;
+$u-button-text-font-size: 15px !default;
+$u-button-loading-text-font-size: 15px !default;
+$u-button-loading-text-margin-left: 4px !default;
+$u-button-large-width: 100% !default;
+$u-button-large-height: 50px !default;
+$u-button-normal-padding: 0 12px !default;
+$u-button-large-padding: 0 15px !default;
+$u-button-normal-font-size: 14px !default;
+$u-button-small-min-width: 60px !default;
+$u-button-small-height: 30px !default;
+$u-button-small-padding: 0px 8px !default;
+$u-button-mini-padding: 0px 8px !default;
+$u-button-small-font-size: 12px !default;
+$u-button-mini-height: 22px !default;
+$u-button-mini-font-size: 10px !default;
+$u-button-mini-min-width: 50px !default;
+$u-button-disabled-opacity: 0.5 !default;
+$u-button-info-color: #323233 !default;
+$u-button-info-background-color: #fff !default;
+$u-button-info-border-color: #ebedf0 !default;
+$u-button-info-border-width: 1px !default;
+$u-button-info-border-style: solid !default;
+$u-button-success-color: #fff !default;
+$u-button-success-background-color: $u-success !default;
+$u-button-success-border-color: $u-button-success-background-color !default;
+$u-button-success-border-width: 1px !default;
+$u-button-success-border-style: solid !default;
+$u-button-primary-color: #fff !default;
+$u-button-primary-background-color: $u-primary !default;
+$u-button-primary-border-color: $u-button-primary-background-color !default;
+$u-button-primary-border-width: 1px !default;
+$u-button-primary-border-style: solid !default;
+$u-button-error-color: #fff !default;
+$u-button-error-background-color: $u-error !default;
+$u-button-error-border-color: $u-button-error-background-color !default;
+$u-button-error-border-width: 1px !default;
+$u-button-error-border-style: solid !default;
+$u-button-warning-color: #fff !default;
+$u-button-warning-background-color: $u-warning !default;
+$u-button-warning-border-color: $u-button-warning-background-color !default;
+$u-button-warning-border-width: 1px !default;
+$u-button-warning-border-style: solid !default;
+$u-button-block-width: 100% !default;
+$u-button-circle-border-top-right-radius: 100px !default;
+$u-button-circle-border-top-left-radius: 100px !default;
+$u-button-circle-border-bottom-left-radius: 100px !default;
+$u-button-circle-border-bottom-right-radius: 100px !default;
+$u-button-square-border-top-right-radius: 3px !default;
+$u-button-square-border-top-left-radius: 3px !default;
+$u-button-square-border-bottom-left-radius: 3px !default;
+$u-button-square-border-bottom-right-radius: 3px !default;
+$u-button-icon-min-width: 1em !default;
+$u-button-plain-background-color: #fff !default;
+$u-button-hairline-border-width: 0.5px !default;
+
+.u-button {
+    height: $u-button-u-button-height;
+    position: relative;
+    align-items: center;
+    justify-content: center;
+    @include flex;
+    /* #ifndef APP-NVUE */
+    box-sizing: border-box;
+    /* #endif */
+    flex-direction: row;
+
+    &__text {
+        font-size: $u-button-text-font-size;
+    }
+
+    &__loading-text {
+        font-size: $u-button-loading-text-font-size;
+        margin-left: $u-button-loading-text-margin-left;
+    }
+
+    &--large {
+        /* #ifndef APP-NVUE */
+        width: $u-button-large-width;
+        /* #endif */
+        height: $u-button-large-height;
+        padding: $u-button-large-padding;
+    }
+
+    &--normal {
+        padding: $u-button-normal-padding;
+        font-size: $u-button-normal-font-size;
+    }
+
+    &--small {
+        /* #ifndef APP-NVUE */
+        min-width: $u-button-small-min-width;
+        /* #endif */
+        height: $u-button-small-height;
+        padding: $u-button-small-padding;
+        font-size: $u-button-small-font-size;
+    }
+
+    &--mini {
+        height: $u-button-mini-height;
+        font-size: $u-button-mini-font-size;
+        /* #ifndef APP-NVUE */
+        min-width: $u-button-mini-min-width;
+        /* #endif */
+        padding: $u-button-mini-padding;
+    }
+
+    &--disabled {
+        opacity: $u-button-disabled-opacity;
+    }
+
+    &--info {
+        color: $u-button-info-color;
+        background-color: $u-button-info-background-color;
+        border-color: $u-button-info-border-color;
+        border-width: $u-button-info-border-width;
+        border-style: $u-button-info-border-style;
+    }
+
+    &--success {
+        color: $u-button-success-color;
+        background-color: $u-button-success-background-color;
+        border-color: $u-button-success-border-color;
+        border-width: $u-button-success-border-width;
+        border-style: $u-button-success-border-style;
+    }
+
+    &--primary {
+        color: $u-button-primary-color;
+        background-color: $u-button-primary-background-color;
+        border-color: $u-button-primary-border-color;
+        border-width: $u-button-primary-border-width;
+        border-style: $u-button-primary-border-style;
+    }
+
+    &--error {
+        color: $u-button-error-color;
+        background-color: $u-button-error-background-color;
+        border-color: $u-button-error-border-color;
+        border-width: $u-button-error-border-width;
+        border-style: $u-button-error-border-style;
+    }
+
+    &--warning {
+        color: $u-button-warning-color;
+        background-color: $u-button-warning-background-color;
+        border-color: $u-button-warning-border-color;
+        border-width: $u-button-warning-border-width;
+        border-style: $u-button-warning-border-style;
+    }
+
+    &--block {
+        @include flex;
+        width: $u-button-block-width;
+    }
+
+    &--circle {
+        border-top-right-radius: $u-button-circle-border-top-right-radius;
+        border-top-left-radius: $u-button-circle-border-top-left-radius;
+        border-bottom-left-radius: $u-button-circle-border-bottom-left-radius;
+        border-bottom-right-radius: $u-button-circle-border-bottom-right-radius;
+    }
+
+    &--square {
+        border-bottom-left-radius: $u-button-square-border-top-right-radius;
+        border-bottom-right-radius: $u-button-square-border-top-left-radius;
+        border-top-left-radius: $u-button-square-border-bottom-left-radius;
+        border-top-right-radius: $u-button-square-border-bottom-right-radius;
+    }
+
+    &__icon {
+        /* #ifndef APP-NVUE */
+        min-width: $u-button-icon-min-width;
+        line-height: inherit !important;
+        vertical-align: top;
+        /* #endif */
+    }
+
+    &--plain {
+        background-color: $u-button-plain-background-color;
+    }
+
+    &--hairline {
+        border-width: $u-button-hairline-border-width !important;
+    }
+}
+</style>

+ 81 - 0
uni_modules/uview-plus/components/u-button/vue.scss

@@ -0,0 +1,81 @@
+// nvue下hover-class无效
+$u-button-before-top:50% !default;
+$u-button-before-left:50% !default;
+$u-button-before-width:100% !default;
+$u-button-before-height:100% !default;
+$u-button-before-transform:translate(-50%, -50%) !default;
+$u-button-before-opacity:0 !default;
+$u-button-before-background-color:#000 !default;
+$u-button-before-border-color:#000 !default;
+$u-button-active-before-opacity:.15 !default;
+$u-button-icon-margin-left:4px !default;
+$u-button-plain-u-button-info-color:$u-info;
+$u-button-plain-u-button-success-color:$u-success;
+$u-button-plain-u-button-error-color:$u-error;
+$u-button-plain-u-button-warning-color:$u-warning;
+
+.u-button {
+	width: 100%;
+	white-space: nowrap;
+	
+	&__text {
+		white-space: nowrap;
+		line-height: 1;
+	}
+	
+	&:before {
+		position: absolute;
+		top:$u-button-before-top;
+		left:$u-button-before-left;
+		width:$u-button-before-width;
+		height:$u-button-before-height;
+		border: inherit;
+		border-radius: inherit;
+		transform:$u-button-before-transform;
+		opacity:$u-button-before-opacity;
+		content: " ";
+		background-color:$u-button-before-background-color;
+		border-color:$u-button-before-border-color;
+	}
+	
+	&--active {
+		&:before {
+			opacity: .15
+		}
+	}
+	
+	&__icon+&__text:not(:empty),
+	&__loading-text {
+		margin-left:$u-button-icon-margin-left;
+	}
+	
+	&--plain {
+		&.u-button--primary {
+			color: $u-primary;
+		}
+	}
+	
+	&--plain {
+		&.u-button--info {
+			color:$u-button-plain-u-button-info-color;
+		}
+	}
+	
+	&--plain {
+		&.u-button--success {
+			color:$u-button-plain-u-button-success-color;
+		}
+	}
+	
+	&--plain {
+		&.u-button--error {
+			color:$u-button-plain-u-button-error-color;
+		}
+	}
+	
+	&--plain {
+		&.u-button--warning {
+			color:$u-button-plain-u-button-warning-color;
+		}
+	}
+}

+ 48 - 0
uni_modules/uview-plus/components/u-calendar/calendar.js

@@ -0,0 +1,48 @@
+/*
+ * @Author       : LQ
+ * @Description  :
+ * @version      : 1.0
+ * @Date         : 2021-08-20 16:44:21
+ * @LastAuthor   : LQ
+ * @lastTime     : 2021-08-20 16:52:43
+ * @FilePath     : /u-view2.0/uview-ui/libs/config/props/calendar.js
+ */
+import { t } from '../../libs/i18n'
+export default {
+    // calendar 组件
+    calendar: {
+        title: t("up.calendar.chooseDates"),
+        showTitle: true,
+        showSubtitle: true,
+        mode: 'single',
+        startText: t("up.common.start"),
+        endText: t("up.common.end"),
+        customList: [],
+        color: '#3c9cff',
+        minDate: 0,
+        maxDate: 0,
+        defaultDate: null,
+        maxCount: Number.MAX_SAFE_INTEGER, // Infinity
+        rowHeight: 56,
+        formatter: null,
+        showLunar: false,
+        showMark: true,
+        confirmText: t("up.common.confirm"),
+        confirmDisabledText: t("up.common.confirm"),
+        show: false,
+        closeOnClickOverlay: false,
+        readonly: false,
+        showConfirm: true,
+        maxRange: Number.MAX_SAFE_INTEGER, // Infinity
+        rangePrompt: '',
+        showRangePrompt: true,
+        allowSameDay: false,
+		round: 0,
+		monthNum: 3,
+        weekText: [t("up.week.one"), t("up.week.two"), t("up.week.three"), t("up.week.four"), t("up.week.five"), t("up.week.six"), t("up.week.seven")],
+        forbidDays: [],
+        forbidDaysToast: t("up.calendar.disabled"),
+        monthFormat: '',
+        pageInline: false
+    }
+}

+ 109 - 0
uni_modules/uview-plus/components/u-calendar/header.vue

@@ -0,0 +1,109 @@
+<template>
+	<view class="u-calendar-header u-border-bottom">
+		<text
+			class="u-calendar-header__title"
+			v-if="showTitle"
+		>{{ title }}</text>
+		<text
+			class="u-calendar-header__subtitle"
+			v-if="showSubtitle"
+		>{{ subtitle }}</text>
+		<view class="u-calendar-header__weekdays">
+			<text class="u-calendar-header__weekdays__weekday">{{ weekText[0] }}</text>
+			<text class="u-calendar-header__weekdays__weekday">{{ weekText[1] }}</text>
+			<text class="u-calendar-header__weekdays__weekday">{{ weekText[2] }}</text>
+			<text class="u-calendar-header__weekdays__weekday">{{ weekText[3] }}</text>
+			<text class="u-calendar-header__weekdays__weekday">{{ weekText[4] }}</text>
+			<text class="u-calendar-header__weekdays__weekday">{{ weekText[5] }}</text>
+			<text class="u-calendar-header__weekdays__weekday">{{ weekText[6] }}</text>
+		</view>
+	</view>
+</template>
+
+<script>
+	import { mpMixin } from '../../libs/mixin/mpMixin';
+	import { mixin } from '../../libs/mixin/mixin';
+	export default {
+		name: 'u-calendar-header',
+		mixins: [mpMixin, mixin],
+		props: {
+			// 标题
+			title: {
+				type: String,
+				default: ''
+			},
+			// 副标题
+			subtitle: {
+				type: String,
+				default: ''
+			},
+			// 是否显示标题
+			showTitle: {
+				type: Boolean,
+				default: true
+			},
+			// 是否显示副标题
+			showSubtitle: {
+				type: Boolean,
+				default: true
+			},
+			// 星期文本
+			weekText: {
+				type: Array,
+				default: () => {
+					return []
+				}
+			},
+		},
+		data() {
+			return {
+
+			}
+		},
+		methods: {
+			name() {
+
+			}
+		},
+	}
+</script>
+
+<style lang="scss" scoped>
+
+	.u-calendar-header {
+		display: flex;
+		flex-direction: column;
+		padding-bottom: 4px;
+
+		&__title {
+			font-size: 16px;
+			color: $u-main-color;
+			text-align: center;
+			height: 42px;
+			line-height: 42px;
+			font-weight: bold;
+		}
+
+		&__subtitle {
+			font-size: 14px;
+			color: $u-main-color;
+			height: 40px;
+			text-align: center;
+			line-height: 40px;
+			font-weight: bold;
+		}
+
+		&__weekdays {
+			@include flex;
+			justify-content: space-between;
+
+			&__weekday {
+				font-size: 13px;
+				color: $u-main-color;
+				line-height: 30px;
+				flex: 1;
+				text-align: center;
+			}
+		}
+	}
+</style>

+ 616 - 0
uni_modules/uview-plus/components/u-calendar/month.vue

@@ -0,0 +1,616 @@
+<template>
+	<view class="u-calendar-month-wrapper" ref="u-calendar-month-wrapper">
+		<view v-for="(item, index) in months" :key="index" :class="[`u-calendar-month-${index}`]"
+			:ref="`u-calendar-month-${index}`" :id="`month-${index}`">
+			<text v-if="index !== 0" class="u-calendar-month__title">{{ monthTitle(item) }}</text>
+			<view class="u-calendar-month__days">
+				<view v-if="showMark" class="u-calendar-month__days__month-mark-wrapper">
+					<text class="u-calendar-month__days__month-mark-wrapper__text">{{ item.month }}</text>
+				</view>
+				<view class="u-calendar-month__days__day" v-for="(item1, index1) in item.date" :key="index1"
+					:style="[dayStyle(index, index1, item1)]" @tap="clickHandler(index, index1, item1)"
+					:class="[item1.selected && 'u-calendar-month__days__day__select--selected']">
+					<view class="u-calendar-month__days__day__select" :style="[daySelectStyle(index, index1, item1)]">
+						<text class="u-calendar-month__days__day__select__info"
+							:class="[(item1.disabled || isForbid(item1) ) ? 'u-calendar-month__days__day__select__info--disabled' : '']"
+							:style="[textStyle(item1)]">{{ item1.day }}</text>
+						<text v-if="getBottomInfo(index, index1, item1)"
+							class="u-calendar-month__days__day__select__buttom-info"
+							:class="[(item1.disabled || isForbid(item1) ) ? 'u-calendar-month__days__day__select__buttom-info--disabled' : '']"
+							:style="[textStyle(item1)]">{{ getBottomInfo(index, index1, item1) }}</text>
+						<text v-if="item1.dot" class="u-calendar-month__days__day__select__dot"></text>
+					</view>
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	// #ifdef APP-NVUE
+	// 由于nvue不支持百分比单位,需要查询宽度来计算每个日期的宽度
+	const dom = uni.requireNativePlugin('dom')
+	// #endif
+	import { mpMixin } from '../../libs/mixin/mpMixin';
+	import { mixin } from '../../libs/mixin/mixin';
+	import { addUnit, deepClone, toast, sleep } from '../../libs/function/index';
+	import { colorGradient } from '../../libs/function/colorGradient';
+	import test from '../../libs/function/test';
+	import defProps from '../../libs/config/props';
+	import dayjs from '../u-datetime-picker/dayjs.esm.min.js';
+	import { t } from '../../libs/i18n'
+	export default {
+		name: 'u-calendar-month',
+		mixins: [mpMixin, mixin],
+		props: {
+			// 是否显示月份背景色
+			showMark: {
+				type: Boolean,
+				default: true
+			},
+			// 主题色,对底部按钮和选中日期有效
+			color: {
+				type: String,
+				default: '#3c9cff'
+			},
+			// 月份数据
+			months: {
+				type: Array,
+				default: () => []
+			},
+			// 日期选择类型
+			mode: {
+				type: String,
+				default: 'single'
+			},
+			// 日期行高
+			rowHeight: {
+				type: [String, Number],
+				default: 58
+			},
+			// mode=multiple时,最多可选多少个日期
+			maxCount: {
+				type: [String, Number],
+				default: Infinity
+			},
+			// mode=range时,第一个日期底部的提示文字
+			startText: {
+				type: String,
+				default: '开始'
+			},
+			// mode=range时,最后一个日期底部的提示文字
+			endText: {
+				type: String,
+				default: '结束'
+			},
+			// 默认选中的日期,mode为multiple或range是必须为数组格式
+			defaultDate: {
+				type: [Array, String, Date],
+				default: null
+			},
+			// 最小的可选日期
+			minDate: {
+				type: [String, Number],
+				default: 0
+			},
+			// 最大可选日期
+			maxDate: {
+				type: [String, Number],
+				default: 0
+			},
+			// 如果没有设置maxDate,则往后推多少个月
+			maxMonth: {
+				type: [String, Number],
+				default: 2
+			},
+			// 是否为只读状态,只读状态下禁止选择日期
+			readonly: {
+				type: Boolean,
+				default: () => defProps.calendar.readonly
+			},
+			// 日期区间最多可选天数,默认无限制,mode = range时有效
+			maxRange: {
+				type: [Number, String],
+				default: Infinity
+			},
+			// 范围选择超过最多可选天数时的提示文案,mode = range时有效
+			rangePrompt: {
+				type: String,
+				default: ''
+			},
+			// 范围选择超过最多可选天数时,是否展示提示文案,mode = range时有效
+			showRangePrompt: {
+				type: Boolean,
+				default: true
+			},
+			// 是否允许日期范围的起止时间为同一天,mode = range时有效
+			allowSameDay: {
+				type: Boolean,
+				default: false
+			},
+			forbidDays: {
+				type: Array,
+				default: () => []
+			},
+			forbidDaysToast: {
+				type: String,
+				default: ''
+			}
+		},
+		data() {
+			return {
+				// 每个日期的宽度
+				width: 0,
+				// 当前选中的日期item
+				item: {},
+				selected: []
+			}
+		},
+		watch: {
+			selectedChange: {
+				immediate: true,
+				handler(n) {
+					this.setDefaultDate()
+				}
+			}
+		},
+		computed: {
+			// 多个条件的变化,会引起选中日期的变化,这里统一管理监听
+			selectedChange() {
+				return [this.minDate, this.maxDate, this.defaultDate]
+			},
+			dayStyle(index1, index2, item) {
+				return (index1, index2, item) => {
+					const style = {}
+					let week = item.week
+					// 不进行四舍五入的形式保留2位小数
+					const dayWidth = Number(parseFloat(this.width / 7).toFixed(3).slice(0, -1))
+					// 得出每个日期的宽度
+					// #ifdef APP-NVUE
+					style.width = addUnit(dayWidth, 'px')
+					// #endif
+					style.height = addUnit(this.rowHeight, 'px')
+					if (index2 === 0) {
+						// 获取当前为星期几,如果为0,则为星期天,减一为每月第一天时,需要向左偏移的item个数
+						week = (week === 0 ? 7 : week) - 1
+						style.marginLeft = addUnit(week * dayWidth, 'px')
+					}
+					if (this.mode === 'range') {
+						// 之所以需要这么写,是因为DCloud公司的iOS客户端导致的bug
+						style.paddingLeft = 0
+						style.paddingRight = 0
+						style.paddingBottom = 0
+						style.paddingTop = 0
+					}
+					return style
+				}
+			},
+			daySelectStyle() {
+				return (index1, index2, item) => {
+					let date = dayjs(item.date).format("YYYY-MM-DD"),
+						style = {}
+					// 判断date是否在selected数组中,因为月份可能会需要补0,所以使用dateSame判断,而不用数组的includes判断
+					if (this.selected.some(item => this.dateSame(item, date))) {
+						style.backgroundColor = this.color
+					}
+					if (this.mode === 'single') {
+						if (date === this.selected[0]) {
+							// 因为需要对nvue的兼容,只能这么写,无法缩写,也无法通过类名控制等等
+							style.borderTopLeftRadius = '3px'
+							style.borderBottomLeftRadius = '3px'
+							style.borderTopRightRadius = '3px'
+							style.borderBottomRightRadius = '3px'
+						}
+					} else if (this.mode === 'range') {
+						if (this.selected.length >= 2) {
+							const len = this.selected.length - 1
+							// 第一个日期设置左上角和左下角的圆角
+							if (this.dateSame(date, this.selected[0])) {
+								style.borderTopLeftRadius = '3px'
+								style.borderBottomLeftRadius = '3px'
+							}
+							// 最后一个日期设置右上角和右下角的圆角
+							if (this.dateSame(date, this.selected[len])) {
+								style.borderTopRightRadius = '3px'
+								style.borderBottomRightRadius = '3px'
+							}
+							// 处于第一和最后一个之间的日期,背景色设置为浅色,通过将对应颜色进行等分,再取其尾部的颜色值
+							if (dayjs(date).isAfter(dayjs(this.selected[0])) && dayjs(date).isBefore(dayjs(this
+									.selected[len]))) {
+								style.backgroundColor = colorGradient(this.color, '#ffffff', 100)[90]
+								// 增加一个透明度,让范围区间的背景色也能看到底部的mark水印字符
+								style.opacity = 0.7
+							}
+						} else if (this.selected.length === 1) {
+							// 之所以需要这么写,是因为uni-app的iOS客户端的bug
+							// 进行还原操作,否则在nvue的iOS,uni-app有bug,会导致诡异的表现
+							style.borderTopLeftRadius = '3px'
+							style.borderBottomLeftRadius = '3px'
+						}
+					} else {
+						if (this.selected.some(item => this.dateSame(item, date))) {
+							style.borderTopLeftRadius = '3px'
+							style.borderBottomLeftRadius = '3px'
+							style.borderTopRightRadius = '3px'
+							style.borderBottomRightRadius = '3px'
+						}
+					}
+					return style
+				}
+			},
+			// 某个日期是否被选中
+			textStyle() {
+				return (item) => {
+					const date = dayjs(item.date).format("YYYY-MM-DD"),
+						style = {}
+					// 选中的日期,提示文字设置白色
+					if (this.selected.some(item => this.dateSame(item, date))) {
+						style.color = '#ffffff'
+					}
+					if (this.mode === 'range') {
+						const len = this.selected.length - 1
+						// 如果是范围选择模式,第一个和最后一个之间的日期,文字颜色设置为高亮的主题色
+						if (dayjs(date).isAfter(dayjs(this.selected[0])) && dayjs(date).isBefore(dayjs(this
+								.selected[len]))) {
+							style.color = this.color
+						}
+					}
+					return style
+				}
+			},
+			// 获取底部的提示文字
+			getBottomInfo() {
+				return (index1, index2, item) => {
+					const date = dayjs(item.date).format("YYYY-MM-DD")
+					const bottomInfo = item.bottomInfo
+					// 当为日期范围模式时,且选择的日期个数大于0时
+					if (this.mode === 'range' && this.selected.length > 0) {
+						if (this.selected.length === 1) {
+							// 选择了一个日期时,如果当前日期为数组中的第一个日期,则显示底部文字为“开始”
+							if (this.dateSame(date, this.selected[0])) return this.startText
+							else return bottomInfo
+						} else {
+							const len = this.selected.length - 1
+							// 如果数组中的日期大于2个时,第一个和最后一个显示为开始和结束日期
+							if (this.dateSame(date, this.selected[0]) && this.dateSame(date, this.selected[1]) &&
+								len === 1) {
+								// 如果长度为2,且第一个等于第二个日期,则提示语放在同一个item中
+								return `${this.startText}/${this.endText}`
+							} else if (this.dateSame(date, this.selected[0])) {
+								return this.startText
+							} else if (this.dateSame(date, this.selected[len])) {
+								return this.endText
+							} else {
+								return bottomInfo
+							}
+						}
+					} else {
+						return bottomInfo
+					}
+				}
+			}
+		},
+		mounted() {
+			this.init()
+		},
+		emits: ['monthSelected', 'updateMonthTop'],
+		methods: {
+			init() {
+				// 初始化默认选中
+				this.$emit('monthSelected', this.selected)
+				this.$nextTick(() => {
+					// 这里需要另一个延时,因为获取宽度后,会进行月份数据渲染,只有渲染完成之后,才有真正的高度
+					// 因为nvue下,$nextTick并不是100%可靠的
+					sleep(10).then(() => {
+						this.getWrapperWidth()
+						this.getMonthRect()
+					})
+				})
+			},
+			monthTitle(item) {
+				if (uni.getLocale() == 'zh-Hans' || uni.getLocale() == 'zh-Hant') {
+					return item.year + '年' + (item.month < 10 ? '0' + item.month : item.month) + '月'
+				} else {
+					return (item.month < 10 ? '0' + item.month : item.month) + '/' + item.year
+				}
+			},
+			isForbid(item) {
+				let date = dayjs(item.date).format("YYYY-MM-DD")
+				if (this.mode !== 'range' && this.forbidDays.includes(date)) {
+					return true
+				}
+				return false
+			},
+			// 判断两个日期是否相等
+			dateSame(date1, date2) {
+				return dayjs(date1).isSame(dayjs(date2))
+			},
+			// 获取月份数据区域的宽度,因为nvue不支持百分比,所以无法通过css设置每个日期item的宽度
+			getWrapperWidth() {
+				// #ifdef APP-NVUE
+				dom.getComponentRect(this.$refs['u-calendar-month-wrapper'], res => {
+					this.width = res.size.width
+				})
+				// #endif
+				// #ifndef APP-NVUE
+				this.$uGetRect('.u-calendar-month-wrapper').then(size => {
+					this.width = size.width
+				})
+				// #endif
+			},
+			getMonthRect() {
+				// 获取每个月份数据的尺寸,用于父组件在scroll-view滚动事件中,监听当前滚动到了第几个月份
+				const promiseAllArr = this.months.map((item, index) => this.getMonthRectByPromise(
+					`u-calendar-month-${index}`))
+				// 一次性返回
+				Promise.all(promiseAllArr).then(
+					sizes => {
+						let height = 1
+						const topArr = []
+						for (let i = 0; i < this.months.length; i++) {
+							// 添加到months数组中,供scroll-view滚动事件中,判断当前滚动到哪个月份
+							topArr[i] = height
+							height += sizes[i].height
+						}
+						// 由于微信下,无法通过this.months[i].top的形式(引用类型)去修改父组件的month的top值,所以使用事件形式对外发出
+						this.$emit('updateMonthTop', topArr)
+					})
+			},
+			// 获取每个月份区域的尺寸
+			getMonthRectByPromise(el) {
+				// #ifndef APP-NVUE
+				// $uGetRect为uView自带的节点查询简化方法,详见文档介绍:https://ijry.github.io/uview-plus/js/getRect.html
+				// 组件内部一般用this.$uGetRect,对外的为uni.$u.getRect,二者功能一致,名称不同
+				return new Promise(resolve => {
+					this.$uGetRect(`.${el}`).then(size => {
+						resolve(size)
+					})
+				})
+				// #endif
+
+				// #ifdef APP-NVUE
+				// nvue下,使用dom模块查询元素高度
+				// 返回一个promise,让调用此方法的主体能使用then回调
+				return new Promise(resolve => {
+					dom.getComponentRect(this.$refs[el][0], res => {
+						resolve(res.size)
+					})
+				})
+				// #endif
+			},
+			// 点击某一个日期
+			clickHandler(index1, index2, item) {
+				if (this.readonly) {
+					return;
+				}
+				this.item = item
+				const date = dayjs(item.date).format("YYYY-MM-DD")
+				if (item.disabled) return
+				if (this.isForbid(item)) {
+					uni.showToast({
+						title: this.forbidDaysToast
+					})
+					return
+				}
+				// 对上一次选择的日期数组进行深度克隆
+				let selected = deepClone(this.selected)
+				if (this.mode === 'single') {
+					// 单选情况下,让数组中的元素为当前点击的日期
+					selected = [date]
+				} else if (this.mode === 'multiple') {
+					if (selected.some(item => this.dateSame(item, date))) {
+						// 如果点击的日期已在数组中,则进行移除操作,也就是达到反选的效果
+						const itemIndex = selected.findIndex(item => item === date)
+						selected.splice(itemIndex, 1)
+					} else {
+						// 如果点击的日期不在数组中,且已有的长度小于总可选长度时,则添加到数组中去
+						if (selected.length < this.maxCount) selected.push(date)
+					}
+				} else {
+					// 选择区间形式
+					if (selected.length === 0 || selected.length >= 2) {
+						// 如果原来就为0或者大于2的长度,则当前点击的日期,就是开始日期
+						selected = [date]
+					} else if (selected.length === 1) {
+						// 如果已经选择了开始日期
+						const existsDate = selected[0]
+						// 如果当前选择的日期小于上一次选择的日期,则当前的日期定为开始日期
+						if (dayjs(date).isBefore(existsDate)) {
+							selected = [date]
+						} else if (dayjs(date).isAfter(existsDate)) {
+							// 当前日期减去最大可选的日期天数,如果大于起始时间,则进行提示
+							if(dayjs(dayjs(date).subtract(this.maxRange, 'day')).isAfter(dayjs(selected[0])) && this.showRangePrompt) {
+								if(this.rangePrompt) {
+									toast(this.rangePrompt)
+								} else {
+									toast(t("up.calendar.daysExceed", { days: this.maxRange }))
+								}
+								return
+							}
+							// 如果当前日期大于已有日期,将当前的添加到数组尾部
+							selected.push(date)
+							const startDate = selected[0]
+							const endDate = selected[1]
+							const arr = []
+							let i = 0
+							do {
+								// 将开始和结束日期之间的日期添加到数组中
+								arr.push(dayjs(startDate).add(i, 'day').format("YYYY-MM-DD"))
+								i++
+								// 累加的日期小于结束日期时,继续下一次的循环
+							} while (dayjs(startDate).add(i, 'day').isBefore(dayjs(endDate)))
+							// 为了一次性修改数组,避免computed中多次触发,这里才用arr变量一次性赋值的方式,同时将最后一个日期添加近来
+							arr.push(endDate)
+							selected = arr
+						} else {
+							// 选择区间时,只有一个日期的情况下,且不允许选择起止为同一天的话,不允许选择自己
+							if (selected[0] === date && !this.allowSameDay) return
+							selected.push(date)
+						}
+					}
+				}
+				this.setSelected(selected)
+			},
+			// 设置默认日期
+			setDefaultDate() {
+				if (!this.defaultDate) {
+					// 如果没有设置默认日期,则将当天日期设置为默认选中的日期
+					const selected = [dayjs().format("YYYY-MM-DD")]
+					return this.setSelected(selected, false)
+				}
+				let defaultDate = []
+				const minDate = this.minDate || dayjs().format("YYYY-MM-DD")
+				const maxDate = this.maxDate || dayjs(minDate).add(this.maxMonth - 1, 'month').format("YYYY-MM-DD")
+				if (this.mode === 'single') {
+					// 单选模式,可以是字符串或数组,Date对象等
+					if (!test.array(this.defaultDate)) {
+						defaultDate = [dayjs(this.defaultDate).format("YYYY-MM-DD")]
+					} else {
+						defaultDate = [this.defaultDate[0]]
+					}
+				} else {
+					// 如果为非数组,则不执行
+					if (!test.array(this.defaultDate)) return
+					defaultDate = this.defaultDate
+				}
+				// 过滤用户传递的默认数组,取出只在可允许最大值与最小值之间的元素
+				defaultDate = defaultDate.filter(item => {
+					return dayjs(item).isAfter(dayjs(minDate).subtract(1, 'day')) && dayjs(item).isBefore(dayjs(
+						maxDate).add(1, 'day'))
+				})
+				this.setSelected(defaultDate, false)
+			},
+			setSelected(selected, event = true) {
+				this.selected = selected
+				event && this.$emit('monthSelected', this.selected,'tap')
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+
+	.u-calendar-month-wrapper {
+		margin-top: 4px;
+	}
+
+	.u-calendar-month {
+
+		&__title {
+			display: flex;
+			flex-direction: column;
+			font-size: 14px;
+			line-height: 42px;
+			height: 42px;
+			color: $u-main-color;
+			text-align: center;
+			font-weight: bold;
+		}
+
+		&__days {
+			position: relative;
+			@include flex;
+			flex-wrap: wrap;
+
+			&__month-mark-wrapper {
+				position: absolute;
+				top: 0;
+				bottom: 0;
+				left: 0;
+				right: 0;
+				@include flex;
+				justify-content: center;
+				align-items: center;
+
+				&__text {
+					font-size: 155px;
+					color: rgba(231, 232, 234, 0.83);
+				}
+			}
+
+			&__day {
+				@include flex;
+				padding: 2px;
+				/* #ifndef APP-NVUE */
+				// vue下使用css进行宽度计算,因为某些安卓机会无法进行js获取父元素宽度进行计算得出,会有偏移
+				width: calc(100% / 7);
+				box-sizing: border-box;
+				/* #endif */
+
+				&__select {
+					flex: 1;
+					@include flex;
+					align-items: center;
+					justify-content: center;
+					position: relative;
+
+					&__dot {
+						width: 7px;
+						height: 7px;
+						border-radius: 100px;
+						background-color: $u-error;
+						position: absolute;
+						top: 12px;
+						right: 7px;
+					}
+
+					&__buttom-info {
+						color: $u-content-color;
+						text-align: center;
+						position: absolute;
+						bottom: 5px;
+						font-size: 10px;
+						text-align: center;
+						left: 0;
+						right: 0;
+
+						&--selected {
+							color: #ffffff;
+						}
+
+						&--disabled {
+							color: #cacbcd;
+						}
+					}
+
+					&__info {
+						text-align: center;
+						font-size: 16px;
+
+						&--selected {
+							color: #ffffff;
+						}
+
+						&--disabled {
+							color: #cacbcd;
+						}
+					}
+
+					&--selected {
+						background-color: $u-primary;
+						@include flex;
+						justify-content: center;
+						align-items: center;
+						flex: 1;
+						border-radius: 3px;
+					}
+
+					&--range-selected {
+						opacity: 0.3;
+						border-radius: 0;
+					}
+
+					&--range-start-selected {
+						border-top-right-radius: 0;
+						border-bottom-right-radius: 0;
+					}
+
+					&--range-end-selected {
+						border-top-left-radius: 0;
+						border-bottom-left-radius: 0;
+					}
+				}
+			}
+		}
+	}
+</style>

+ 169 - 0
uni_modules/uview-plus/components/u-calendar/props.js

@@ -0,0 +1,169 @@
+import { defineMixin } from '../../libs/vue'
+import defProps from '../../libs/config/props.js'
+
+export const props = defineMixin({
+    props: {
+        // 日历顶部标题
+        title: {
+            type: String,
+            default: () => defProps.calendar.title
+        },
+        // 是否显示标题
+        showTitle: {
+            type: Boolean,
+            default: () => defProps.calendar.showTitle
+        },
+        // 是否显示副标题
+        showSubtitle: {
+            type: Boolean,
+            default: () => defProps.calendar.showSubtitle
+        },
+        // 日期类型选择,single-选择单个日期,multiple-可以选择多个日期,range-选择日期范围
+        mode: {
+            type: String,
+            default: () => defProps.calendar.mode
+        },
+        // mode=range时,第一个日期底部的提示文字
+        startText: {
+            type: String,
+            default: () => defProps.calendar.startText
+        },
+        // mode=range时,最后一个日期底部的提示文字
+        endText: {
+            type: String,
+            default: () => defProps.calendar.endText
+        },
+        // 自定义列表
+        customList: {
+            type: Array,
+            default: () => defProps.calendar.customList
+        },
+        // 主题色,对底部按钮和选中日期有效
+        color: {
+            type: String,
+            default: () => defProps.calendar.color
+        },
+        // 最小的可选日期
+        minDate: {
+            type: [String, Number],
+            default: () => defProps.calendar.minDate
+        },
+        // 最大可选日期
+        maxDate: {
+            type: [String, Number],
+            default: () => defProps.calendar.maxDate
+        },
+        // 默认选中的日期,mode为multiple或range是必须为数组格式
+        defaultDate: {
+            type: [Array, String, Date, null],
+            default: () => defProps.calendar.defaultDate
+        },
+        // mode=multiple时,最多可选多少个日期
+        maxCount: {
+            type: [String, Number],
+            default: () => defProps.calendar.maxCount
+        },
+        // 日期行高
+        rowHeight: {
+            type: [String, Number],
+            default: () => defProps.calendar.rowHeight
+        },
+        // 日期格式化函数
+        formatter: {
+            type: [Function, null],
+            default: () => defProps.calendar.formatter
+        },
+        // 是否显示农历
+        showLunar: {
+            type: Boolean,
+            default: () => defProps.calendar.showLunar
+        },
+        // 是否显示月份背景色
+        showMark: {
+            type: Boolean,
+            default: () => defProps.calendar.showMark
+        },
+        // 确定按钮的文字
+        confirmText: {
+            type: String,
+            default: () => defProps.calendar.confirmText
+        },
+        // 确认按钮处于禁用状态时的文字
+        confirmDisabledText: {
+            type: String,
+            default: () => defProps.calendar.confirmDisabledText
+        },
+        // 是否显示日历弹窗
+        show: {
+            type: Boolean,
+            default: () => defProps.calendar.show
+        },
+        // 是否允许点击遮罩关闭日历
+        closeOnClickOverlay: {
+            type: Boolean,
+            default: () => defProps.calendar.closeOnClickOverlay
+        },
+        // 是否为只读状态,只读状态下禁止选择日期
+        readonly: {
+            type: Boolean,
+            default: () => defProps.calendar.readonly
+        },
+        // 	是否展示确认按钮
+        showConfirm: {
+            type: Boolean,
+            default: () => defProps.calendar.showConfirm
+        },
+        // 日期区间最多可选天数,默认无限制,mode = range时有效
+        maxRange: {
+            type: [Number, String],
+            default: () => defProps.calendar.maxRange
+        },
+        // 范围选择超过最多可选天数时的提示文案,mode = range时有效
+        rangePrompt: {
+            type: String,
+            default: () => defProps.calendar.rangePrompt
+        },
+        // 范围选择超过最多可选天数时,是否展示提示文案,mode = range时有效
+        showRangePrompt: {
+            type: Boolean,
+            default: () => defProps.calendar.showRangePrompt
+        },
+        // 是否允许日期范围的起止时间为同一天,mode = range时有效
+        allowSameDay: {
+            type: Boolean,
+            default: () => defProps.calendar.allowSameDay
+        },
+		// 圆角值
+		round: {
+		    type: [Boolean, String, Number],
+		    default: () => defProps.calendar.round
+		},
+		// 最多展示月份数量
+		monthNum: {
+			type: [Number, String],
+			default: 3
+		},
+        // 星期文案
+        weekText: {
+			type: Array,
+			default: defProps.calendar.weekText
+		},
+        forbidDays: {
+			type: Array,
+			default: defProps.calendar.forbidDays
+		},
+        forbidDaysToast:{
+			type: String,
+			default: defProps.calendar.forbidDaysToast
+		},
+        monthFormat:{
+			type: String,
+			default: defProps.calendar.monthFormat
+		},
+        // 是否页面内展示
+        pageInline:{
+			type: Boolean,
+			default: defProps.calendar.pageInline
+		}
+    }
+})

+ 421 - 0
uni_modules/uview-plus/components/u-calendar/u-calendar.vue

@@ -0,0 +1,421 @@
+<template>
+	<u-popup
+		:show="show"
+		mode="bottom"
+		:closeable="!pageInline"
+		@close="close"
+		:round="round"
+		:pageInline="pageInline"
+		:closeOnClickOverlay="closeOnClickOverlay"
+	>
+		<view class="u-calendar">
+			<uHeader
+				:title="title"
+				:subtitle="subtitle"
+				:showSubtitle="showSubtitle"
+				:showTitle="showTitle"
+				:weekText="weekText"
+			></uHeader>
+			<scroll-view
+				:style="{
+                    height: addUnit(listHeight, 'px')
+                }"
+				scroll-y
+				@scroll="onScroll"
+				:scroll-top="scrollTop"
+				:scrollIntoView="scrollIntoView"
+			>
+				<uMonth
+					:color="color"
+					:rowHeight="rowHeight"
+					:showMark="showMark"
+					:months="months"
+					:mode="mode"
+					:maxCount="maxCount"
+					:startText="startText"
+					:endText="endText"
+					:defaultDate="defaultDate"
+					:minDate="innerMinDate"
+					:maxDate="innerMaxDate"
+					:maxMonth="monthNum"
+					:readonly="readonly"
+					:maxRange="maxRange"
+					:rangePrompt="rangePrompt"
+					:showRangePrompt="showRangePrompt"
+					:allowSameDay="allowSameDay"
+					:forbidDays="forbidDays"
+					:forbidDaysToast="forbidDaysToast"
+					:monthFormat="monthFormat"
+					ref="month"
+					@monthSelected="monthSelected"
+					@updateMonthTop="updateMonthTop"
+				></uMonth>
+			</scroll-view>
+			<slot name="footer" v-if="showConfirm">
+				<view class="u-calendar__confirm">
+					<u-button
+						shape="circle"
+						:text="
+                            buttonDisabled ? confirmDisabledText : confirmText
+                        "
+						:color="color"
+						@click="confirm"
+						:disabled="buttonDisabled"
+					></u-button>
+				</view>
+			</slot>
+		</view>
+	</u-popup>
+</template>
+
+<script>
+import uHeader from './header.vue'
+import uMonth from './month.vue'
+import { props } from './props.js'
+import util from './util.js'
+import dayjs from '../u-datetime-picker/dayjs.esm.min.js';
+import Calendar from '../../libs/util/calendar.js'
+import { mpMixin } from '../../libs/mixin/mpMixin.js'
+import { mixin } from '../../libs/mixin/mixin.js'
+import { addUnit, getPx, range, error, padZero } from '../../libs/function/index';
+import test from '../../libs/function/test';
+/**
+ * Calendar 日历
+ * @description  此组件用于单个选择日期,范围选择日期等,日历被包裹在底部弹起的容器中.
+ * @tutorial https://ijry.github.io/uview-plus/components/calendar.html
+ *
+ * @property {String}				title				标题内容 (默认 日期选择 )
+ * @property {Boolean}				showTitle			是否显示标题  (默认 true )
+ * @property {Boolean}				showSubtitle		是否显示副标题	(默认 true )
+ * @property {String}				mode				日期类型选择  single-选择单个日期,multiple-可以选择多个日期,range-选择日期范围 ( 默认 'single' )
+ * @property {String}				startText			mode=range时,第一个日期底部的提示文字  (默认 '开始' )
+ * @property {String}				endText				mode=range时,最后一个日期底部的提示文字 (默认 '结束' )
+ * @property {Array}				customList			自定义列表
+ * @property {String}				color				主题色,对底部按钮和选中日期有效  (默认 ‘#3c9cff' )
+ * @property {String | Number}		minDate				最小的可选日期	 (默认 0 )
+ * @property {String | Number}		maxDate				最大可选日期  (默认 0 )
+ * @property {Array | String| Date}	defaultDate			默认选中的日期,mode为multiple或range是必须为数组格式
+ * @property {String | Number}		maxCount			mode=multiple时,最多可选多少个日期  (默认 	Number.MAX_SAFE_INTEGER  )
+ * @property {String | Number}		rowHeight			日期行高 (默认 56 )
+ * @property {Function}				formatter			日期格式化函数
+ * @property {Boolean}				showLunar			是否显示农历  (默认 false )
+ * @property {Boolean}				showMark			是否显示月份背景色 (默认 true )
+ * @property {String}				confirmText			确定按钮的文字 (默认 '确定' )
+ * @property {String}				confirmDisabledText	确认按钮处于禁用状态时的文字 (默认 '确定' )
+ * @property {Boolean}				show				是否显示日历弹窗 (默认 false )
+ * @property {Boolean}				closeOnClickOverlay	是否允许点击遮罩关闭日历 (默认 false )
+ * @property {Boolean}				readonly	        是否为只读状态,只读状态下禁止选择日期 (默认 false )
+ * @property {String | Number}		maxRange	        日期区间最多可选天数,默认无限制,mode = range时有效
+ * @property {String}				rangePrompt	        范围选择超过最多可选天数时的提示文案,mode = range时有效
+ * @property {Boolean}				showRangePrompt	    范围选择超过最多可选天数时,是否展示提示文案,mode = range时有效 (默认 true )
+ * @property {Boolean}				allowSameDay	    是否允许日期范围的起止时间为同一天,mode = range时有效 (默认 false )
+ * @property {Number|String}	    round				圆角值,默认无圆角  (默认 0 )
+ * @property {Number|String}	    monthNum			最多展示的月份数量  (默认 3 )
+ * @property {Array}	            weekText			星期文案  (默认 ['一', '二', '三', '四', '五', '六', '日'] )
+ *
+ * @event {Function()} confirm 		点击确定按钮时触发		选择日期相关的返回参数
+ * @event {Function()} close 		日历关闭时触发			可定义页面关闭时的回调事件
+ * @example <u-calendar  :defaultDate="defaultDateMultiple" :show="show" mode="multiple" @confirm="confirm">
+	</u-calendar>
+ * */
+export default {
+	name: 'u-calendar',
+	mixins: [mpMixin, mixin, props],
+	components: {
+		uHeader,
+		uMonth
+	},
+	data() {
+		return {
+			// 需要显示的月份的数组
+			months: [],
+			// 在月份滚动区域中,当前视图中月份的index索引
+			monthIndex: 0,
+			// 月份滚动区域的高度
+			listHeight: 0,
+			// month组件中选择的日期数组
+			selected: [],
+			scrollIntoView: '',
+			scrollIntoViewScroll: '',
+			scrollTop:0,
+			// 过滤处理方法
+			innerFormatter: (value) => value
+		}
+	},
+	watch: {
+		scrollIntoView: {
+			immediate: true,
+			handler(n) {
+				// console.log('scrollIntoView', n)
+			}
+		},
+		selectedChange: {
+			immediate: true,
+			handler(n) {
+				this.setMonth()
+			}
+		},
+		// 打开弹窗时,设置月份数据
+		show: {
+			immediate: true,
+			handler(n) {
+				if (n) {
+					this.setMonth()
+				} else {
+					// 关闭时重置scrollIntoView,否则会出现二次打开日历,当前月份数据显示不正确。
+					// scrollIntoView需要有一个值变动过程,才会产生作用。
+					this.scrollIntoView = ''
+				}
+			}
+		}
+	},
+	computed: {
+		// 由于maxDate和minDate可以为字符串(2021-10-10),或者数值(时间戳),但是dayjs如果接受字符串形式的时间戳会有问题,这里进行处理
+		innerMaxDate() {
+			return test.number(this.maxDate)
+				? Number(this.maxDate)
+				: this.maxDate
+		},
+		innerMinDate() {
+			return test.number(this.minDate)
+				? Number(this.minDate)
+				: this.minDate
+		},
+		// 多个条件的变化,会引起选中日期的变化,这里统一管理监听
+		selectedChange() {
+			return [this.innerMinDate, this.innerMaxDate, this.defaultDate]
+		},
+		subtitle() {
+			// 初始化时,this.months为空数组,所以需要特别判断处理
+			if (this.months.length) {
+				if (uni.getLocale() == 'zh-Hans' || uni.getLocale() == 'zh-Hant') {
+					return this.months[this.monthIndex].year + '年' + (this.months[this.monthIndex].month < 10 ? '0' + this.months[this.monthIndex].month : this.months[this.monthIndex].month) + '月'
+				} else {
+					return (this.months[this.monthIndex].month < 10 ? '0' + this.months[this.monthIndex].month : this.months[this.monthIndex].month) + '/' + this.months[this.monthIndex].year
+				}
+			} else {
+				return ''
+			}
+		},
+		buttonDisabled() {
+			// 如果为range类型,且选择的日期个数不足1个时,让底部的按钮出于disabled状态
+			if (this.mode === 'range') {
+				if (this.selected.length <= 1) {
+					return true
+				} else {
+					return false
+				}
+			} else {
+				return false
+			}
+		}
+	},
+	mounted() {
+		this.start = Date.now()
+		this.init()
+	},
+	emits: ["confirm", "close"],
+	methods: {
+		addUnit,
+		// 在微信小程序中,不支持将函数当做props参数,故只能通过ref形式调用
+		setFormatter(e) {
+			this.innerFormatter = e
+		},
+		// month组件内部选择日期后,通过事件通知给父组件
+		monthSelected(e,scene ='init') {
+			this.selected = e
+			if (!this.showConfirm) {
+				// 在不需要确认按钮的情况下,如果为单选,或者范围多选且已选长度大于2,则直接进行返还
+				if (
+					this.mode === 'multiple' ||
+					this.mode === 'single' ||
+					(this.mode === 'range' && this.selected.length >= 2)
+				) {
+				   if( scene === 'init'){
+					 return
+				   }
+				   if( scene === 'tap') {
+					 this.$emit('confirm', this.selected)
+				   }
+				}
+			}
+		},
+		init() {
+			// 校验maxDate,不能小于minDate。
+			if (
+				this.innerMaxDate &&
+                this.innerMinDate &&
+				new Date(this.innerMaxDate).getTime() < new Date(this.innerMinDate).getTime()
+			) {
+				return error('maxDate不能小于minDate时间')
+			}
+			// 滚动区域的高度
+			let bottomPadding = 0;
+			if (this.pageInline) {
+				bottomPadding = 0
+			} else {
+				bottomPadding = 30
+			}
+			this.listHeight = this.rowHeight * 5 + bottomPadding
+			this.setMonth()
+		},
+		close() {
+			this.$emit('close')
+		},
+		// 点击确定按钮
+		confirm() {
+			if (!this.buttonDisabled) {
+				this.$emit('confirm', this.selected)
+			}
+		},
+		// 获得两个日期之间的月份数
+		getMonths(minDate, maxDate) {
+			const minYear = dayjs(minDate).year()
+			const minMonth = dayjs(minDate).month() + 1
+			const maxYear = dayjs(maxDate).year()
+			const maxMonth = dayjs(maxDate).month() + 1
+			return (maxYear - minYear) * 12 + (maxMonth - minMonth) + 1
+		},
+		// 设置月份数据
+		setMonth() {
+			// 最小日期的毫秒数
+			const minDate = this.innerMinDate || dayjs().valueOf()
+			// 如果没有指定最大日期,则往后推3个月
+			const maxDate =
+				this.innerMaxDate ||
+				dayjs(minDate)
+					.add(this.monthNum - 1, 'month')
+					.valueOf()
+			// 最大最小月份之间的共有多少个月份,
+			const months = range(
+				1,
+				this.monthNum,
+				this.getMonths(minDate, maxDate)
+			)
+			// 先清空数组
+			this.months = []
+			for (let i = 0; i < months; i++) {
+				this.months.push({
+					date: new Array(
+						dayjs(minDate).add(i, 'month').daysInMonth()
+					)
+						.fill(1)
+						.map((item, index) => {
+							// 日期,取值1-31
+							let day = index + 1
+							// 星期,0-6,0为周日
+							const week = dayjs(minDate)
+								.add(i, 'month')
+								.date(day)
+								.day()
+							const date = dayjs(minDate)
+								.add(i, 'month')
+								.date(day)
+								.format('YYYY-MM-DD')
+							let bottomInfo = ''
+							if (this.showLunar) {
+								// 将日期转为农历格式
+								const lunar = Calendar.solar2lunar(
+									dayjs(date).year(),
+									dayjs(date).month() + 1,
+									dayjs(date).date()
+								)
+								bottomInfo = lunar.IDayCn
+							}
+							let config = {
+								day,
+								week,
+								// 小于最小允许的日期,或者大于最大的日期,则设置为disabled状态
+								disabled:
+									dayjs(date).isBefore(
+										dayjs(minDate).format('YYYY-MM-DD')
+									) ||
+									dayjs(date).isAfter(
+										dayjs(maxDate).format('YYYY-MM-DD')
+									),
+								// 返回一个日期对象,供外部的formatter获取当前日期的年月日等信息,进行加工处理
+								date: new Date(date),
+								bottomInfo,
+								dot: false,
+								month:
+									dayjs(minDate).add(i, 'month').month() + 1
+							}
+							const formatter =
+								this.formatter || this.innerFormatter
+							return formatter(config)
+						}),
+					// 当前所属的月份
+					month: dayjs(minDate).add(i, 'month').month() + 1,
+					// 当前年份
+					year: dayjs(minDate).add(i, 'month').year()
+				})
+			}
+		},
+		// 滚动到默认设置的月份
+		scrollIntoDefaultMonth(selected) {
+			// 查询默认日期在可选列表的下标
+			const _index = this.months.findIndex(({
+				  year,
+				  month
+			  }) => {
+				month = padZero(month)
+				return `${year}-${month}` === selected
+			})
+			if (_index !== -1) {
+				// #ifndef MP-WEIXIN
+				this.$nextTick(() => {
+					this.scrollIntoView = `month-${_index}`
+					this.scrollIntoViewScroll = this.scrollIntoView
+				})
+				// #endif
+				// #ifdef MP-WEIXIN
+				this.scrollTop = this.months[_index].top || 0;
+				// #endif
+			}
+		},
+		// scroll-view滚动监听
+		onScroll(event) {
+			// 不允许小于0的滚动值,如果scroll-view到顶了,继续下拉,会出现负数值
+			const scrollTop = Math.max(0, event.detail.scrollTop)
+			// 将当前滚动条数值,除以滚动区域的高度,可以得出当前滚动到了哪一个月份的索引
+			for (let i = 0; i < this.months.length; i++) {
+				if (scrollTop >= (this.months[i].top || this.listHeight)) {
+					this.monthIndex = i
+					this.scrollIntoViewScroll = `month-${i}`
+				}
+			}
+		},
+		// 更新月份的top值
+		updateMonthTop(topArr = []) {
+			// 设置对应月份的top值,用于onScroll方法更新月份
+			topArr.map((item, index) => {
+				this.months[index].top = item
+			})
+
+			// 获取默认日期的下标
+			if (!this.defaultDate) {
+				// 如果没有设置默认日期,则将当天日期设置为默认选中的日期
+				const selected = dayjs().format("YYYY-MM")
+				this.scrollIntoDefaultMonth(selected)
+				return
+			}
+			let selected = dayjs().format("YYYY-MM");
+			// 单选模式,可以是字符串或数组,Date对象等
+			if (!test.array(this.defaultDate)) {
+				selected = dayjs(this.defaultDate).format("YYYY-MM")
+			} else {
+				selected = dayjs(this.defaultDate[0]).format("YYYY-MM");
+			}
+			this.scrollIntoDefaultMonth(selected)
+		}
+	}
+}
+</script>
+
+<style lang="scss" scoped>
+.u-calendar {
+	&__confirm {
+		padding: 7px 18px;
+	}
+}
+</style>

+ 86 - 0
uni_modules/uview-plus/components/u-calendar/util.js

@@ -0,0 +1,86 @@
+import dayjs from '../u-datetime-picker/dayjs.esm.min.js';
+export default {
+    methods: {
+        // 设置月份数据
+        setMonth() {
+            // 月初是周几
+            const day = dayjs(this.date).date(1).day()
+            const start = day == 0 ? 6 : day - 1
+
+            // 本月天数
+            const days = dayjs(this.date).endOf('month').format('D')
+
+            // 上个月天数
+            const prevDays = dayjs(this.date).endOf('month').subtract(1, 'month').format('D')
+
+            // 日期数据
+            const arr = []
+            // 清空表格
+            this.month = []
+
+            // 添加上月数据
+            arr.push(
+                ...new Array(start).fill(1).map((e, i) => {
+                    const day = prevDays - start + i + 1
+
+                    return {
+                        value: day,
+                        disabled: true,
+                        date: dayjs(this.date).subtract(1, 'month').date(day).format('YYYY-MM-DD')
+                    }
+                })
+            )
+
+            // 添加本月数据
+            arr.push(
+                ...new Array(days - 0).fill(1).map((e, i) => {
+                    const day = i + 1
+
+                    return {
+                        value: day,
+                        date: dayjs(this.date).date(day).format('YYYY-MM-DD')
+                    }
+                })
+            )
+
+            // 添加下个月
+            arr.push(
+                ...new Array(42 - days - start).fill(1).map((e, i) => {
+                    const day = i + 1
+
+                    return {
+                        value: day,
+                        disabled: true,
+                        date: dayjs(this.date).add(1, 'month').date(day).format('YYYY-MM-DD')
+                    }
+                })
+            )
+
+            // 分割数组
+            for (let n = 0; n < arr.length; n += 7) {
+                this.month.push(
+                    arr.slice(n, n + 7).map((e, i) => {
+                        e.index = i + n
+
+                        // 自定义信息
+                        const custom = this.customList.find((c) => c.date == e.date)
+
+                        // 农历
+                        if (this.lunar) {
+                            const {
+                                IDayCn,
+                                IMonthCn
+                            } = this.getLunar(e.date)
+                            e.lunar = IDayCn == '初一' ? IMonthCn : IDayCn
+                        }
+
+                        return {
+                            ...e,
+                            ...custom
+                        }
+                    })
+                )
+            }
+        }
+    }
+}

+ 15 - 0
uni_modules/uview-plus/components/u-car-keyboard/carKeyboard.js

@@ -0,0 +1,15 @@
+/*
+ * @Author       : LQ
+ * @Description  :
+ * @version      : 1.0
+ * @Date         : 2021-08-20 16:44:21
+ * @LastAuthor   : LQ
+ * @lastTime     : 2021-08-20 16:53:20
+ * @FilePath     : /u-view2.0/uview-ui/libs/config/props/carKeyboard.js
+ */
+export default {
+    // 车牌号键盘
+    carKeyboard: {
+        random: false
+    }
+}

+ 17 - 0
uni_modules/uview-plus/components/u-car-keyboard/props.js

@@ -0,0 +1,17 @@
+import { defineMixin } from '../../libs/vue'
+import defProps from '../../libs/config/props.js'
+
+export const props = defineMixin({
+    props: {
+        // 是否打乱键盘按键的顺序
+        random: {
+            type: Boolean,
+            default: false
+        },
+        // 输入一个中文后,是否自动切换到英文
+        autoChange: {
+            type: Boolean,
+            default: false
+        }
+    }
+})

+ 314 - 0
uni_modules/uview-plus/components/u-car-keyboard/u-car-keyboard.vue

@@ -0,0 +1,314 @@
+<template>
+	<view
+		class="u-keyboard"
+		@touchmove.stop.prevent="noop"
+	>
+		<view
+			v-for="(group, i) in abc ? engKeyBoardList : areaList"
+			:key="i"
+			class="u-keyboard__button"
+			:index="i"
+			:class="[i + 1 === 4 && 'u-keyboard__button--center']"
+		>
+			<view
+				v-if="i === 3"
+				class="u-keyboard__button__inner-wrapper"
+			>
+				<view
+					class="u-keyboard__button__inner-wrapper__left"
+					hover-class="u-hover-class"
+					:hover-stay-time="200"
+					@tap="changeCarInputMode"
+				>
+					<text
+						class="u-keyboard__button__inner-wrapper__left__lang"
+						:class="[!abc && 'u-keyboard__button__inner-wrapper__left__lang--active']"
+					>中</text>
+					<text class="u-keyboard__button__inner-wrapper__left__line">/</text>
+					<text
+						class="u-keyboard__button__inner-wrapper__left__lang"
+						:class="[abc && 'u-keyboard__button__inner-wrapper__left__lang--active']"
+					>英</text>
+				</view>
+			</view>
+			<view
+				class="u-keyboard__button__inner-wrapper"
+				v-for="(item, j) in group"
+				:key="j"
+			>
+				<view
+					class="u-keyboard__button__inner-wrapper__inner"
+					:hover-stay-time="200"
+					@tap="carInputClick(i, j)"
+					hover-class="u-hover-class"
+				>
+					<text class="u-keyboard__button__inner-wrapper__inner__text">{{ item }}</text>
+				</view>
+			</view>
+			<view
+				v-if="i === 3"
+				@touchstart="backspaceClick"
+				@touchend="clearTimer"
+				class="u-keyboard__button__inner-wrapper"
+			>
+				<view
+					class="u-keyboard__button__inner-wrapper__right"
+					hover-class="u-hover-class"
+					:hover-stay-time="200"
+				>
+					<up-icon
+						size="28"
+						name="backspace"
+						color="#303133"
+					></up-icon>
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	import { props } from './props';
+	import { mpMixin } from '../../libs/mixin/mpMixin';
+	import { mixin } from '../../libs/mixin/mixin';
+	import { randomArray, sleep } from '../../libs/function/index';
+	/**
+	 * keyboard 键盘组件
+	 * @description 此为uview-plus自定义的键盘面板,内含了数字键盘,车牌号键,身份证号键盘3种模式,都有可以打乱按键顺序的选项。
+	 * @tutorial https://uview-plus.jiangruyi.com/components/keyboard.html
+	 * @property {Boolean} random 是否打乱键盘的顺序
+	 * @event {Function} change 点击键盘触发
+	 * @event {Function} backspace 点击退格键触发
+	 * @example <u-keyboard ref="uKeyboard" mode="car" v-model="show"></u-keyboard>
+	 */
+	export default {
+		name: "u-car-keyboard",
+		mixins: [mpMixin, mixin, props],
+		data() {
+			return {
+				// 车牌输入时,abc=true为输入车牌号码,bac=false为输入省份中文简称
+				abc: false
+			};
+		},
+		computed: {
+			areaList() {
+				let data = [
+					'京',
+					'沪',
+					'粤',
+					'津',
+					'冀',
+					'豫',
+					'云',
+					'辽',
+					'黑',
+					'湘',
+					'皖',
+					'鲁',
+					'苏',
+					'浙',
+					'赣',
+					'鄂',
+					'桂',
+					'甘',
+					'晋',
+					'陕',
+					'蒙',
+					'吉',
+					'闽',
+					'贵',
+					'渝',
+					'川',
+					'青',
+					'琼',
+					'宁',
+					'挂',
+					'藏',
+					'港',
+					'澳',
+					'新',
+					'使',
+					'学'
+				];
+				let tmp = [];
+				// 打乱顺序
+				if (this.random) data = randomArray(data);
+				// 切割成二维数组
+				tmp[0] = data.slice(0, 10);
+				tmp[1] = data.slice(10, 20);
+				tmp[2] = data.slice(20, 30);
+				tmp[3] = data.slice(30, 36);
+				return tmp;
+			},
+			engKeyBoardList() {
+				let data = [
+					1,
+					2,
+					3,
+					4,
+					5,
+					6,
+					7,
+					8,
+					9,
+					0,
+					'Q',
+					'W',
+					'E',
+					'R',
+					'T',
+					'Y',
+					'U',
+					'I',
+					'O',
+					'P',
+					'A',
+					'S',
+					'D',
+					'F',
+					'G',
+					'H',
+					'J',
+					'K',
+					'L',
+					'Z',
+					'X',
+					'C',
+					'V',
+					'B',
+					'N',
+					'M'
+				];
+				let tmp = [];
+				if (this.random) data = randomArray(data);
+				tmp[0] = data.slice(0, 10);
+				tmp[1] = data.slice(10, 20);
+				tmp[2] = data.slice(20, 30);
+				tmp[3] = data.slice(30, 36);
+				return tmp;
+			}
+		},
+		emits: ["change", "backspace"],
+		methods: {
+			// 点击键盘按钮
+			carInputClick(i, j) {
+				let value = '';
+				// 不同模式,获取不同数组的值
+				if (this.abc) value = this.engKeyBoardList[i][j];
+				else value = this.areaList[i][j];
+				// 如果允许自动切换,则将中文状态切换为英文
+				if (!this.abc && this.autoChange) sleep(200).then(() => this.abc = true)
+				this.$emit('change', value);
+			},
+			// 修改汽车牌键盘的输入模式,中文|英文
+			changeCarInputMode() {
+				this.abc = !this.abc;
+			},
+			// 点击退格键
+			backspaceClick() {
+				this.$emit('backspace');
+				clearInterval(this.timer); //再次清空定时器,防止重复注册定时器
+				this.timer = null;
+				this.timer = setInterval(() => {
+					this.$emit('backspace');
+				}, 250);
+			},
+			clearTimer() {
+				clearInterval(this.timer);
+				this.timer = null;
+			},
+		}
+	};
+</script>
+
+<style lang="scss" scoped>
+	$u-car-keyboard-background-color: rgb(224, 228, 230) !default;
+	$u-car-keyboard-padding:6px 0 6px !default;
+	$u-car-keyboard-button-inner-width:64rpx !default;
+	$u-car-keyboard-button-inner-background-color:#FFFFFF !default;
+	$u-car-keyboard-button-height:80rpx !default;
+	$u-car-keyboard-button-inner-box-shadow:0 1px 0px #999992 !default;
+	$u-car-keyboard-button-border-radius:4px !default;
+	$u-car-keyboard-button-inner-margin:8rpx 5rpx !default;
+	$u-car-keyboard-button-text-font-size:16px !default;
+	$u-car-keyboard-button-text-color:$u-main-color !default;
+	$u-car-keyboard-center-inner-margin: 0 4rpx !default;
+	$u-car-keyboard-special-button-width:134rpx !default;
+	$u-car-keyboard-lang-font-size:16px !default;
+	$u-car-keyboard-lang-color:$u-main-color !default;
+	$u-car-keyboard-active-color:$u-primary !default;
+	$u-car-keyboard-line-font-size:15px !default;
+	$u-car-keyboard-line-color:$u-main-color !default;
+	$u-car-keyboard-line-margin:0 1px !default;
+	$u-car-keyboard-u-hover-class-background-color:#BBBCC6 !default;
+
+	.u-keyboard {
+		@include flex(column);
+		justify-content: space-around;
+		background-color: $u-car-keyboard-background-color;
+		align-items: stretch;
+		padding: $u-car-keyboard-padding;
+
+		&__button {
+			@include flex;
+			justify-content: center;
+			flex: 1;
+			/* #ifndef APP-NVUE */
+			/* #endif */
+
+			&__inner-wrapper {
+				box-shadow: $u-car-keyboard-button-inner-box-shadow;
+				margin: $u-car-keyboard-button-inner-margin;
+				border-radius: $u-car-keyboard-button-border-radius;
+
+				&__inner {
+					@include flex;
+					justify-content: center;
+					align-items: center;
+					width: $u-car-keyboard-button-inner-width;
+					background-color: $u-car-keyboard-button-inner-background-color;
+					height: $u-car-keyboard-button-height;
+					border-radius: $u-car-keyboard-button-border-radius;
+
+					&__text {
+						font-size: $u-car-keyboard-button-text-font-size;
+						color: $u-car-keyboard-button-text-color;
+					}
+				}
+
+				&__left,
+				&__right {
+					border-radius: $u-car-keyboard-button-border-radius;
+					width: $u-car-keyboard-special-button-width;
+					height: $u-car-keyboard-button-height;
+					background-color: $u-car-keyboard-u-hover-class-background-color;
+					@include flex;
+					justify-content: center;
+					align-items: center;
+					box-shadow: $u-car-keyboard-button-inner-box-shadow;
+				}
+
+				&__left {
+					&__line {
+						font-size: $u-car-keyboard-line-font-size;
+						color: $u-car-keyboard-line-color;
+						margin: $u-car-keyboard-line-margin;
+					}
+
+					&__lang {
+						font-size: $u-car-keyboard-lang-font-size;
+						color: $u-car-keyboard-lang-color;
+
+						&--active {
+							color: $u-car-keyboard-active-color;
+						}
+					}
+				}
+			}
+		}
+	}
+
+	.u-hover-class {
+		background-color: $u-car-keyboard-u-hover-class-background-color;
+	}
+</style>

+ 40 - 0
uni_modules/uview-plus/components/u-card/card.js

@@ -0,0 +1,40 @@
+/*
+ * @Author       : jry
+ * @Description  :
+ * @version      : 3.0
+ * @Date         : 2025-04-26 16:37:21
+ * @LastAuthor   : jry
+ * @lastTime     : 2025-04-26 16:37:21
+ * @FilePath     : /uview-plus/libs/config/props/card.js
+ */
+export default {
+	// card组件的props
+	card: {
+		full: false,
+		title: '',
+		titleColor: '#303133',
+		titleSize: '15px',
+		subTitle: '',
+		subTitleColor: '#909399',
+		subTitleSize: '13px',
+		border: true,
+		index: '',
+		margin: '15px',
+		borderRadius: '8px',
+		headStyle: {},
+		bodyStyle: {},
+		footStyle: {},
+		headBorderBottom: true,
+		footBorderTop: true,
+		thumb: '',
+		thumbWidth: '30px',
+		thumbCircle: false,
+		padding: '15px',
+		paddingHead: '',
+        paddingBody: '',
+        paddingFoot: '',
+        showHead: true,
+        showFoot: true,
+        boxShadow: 'none'
+	}
+}

+ 134 - 0
uni_modules/uview-plus/components/u-card/props.js

@@ -0,0 +1,134 @@
+import { defineMixin } from '../../libs/vue'
+import defProps from '../../libs/config/props.js'
+
+export const propsCard = defineMixin({
+    props: {
+        // 与屏幕两侧是否留空隙
+		full: {
+			type: Boolean,
+			default: () => defProps.card.full
+		},
+		// 标题
+		title: {
+			type: String,
+			default: () => defProps.card.title
+		},
+		// 标题颜色
+		titleColor: {
+			type: String,
+			default: () => defProps.card.titleColor
+		},
+		// 标题字体大小
+		titleSize: {
+			type: [Number, String],
+			default: () => defProps.card.titleSize
+		},
+		// 副标题
+		subTitle: {
+			type: String,
+			default: () => defProps.card.subTitle
+		},
+		// 副标题颜色
+		subTitleColor: {
+			type: String,
+			default: () => defProps.card.subTitleColor
+		},
+		// 副标题字体大小
+		subTitleSize: {
+			type: [Number, String],
+			default: () => defProps.card.subTitleSize
+		},
+		// 是否显示外部边框,只对full=false时有效(卡片与边框有空隙时)
+		border: {
+			type: Boolean,
+			default: () => defProps.card.border
+		},
+		// 用于标识点击了第几个
+		index: {
+			type: [Number, String, Object],
+			default: () => defProps.card.index
+		},
+		// 用于隔开上下左右的边距,带单位的写法,如:"30px 30px","20px 20px 30px 30px"
+		margin: {
+			type: String,
+			default: () => defProps.card.margin
+		},
+		// card卡片的圆角
+		borderRadius: {
+			type: [Number, String],
+			default: () => defProps.card.borderRadius
+		},
+		// 头部自定义样式,对象形式
+		headStyle: {
+			type: Object,
+			default: () => defProps.card.headStyle
+		},
+		// 主体自定义样式,对象形式
+		bodyStyle: {
+			type: Object,
+			default: () => defProps.card.bodyStyle
+		},
+		// 底部自定义样式,对象形式
+		footStyle: {
+			type: Object,
+			default: () => defProps.card.footStyle
+		},
+		// 头部是否下边框
+		headBorderBottom: {
+			type: Boolean,
+			default: () => defProps.card.headBorderBottom
+		},
+		// 底部是否有上边框
+		footBorderTop: {
+			type: Boolean,
+			default: () => defProps.card.footBorderTop
+		},
+		// 标题左边的缩略图
+		thumb: {
+			type: String,
+			default: () => defProps.card.thumb
+		},
+		// 缩略图宽高
+		thumbWidth: {
+			type: [String, Number],
+			default: () => defProps.card.thumbWidth
+		},
+		// 缩略图是否为圆形
+		thumbCircle: {
+			type: Boolean,
+			default: () => defProps.card.thumbCircle
+		},
+		// 给head,body,foot的内边距
+		padding: {
+			type: [String, Number],
+			default: () => defProps.card.padding
+		},
+		paddingHead: {
+			type: [String, Number],
+			default: () => defProps.card.paddingHead
+		},
+		paddingBody: {
+			type: [String, Number],
+			default: () => defProps.card.paddingBody
+		},
+		paddingFoot: {
+			type: [String, Number],
+			default: () => defProps.card.paddingFoot
+		},
+		// 是否显示头部
+		showHead: {
+			type: Boolean,
+			default: () => defProps.card.showHead
+		},
+		// 是否显示尾部
+		showFoot: {
+			type: Boolean,
+			default: () => defProps.card.showFoot
+		},
+		// 卡片外围阴影,字符串形式
+		boxShadow: {
+			type: String,
+			default: () => defProps.card.boxShadow
+		}
+    }
+})

+ 184 - 0
uni_modules/uview-plus/components/u-card/u-card.vue

@@ -0,0 +1,184 @@
+<template>
+	<view
+		class="u-card"
+		@tap.stop="click"
+		:class="{ 'u-border': border, 'u-card-full': full, 'u-card--border': getPx(borderRadius) > 0 }"
+		:style="{
+			borderRadius: addUnit(borderRadius),
+			margin: margin,
+			boxShadow: boxShadow
+		}"
+	>
+		<view
+			v-if="showHead"
+			class="u-card__head"
+			:style="[{padding: addUnit(paddingHead || padding)}, headStyle]"
+			:class="{
+				'u-border-bottom': headBorderBottom
+			}"
+			@tap="headClick"
+		>
+			<view v-if="!$slots.head" class="u-flex u-flex-between">
+				<view class="u-card__head--left u-flex u-line-1" v-if="title">
+					<image
+						:src="thumb"
+						class="u-card__head--left__thumb"
+						mode="aspectFill"
+						v-if="thumb"
+						:style="{ 
+							height: addUnit(thumbWidth), 
+							width: addUnit(thumbWidth), 
+							borderRadius: thumbCircle ? '50px' : '4px' 
+						}"
+					></image>
+					<text
+						class="u-card__head--left__title u-line-1"
+						:style="{
+							fontSize: addUnit(titleSize),
+							color: titleColor
+						}"
+					>
+						{{ title }}
+					</text>
+				</view>
+				<view class="u-card__head--right u-line-1" v-if="subTitle">
+					<text
+						class="u-card__head__title__text"
+						:style="{
+							fontSize: addUnit(subTitleSize),
+							color: subTitleColor
+						}"
+					>
+						{{ subTitle }}
+					</text>
+				</view>
+			</view>
+			<slot name="head" v-else />
+		</view>
+		<view @tap="bodyClick" class="u-card__body"
+			:style="[{padding: addUnit(paddingBody || padding)}, bodyStyle]"><slot name="body" /></view>
+		<view
+			v-if="showFoot"
+			class="u-card__foot"
+			 @tap="footClick"
+			:style="[{padding: $slots.foot ? addUnit(paddingFoot || padding) : 0}, footStyle]"
+			:class="{
+				'u-border-top': footBorderTop
+			}"
+		>
+			<slot name="foot" />
+		</view>
+	</view>
+</template>
+
+<script>
+    import { propsCard } from './props';
+    import { mpMixin } from '../../libs/mixin/mpMixin';
+    import { mixin } from '../../libs/mixin/mixin';
+    import { addStyle, addUnit, getPx } from '../../libs/function/index';
+    /**
+     * card 卡片
+     * @description 卡片组件一般用于多个列表条目,且风格统一的场景
+     * @tutorial https://uview-plus.jiangruyi.com/components/card.html
+     * @property {Boolean} full 卡片与屏幕两侧是否留空隙(默认false)
+     * @property {String} title 头部左边的标题
+     * @property {String} title-color 标题颜色(默认#303133)
+     * @property {String | Number} title-size 标题字体大小,单位rpx(默认15px)
+     * @property {String} sub-title 头部右边的副标题
+     * @property {String} sub-title-color 副标题颜色(默认#909399)
+     * @property {String | Number} sub-title-size 副标题字体大小(默认13px
+     * @property {Boolean} border 是否显示边框(默认true)
+     * @property {String | Number} index 用于标识点击了第几个卡片
+     * @property {String} box-shadow 卡片外围阴影,字符串形式(默认none)
+     * @property {String} margin 卡片与屏幕两边和上下元素的间距,需带单位,如"30px 20px"(默认15px)
+     * @property {String | Number} border-radius 卡片整体的圆角值,单位rpx(默认8px)
+     * @property {Object} head-style 头部自定义样式,对象形式
+     * @property {Object} body-style 中部自定义样式,对象形式
+     * @property {Object} foot-style 底部自定义样式,对象形式
+     * @property {Boolean} head-border-bottom 是否显示头部的下边框(默认true)
+     * @property {Boolean} foot-border-top 是否显示底部的上边框(默认true)
+     * @property {Boolean} show-head 是否显示头部(默认true)
+     * @property {Boolean} show-foot 是否显示尾部(默认true)
+     * @property {String} thumb 缩略图路径,如设置将显示在标题的左边,不建议使用相对路径
+     * @property {String | Number} thumb-width 缩略图的宽度,高等于宽,单位px(默认30px)
+     * @property {Boolean} thumb-circle 缩略图是否为圆形(默认false)
+     * @event {Function} click 整个卡片任意位置被点击时触发
+     * @event {Function} head-click 卡片头部被点击时触发
+     * @event {Function} body-click 卡片主体部分被点击时触发
+     * @event {Function} foot-click 卡片底部部分被点击时触发
+     * @example <u-card paddingFoot="2px 15px" title="card"></u-card>
+     */
+    export default {
+        name: 'up-card',
+        data() {
+            return {};
+        },
+        mixins: [mpMixin, mixin, propsCard],
+        emits: ['click', 'head-click', 'body-click', 'foot-click'],
+        methods: {
+			addStyle,
+			addUnit,
+			getPx,
+            click() {
+                this.$emit('click', this.index);
+            },
+            headClick() {
+                this.$emit('head-click', this.index);
+            },
+            bodyClick() {
+                this.$emit('body-click', this.index);
+            },
+            footClick() {
+                this.$emit('foot-click', this.index);
+            }
+        }
+    };
+</script>
+
+<style lang="scss" scoped>
+.u-card {
+	position: relative;
+	overflow: hidden;
+	font-size: 28rpx;
+	background-color: #ffffff;
+	box-sizing: border-box;
+	
+	&-full {
+		// 如果是与屏幕之间不留空隙,应该设置左右边距为0
+		margin-left: 0 !important;
+		margin-right: 0 !important;
+		width: 100%;
+	}
+	
+	&--border:after {
+		border-radius: 16rpx;
+	}
+
+	&__head {
+		&--left {
+			color: $u-main-color;
+			
+			&__thumb {
+				margin-right: 16rpx;
+			}
+			
+			&__title {
+				max-width: 400rpx;
+			}
+		}
+
+		&--right {
+			color: $u-tips-color;
+			margin-left: 6rpx;
+		}
+	}
+
+	&__body {
+		color: $u-content-color;
+	}
+
+	&__foot {
+		color: $u-tips-color;
+	}
+}
+</style>

+ 333 - 0
uni_modules/uview-plus/components/u-cascader/u-cascader.vue

@@ -0,0 +1,333 @@
+<template>
+	<up-popup :show="popupShow" mode="bottom" :popup="false"
+		:mask="true" :closeable="true" :safe-area-inset-bottom="true"
+		close-icon-color="#ffffff" :z-index="uZIndex"
+		:maskCloseAble="maskCloseAble" @close="close">
+		<view class="up-p-t-30 up-p-l-20 up-m-b-10" v-if="headerDirection =='column'">
+			<up-steps v-if="popupShow" dot direction="column" v-model:current="tabsIndex">
+				<up-steps-item  v-for="(item, index) in genTabsList"
+					@click="tabsIndex = index" :title="item.name"></up-steps-item>
+			</up-steps>
+		</view>
+		<view class="up-p-t-20 up-m-b-10" v-else>
+			<up-tabs v-if="popupShow" :list="genTabsList"
+				:scrollable="true" v-model:current="tabsIndex" @change="tabsChange" ref="tabs"></up-tabs>
+		</view>
+		<view class="area-box">
+			<view class="u-flex" :class="{ 'change':isChange }"
+				:style="{transform: optionsCols == 2 && isChange ? 'translateX(-33.3333333%)' : ''}">
+				<template v-for="(levelData, levelIndex) in levelList" :key="levelIndex">
+					<view v-if="optionsCols == 2 || levelIndex == tabsIndex" class="area-item"
+						:style="{ width: optionsCols == 2 ? '33.33333%' : '750rpx'}">
+						<view class="u-padding-10 u-bg-gray" style="height: 100%;">
+							<scroll-view :scroll-y="true" style="height: 100%">
+								<up-cell-group v-if="levelIndex === 0 || selectedValueIndexs[levelIndex - 1] !== undefined">
+									<up-cell v-for="(item,index) in levelData"
+										:title="item[labelKey]" :arrow="false"
+										:index="index" :key="index"
+										@click="levelChange(levelIndex, index)">
+										<template v-slot:right-icon>
+											<up-icon v-if="selectedValueIndexs[levelIndex] === index"
+												size="17" name="checkbox-mark"></up-icon>
+										</template>
+									</up-cell>
+								</up-cell-group>
+							</scroll-view>
+						</view>
+					</view>
+				</template>
+			</view>
+		</view>
+		<!-- 添加按钮区域 -->
+		<view class="u-cascader-action up-flex up-flex-between">
+			<view class="u-padding-20 up-flex-fill">
+				<up-button @click="handleCancel" type="default">{{ t("up.common.cancel") }}</up-button>
+			</view>
+			<view class="u-padding-20 up-flex-fill">
+				<up-button @click="handleConfirm" type="primary">{{ t("up.common.confirm") }}</up-button>
+			</view>
+		</view>
+	</up-popup>
+</template>
+
+<script>
+	/**
+	 * u-cascader 通用无限级联选择器
+	 * @property {String Number} z-index 弹出时的z-index值(默认1075)
+	 * @property {Boolean} mask-close-able 是否允许通过点击遮罩关闭Picker(默认true)
+	 * @property {Array} data 级联数据
+	 * @property {Array} default-value 默认选中的值
+	 * @property {String} valueKey 指定选项的值为选项对象中的哪个属性值
+	 * @property {String} labelKey 指定选项标签为选项对象中的哪个属性值
+	 * @property {String} childrenKey 指定选项的子选项为选项对象中的哪个属性值
+	 * @property {Boolean} autoClose 是否在选择最后一级时自动关闭并触发confirm(默认false)
+	 */
+	import { t } from '../../libs/i18n'
+	export default {
+		name: 'up-cascader',
+		props: {
+			// 通过双向绑定控制组件的弹出与收起
+			show: {
+				type: Boolean,
+				default: false
+			},
+			// 级联数据
+			data: {
+				type: Array,
+				default() {
+					return [];
+				}
+			},
+			// 默认选中的值
+			modelValue: {
+				type: Array,
+				default() {
+					return [];
+				}
+			},
+			// 指定选项的值为选项对象中的哪个属性值
+			valueKey: {
+				type: String,
+				default: 'value'
+			},
+			// 指定选项标签为选项对象中的哪个属性值
+			labelKey: {
+				type: String,
+				default: 'label'
+			},
+			// 指定选项的子选项为选项对象中的哪个属性值
+			childrenKey: {
+				type: String,
+				default: 'children'
+			},
+			// 是否允许通过点击遮罩关闭Picker
+			maskCloseAble: {
+				type: Boolean,
+				default: true
+			},
+			// 弹出的z-index值
+			zIndex: {
+				type: [String, Number],
+				default: 0
+			},
+			// 是否在选择最后一级时自动关闭并触发confirm
+			autoClose: {
+				type: Boolean,
+				default: false
+			},
+			// 选中项目的展示方向direction垂直方向适合文字长度过长
+			headerDirection: {
+				type: String,
+				default: 'row'
+			},
+			// 选项区域列数,支持1列和2列,默认为2列
+			optionsCols: {
+				type: [Number],
+				default: 2
+			}
+		},
+		data() {
+			return {
+				// 存储每一级的数据
+				levelList: [],
+				// 存储每一级选中的索引
+				selectedValueIndexs: [],
+				tabsIndex: 0,
+				popupShow: false,
+				// 新增confirmValues用于存储确认的值
+				confirmValues: []
+			}
+		},
+		watch: {
+			data: {
+				handler() {
+					this.initLevelList();
+				},
+				immediate: true
+			},
+			show() {
+				this.popupShow = this.show;
+			},
+			modelValue: {
+				handler() {
+					this.init();
+				},
+				immediate: true
+			}
+		},
+		computed: {
+			isChange() {
+				return this.tabsIndex > 1;
+			},
+			genTabsList() {
+				let tabsList = [{
+					name: "请选择"
+				}];
+				
+				// 根据选中的值动态生成tabs
+				for (let i = 0; i < this.selectedValueIndexs.length; i++) {
+					if (this.selectedValueIndexs[i] !== undefined && this.levelList[i]) {
+						const selectedItem = this.levelList[i][this.selectedValueIndexs[i]];
+						if (selectedItem) {
+							tabsList[i] = {
+								name: selectedItem[this.labelKey]
+							};
+							// 如果还有下一级,则添加"请选择"
+							if (i === this.selectedValueIndexs.length - 1 && 
+								selectedItem[this.childrenKey] && 
+								selectedItem[this.childrenKey].length > 0) {
+								tabsList.push({
+									name: "请选择"
+								});
+							}
+						}
+					}
+				}
+				
+				return tabsList;
+			},
+			uZIndex() {
+				// 如果用户有传递z-index值,优先使用
+				return this.zIndex ? this.zIndex : this.$u.zIndex.popup;
+			}
+		},
+		// 新增confirm事件
+		emits: ['update:modelValue', 'change', 'confirm'],
+		methods: {
+			t,
+			init() {
+				// 初始化选中值
+				if (this.modelValue && this.modelValue.length > 0) {
+					this.setDefaultValue();
+				}
+			},
+			initLevelList() {
+				// 初始化第一级数据
+				if (this.data && this.data.length > 0) {
+					this.levelList = [this.data];
+					this.selectedValueIndexs = [];
+				}
+			},
+			setDefaultValue() {
+				// 根据默认值设置选中项
+				// 根据modelValue获取indexs给selectedValueIndexs
+				this.selectedValueIndexs = [];
+				let currentLevelData = this.data;
+				
+				for (let i = 0; i < this.modelValue.length; i++) {
+					const value = this.modelValue[i];
+					const index = currentLevelData.findIndex(item => item[this.valueKey] === value);
+					
+					if (index !== -1) {
+						this.selectedValueIndexs.push(index);
+						// 更新下一级的数据
+						if (currentLevelData[index][this.childrenKey]) {
+							currentLevelData = currentLevelData[index][this.childrenKey];
+						} else {
+							// 如果没有子级数据,则停止处理
+							break;
+						}
+					} else {
+						// 如果找不到匹配项,则停止处理
+						break;
+					}
+				}
+			},
+			close() {
+				this.$emit('update:show', false);
+			},
+			tabsChange(item) {
+			},
+			levelChange(levelIndex, index) {
+				// 设置当前级的选中值
+				this.$set(this.selectedValueIndexs, levelIndex, index);
+				
+				// 清除后续级别的选中值
+				for (let i = levelIndex + 1; i < this.selectedValueIndexs.length; i++) {
+					this.$set(this.selectedValueIndexs, i, undefined);
+				}
+				
+				// 获取当前选中项
+				const currentItem = this.levelList[levelIndex][index];
+				
+				// 如果有子级数据,则初始化下一级
+				if (currentItem && currentItem[this.childrenKey] && currentItem[this.childrenKey].length > 0) {
+					// 确保levelList数组足够长
+					if (this.levelList.length <= levelIndex + 1) {
+						this.levelList.push(currentItem[this.childrenKey]);
+					} else {
+						this.$set(this.levelList, levelIndex + 1, currentItem[this.childrenKey]);
+					}
+					// 切换到下一级tab
+					this.tabsIndex = levelIndex + 1;
+				} else {
+					// 没有子级数据,说明是最后一级
+					if (this.autoClose) {
+						// 如果启用自动关闭,则触发change事件并关闭
+						this.emitChange();
+					} else {
+						// 否则只触发change事件,不关闭
+						this.emitChange(false);
+					}
+				}
+			},
+			// 修改emitChange方法,增加closePopup参数
+			emitChange(closePopup = true) {
+				// 构造选中结果
+				const result = [];
+				for (let i = 0; i < this.selectedValueIndexs.length; i++) {
+					if (this.selectedValueIndexs[i] !== undefined && this.levelList[i]) {
+						result.push(this.levelList[i][this.selectedValueIndexs[i]][this.valueKey]);
+					}
+				}
+				
+				// 更新confirmValues
+				this.confirmValues = [...result];
+				
+				// 触发change事件,返回value数组
+				this.$emit('change', this.confirmValues);
+				
+				// 根据参数决定是否关闭弹窗
+				if (closePopup) {
+					this.close();
+				}
+			},
+			handleCancel() {
+				this.close();
+			},
+			handleConfirm() {
+				// 确认时触发confirm事件
+				this.$emit('update:modelValue', this.confirmValues);
+				this.$emit('confirm', this.confirmValues);
+				this.close();
+			}
+		}
+	}
+</script>
+<style lang="scss">
+	.area-box {
+		width: 100%;
+		overflow: hidden;
+		height: 800rpx;
+
+		>view {
+			width: 150%;
+			transition: transform 0.3s ease-in-out 0s;
+			transform: translateX(0);
+
+			&.change {
+				// transform: translateX(-33.3333333%);
+			}
+		}
+
+		.area-item {
+			// width: 750rpx;
+			height: 800rpx;
+		}
+	}
+	
+	// 添加按钮区域样式
+	.u-cascader-action {
+		border-top: 1px solid #eee;
+	}
+</style>

+ 381 - 0
uni_modules/uview-plus/components/u-cate-tab/u-cate-tab.vue

@@ -0,0 +1,381 @@
+<template>
+	<view class="u-cate-tab" :style="{ height: addUnit(height) }">
+		<view class="u-cate-tab__wrap">
+			<scroll-view class="u-cate-tab__view u-cate-tab__menu-scroll-view"
+                scroll-y scroll-with-animation :scroll-top="scrollTop"
+			    :scroll-into-view="itemId">
+				<view v-for="(item, index) in tabList" :key="index" class="u-cate-tab__item"
+                    :class="[innerCurrent == index ? 'u-cate-tab__item-active' : '']"
+				 @tap.stop="swichMenu(index)">
+					<slot name="tabItem" :item="item">
+                    </slot>
+                    <text v-if="!$slots['tabItem']" class="u-line-1">{{item[tabKeyName]}}</text>
+				</view>
+			</scroll-view>
+			<scroll-view :scroll-top="scrollRightTop" scroll-with-animation :scroll-into-view="scrollIntoView"
+				scroll-y class="u-cate-tab__right-box" @scroll="rightScroll">
+				<view class="u-cate-tab__right-top">
+					<slot name="rightTop" :tabList="tabList">
+                	</slot>
+				</view>
+				<view class="u-cate-tab__page-view">
+					<template :key="index" v-for="(item , index) in tabList">
+						<view v-if="mode == 'follow' || ( mode == 'tab' && index == innerCurrent)"
+							class="u-cate-tab__page-item" :id="'item' + index">
+							<slot name="itemList" :item="item">
+							</slot>
+							<template v-if="!$slots['itemList']">
+								<view class="item-title">
+									<text>{{item[tabKeyName]}}</text>
+								</view>
+								<view class="item-container">
+									<template v-for="(item1, index1) in item.children" :key="index1">
+										<slot name="pageItem" :pageItem="item1">
+											<view class="thumb-box" >
+												<image class="item-menu-image" :src="item1.icon" mode=""></image>
+												<view class="item-menu-name">{{item1[itemKeyName]}}</view>
+											</view>
+										</slot>
+									</template>
+								</view>
+							</template>
+						</view>
+					</template>
+				</view>
+			</scroll-view>
+		</view>
+	</view>
+</template>
+<script>
+	import { addUnit, sleep } from '../../libs/function/index';
+	export default {
+		name: 'up-cate-tab',
+        props: {
+			mode: {
+                type: String,
+                default: 'follow' // follo跟随联动, tab单一显示。
+            },
+			height: {
+                type: String,
+                default: '100%'
+            },
+            tabList: {
+                type: Array,
+                default: () => {
+                    return []
+                }
+            },
+            tabKeyName: {
+                type: String,
+                default: 'name'
+            },
+            itemKeyName: {
+                type: String,
+                default: 'name'
+            },
+			current: {
+                type: Number,
+                default: 0
+            }
+        },
+        watch: {
+			tabList: {
+				deep: true,
+				handler(newVal, oldVal) {
+					// this.observer();
+					sleep(30);
+					this.getMenuItemTop();
+					this.leftMenuStatus(this.innerCurrent);
+				}
+			},
+			current(nval) {
+				this.innerCurrent = nval;
+				this.leftMenuStatus(this.innerCurrent);
+			},
+			height() {
+				// console.log('height change');
+				this.getMenuItemTop();
+				this.leftMenuStatus(this.innerCurrent);
+			}
+        },
+		emits: ['update:current'],
+		data() {
+			return {
+				scrollTop: 0, //tab标题的滚动条位置
+				scrollIntoView: '', // 滚动至哪个元素
+				oldScrollTop: 0,
+				innerCurrent: 0, // 预设当前项的值
+				menuHeight: 0, // 左边菜单的高度
+				menuItemHeight: 0, // 左边菜单item的高度
+				itemId: '', // 栏目右边scroll-view用于滚动的id
+				menuItemPos: [],
+                rects: [],
+				arr: [],
+				scrollRightTop: 0, // 右边栏目scroll-view的滚动条高度
+				timer: null, // 定时器
+			}
+		},
+		mounted() {
+			// this.observer();
+			this.innerCurrent = this.current;
+			this.leftMenuStatus(this.innerCurrent);
+			this.getMenuItemTop()
+		},
+		methods: {
+			addUnit,
+			// 点击左边的栏目切换
+			async swichMenu(index) {
+				if (this.mode == 'follow') {
+					if(this.arr.length == 0) {
+						await this.getMenuItemTop();
+					}
+					this.scrollIntoView = 'item' + index;
+				}
+
+				if (index == this.innerCurrent) return;
+				this.$nextTick(function(){
+					this.innerCurrent = index;
+					this.$emit('update:current', index);
+				})
+			},
+			// 获取一个目标元素的高度
+			getElRect(elClass, dataVal) {
+				return new Promise((resolve, reject) => {
+					const query = uni.createSelectorQuery().in(this);
+					query.select('.' + elClass).fields({
+						size: true
+					}, res => {
+						// 如果节点尚未生成,res值为null,循环调用执行
+						if (!res) {
+							setTimeout(() => {
+								this.getElRect(elClass);
+							}, 10);
+							return;
+						}
+						this[dataVal] = res.height;
+						resolve();
+					}).exec();
+				})
+			},
+			// 观测元素相交状态
+			async observer() {
+				await this.$nextTick();
+				// 清除之前的观察器
+				if (this._observerList) {
+					this._observerList.forEach(observer => {
+						observer.disconnect();
+					});
+				}
+				this._observerList = [];
+				
+				this.tabList.map((val, index) => {
+					let observer = uni.createIntersectionObserver(this);
+					this._observerList.push(observer);
+					// 检测相交状态
+					observer.relativeTo('.u-cate-tab__right-box', {
+						top: 10
+					}).observe('#item' + index, (res) => {
+						if (res.intersectionRatio > 0) {
+							console.log('res', res);
+							// 修复:确保正确获取索引
+							let id = res.id ? res.id.substring(4) : index;
+							this.leftMenuStatus(parseInt(id));
+						}
+					})
+				})
+			},
+			// 设置左边菜单的滚动状态
+			async leftMenuStatus(index) {
+				this.innerCurrent = index;
+				this.$emit('update:current', index);
+				// 如果为0,意味着尚未初始化
+				if (this.menuHeight == 0 || this.menuItemHeight == 0) {
+					await this.getElRect('u-cate-tab__menu-scroll-view', 'menuHeight');
+					await this.getElRect('u-cate-tab__item', 'menuItemHeight');
+				}
+				// console.log(this.menuHeight, this.menuItemHeight)
+				// 将菜单活动item垂直居中
+				this.scrollTop = index * this.menuItemHeight + this.menuItemHeight / 2 - this.menuHeight / 2;
+			},
+			// 获取右边菜单每个item到顶部的距离
+			async getMenuItemTop() {
+				// await this.$nextTick();
+				// console.log('getMenuItemTop')
+				return new Promise(resolve => {
+					let selectorQuery = uni.createSelectorQuery().in(this);
+					selectorQuery.selectAll('.u-cate-tab__page-item').boundingClientRect((rects) => {
+						// 如果节点尚未生成,rects值为[](因为用selectAll,所以返回的是数组),循环调用执行
+						if(!rects.length) {
+							setTimeout(() => {
+								this.getMenuItemTop();
+							}, 100);
+							return ;
+						}
+						// console.log(rects)
+                        this.rects = rects;
+						this.arr = [];
+						rects.forEach((rect) => {
+							// 这里减去rects[0].top,是因为第一项顶部可能不是贴到导航栏(比如有个搜索框的情况)
+							this.arr.push(rect.top - rects[0].top);
+						})
+						// console.log(this.arr)
+                        resolve();
+					}).exec()
+				})
+			},
+			// 右边菜单滚动
+			async rightScroll(e) {
+				if (this.mode !== 'follow') return;
+				this.oldScrollTop = e.detail.scrollTop;
+                // console.log(e.detail.scrollTop)
+                // console.log(JSON.stringify(this.arr))
+				if(this.arr.length == 0) {
+					await this.getMenuItemTop();
+				}
+				if(this.timer) return ;
+				if(!this.menuHeight) {
+					await this.getElRect('u-cate-tab__menu-scroll-view', 'menuHeight');
+				}
+				setTimeout(() => { // 节流
+					this.timer = null;
+					// scrollHeight为右边菜单垂直中点位置
+					let scrollHeight = e.detail.scrollTop + 1;
+                    // console.log(e.detail.scrollTop)
+					for (let i = 0; i < this.arr.length; i++) {
+						let height1 = this.arr[i];
+						let height2 = this.arr[i + 1];
+                        // console.log('i', i)
+                        // console.log('height1', height1)
+                        // console.log('height2', height2)
+						// 如果不存在height2,意味着数据循环已经到了最后一个,设置左边菜单为最后一项即可
+						if (!height2 || scrollHeight >= height1 && scrollHeight <= height2) {
+                            // console.log('scrollHeight', scrollHeight)
+                            // console.log('height1', height1)
+                            // console.log('height2', height2)
+							this.leftMenuStatus(i);
+							return ;
+						}
+					}
+				}, 100)
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	.u-cate-tab {
+		display: flex;
+		flex-direction: column;
+	}
+
+	.u-cate-tab__wrap {
+		flex: 1;
+		display: flex;
+		flex-direction: row;
+		overflow: hidden;
+	}
+
+	.u-search-inner {
+		background-color: rgb(234, 234, 234);
+		border-radius: 100rpx;
+		display: flex;
+		align-items: center;
+		padding: 10rpx 16rpx;
+	}
+
+	.u-search-text {
+		font-size: 26rpx;
+		color: $u-tips-color;
+		margin-left: 10rpx;
+	}
+
+	.u-cate-tab__view {
+		width: 200rpx;
+		height: 100%;
+	}
+
+	.u-cate-tab__item {
+		height: 110rpx;
+		background: #f6f6f6;
+		box-sizing: border-box;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		font-size: 26rpx;
+		color: #444;
+		font-weight: 400;
+		line-height: 1;
+	}
+
+	.u-cate-tab__item-active {
+		position: relative;
+		color: #000;
+		font-size: 30rpx;
+		font-weight: 600;
+		background: #fff;
+	}
+
+	.u-cate-tab__item-active::before {
+		content: "";
+		position: absolute;
+		border-left: 4px solid $u-primary;
+		height: 32rpx;
+		left: 0;
+		top: 39rpx;
+	}
+
+	.u-cate-tab__view {
+		height: 100%;
+	}
+
+	.u-cate-tab__right-box {
+		flex: 1;
+		background-color: rgb(250, 250, 250);
+	}
+
+	.u-cate-tab__page-view {
+		padding: 16rpx;
+	}
+
+	.u-cate-tab__page-item {
+		margin-bottom: 30rpx;
+		background-color: #fff;
+		padding: 16rpx;
+		border-radius: 8rpx;
+	}
+
+	.u-cate-tab__page-item:last-child {
+		min-height: 100vh;
+	}
+
+	.item-title {
+		font-size: 26rpx;
+		color: $u-main-color;
+		font-weight: bold;
+	}
+
+	.item-menu-name {
+		font-weight: normal;
+		font-size: 24rpx;
+		color: $u-main-color;
+	}
+
+	.item-container {
+		display: flex;
+		flex-wrap: wrap;
+	}
+
+	.thumb-box {
+		width: 33.333333%;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		flex-direction: column;
+		margin-top: 20rpx;
+	}
+
+	.item-menu-image {
+		width: 120rpx;
+		height: 120rpx;
+	}
+</style>

+ 0 - 0
uni_modules/uview-plus/components/u-cell-group/cellGroup.js


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