Przeglądaj źródła

feat: 添加等级权益页面、webview页面

ext.zhangbin71 1 dzień temu
rodzic
commit
93d1988fc7
2 zmienionych plików z 783 dodań i 0 usunięć
  1. 551 0
      pages/VIP/VIP.vue
  2. 232 0
      pages/webview/index.vue

+ 551 - 0
pages/VIP/VIP.vue

@@ -0,0 +1,551 @@
+<template>
+  <view class="level-container">
+    <!-- 顶部会员信息区域 -->
+    <view class="vip-info">
+      <view class="flex-between vip-box">
+        <view class="flex_2">
+          <view class="flex-between">
+            <view class="vip-text">
+              <view class="vip-title">VIP会员</view>
+              <view class="vip-desc"
+                >会员等级 {{ appStore?.$userInfo?.level || 0 }}</view
+              >
+            </view>
+            <view class="point-info">
+              <text class="point-text">{{
+                appStore?.$userInfo?.integral || 0
+              }}</text>
+              <text class="point-desc">会员贝币</text>
+            </view>
+          </view>
+          <view>
+            <view class="growth-info">
+              <text>成长值 {{ appStore?.$userInfo?.experience || 0 }}</text>
+              <text class="growth-progress">
+                {{ appStore?.$userInfo?.experience || 0 }}/{{
+                  appStore?.$userInfo?.experienceCount || 0
+                }}
+              </text>
+            </view>
+            <up-line-progress
+              :percentage="percentExperience"
+              activeColor="#d6c3a3"
+              inactiveColor="#808080"
+              height="6"
+              :showText="false"
+            ></up-line-progress>
+          </view>
+        </view>
+      </view>
+    </view>
+
+    <view class="vip-benefit">
+      <!-- VIP尊享权益区域 -->
+      <view class="vip-benefit-title">VIP尊享权益</view>
+      <view class="benefit-content">
+        <view class="benefit-list">
+          <view
+            class="benefit-item"
+            v-for="(item, index) in benefitList"
+            :key="index"
+          >
+            <image :src="item.icon" class="benefit-icon"></image>
+            <text class="benefit-text">{{ item.text }}</text>
+          </view>
+        </view>
+        <view class="benefit-desc">
+          <view>当前已解锁权益:</view>
+          <view>
+            买金-<text>{{ appStore.$userInfo?.buy || 0 }}</text
+            >元/克
+          </view>
+          <view>
+            回收+<text
+              ><text>{{ appStore.$userInfo?.sold || 0 }}</text></text
+            >元/克
+          </view>
+          <view>
+            购物折扣:<text>{{ appStore.$userInfo?.discount || "无" }}</text
+            >折
+          </view>
+        </view>
+      </view>
+
+      <!-- 做任务赚成长值/贝币区域 -->
+      <view class="task-title">做任务赚成长值/贝币</view>
+      <view class="task-item" v-for="(task, index) in taskList" :key="index">
+        <view class="task-desc">
+          <text class="task-name">{{ task.name }}</text>
+          <text class="task-reward">{{ task.reward }}</text>
+        </view>
+		<view class="submit-btn">
+			<up-button
+			  v-if="!appStore.$wxConfig?.auditModeEnabled"
+			  @click="toPages(task)"
+			  type="warning"
+			  :text="task.btnText"
+			  size="mini"
+			  shape="circle"
+			></up-button>
+		</view>
+        
+      </view>
+    </view>
+
+    <!-- vip成长系统 -->
+    <view class="vip-level-box">
+      <view class="vip-level-title">会员成长系统</view>
+      <view class="title-box">
+        <view class="title-item">等级</view>
+        <view class="title-item">图标</view>
+        <view class="title-item">成长值</view>
+        <view class="title-item">权益</view>
+      </view>
+
+      <scroll-view class="title-content" :scroll-y="true">
+        <view
+          class="content-list"
+          v-for="(item, index) in leveList"
+          :key="index"
+        >
+          <view class="content-item">{{ index + 1 }}</view>
+          <view class="content-item">
+            <image mode="heightFix" :src="item.icon"></image>
+          </view>
+          <view class="content-item">{{ item.experience }}</view>
+          <view class="content-item quanyi" style="width: 27%">
+            <text
+              >买金- <text class="gw"> {{ item.buy }}元/克</text>
+            </text>
+            <text>
+              卖金+<text class="gw"> {{ item.sold }}元/克</text>
+            </text>
+            <text
+              >购物 :<text class="gw zk">{{ item.discount }} 折</text></text
+            >
+          </view>
+        </view>
+        <veiw class="content-desc">
+          以上显示{{ leveList.length }}级,继续努力,更多等级权益等你探索~
+        </veiw>
+      </scroll-view>
+    </view>
+
+    <!-- 邀请新人弹窗 -->
+    <up-popup
+      :show="showInvite"
+      closeOnClickOverlay
+      mode="center"
+      @close="showInvite = !showInvite"
+    >
+      <view class="upopup-box">
+        <image
+          style="width: 100%"
+          src="https://sb-admin.oss-cn-shenzhen.aliyuncs.com/shuibei-mini/Invite/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20250807170542.jpg"
+          mode="widthFix"
+          showMenuByLongpress
+        ></image>
+        <button open-type="share" class="upopup-button">点击邀约</button>
+      </view>
+    </up-popup>
+  </view>
+</template>
+
+<script setup>
+import { ref, computed, onMounted } from "vue";
+import { useAppStore } from "@/stores/app";
+import { onLoad, onShareAppMessage } from "@dcloudio/uni-app";
+import { levelGrowthList } from "@/api/svip";
+
+const appStore = useAppStore();
+let leveList = ref([]);
+const showInvite = ref(false);
+
+onMounted(() => {
+  console.log("mounted1", appStore.$userInfo);
+
+  getLevelGrowthList();
+});
+
+// 权益列表数据
+const benefitList = ref([
+  {
+    icon: "https://sb-admin.oss-cn-shenzhen.aliyuncs.com/shuibei-mini/vip/storeGold.png",
+    text: "存金",
+  },
+  {
+    icon: "https://sb-admin.oss-cn-shenzhen.aliyuncs.com/shuibei-mini/vip/swapModel.png",
+    text: "换款",
+  },
+  {
+    icon: "https://sb-admin.oss-cn-shenzhen.aliyuncs.com/shuibei-mini/vip/Recycling.png",
+    text: "回收",
+  },
+  {
+    icon: "https://sb-admin.oss-cn-shenzhen.aliyuncs.com/shuibei-mini/vip/exchange.png",
+    text: "兑换",
+  },
+]);
+
+// 任务列表数据
+const taskList = ref([
+  {
+    name: "完善基本资料",
+    reward: "获得200成长值,200贝币",
+    btnText: "立即前往",
+    url: "/pages/personal_info/personal_info",
+  },
+  // {
+  //   name: "每日签到",
+  //   reward: "获得20成长值,20贝币",
+  //   btnText: "立即前往",
+  //   url: "/pages/bb_mall/index",
+  // },
+  {
+    name: "卖料",
+    reward: "每回收黄金1g,获得20成长值,20贝币",
+    btnText: "立即前往",
+    url: "/pages/users/vault/storeMetal/index",
+  },
+  {
+    name: "买料",
+    reward: "每买料黄金1g,获得20成长值,20贝币",
+    btnText: "立即前往",
+    url: "/pages/users/vault/rechargeGold",
+  },
+  {
+    name: "攒金",
+    reward: "每攒1克,获得20成长值,20贝币",
+    btnText: "立即前往",
+    url: "/pages/users/utils/wool/index",
+  },
+  {
+    name: "消费",
+    reward: "每消费1元工费,获得1成长值,1贝币",
+    btnText: "立即前往",
+    url: "/pages/users/vault/rechargeGold",
+  },
+  {
+    name: "邀请新人",
+    reward:
+      "邀请一位新人注册,可获得100成长值,100贝币;新人升级会员,可获得200成长值,200贝币。",
+    btnText: "立即邀请",
+    url: "invite",
+  },
+  {
+    name: "升级SVIP",
+    reward: "升级SVIP解锁更多功能,加速成长值成长",
+    btnText: "立即前往",
+    url: "/pages/SVIP/SVIP",
+  },
+]);
+
+// 跳转任务页面
+function toPages(task) {
+  // 买料、卖料、消费需要通过webview跳转
+  if (
+    [
+      "/pages/users/vault/rechargeGold",
+      "/pages/users/vault/storeMetal/index",
+      "/pages/users/vault/storeMetal/order",
+    ].includes(task.url)
+  ) {
+    const webviewPageUrl = `/pages/webview/index?path=${task.url}`;
+    uni.navigateTo({
+      url: webviewPageUrl,
+      fail: (err) => {
+        console.error("跳转到webview页面失败:", err);
+        uni.showToast({
+          title: "跳转失败,请重试",
+          icon: "none",
+          duration: 1500,
+        });
+      },
+    });
+    return; // 阻止继续执行后续跳转逻辑
+  }
+
+  // 其他任务的跳转逻辑
+  if (["/pages/index/index"].includes(task.url)) {
+    uni.switchTab({ url: task.url });
+  } else if (task.url === "invite") {
+    showInvite.value = true;
+  } else {
+    uni.navigateTo({ url: task.url });
+  }
+}
+
+// 计算成长值百分比
+const percentExperience = computed(() => {
+  if (appStore.isLogin) {
+    const percent = Math.floor(
+      (appStore?.$userInfo?.experience / appStore?.$userInfo?.experienceCount) *
+        100
+    );
+    return percent > 100 ? 100 : percent;
+  }
+  return 0;
+});
+
+const getLevelGrowthList = async () => {
+  const res = await levelGrowthList();
+  leveList.value = res.data;
+};
+
+// 分享功能
+onShareAppMessage(() => {
+  return {
+    title: "邀请您加入水贝搬运工",
+    path: `/pages/users/login/index?invite_code=${appStore.$userInfo.inviteCode}`,
+    imageUrl:
+      "https://sb-admin.oss-cn-shenzhen.aliyuncs.com/shuibei-mini/Invite/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20250807170542.jpg",
+  };
+});
+</script>
+
+<style scoped lang="scss">
+// 邀请新人弹窗
+.upopup-box {
+  width: 600rpx;
+  border-radius: 30rpx;
+  overflow: hidden;
+  position: relative;
+  background-color: none;
+
+  .upopup-button {
+    position: absolute;
+    bottom: 80rpx;
+    left: 50%;
+    transform: translateX(-50%);
+    width: 200rpx;
+    height: 60rpx;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    background-color: #cc9933;
+    color: #ffffff;
+    font-size: 30rpx;
+    border-radius: 40rpx;
+    box-shadow: 0 0 10rpx rgba(0, 0, 0, 0.2);
+  }
+}
+
+.level-container {
+  // background-color: #fff;
+  .up-nav-slot-title {
+    color: #fff;
+  }
+  color: black;
+  position: relative;
+  top: -130rpx;
+}
+
+/* 顶部会员信息 */
+.vip-info {
+  color: #dfd7bc;
+  background: url("https://sb-admin.oss-cn-shenzhen.aliyuncs.com/shuibei-mini/svip/vip-bg.jpg")
+    no-repeat center top;
+  background-size: 100%;
+  height: 450rpx;
+  overflow: hidden;
+
+  .vip-box {
+    margin: 200rpx 90rpx 100rpx 265rpx;
+  }
+}
+.vip-avatar {
+  width: 151rpx;
+  height: 161rpx;
+  margin-right: 70rpx;
+  margin-top: 50rpx;
+}
+.vip-title {
+  font-size: 36rpx;
+  font-weight: bold;
+  margin-bottom: 8rpx;
+}
+.vip-desc {
+  font-size: 24rpx;
+  margin-bottom: 12rpx;
+}
+.growth-info {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-top: 20rpx;
+  margin-bottom: 5rpx;
+  font-size: 20rpx;
+}
+.point-info {
+  text-align: right;
+}
+.point-text {
+  font-size: 40rpx;
+  font-weight: bold;
+  display: block;
+}
+.point-desc {
+  font-size: 20rpx;
+  opacity: 0.8;
+}
+
+/* VIP尊享权益标题 */
+.vip-benefit {
+  padding: 30rpx;
+  .vip-benefit-title {
+    font-size: 32rpx;
+    font-weight: bold;
+    margin: 32rpx 0 16rpx 0;
+  }
+  .benefit-content {
+    background-color: #323141;
+    padding: 40rpx 30rpx;
+    padding-bottom: 10rpx;
+    border-radius: 20rpx;
+    /* 权益列表 */
+    .benefit-list {
+      display: flex;
+      justify-content: space-between;
+      padding-bottom: 20rpx;
+      border-bottom: 1px dashed #797887;
+    }
+    .benefit-desc {
+      display: flex;
+      justify-content: space-between;
+      align-items: flex-end;
+      font-size: 18rpx;
+      color: #bdc3c7;
+      padding: 10rpx 0;
+      box-sizing: border-box;
+
+      text {
+        color: #d1bb9c;
+        font-size: 18rpx;
+      }
+    }
+    .benefit-item {
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+    }
+    .benefit-icon {
+      width: 100rpx;
+      height: 100rpx;
+      margin-bottom: 8rpx;
+    }
+    .benefit-text {
+      font-size: 20rpx;
+      color: #fff;
+    }
+  }
+}
+
+/* 任务标题 */
+.task-title {
+  font-size: 32rpx;
+  font-weight: bold;
+  margin: 32rpx 0 16rpx 0;
+}
+
+/* 任务项 */
+.task-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 20rpx 30rpx;
+  border: 1px solid #f2f2f2;
+  border-radius: 8rpx;
+  margin-bottom: 16rpx;
+  background-color: #f4f3f1;
+  .submit-btn {
+    width: 120rpx;
+  }
+}
+.task-desc {
+  display: flex;
+  flex-direction: column;
+  flex: 1;
+}
+.task-name {
+  font-size: 28rpx;
+  font-weight: bold;
+  margin-bottom: 4rpx;
+}
+.task-reward {
+  font-size: 24rpx;
+  color: #999;
+}
+
+.vip-level-box {
+  .vip-level-title {
+    font-size: 32rpx;
+    font-weight: bold;
+    text-align: center;
+    margin-bottom: 30rpx;
+  }
+  .title-box {
+    height: 60rpx;
+    background: #302f3f;
+    display: flex;
+    align-items: center;
+    .title-item {
+      flex: 1;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      color: #fff;
+    }
+  }
+  .title-content {
+    width: 100%;
+
+    min-height: 500rpx;
+
+    .content-list {
+      display: flex;
+    }
+
+    .content-item {
+      flex: 1;
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      justify-content: center;
+      font-size: 22rpx;
+      height: 120rpx;
+      border-bottom: 1px dashed #eee;
+      // width: 24%;
+      image {
+        height: 25rpx;
+        // height: 100%;
+      }
+    }
+    .quanyi {
+      align-items: flex-start;
+      // margin-right: 30rpx;
+    }
+    .gw {
+      margin-left: 5rpx;
+      &.zk {
+        letter-spacing: 6rpx;
+        margin-left: 9rpx;
+      }
+    }
+  }
+  .vip-level-img {
+    width: 100%;
+  }
+}
+.content-desc {
+  width: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-top: 20rpx;
+  font-size: 22rpx;
+}
+
+uni-page-body{
+	background-color: #fff !important;
+}
+</style>

