SVIP.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411
  1. <template>
  2. <view class="level-container">
  3. <!-- 顶部会员信息区域 -->
  4. <view class="vip-info">
  5. <view class="flex-between vip-box">
  6. <view class="flex_2">
  7. <view class="flex-between">
  8. <view class="vip-text">
  9. <view class="vip-title">SVIP会员</view>
  10. <view class="vip-desc"
  11. >会员等级 {{ appStore?.$userInfo?.level || 0 }}</view
  12. >
  13. </view>
  14. <view class="point-info">
  15. <text class="point-text">{{
  16. appStore?.$userInfo?.integral || 0
  17. }}</text>
  18. <text class="point-desc">会员贝币</text>
  19. </view>
  20. </view>
  21. <view>
  22. <view class="growth-info">
  23. <text>成长值 {{ appStore?.$userInfo?.experience || 0 }}</text>
  24. <text class="growth-progress">
  25. {{ appStore?.$userInfo?.experience || 0 }}/{{
  26. appStore?.$userInfo?.experienceCount || 0
  27. }}
  28. </text>
  29. </view>
  30. <up-line-progress
  31. :percentage="percentExperience"
  32. activeColor="#d6c3a3"
  33. inactiveColor="#808080"
  34. height="6"
  35. :showText="false"
  36. ></up-line-progress>
  37. </view>
  38. </view>
  39. </view>
  40. </view>
  41. <!-- SVIP尊享权益 -->
  42. <view class="vip-benefit">
  43. <view class="task-title">SVIP尊享权益</view>
  44. <view class="task-item" v-for="(task, index) in taskList" :key="index">
  45. <image class="task-icon" :src="task.icon"></image>
  46. <view class="task-desc flex-center-between flex_1">
  47. <text class="task-name">{{ task.name }}</text>
  48. <text class="task-reward flex_1">{{ task.reward }}</text>
  49. </view>
  50. </view>
  51. </view>
  52. <!-- SVIP购买按钮(仅非SVIP用户显示) -->
  53. <view class="svip-price" v-if="!isSvip">
  54. <view class="flex-center" @click="handlePayOpen">
  55. <view class="svip-price-btn">
  56. <text class="svip-price-name">年费会员¥</text>
  57. <text class="svip-price-num">{{ totalPrice }}</text>
  58. </view>
  59. </view>
  60. </view>
  61. <WechatPayment ref="wechatPaymentRef" />
  62. </view>
  63. </template>
  64. <script setup>
  65. import { ref, computed } from "vue";
  66. import { useAppStore } from "@/stores/app";
  67. import { svipPrice, svipsaveAPI } from "@/api/svip";
  68. import { getUserInfo } from "@/api/user";
  69. // 引入封装的微信支付组件(替换原paymentCommon)
  70. import WechatPayment from "@/components/payment/WechatPayment.vue";
  71. // 引入后端接口(按需补充:如“创建会员订单”“标记SVIP身份”的接口)
  72. // import { createVipOrder, markUserSvip } from "@/api/vip";
  73. import { onLoad } from "@dcloudio/uni-app";
  74. import { getMiniProgramData } from "@/api/api";
  75. // 状态管理
  76. const appStore = useAppStore();
  77. const wechatPaymentRef = ref(null); // 微信支付组件引用
  78. const totalPrice = ref(0); // 会员价格(单位:元,从接口获取)
  79. const orderId = ref(""); // 后端生成的会员订单ID(用于后续标记SVIP)
  80. // 计算是否为SVIP用户
  81. const isSvip = computed(() => {
  82. return appStore.isLogin && !!appStore?.$userInfo?.svip;
  83. });
  84. // 获取用户信息
  85. async function fetchUserInfo() {
  86. const { data } = await getUserInfo();
  87. appStore.UPDATE_USERINFO(data);
  88. }
  89. // 会员权益列表
  90. const taskList = ref([
  91. {
  92. name: "每日福利",
  93. reward: "200成长值",
  94. icon: "https://sb-admin.oss-cn-shenzhen.aliyuncs.com/shuibei-mini/svip/%E8%B7%AF%E5%BE%84733%403x.png",
  95. },
  96. {
  97. name: "回收特权",
  98. reward: "回收价+0.02/克",
  99. icon: "https://sb-admin.oss-cn-shenzhen.aliyuncs.com/shuibei-mini/svip/huishoutequan.png",
  100. },
  101. {
  102. name: "整点秒杀",
  103. reward: "抄底补贴,限时抢购",
  104. icon: "https://sb-admin.oss-cn-shenzhen.aliyuncs.com/shuibei-mini/svip/zhengdianmiaosha.png",
  105. },
  106. {
  107. name: "超级工具",
  108. reward: "自动回收,金价预警使用权限",
  109. icon: "https://sb-admin.oss-cn-shenzhen.aliyuncs.com/shuibei-mini/svip/chaojigongju.png",
  110. },
  111. {
  112. name: "邮费减半",
  113. reward: "邮费全部五折",
  114. icon: "https://sb-admin.oss-cn-shenzhen.aliyuncs.com/shuibei-mini/svip/youfeijianban.png",
  115. },
  116. {
  117. name: "专属服务群",
  118. reward: "专属福利,优先传达",
  119. icon: "https://sb-admin.oss-cn-shenzhen.aliyuncs.com/shuibei-mini/svip/zhuanshufuli.png",
  120. },
  121. ]);
  122. // 成长值进度百分比
  123. const percentExperience = computed(() => {
  124. if (!appStore.isLogin) return 0;
  125. const current = appStore.$userInfo.experience || 0;
  126. const target = appStore.$userInfo.experienceCount || 1; // 避免除以0
  127. const percent = Math.floor((current / target) * 100);
  128. return percent > 100 ? 100 : percent;
  129. });
  130. // 页面加载时获取会员价格
  131. onLoad(() => {
  132. getSvipPrice();
  133. appStore.isLogin && updateWxSettingInfo();
  134. });
  135. const updateWxSettingInfo = async () => {
  136. const { data } = await getMiniProgramData();
  137. if (!data?.metalConfigs) return;
  138. // svip权益
  139. let svipGoldPrice = 0;
  140. let svipPtPrice = 0;
  141. let svipAgPrice = 0;
  142. data.metalConfigs.forEach((item) => {
  143. switch (item.metalType) {
  144. case "au":
  145. svipGoldPrice = item.svipDiscountPerGram || 0;
  146. break;
  147. case "pt":
  148. svipPtPrice = item.svipDiscountPerGram || 0;
  149. break;
  150. case "ag":
  151. svipAgPrice = item.svipDiscountPerGram || 0;
  152. break;
  153. default:
  154. break;
  155. }
  156. });
  157. taskList.value[1].reward = `黄金回收价+${svipGoldPrice}/克\n铂金回收价+${svipPtPrice}/克\n白银回收价+${svipAgPrice}/克`;
  158. };
  159. /**
  160. * 获取SVIP会员价格(单位:元)
  161. */
  162. const getSvipPrice = async () => {
  163. try {
  164. const res = await svipPrice();
  165. totalPrice.value = Number(res.data) || 99; // 默认99元,防止接口返回异常
  166. } catch (err) {
  167. totalPrice.value = 99; // 兜底价格
  168. }
  169. };
  170. /**
  171. * 点击“开通会员”触发:发起微信支付
  172. */
  173. const handlePayOpen = async () => {
  174. // 1. 前置校验:是否登录、是否已为SVIP
  175. if (!appStore.isLogin) {
  176. uni.showToast({ title: "未登录", icon: "none" });
  177. console.log("未登录");
  178. return;
  179. }
  180. if (isSvip.value) {
  181. uni.showToast({ title: "已经是svip会员", icon: "none" });
  182. console.log("已经是svip会员");
  183. return;
  184. }
  185. console.log("没有openId:", appStore?.$userInfo);
  186. // 2. 校验用户openId(微信支付必需)
  187. const userOpenId = appStore?.$userInfo?.openId; // 假设用户信息中存储了openId
  188. if (!userOpenId) {
  189. uni.showToast({ title: "用户openId获取失败", icon: "none" });
  190. console.log("没有openId:");
  191. return;
  192. }
  193. // 3. 调用微信支付组件的支付方法
  194. wechatPaymentRef.value.createUniPay({
  195. // 支付金额:元转分(微信支付要求单位为“分”)
  196. amount: Math.round(totalPrice.value * 100),
  197. description: `SVIP年费会员(¥${totalPrice.value})`, // 商品描述(显示在微信支付账单中)
  198. openId: userOpenId, // 微信支付必需:用户的微信openId
  199. orderPrefix: "SVIP", // 订单号前缀(与公共组件generateCustomId匹配)
  200. /**
  201. * 回调1:支付订单创建成功(用户尚未支付)
  202. * 作用:记录后端生成的会员订单ID,用于后续支付成功后标记SVIP
  203. */
  204. onUniPayCreate: async (payRes) => {
  205. console.log("会员支付订单创建成功:", payRes);
  206. try {
  207. } catch (err) {
  208. console.error("创建会员订单异常:", err);
  209. }
  210. },
  211. /**
  212. * 回调2:支付成功(核心逻辑)
  213. * 作用:通知后端标记用户为SVIP,刷新用户状态
  214. */
  215. onUniPaySuccess: async (payStatusRes) => {
  216. console.log("SVIP会员支付成功:", payStatusRes);
  217. try {
  218. const res = await svipsaveAPI({
  219. orderId: payStatusRes.out_trade_no,
  220. price: totalPrice.value,
  221. payType: "weixin",
  222. });
  223. appStore.USERINFO();
  224. } catch (err) {
  225. console.error("标记SVIP身份异常:", err);
  226. }
  227. },
  228. /**
  229. * 回调3:用户取消支付
  230. */
  231. onUniPayCancel: () => {
  232. console.log("用户取消SVIP支付");
  233. },
  234. /**
  235. * 回调4:支付失败(如网络异常、余额不足等)
  236. */
  237. onUniPayFail: (err) => {
  238. const errMsg = err.message || "支付失败,请重新尝试";
  239. console.error("SVIP会员支付失败:", err);
  240. },
  241. });
  242. };
  243. </script>
  244. <style scoped lang="scss">
  245. .level-container {
  246. color: black;
  247. position: relative;
  248. top: -130rpx;
  249. }
  250. /* 顶部会员信息 */
  251. .vip-info {
  252. color: #dfd7bc;
  253. margin-bottom: -25rpx;
  254. overflow: hidden;
  255. background-image: url("https://sb-admin.oss-cn-shenzhen.aliyuncs.com/shuibei-mini/svip/svip-bg.jpg");
  256. background-size: 100%;
  257. background-repeat: no-repeat;
  258. height: 450rpx;
  259. .vip-box {
  260. margin: 200rpx 90rpx 100rpx 265rpx;
  261. }
  262. }
  263. .vip-avatar {
  264. width: 151rpx;
  265. height: 161rpx;
  266. margin-right: 70rpx;
  267. margin-top: 50rpx;
  268. }
  269. .vip-title {
  270. font-size: 36rpx;
  271. font-weight: bold;
  272. margin-bottom: 8rpx;
  273. }
  274. .vip-desc {
  275. font-size: 24rpx;
  276. margin-bottom: 12rpx;
  277. }
  278. .growth-info {
  279. display: flex;
  280. justify-content: space-between;
  281. align-items: center;
  282. margin-top: 20rpx;
  283. margin-bottom: 5rpx;
  284. font-size: 20rpx;
  285. }
  286. .point-info {
  287. text-align: right;
  288. }
  289. .point-text {
  290. font-size: 40rpx;
  291. font-weight: bold;
  292. display: block;
  293. }
  294. .point-desc {
  295. font-size: 20rpx;
  296. opacity: 0.8;
  297. }
  298. /* VIP尊享权益 */
  299. .vip-benefit {
  300. padding: 30rpx;
  301. }
  302. .task-title {
  303. font-size: 32rpx;
  304. font-weight: bold;
  305. margin: 32rpx 0 16rpx 0;
  306. }
  307. .task-item {
  308. display: flex;
  309. justify-content: space-between;
  310. align-items: center;
  311. padding: 20rpx 30rpx;
  312. border: 1px solid #f2f2f2;
  313. border-radius: 8rpx;
  314. margin-bottom: 16rpx;
  315. background-color: #f4f3f1;
  316. .task-icon {
  317. width: 70rpx;
  318. height: 70rpx;
  319. margin-right: 30rpx;
  320. }
  321. }
  322. .task-desc {
  323. .task-name {
  324. font-size: 28rpx;
  325. font-weight: bold;
  326. margin-right: 30rpx;
  327. width: 160rpx;
  328. text-align: center;
  329. }
  330. .task-reward {
  331. font-size: 24rpx;
  332. color: #5d5c5a;
  333. }
  334. }
  335. /* SVIP购买按钮 */
  336. .svip-price {
  337. .svip-price-title {
  338. font-size: 32rpx;
  339. font-weight: bold;
  340. text-align: center;
  341. }
  342. .flex-center {
  343. display: flex;
  344. justify-content: center;
  345. align-items: center;
  346. }
  347. .svip-price-btn {
  348. margin-top: 20rpx;
  349. display: inline-block;
  350. border: 1px solid #fdfdf9;
  351. border-radius: 20rpx;
  352. padding: 40rpx 100rpx;
  353. background-color: #fee1a9;
  354. color: #080604;
  355. font-size: 24rpx;
  356. cursor: pointer;
  357. .svip-price-num {
  358. font-size: 40rpx;
  359. font-weight: bold;
  360. margin-left: 10rpx;
  361. }
  362. }
  363. }
  364. /* 工具类(如果项目中没有全局flex工具类,需保留) */
  365. .flex-between {
  366. display: flex;
  367. justify-content: space-between;
  368. align-items: center;
  369. }
  370. .flex-center-between {
  371. display: flex;
  372. justify-content: space-between;
  373. align-items: center;
  374. }
  375. .flex_1 {
  376. flex: 1;
  377. }
  378. .flex_2 {
  379. flex: 2;
  380. }
  381. </style>