+ 232 - 0
pages/webview/index.vue

@@ -0,0 +1,232 @@
+<template>
+  <view class="webview-wrapper">
+    <!-- 加载状态提示 -->
+    <view class="loading-mask" v-if="isLoading">
+      <uni-loading-icon type="circle" size="24" color="#333"></uni-loading-icon>
+      <text class="loading-txt">加载中...</text>
+    </view>
+
+    <web-view
+      :src="h5Url"
+      id="any-id"
+      @load="onWebViewLoad"
+      @error="onWebViewError"
+      @message="onWebViewMessage"
+    ></web-view>
+  </view>
+</template>
+
+<script setup>
+import { ref, onUnmounted } from "vue";
+import { onLoad } from "@dcloudio/uni-app";
+import { H5_BASE_URL, TOKENNAME, WHITELIST } from "@/config/app";
+import { useAppStore } from "@/stores/app";
+import { toLogin, checkLogin } from "@/libs/login";
+
+const h5Url = ref("");
+const appStore = useAppStore();
+const isLoading = ref(true);
+const errorMsg = ref("");
+
+onUnmounted(() => {
+  isLoading.value = false;
+});
+
+onLoad((query) => {
+  try {
+    const { path = "/", ...otherParams } = query;
+    let normalizedPath = normalizeH5Path(path);
+    const token = appStore.tokenComputed;
+
+    if (!token && !WHITELIST.includes(normalizedPath)) {
+      uni.showToast({
+        title: "登录已失效,请重新登录",
+        icon: "none",
+        duration: 1500,
+      });
+      setTimeout(() => {
+        toLogin();
+      }, 1500);
+      return;
+    }
+
+    if (!normalizedPath) {
+      uni.showToast({
+        title: "页面路径无效,默认跳转首页",
+        icon: "none",
+        duration: 1500,
+      });
+      normalizedPath = "/";
+    }
+
+    // 参数处理
+    const params = {
+      ...parseComplexParams(otherParams),
+      [TOKENNAME]: token,
+    };
+
+    const queryString = buildQueryString(params);
+
+    h5Url.value = `${H5_BASE_URL}/#${normalizedPath}${
+      queryString ? `?${queryString}` : ""
+    }`;
+    console.log("WebView 加载地址:", h5Url.value);
+  } catch (err) {
+    console.error("WebView 初始化失败:", err);
+    uni.showToast({ title: "页面加载异常", icon: "none", duration: 1500 });
+    setTimeout(() => uni.navigateBack(), 1500);
+  }
+});
+
+// 标准化H5路径
+const normalizeH5Path = (path) => {
+  if (typeof path !== "string") {
+    console.warn("H5路径格式错误:非字符串类型");
+    return null;
+  }
+
+  // URL解码
+  let decodedPath;
+  try {
+    decodedPath = decodeURIComponent(path);
+  } catch (err) {
+    console.error("H5路径解码失败(非法编码格式):", err, "原始路径:", path);
+    return null;
+  }
+
+  const purePath = decodedPath.split(/[?#]/)[0];
+
+  const pathReg = /^\/[a-zA-Z0-9\/:\.\-_\u4e00-\u9fa5]*$/;
+  const isValid = pathReg.test(purePath);
+
+  if (!isValid) {
+    console.warn("H5路径格式非法(含不允许字符):", purePath);
+    return null;
+  }
+
+  // 第五步:最终处理(去除末尾多余的/,避免重复路径如/path// → /path)
+  const normalizedPath = purePath.replace(/\/+$/, "") || "/";
+
+  return normalizedPath;
+};
+/**
+ * 解析复杂参数
+ * @param {object} params - 原始参数
+ * @returns {object} 处理后的参数
+ */
+const parseComplexParams = (params) => {
+  const result = {};
+  Object.entries(params).forEach(([key, value]) => {
+    if (value === undefined || value === null) return;
+
+    if (Array.isArray(value)) {
+      value.forEach((item) => {
+        const encodedItem = encodeURIComponent(item);
+        if (result[key]) {
+          result[key] = `${result[key]},${encodedItem}`;
+        } else {
+          result[key] = encodedItem;
+        }
+      });
+    } else if (typeof value === "object") {
+      result[key] = encodeURIComponent(JSON.stringify(value));
+    }
+    // 基础类型直接保留
+    else {
+      result[key] = value;
+    }
+  });
+  return result;
+};
+
+/**
+ * 构建编码后的查询参数串
+ * @param {object} params - 处理后的参数
+ * @returns {string} 编码后的查询串
+ */
+const buildQueryString = (params) => {
+  return Object.entries(params)
+    .filter(
+      ([_, value]) => value !== undefined && value !== null && value !== ""
+    )
+    .map(
+      ([key, value]) =>
+        `${encodeURIComponent(key)}=${encodeURIComponent(value)}`
+    )
+    .join("&");
+};
+
+/**
+ * web-view 加载成功
+ */
+const onWebViewLoad = () => {
+  isLoading.value = false; // 隐藏加载中
+};
+
+/**
+ * web-view 加载失败
+ * @param {object} err - 错误信息
+ */
+const onWebViewError = (err) => {
+  isLoading.value = false;
+  errorMsg.value = `页面加载失败:${err.detail.errMsg}`;
+  uni.showToast({ title: errorMsg.value, icon: "none", duration: 2000 });
+  console.error("WebView 加载错误:", err);
+  setTimeout(() => uni.navigateBack(), 1500);
+};
+
+/**
+ * 接收 H5 发送的消息
+ * @param {object} e - 消息事件
+ */
+const onWebViewMessage = (e) => {
+  const h5Msg = e.detail.data[0]; // H5 发送的消息格式为 { data: [消息体] }
+  console.log("接收 H5 消息:", h5Msg);
+
+  // 示例:H5 触发「返回小程序」
+  if (h5Msg.type === "navigateBack") {
+    uni.navigateBack({ delta: h5Msg.delta || 1 });
+  }
+
+  if (h5Msg.type === "refreshToken") {
+    appStore.refreshToken().then((newToken) => {
+      const webview = uni.createSelectorQuery().select("#any-id");
+      webview
+        .context((res) => {
+          res.context.postMessage({
+            data: { type: "newToken", token: newToken },
+          });
+        })
+        .exec();
+    });
+  }
+};
+</script>
+
+<style scoped>
+.webview-wrapper {
+  width: 100vw;
+  height: 100vh;
+  position: relative;
+}
+
+.loading-mask {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background: rgba(255, 255, 255, 0.8);
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  z-index: 999;
+}
+
+.loading-txt {
+  margin-top: 16rpx;
+  font-size: 28rpx;
+  color: #666;
+}
+</style>