晚上邀请逻辑.
This commit is contained in:
parent
18b5304119
commit
3257210c39
|
|
@ -1,13 +1,55 @@
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
const axios = require('axios');
|
||||||
const { authenticateUser } = require('../middleware/auth');
|
const { authenticateUser } = require('../middleware/auth');
|
||||||
const User = require('../models/User');
|
const User = require('../models/User');
|
||||||
|
const env = require('../config/env');
|
||||||
|
|
||||||
|
// 缓存 access_token
|
||||||
|
let accessTokenCache = {
|
||||||
|
token: null,
|
||||||
|
expiresAt: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取微信 access_token
|
||||||
|
*/
|
||||||
|
async function getAccessToken() {
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// 如果缓存有效,直接返回
|
||||||
|
if (accessTokenCache.token && accessTokenCache.expiresAt > now) {
|
||||||
|
return accessTokenCache.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用微信API获取 access_token
|
||||||
|
const url = 'https://api.weixin.qq.com/cgi-bin/token';
|
||||||
|
const response = await axios.get(url, {
|
||||||
|
params: {
|
||||||
|
grant_type: 'client_credential',
|
||||||
|
appid: env.wechat.appId,
|
||||||
|
secret: env.wechat.appSecret
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.errcode) {
|
||||||
|
throw new Error(`获取access_token失败: ${response.data.errmsg}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缓存 token,提前5分钟过期
|
||||||
|
accessTokenCache = {
|
||||||
|
token: response.data.access_token,
|
||||||
|
expiresAt: now + (response.data.expires_in - 300) * 1000
|
||||||
|
};
|
||||||
|
|
||||||
|
return accessTokenCache.token;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @swagger
|
* @swagger
|
||||||
* /api/v1/qrcode/miniprogram:
|
* /api/v1/qrcode/miniprogram:
|
||||||
* post:
|
* post:
|
||||||
* summary: 获取小程序码信息
|
* summary: 获取小程序码
|
||||||
* tags: [QRCode]
|
* tags: [QRCode]
|
||||||
* security:
|
* security:
|
||||||
* - bearerAuth: []
|
* - bearerAuth: []
|
||||||
|
|
@ -17,18 +59,17 @@ const User = require('../models/User');
|
||||||
* schema:
|
* schema:
|
||||||
* type: object
|
* type: object
|
||||||
* properties:
|
* properties:
|
||||||
* path:
|
|
||||||
* type: string
|
|
||||||
* width:
|
* width:
|
||||||
* type: number
|
* type: number
|
||||||
|
* description: 二维码宽度,默认430
|
||||||
* responses:
|
* responses:
|
||||||
* 200:
|
* 200:
|
||||||
* description: 成功
|
* description: 成功返回小程序码图片的base64
|
||||||
*/
|
*/
|
||||||
router.post('/miniprogram', authenticateUser, async (req, res) => {
|
router.post('/miniprogram', authenticateUser, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const userId = req.user.id;
|
const userId = req.user.id;
|
||||||
const { path, width } = req.body;
|
const { width = 430 } = req.body;
|
||||||
|
|
||||||
// 获取用户邀请码
|
// 获取用户邀请码
|
||||||
const user = await User.findByPk(userId, {
|
const user = await User.findByPk(userId, {
|
||||||
|
|
@ -37,8 +78,9 @@ router.post('/miniprogram', authenticateUser, async (req, res) => {
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
success: false,
|
code: 404,
|
||||||
error: { code: 'USER_NOT_FOUND', message: 'User not found' }
|
message: 'User not found',
|
||||||
|
data: null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -49,20 +91,52 @@ router.post('/miniprogram', authenticateUser, async (req, res) => {
|
||||||
await user.update({ invitationCode });
|
await user.update({ invitationCode });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 目前返回邀请码信息,前端可以用 canvas 绘制
|
// 获取 access_token
|
||||||
|
const accessToken = await getAccessToken();
|
||||||
|
|
||||||
|
// 调用微信API生成小程序码(使用 getUnlimited 接口,无数量限制)
|
||||||
|
const wxUrl = `https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=${accessToken}`;
|
||||||
|
const wxResponse = await axios.post(wxUrl, {
|
||||||
|
scene: `inviteCode=${invitationCode}`, // scene 参数,最长32字符
|
||||||
|
page: 'pages/index/index', // 必须是已发布的小程序页面
|
||||||
|
width: width,
|
||||||
|
auto_color: false,
|
||||||
|
line_color: { r: 0, g: 0, b: 0 },
|
||||||
|
is_hyaline: false
|
||||||
|
}, {
|
||||||
|
responseType: 'arraybuffer' // 返回的是图片二进制数据
|
||||||
|
});
|
||||||
|
|
||||||
|
// 检查是否返回错误(错误时返回JSON)
|
||||||
|
const contentType = wxResponse.headers['content-type'];
|
||||||
|
if (contentType && contentType.includes('application/json')) {
|
||||||
|
const errorData = JSON.parse(wxResponse.data.toString());
|
||||||
|
console.error('微信小程序码生成失败:', errorData);
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
code: 500,
|
||||||
|
message: errorData.errmsg || '生成小程序码失败',
|
||||||
|
data: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为 base64
|
||||||
|
const base64Image = Buffer.from(wxResponse.data).toString('base64');
|
||||||
|
const dataUrl = `data:image/png;base64,${base64Image}`;
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
code: 0,
|
||||||
|
message: 'success',
|
||||||
data: {
|
data: {
|
||||||
invitationCode,
|
invitationCode,
|
||||||
path: path || `pages/index/index?inviteCode=${invitationCode}`,
|
qrcodeUrl: dataUrl // base64格式的小程序码图片
|
||||||
message: '请使用邀请码分享给好友'
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Get miniprogram qrcode error:', error);
|
console.error('Get miniprogram qrcode error:', error);
|
||||||
return res.status(500).json({
|
return res.status(200).json({
|
||||||
success: false,
|
code: 500,
|
||||||
error: { code: 'QRCODE_ERROR', message: error.message }
|
message: error.message || '生成小程序码失败',
|
||||||
|
data: null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
4
miniprogram/dist/dev/mp-weixin/app.js
vendored
4
miniprogram/dist/dev/mp-weixin/app.js
vendored
|
|
@ -44,8 +44,10 @@ const _sfc_main = {
|
||||||
user: null,
|
user: null,
|
||||||
shouldRefresh: false,
|
shouldRefresh: false,
|
||||||
loginTime: 0,
|
loginTime: 0,
|
||||||
config: null
|
config: null,
|
||||||
// 应用配置(从后端获取)
|
// 应用配置(从后端获取)
|
||||||
|
inviteCode: ""
|
||||||
|
// 邀请码(从分享链接获取)
|
||||||
},
|
},
|
||||||
onLaunch: function() {
|
onLaunch: function() {
|
||||||
console.log("App Launch");
|
console.log("App Launch");
|
||||||
|
|
|
||||||
1864
miniprogram/dist/dev/mp-weixin/common/vendor.js
vendored
1864
miniprogram/dist/dev/mp-weixin/common/vendor.js
vendored
File diff suppressed because it is too large
Load Diff
|
|
@ -185,9 +185,13 @@ AppServer.prototype.deleteData = async function(url) {
|
||||||
return err;
|
return err;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
AppServer.prototype.WechatLogin = async function(code) {
|
AppServer.prototype.WechatLogin = async function(code, invitationCode) {
|
||||||
var url = serverConfig.apiUrl_Auth_WechatLogin;
|
var url = serverConfig.apiUrl_Auth_WechatLogin;
|
||||||
return this.postData(url, { code }).then((data) => {
|
var postData = { code };
|
||||||
|
if (invitationCode) {
|
||||||
|
postData.invitationCode = invitationCode;
|
||||||
|
}
|
||||||
|
return this.postData(url, postData).then((data) => {
|
||||||
return data;
|
return data;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,363 +0,0 @@
|
||||||
"use strict";
|
|
||||||
const common_vendor = require("../../../../common/vendor.js");
|
|
||||||
let qrcode;
|
|
||||||
const _sfc_main = {
|
|
||||||
name: "u-qrcode",
|
|
||||||
props: {
|
|
||||||
cid: {
|
|
||||||
type: String,
|
|
||||||
default: () => `u-qrcode-canvas${Math.floor(Math.random() * 1e6)}`
|
|
||||||
},
|
|
||||||
size: {
|
|
||||||
type: Number,
|
|
||||||
default: 200
|
|
||||||
},
|
|
||||||
unit: {
|
|
||||||
type: String,
|
|
||||||
default: "px"
|
|
||||||
},
|
|
||||||
show: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
val: {
|
|
||||||
type: String,
|
|
||||||
default: ""
|
|
||||||
},
|
|
||||||
background: {
|
|
||||||
type: String,
|
|
||||||
default: "#ffffff"
|
|
||||||
},
|
|
||||||
foreground: {
|
|
||||||
type: String,
|
|
||||||
default: "#000000"
|
|
||||||
},
|
|
||||||
pdground: {
|
|
||||||
type: String,
|
|
||||||
default: "#000000"
|
|
||||||
},
|
|
||||||
icon: {
|
|
||||||
type: String,
|
|
||||||
default: ""
|
|
||||||
},
|
|
||||||
iconSize: {
|
|
||||||
type: Number,
|
|
||||||
default: 40
|
|
||||||
},
|
|
||||||
lv: {
|
|
||||||
type: Number,
|
|
||||||
default: 3
|
|
||||||
},
|
|
||||||
onval: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
loadMake: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
usingComponents: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
showLoading: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
loadingText: {
|
|
||||||
type: String,
|
|
||||||
default: "生成中"
|
|
||||||
},
|
|
||||||
allowPreview: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
// 是否使用根节点宽高
|
|
||||||
useRootHeightAndWidth: {
|
|
||||||
type: Boolean,
|
|
||||||
default: () => false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
emits: ["result", "longpressCallback"],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
loading: false,
|
|
||||||
result: "",
|
|
||||||
popupShow: false,
|
|
||||||
list: [
|
|
||||||
{
|
|
||||||
name: "保存二维码"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
rootId: `rootId${Number(Math.random() * 100).toFixed(0)}`,
|
|
||||||
ganvas: null,
|
|
||||||
context: "",
|
|
||||||
canvasObj: {},
|
|
||||||
sizeLocal: this.size,
|
|
||||||
ctx: null,
|
|
||||||
// ctx 在new Qrcode 时js文件内部设置
|
|
||||||
canvas: null
|
|
||||||
// ctx 在new Qrcode 时js文件内部设置
|
|
||||||
};
|
|
||||||
},
|
|
||||||
async mounted() {
|
|
||||||
if (this.useRootHeightAndWidth) {
|
|
||||||
await this.setNewSize();
|
|
||||||
}
|
|
||||||
if (this.loadMake) {
|
|
||||||
if (!this._empty(this.val)) {
|
|
||||||
setTimeout(() => {
|
|
||||||
setTimeout(() => {
|
|
||||||
this._makeCode();
|
|
||||||
});
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
_makeCode() {
|
|
||||||
let that2 = this;
|
|
||||||
if (!this._empty(this.val)) {
|
|
||||||
this.loading = true;
|
|
||||||
qrcode = new common_vendor.QRCode({
|
|
||||||
context: that2,
|
|
||||||
// 上下文环境
|
|
||||||
canvasId: that2.cid,
|
|
||||||
// canvas-id
|
|
||||||
nvueContext: that2.context,
|
|
||||||
usingComponents: that2.usingComponents,
|
|
||||||
// 是否是自定义组件
|
|
||||||
showLoading: false,
|
|
||||||
// 是否显示loading
|
|
||||||
loadingText: that2.loadingText,
|
|
||||||
// loading文字
|
|
||||||
text: that2.val,
|
|
||||||
// 生成内容
|
|
||||||
size: that2.sizeLocal,
|
|
||||||
// 二维码大小
|
|
||||||
background: that2.background,
|
|
||||||
// 背景色
|
|
||||||
foreground: that2.foreground,
|
|
||||||
// 前景色
|
|
||||||
pdground: that2.pdground,
|
|
||||||
// 定位角点颜色
|
|
||||||
correctLevel: that2.lv,
|
|
||||||
// 容错级别
|
|
||||||
image: that2.icon,
|
|
||||||
// 二维码图标
|
|
||||||
imageSize: that2.iconSize,
|
|
||||||
// 二维码图标大小
|
|
||||||
cbResult: function(res) {
|
|
||||||
that2._result(res);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
common_vendor.index.showToast({
|
|
||||||
title: "二维码内容不能为空",
|
|
||||||
icon: "none",
|
|
||||||
duration: 2e3
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_clearCode() {
|
|
||||||
this._result("");
|
|
||||||
qrcode.clear();
|
|
||||||
},
|
|
||||||
_saveCode() {
|
|
||||||
let that2 = this;
|
|
||||||
if (this.result != "") {
|
|
||||||
common_vendor.index.saveImageToPhotosAlbum({
|
|
||||||
filePath: that2.result,
|
|
||||||
success: function() {
|
|
||||||
common_vendor.index.showToast({
|
|
||||||
title: "二维码保存成功",
|
|
||||||
icon: "success",
|
|
||||||
duration: 2e3
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
preview(e) {
|
|
||||||
if (this.allowPreview) {
|
|
||||||
common_vendor.index.previewImage({
|
|
||||||
urls: [this.result],
|
|
||||||
longPressActions: {
|
|
||||||
itemList: ["保存二维码图片"],
|
|
||||||
success: function(data) {
|
|
||||||
switch (data.tapIndex) {
|
|
||||||
case 0:
|
|
||||||
that._saveCode();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fail: function(err) {
|
|
||||||
console.log(err.errMsg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.$emit("preview", {
|
|
||||||
url: this.result
|
|
||||||
}, e);
|
|
||||||
},
|
|
||||||
async toTempFilePath({ success, fail }) {
|
|
||||||
if (this.context) {
|
|
||||||
this.ctx.toTempFilePath(
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
this.sizeLocal,
|
|
||||||
this.sizeLocal,
|
|
||||||
this.sizeLocal,
|
|
||||||
this.sizeLocal,
|
|
||||||
"",
|
|
||||||
1,
|
|
||||||
(res) => {
|
|
||||||
success(res);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const canvas = await this.getNode(this.cid, true);
|
|
||||||
common_vendor.index.canvasToTempFilePath(
|
|
||||||
{
|
|
||||||
canvas,
|
|
||||||
success: (res) => {
|
|
||||||
success(res);
|
|
||||||
},
|
|
||||||
fail
|
|
||||||
},
|
|
||||||
this
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async longpress() {
|
|
||||||
this.toTempFilePath({
|
|
||||||
success: (res) => {
|
|
||||||
this.$emit("longpressCallback", res.tempFilePath);
|
|
||||||
},
|
|
||||||
fail: (err) => {
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 使用根节点宽高 设置新的size
|
|
||||||
* @return {Promise<void>}
|
|
||||||
*/
|
|
||||||
async setNewSize() {
|
|
||||||
const rootNode = await this.getNode(this.rootId, false);
|
|
||||||
const { width, height } = rootNode;
|
|
||||||
if (width > height) {
|
|
||||||
this.sizeLocal = height;
|
|
||||||
} else {
|
|
||||||
this.sizeLocal = width;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 获取节点
|
|
||||||
* @param id 节点id
|
|
||||||
* @param isCanvas 是否为Canvas节点
|
|
||||||
* @return {Promise<unknown>}
|
|
||||||
*/
|
|
||||||
async getNode(id, isCanvas) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
try {
|
|
||||||
const query = common_vendor.index.createSelectorQuery().in(this);
|
|
||||||
query.select(`#${id}`).fields({ node: true, size: true }).exec((res) => {
|
|
||||||
if (isCanvas) {
|
|
||||||
resolve(res[0].node);
|
|
||||||
} else {
|
|
||||||
resolve(res[0]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
console.error("获取节点失败", e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
selectClick(index) {
|
|
||||||
switch (index) {
|
|
||||||
case 0:
|
|
||||||
alert("保存二维码");
|
|
||||||
this._saveCode();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_result(res) {
|
|
||||||
this.loading = false;
|
|
||||||
this.result = res;
|
|
||||||
this.$emit("result", res);
|
|
||||||
},
|
|
||||||
_empty(v) {
|
|
||||||
let tp = typeof v, rt = false;
|
|
||||||
if (tp == "number" && String(v) == "") {
|
|
||||||
rt = true;
|
|
||||||
} else if (tp == "undefined") {
|
|
||||||
rt = true;
|
|
||||||
} else if (tp == "object") {
|
|
||||||
if (JSON.stringify(v) == "{}" || JSON.stringify(v) == "[]" || v == null)
|
|
||||||
rt = true;
|
|
||||||
} else if (tp == "string") {
|
|
||||||
if (v == "" || v == "undefined" || v == "null" || v == "{}" || v == "[]")
|
|
||||||
rt = true;
|
|
||||||
} else if (tp == "function") {
|
|
||||||
rt = false;
|
|
||||||
}
|
|
||||||
return rt;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
size: function(n, o) {
|
|
||||||
if (n != o && !this._empty(n)) {
|
|
||||||
this.cSize = n;
|
|
||||||
if (!this._empty(this.val)) {
|
|
||||||
setTimeout(() => {
|
|
||||||
this._makeCode();
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
val: function(n, o) {
|
|
||||||
if (this.onval) {
|
|
||||||
if (n != o && !this._empty(n)) {
|
|
||||||
setTimeout(() => {
|
|
||||||
this._makeCode();
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {}
|
|
||||||
};
|
|
||||||
if (!Array) {
|
|
||||||
const _easycom_up_loading_icon2 = common_vendor.resolveComponent("up-loading-icon");
|
|
||||||
_easycom_up_loading_icon2();
|
|
||||||
}
|
|
||||||
const _easycom_up_loading_icon = () => "../u-loading-icon/u-loading-icon.js";
|
|
||||||
if (!Math) {
|
|
||||||
_easycom_up_loading_icon();
|
|
||||||
}
|
|
||||||
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
|
|
||||||
return common_vendor.e({
|
|
||||||
a: $props.cid,
|
|
||||||
b: $props.cid,
|
|
||||||
c: $data.sizeLocal + $props.unit,
|
|
||||||
d: $data.sizeLocal + $props.unit,
|
|
||||||
e: $props.showLoading && $data.loading
|
|
||||||
}, $props.showLoading && $data.loading ? {
|
|
||||||
f: common_vendor.p({
|
|
||||||
vertical: true,
|
|
||||||
text: $props.loadingText,
|
|
||||||
textSize: "14px"
|
|
||||||
}),
|
|
||||||
g: $data.sizeLocal + $props.unit,
|
|
||||||
h: $data.sizeLocal + $props.unit
|
|
||||||
} : {}, {
|
|
||||||
i: common_vendor.o((...args) => $options.preview && $options.preview(...args)),
|
|
||||||
j: $data.rootId,
|
|
||||||
k: $props.useRootHeightAndWidth ? "100%" : "auto",
|
|
||||||
l: $props.useRootHeightAndWidth ? "100%" : "auto",
|
|
||||||
m: common_vendor.o((...args) => $options.longpress && $options.longpress(...args))
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const Component = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render], ["__scopeId", "data-v-444ebaa9"], ["__file", "F:/gitCode/uniapp/appointment_system/miniprogram/node_modules/uview-plus/components/u-qrcode/u-qrcode.vue"]]);
|
|
||||||
wx.createComponent(Component);
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
{
|
|
||||||
"component": true,
|
|
||||||
"usingComponents": {
|
|
||||||
"up-loading-icon": "../u-loading-icon/u-loading-icon"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
<view class="u-qrcode data-v-444ebaa9" id="{{j}}" style="{{'width:' + k + ';' + ('height:' + l)}}" bindlongpress="{{m}}"><view class="u-qrcode__content data-v-444ebaa9" bindtap="{{i}}"><block wx:if="{{r0}}"><canvas class="u-qrcode__canvas data-v-444ebaa9" id="{{a}}" canvas-id="{{b}}" type="2d" style="{{'width:' + c + ';' + ('height:' + d)}}"/></block><view wx:if="{{e}}" class="u-qrcode__loading data-v-444ebaa9" style="{{'width:' + g + ';' + ('height:' + h)}}"><up-loading-icon wx:if="{{f}}" class="data-v-444ebaa9" u-i="444ebaa9-0" bind:__l="__l" u-p="{{f}}"></up-loading-icon></view></view></view>
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
/**
|
|
||||||
* 这里是uni-app内置的常用样式变量
|
|
||||||
*
|
|
||||||
* uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
|
|
||||||
* 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
/**
|
|
||||||
* 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
|
|
||||||
*
|
|
||||||
* 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
|
|
||||||
*/
|
|
||||||
/* 颜色变量 */
|
|
||||||
/* 行为相关颜色 */
|
|
||||||
/* 文字基本颜色 */
|
|
||||||
/* 背景颜色 */
|
|
||||||
/* 边框颜色 */
|
|
||||||
/* 尺寸变量 */
|
|
||||||
/* 文字尺寸 */
|
|
||||||
/* 图片尺寸 */
|
|
||||||
/* Border Radius */
|
|
||||||
/* 水平间距 */
|
|
||||||
/* 垂直间距 */
|
|
||||||
/* 透明度 */
|
|
||||||
/* 文章场景相关 */
|
|
||||||
.u-qrcode__loading.data-v-444ebaa9 {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
background-color: #f7f7f7;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
.u-qrcode__content.data-v-444ebaa9 {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.u-qrcode__content__canvas.data-v-444ebaa9 {
|
|
||||||
position: fixed;
|
|
||||||
top: -99999rpx;
|
|
||||||
left: -99999rpx;
|
|
||||||
z-index: -99999;
|
|
||||||
}
|
|
||||||
|
|
@ -17,7 +17,33 @@ const _sfc_main = {
|
||||||
// 应用名称
|
// 应用名称
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
onLoad() {
|
onLoad(options) {
|
||||||
|
let inviteCode = "";
|
||||||
|
if (options && options.inviteCode) {
|
||||||
|
inviteCode = options.inviteCode;
|
||||||
|
}
|
||||||
|
if (options && options.scene) {
|
||||||
|
const scene = decodeURIComponent(options.scene);
|
||||||
|
console.log("小程序码 scene:", scene);
|
||||||
|
const params = {};
|
||||||
|
scene.split("&").forEach((item) => {
|
||||||
|
const [key, value] = item.split("=");
|
||||||
|
if (key && value) {
|
||||||
|
params[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (params.inviteCode) {
|
||||||
|
inviteCode = params.inviteCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (inviteCode) {
|
||||||
|
console.log("收到邀请码:", inviteCode);
|
||||||
|
const app = getApp();
|
||||||
|
if (app && app.globalData) {
|
||||||
|
app.globalData.inviteCode = inviteCode;
|
||||||
|
}
|
||||||
|
common_vendor.index.setStorageSync("inviteCode", inviteCode);
|
||||||
|
}
|
||||||
this.loadConfig();
|
this.loadConfig();
|
||||||
this.loadBanners();
|
this.loadBanners();
|
||||||
this.loadHotServices();
|
this.loadHotServices();
|
||||||
|
|
|
||||||
|
|
@ -47,8 +47,18 @@ const _sfc_main = {
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
let invitationCode = "";
|
||||||
|
const app = getApp();
|
||||||
|
if (app && app.globalData && app.globalData.inviteCode) {
|
||||||
|
invitationCode = app.globalData.inviteCode;
|
||||||
|
} else {
|
||||||
|
invitationCode = common_vendor.index.getStorageSync("inviteCode") || "";
|
||||||
|
}
|
||||||
|
if (invitationCode) {
|
||||||
|
console.log("使用邀请码登录:", invitationCode);
|
||||||
|
}
|
||||||
const appserver = new modules_api_AppServer.AppServer();
|
const appserver = new modules_api_AppServer.AppServer();
|
||||||
const data = await appserver.WechatLogin(loginRes.code);
|
const data = await appserver.WechatLogin(loginRes.code, invitationCode);
|
||||||
console.log("登录接口返回:", data);
|
console.log("登录接口返回:", data);
|
||||||
if (!data) {
|
if (!data) {
|
||||||
console.error("登录接口无响应");
|
console.error("登录接口无响应");
|
||||||
|
|
@ -80,6 +90,14 @@ const _sfc_main = {
|
||||||
console.log("登录成功,保存认证信息");
|
console.log("登录成功,保存认证信息");
|
||||||
const token = "Bearer " + data.data.token;
|
const token = "Bearer " + data.data.token;
|
||||||
utils_auth.saveAuthData(token, data.data.refreshToken, data.data.user);
|
utils_auth.saveAuthData(token, data.data.refreshToken, data.data.user);
|
||||||
|
if (invitationCode) {
|
||||||
|
const app2 = getApp();
|
||||||
|
if (app2 && app2.globalData) {
|
||||||
|
app2.globalData.inviteCode = "";
|
||||||
|
}
|
||||||
|
common_vendor.index.removeStorageSync("inviteCode");
|
||||||
|
console.log("邀请码已清除");
|
||||||
|
}
|
||||||
common_vendor.index.showToast({
|
common_vendor.index.showToast({
|
||||||
title: this.$t("login.loginSuccess") || "登录成功",
|
title: this.$t("login.loginSuccess") || "登录成功",
|
||||||
icon: "success",
|
icon: "success",
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,12 @@ const _sfc_main = {
|
||||||
qrcodeDataUrl: "",
|
qrcodeDataUrl: "",
|
||||||
qrcodeValue: "",
|
qrcodeValue: "",
|
||||||
// 二维码内容
|
// 二维码内容
|
||||||
|
wxQrcodeImage: "",
|
||||||
|
// 微信小程序码图片(base64)
|
||||||
|
qrcodeLoading: false,
|
||||||
|
// 小程序码加载状态
|
||||||
|
qrcodeError: "",
|
||||||
|
// 小程序码错误信息
|
||||||
appLogo: "",
|
appLogo: "",
|
||||||
// 应用logo,用于分享图片
|
// 应用logo,用于分享图片
|
||||||
applyStep: 1,
|
applyStep: 1,
|
||||||
|
|
@ -124,7 +130,7 @@ const _sfc_main = {
|
||||||
this.commissionStats = res.data;
|
this.commissionStats = res.data;
|
||||||
this.updateWithdrawDisplay();
|
this.updateWithdrawDisplay();
|
||||||
const inviteCode = res.data.invitationCode || "";
|
const inviteCode = res.data.invitationCode || "";
|
||||||
this.qrcodeValue = `https://your-domain.com/invite?code=${inviteCode}`;
|
this.qrcodeValue = `/pages/index/index?inviteCode=${inviteCode}`;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("获取佣金统计失败:", error);
|
console.error("获取佣金统计失败:", error);
|
||||||
|
|
@ -205,21 +211,30 @@ const _sfc_main = {
|
||||||
showCancel: false
|
showCancel: false
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
generateQRCode() {
|
async generateQRCode() {
|
||||||
const inviteCode = this.commissionStats.invitationCode || "";
|
if (this.wxQrcodeImage) {
|
||||||
this.qrcodeValue = `https://your-domain.com/invite?code=${inviteCode}`;
|
|
||||||
this.showQRCodeContent = false;
|
|
||||||
this.showQRCodeModal = true;
|
this.showQRCodeModal = true;
|
||||||
},
|
return;
|
||||||
onQRCodePopupOpen() {
|
}
|
||||||
setTimeout(() => {
|
this.showQRCodeModal = true;
|
||||||
this.showQRCodeContent = true;
|
this.qrcodeLoading = true;
|
||||||
}, 100);
|
this.qrcodeError = "";
|
||||||
|
try {
|
||||||
|
const res = await appServer.GetMiniProgramQRCode({ width: 430 });
|
||||||
|
if (res && res.code === 0 && res.data && res.data.qrcodeUrl) {
|
||||||
|
this.wxQrcodeImage = res.data.qrcodeUrl;
|
||||||
|
} else {
|
||||||
|
this.qrcodeError = (res == null ? void 0 : res.message) || "生成小程序码失败";
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("生成小程序码失败:", error);
|
||||||
|
this.qrcodeError = "网络错误,请重试";
|
||||||
|
} finally {
|
||||||
|
this.qrcodeLoading = false;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
closeQRCodeModal() {
|
closeQRCodeModal() {
|
||||||
this.showQRCodeContent = false;
|
|
||||||
this.showQRCodeModal = false;
|
this.showQRCodeModal = false;
|
||||||
this.qrcodeDataUrl = "";
|
|
||||||
},
|
},
|
||||||
shareQRCodeToFriend() {
|
shareQRCodeToFriend() {
|
||||||
common_vendor.index.showToast({
|
common_vendor.index.showToast({
|
||||||
|
|
@ -228,11 +243,23 @@ const _sfc_main = {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
saveQRCodeImage() {
|
saveQRCodeImage() {
|
||||||
if (this.$refs.uQrcode) {
|
if (!this.wxQrcodeImage) {
|
||||||
this.$refs.uQrcode.toTempFilePath({
|
common_vendor.index.showToast({
|
||||||
success: (res) => {
|
title: "小程序码加载中,请稍候",
|
||||||
|
icon: "none"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const base64Data = this.wxQrcodeImage.replace(/^data:image\/\w+;base64,/, "");
|
||||||
|
const filePath = `${common_vendor.wx$1.env.USER_DATA_PATH}/qrcode_${Date.now()}.png`;
|
||||||
|
const fs = common_vendor.index.getFileSystemManager();
|
||||||
|
fs.writeFile({
|
||||||
|
filePath,
|
||||||
|
data: base64Data,
|
||||||
|
encoding: "base64",
|
||||||
|
success: () => {
|
||||||
common_vendor.index.saveImageToPhotosAlbum({
|
common_vendor.index.saveImageToPhotosAlbum({
|
||||||
filePath: res.tempFilePath,
|
filePath,
|
||||||
success: () => {
|
success: () => {
|
||||||
common_vendor.index.showToast({
|
common_vendor.index.showToast({
|
||||||
title: this.$t("invite.saveSuccess") || "保存成功",
|
title: this.$t("invite.saveSuccess") || "保存成功",
|
||||||
|
|
@ -261,19 +288,13 @@ const _sfc_main = {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
fail: (err) => {
|
fail: (err) => {
|
||||||
console.error("获取二维码图片失败:", err);
|
console.error("写入文件失败:", err);
|
||||||
common_vendor.index.showToast({
|
common_vendor.index.showToast({
|
||||||
title: "获取二维码失败",
|
title: "保存失败",
|
||||||
icon: "none"
|
icon: "none"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
common_vendor.index.showToast({
|
|
||||||
title: "二维码生成中,请稍候",
|
|
||||||
icon: "none"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
shareToFriend() {
|
shareToFriend() {
|
||||||
common_vendor.index.share({
|
common_vendor.index.share({
|
||||||
|
|
@ -439,13 +460,11 @@ const _sfc_main = {
|
||||||
};
|
};
|
||||||
if (!Array) {
|
if (!Array) {
|
||||||
const _easycom_up_popup2 = common_vendor.resolveComponent("up-popup");
|
const _easycom_up_popup2 = common_vendor.resolveComponent("up-popup");
|
||||||
const _easycom_u_qrcode2 = common_vendor.resolveComponent("u-qrcode");
|
_easycom_up_popup2();
|
||||||
(_easycom_up_popup2 + _easycom_u_qrcode2)();
|
|
||||||
}
|
}
|
||||||
const _easycom_up_popup = () => "../../node-modules/uview-plus/components/u-popup/u-popup.js";
|
const _easycom_up_popup = () => "../../node-modules/uview-plus/components/u-popup/u-popup.js";
|
||||||
const _easycom_u_qrcode = () => "../../node-modules/uview-plus/components/u-qrcode/u-qrcode.js";
|
|
||||||
if (!Math) {
|
if (!Math) {
|
||||||
(_easycom_up_popup + _easycom_u_qrcode)();
|
_easycom_up_popup();
|
||||||
}
|
}
|
||||||
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
|
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
|
||||||
return common_vendor.e({
|
return common_vendor.e({
|
||||||
|
|
@ -521,22 +540,17 @@ function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
|
||||||
bgColor: "#ffffff"
|
bgColor: "#ffffff"
|
||||||
}),
|
}),
|
||||||
N: common_vendor.t(_ctx.$t("invite.qrcodeTitle") || "邀请好友,赢现金"),
|
N: common_vendor.t(_ctx.$t("invite.qrcodeTitle") || "邀请好友,赢现金"),
|
||||||
O: $data.showQRCodeContent
|
O: $data.wxQrcodeImage
|
||||||
}, $data.showQRCodeContent ? {
|
}, $data.wxQrcodeImage ? {
|
||||||
P: common_vendor.sr("uQrcode", "830ba560-2,830ba560-1"),
|
P: $data.wxQrcodeImage
|
||||||
Q: common_vendor.p({
|
} : $data.qrcodeLoading ? {} : {
|
||||||
val: $data.qrcodeValue,
|
R: common_vendor.t($data.qrcodeError || "加载失败")
|
||||||
size: 200,
|
}, {
|
||||||
unit: "px",
|
Q: $data.qrcodeLoading,
|
||||||
["load-make"]: true,
|
S: common_vendor.t(_ctx.$t("invite.shareToFriend") || "分享给好友"),
|
||||||
["using-components"]: true
|
T: common_vendor.t(_ctx.$t("invite.saveImage") || "保存图片"),
|
||||||
})
|
U: common_vendor.o((...args) => $options.saveQRCodeImage && $options.saveQRCodeImage(...args)),
|
||||||
} : {}, {
|
V: common_vendor.o($options.closeQRCodeModal),
|
||||||
R: common_vendor.t(_ctx.$t("invite.shareToFriend") || "分享给好友"),
|
|
||||||
S: common_vendor.t(_ctx.$t("invite.saveImage") || "保存图片"),
|
|
||||||
T: common_vendor.o((...args) => $options.saveQRCodeImage && $options.saveQRCodeImage(...args)),
|
|
||||||
U: common_vendor.o($options.closeQRCodeModal),
|
|
||||||
V: common_vendor.o($options.onQRCodePopupOpen),
|
|
||||||
W: common_vendor.p({
|
W: common_vendor.p({
|
||||||
show: $data.showQRCodeModal,
|
show: $data.showQRCodeModal,
|
||||||
mode: "center",
|
mode: "center",
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
"navigationBarTitleText": "",
|
"navigationBarTitleText": "",
|
||||||
"navigationStyle": "custom",
|
"navigationStyle": "custom",
|
||||||
"usingComponents": {
|
"usingComponents": {
|
||||||
"up-popup": "../../node-modules/uview-plus/components/u-popup/u-popup",
|
"up-popup": "../../node-modules/uview-plus/components/u-popup/u-popup"
|
||||||
"u-qrcode": "../../node-modules/uview-plus/components/u-qrcode/u-qrcode"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -480,19 +480,23 @@
|
||||||
margin-bottom: 30rpx;
|
margin-bottom: 30rpx;
|
||||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: visible;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.qrcode-modal-content .qrcode-box.data-v-830ba560 .u-qrcode {
|
.qrcode-modal-content .qrcode-box .wx-qrcode-image.data-v-830ba560 {
|
||||||
position: static !important;
|
width: 400rpx;
|
||||||
|
height: 400rpx;
|
||||||
|
}
|
||||||
|
.qrcode-modal-content .qrcode-box .qrcode-loading.data-v-830ba560 {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
color: #999;
|
||||||
|
font-size: 28rpx;
|
||||||
}
|
}
|
||||||
.qrcode-modal-content .qrcode-box.data-v-830ba560 .u-qrcode__content {
|
.qrcode-modal-content .qrcode-box .qrcode-error.data-v-830ba560 {
|
||||||
position: static !important;
|
color: #ff4d4f;
|
||||||
display: flex;
|
font-size: 26rpx;
|
||||||
align-items: center;
|
text-align: center;
|
||||||
justify-content: center;
|
|
||||||
}
|
}
|
||||||
.qrcode-modal-content .qrcode-box.data-v-830ba560 .u-qrcode__canvas {
|
.qrcode-modal-content .qrcode-box.data-v-830ba560 .u-qrcode__canvas {
|
||||||
position: static !important;
|
position: static !important;
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
"compileType": "miniprogram",
|
"compileType": "miniprogram",
|
||||||
"libVersion": "",
|
"libVersion": "",
|
||||||
"appid": "wx34e8a031d0d78b81",
|
"appid": "wx34e8a031d0d78b81",
|
||||||
"projectname": "appointment_system",
|
"projectname": "一起飞Go Mundo",
|
||||||
"condition": {
|
"condition": {
|
||||||
"search": {
|
"search": {
|
||||||
"current": -1,
|
"current": -1,
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,8 @@
|
||||||
user: null,
|
user: null,
|
||||||
shouldRefresh: false,
|
shouldRefresh: false,
|
||||||
loginTime: 0,
|
loginTime: 0,
|
||||||
config: null // 应用配置(从后端获取)
|
config: null, // 应用配置(从后端获取)
|
||||||
|
inviteCode: '' // 邀请码(从分享链接获取)
|
||||||
},
|
},
|
||||||
onLaunch: function() {
|
onLaunch: function() {
|
||||||
console.log('App Launch')
|
console.log('App Launch')
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"name" : "appointment_system",
|
"name" : "一起飞Go Mundo",
|
||||||
"appid" : "__UNI__C6C43F9",
|
"appid" : "__UNI__C6C43F9",
|
||||||
"description" : "",
|
"description" : "",
|
||||||
"versionName" : "1.0.0",
|
"versionName" : "1.0.0",
|
||||||
|
|
|
||||||
|
|
@ -269,10 +269,15 @@ AppServer.prototype.deleteData = async function(url) {
|
||||||
/**
|
/**
|
||||||
* 微信登录
|
* 微信登录
|
||||||
* @param {String} code - 微信登录code
|
* @param {String} code - 微信登录code
|
||||||
|
* @param {String} invitationCode - 邀请码(可选)
|
||||||
*/
|
*/
|
||||||
AppServer.prototype.WechatLogin = async function(code) {
|
AppServer.prototype.WechatLogin = async function(code, invitationCode) {
|
||||||
var url = serverConfig.apiUrl_Auth_WechatLogin
|
var url = serverConfig.apiUrl_Auth_WechatLogin
|
||||||
return this.postData(url, { code }).then((data) => {
|
var postData = { code }
|
||||||
|
if (invitationCode) {
|
||||||
|
postData.invitationCode = invitationCode
|
||||||
|
}
|
||||||
|
return this.postData(url, postData).then((data) => {
|
||||||
return data;
|
return data;
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,45 @@
|
||||||
appName: '' // 应用名称
|
appName: '' // 应用名称
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLoad() {
|
onLoad(options) {
|
||||||
|
// 处理邀请码参数
|
||||||
|
let inviteCode = ''
|
||||||
|
|
||||||
|
// 方式1:普通分享链接,直接从 options.inviteCode 获取
|
||||||
|
if (options && options.inviteCode) {
|
||||||
|
inviteCode = options.inviteCode
|
||||||
|
}
|
||||||
|
|
||||||
|
// 方式2:小程序码扫码进入,从 scene 参数解析
|
||||||
|
// 小程序码的 scene 格式为 "inviteCode=XXXXXX"
|
||||||
|
if (options && options.scene) {
|
||||||
|
const scene = decodeURIComponent(options.scene)
|
||||||
|
console.log('小程序码 scene:', scene)
|
||||||
|
// 解析 scene 参数
|
||||||
|
const params = {}
|
||||||
|
scene.split('&').forEach(item => {
|
||||||
|
const [key, value] = item.split('=')
|
||||||
|
if (key && value) {
|
||||||
|
params[key] = value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (params.inviteCode) {
|
||||||
|
inviteCode = params.inviteCode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存邀请码
|
||||||
|
if (inviteCode) {
|
||||||
|
console.log('收到邀请码:', inviteCode)
|
||||||
|
// 保存邀请码到全局数据,登录时使用
|
||||||
|
const app = getApp()
|
||||||
|
if (app && app.globalData) {
|
||||||
|
app.globalData.inviteCode = inviteCode
|
||||||
|
}
|
||||||
|
// 同时保存到本地存储,防止丢失
|
||||||
|
uni.setStorageSync('inviteCode', inviteCode)
|
||||||
|
}
|
||||||
|
|
||||||
// 不在首页检查登录状态,允许游客浏览
|
// 不在首页检查登录状态,允许游客浏览
|
||||||
this.loadConfig()
|
this.loadConfig()
|
||||||
this.loadBanners()
|
this.loadBanners()
|
||||||
|
|
|
||||||
|
|
@ -104,9 +104,22 @@
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 调用后端登录接口
|
// 获取邀请码(从全局数据或本地存储)
|
||||||
|
let invitationCode = ''
|
||||||
|
const app = getApp()
|
||||||
|
if (app && app.globalData && app.globalData.inviteCode) {
|
||||||
|
invitationCode = app.globalData.inviteCode
|
||||||
|
} else {
|
||||||
|
invitationCode = uni.getStorageSync('inviteCode') || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
if (invitationCode) {
|
||||||
|
console.log('使用邀请码登录:', invitationCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用后端登录接口(带邀请码)
|
||||||
const appserver = new AppServer();
|
const appserver = new AppServer();
|
||||||
const data = await appserver.WechatLogin(loginRes.code);
|
const data = await appserver.WechatLogin(loginRes.code, invitationCode);
|
||||||
|
|
||||||
console.log('登录接口返回:', data);
|
console.log('登录接口返回:', data);
|
||||||
|
|
||||||
|
|
@ -149,6 +162,16 @@
|
||||||
const token = "Bearer " + data.data.token
|
const token = "Bearer " + data.data.token
|
||||||
saveAuthData(token, data.data.refreshToken, data.data.user)
|
saveAuthData(token, data.data.refreshToken, data.data.user)
|
||||||
|
|
||||||
|
// 清除邀请码(已使用)
|
||||||
|
if (invitationCode) {
|
||||||
|
const app = getApp()
|
||||||
|
if (app && app.globalData) {
|
||||||
|
app.globalData.inviteCode = ''
|
||||||
|
}
|
||||||
|
uni.removeStorageSync('inviteCode')
|
||||||
|
console.log('邀请码已清除')
|
||||||
|
}
|
||||||
|
|
||||||
// 登录成功
|
// 登录成功
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: this.$t('login.loginSuccess') || '登录成功',
|
title: this.$t('login.loginSuccess') || '登录成功',
|
||||||
|
|
|
||||||
179
miniprogram/src/pages/me/INVITE_SYSTEM.md
Normal file
179
miniprogram/src/pages/me/INVITE_SYSTEM.md
Normal file
|
|
@ -0,0 +1,179 @@
|
||||||
|
# 邀请新人系统文档
|
||||||
|
|
||||||
|
## 功能概述
|
||||||
|
|
||||||
|
用户可以通过分享邀请链接或小程序码邀请新用户注册,新用户注册后会自动绑定邀请关系,邀请人可获得佣金奖励。
|
||||||
|
|
||||||
|
## 完整流程
|
||||||
|
|
||||||
|
```
|
||||||
|
邀请者分享 → 新用户点击/扫码 → 进入小程序 → 保存邀请码 → 登录注册 → 绑定邀请关系
|
||||||
|
```
|
||||||
|
|
||||||
|
## 分享方式
|
||||||
|
|
||||||
|
### 1. 分享给微信好友
|
||||||
|
- 入口:邀请奖励页面 → 分享给好友按钮
|
||||||
|
- 实现:`onShareAppMessage()`
|
||||||
|
- 链接格式:`/pages/index/index?inviteCode=XXXXXX`
|
||||||
|
|
||||||
|
### 2. 分享到朋友圈
|
||||||
|
- 入口:邀请奖励页面 → 右上角分享
|
||||||
|
- 实现:`onShareTimeline()`
|
||||||
|
- 参数格式:`inviteCode=XXXXXX`
|
||||||
|
|
||||||
|
### 3. 小程序码
|
||||||
|
- 入口:邀请奖励页面 → 生成二维码按钮
|
||||||
|
- 实现:调用后端 `/api/v1/qrcode/miniprogram` 接口
|
||||||
|
- 参数格式:`scene=inviteCode=XXXXXX`
|
||||||
|
|
||||||
|
## 技术实现
|
||||||
|
|
||||||
|
### 前端文件
|
||||||
|
|
||||||
|
| 文件 | 功能 |
|
||||||
|
|------|------|
|
||||||
|
| `App.vue` | 定义 `globalData.inviteCode` 存储邀请码 |
|
||||||
|
| `pages/index/index.vue` | 接收并保存邀请码 |
|
||||||
|
| `pages/login/login-page.vue` | 登录时传递邀请码 |
|
||||||
|
| `pages/me/invite-reward-page.vue` | 邀请奖励页面,分享功能 |
|
||||||
|
| `modules/api/AppServer.js` | API调用,`WechatLogin(code, invitationCode)` |
|
||||||
|
|
||||||
|
### 后端文件
|
||||||
|
|
||||||
|
| 文件 | 功能 |
|
||||||
|
|------|------|
|
||||||
|
| `services/authService.js` | 微信登录,处理邀请码绑定 |
|
||||||
|
| `routes/qrcodeRoutes.js` | 生成小程序码 |
|
||||||
|
| `models/User.js` | 用户模型,`invitedBy` 字段 |
|
||||||
|
| `models/Invitation.js` | 邀请关系模型 |
|
||||||
|
|
||||||
|
### 邀请码接收逻辑 (index.vue)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
onLoad(options) {
|
||||||
|
let inviteCode = ''
|
||||||
|
|
||||||
|
// 方式1:普通分享链接
|
||||||
|
if (options && options.inviteCode) {
|
||||||
|
inviteCode = options.inviteCode
|
||||||
|
}
|
||||||
|
|
||||||
|
// 方式2:小程序码扫码(scene参数)
|
||||||
|
if (options && options.scene) {
|
||||||
|
const scene = decodeURIComponent(options.scene)
|
||||||
|
const params = {}
|
||||||
|
scene.split('&').forEach(item => {
|
||||||
|
const [key, value] = item.split('=')
|
||||||
|
if (key && value) params[key] = value
|
||||||
|
})
|
||||||
|
if (params.inviteCode) inviteCode = params.inviteCode
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存邀请码
|
||||||
|
if (inviteCode) {
|
||||||
|
getApp().globalData.inviteCode = inviteCode
|
||||||
|
uni.setStorageSync('inviteCode', inviteCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 登录绑定逻辑 (login-page.vue)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async handleWechatLogin() {
|
||||||
|
// 获取邀请码
|
||||||
|
let invitationCode = ''
|
||||||
|
const app = getApp()
|
||||||
|
if (app?.globalData?.inviteCode) {
|
||||||
|
invitationCode = app.globalData.inviteCode
|
||||||
|
} else {
|
||||||
|
invitationCode = uni.getStorageSync('inviteCode') || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用登录接口(带邀请码)
|
||||||
|
const data = await appserver.WechatLogin(loginRes.code, invitationCode)
|
||||||
|
|
||||||
|
// 登录成功后清除邀请码
|
||||||
|
if (invitationCode) {
|
||||||
|
app.globalData.inviteCode = ''
|
||||||
|
uni.removeStorageSync('inviteCode')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 后端绑定逻辑 (authService.js)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const wechatLogin = async (code, userInfo, invitationCode, deviceInfo) => {
|
||||||
|
// ... 获取openId
|
||||||
|
|
||||||
|
let user = await User.findOne({ where: { wechatOpenId: openId } })
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
// 新用户注册
|
||||||
|
const userData = {
|
||||||
|
wechatOpenId: openId,
|
||||||
|
invitationCode: await generateUniqueInvitationCode(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理邀请关系
|
||||||
|
if (invitationCode) {
|
||||||
|
const inviter = await User.findOne({ where: { invitationCode } })
|
||||||
|
if (inviter) {
|
||||||
|
userData.invitedBy = inviter.id
|
||||||
|
// 创建邀请记录
|
||||||
|
await Invitation.create({
|
||||||
|
inviterId: inviter.id,
|
||||||
|
inviteeId: user.id,
|
||||||
|
invitationCode,
|
||||||
|
registeredAt: new Date(),
|
||||||
|
rewardStatus: 'pending',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
user = await User.create(userData)
|
||||||
|
}
|
||||||
|
// ... 返回token
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 数据模型
|
||||||
|
|
||||||
|
### User 表
|
||||||
|
- `invitationCode`: 用户的邀请码(唯一)
|
||||||
|
- `invitedBy`: 邀请人ID
|
||||||
|
|
||||||
|
### Invitation 表
|
||||||
|
- `inviterId`: 邀请人ID
|
||||||
|
- `inviteeId`: 被邀请人ID
|
||||||
|
- `invitationCode`: 使用的邀请码
|
||||||
|
- `registeredAt`: 注册时间
|
||||||
|
- `firstPaymentAt`: 首次付款时间
|
||||||
|
- `rewardAmount`: 奖励金额
|
||||||
|
- `rewardStatus`: 奖励状态 (pending/credited/cancelled)
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **小程序码限制**:`page` 参数必须是已发布的小程序页面
|
||||||
|
2. **开发环境**:开发版/体验版扫码可能无法正常跳转
|
||||||
|
3. **邀请码有效性**:只对新用户有效,老用户登录不会重复绑定
|
||||||
|
4. **scene参数限制**:最长32字符,格式为 `inviteCode=XXXXXX`
|
||||||
|
|
||||||
|
## API接口
|
||||||
|
|
||||||
|
### 生成小程序码
|
||||||
|
```
|
||||||
|
POST /api/v1/qrcode/miniprogram
|
||||||
|
Authorization: Bearer {token}
|
||||||
|
Body: { width: 430 }
|
||||||
|
Response: { code: 0, data: { invitationCode, qrcodeUrl } }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 微信登录(带邀请码)
|
||||||
|
```
|
||||||
|
POST /api/v1/auth/wechat-login
|
||||||
|
Body: { code, invitationCode }
|
||||||
|
Response: { code: 0, data: { token, user } }
|
||||||
|
```
|
||||||
|
|
@ -159,11 +159,15 @@
|
||||||
</up-popup>
|
</up-popup>
|
||||||
|
|
||||||
<!-- 二维码弹窗 -->
|
<!-- 二维码弹窗 -->
|
||||||
<up-popup :show="showQRCodeModal" mode="center" :round="20" :overlay="true" :closeOnClickOverlay="true" bgColor="#ffffff" @close="closeQRCodeModal" @open="onQRCodePopupOpen">
|
<up-popup :show="showQRCodeModal" mode="center" :round="20" :overlay="true" :closeOnClickOverlay="true" bgColor="#ffffff" @close="closeQRCodeModal">
|
||||||
<view class="qrcode-modal-content">
|
<view class="qrcode-modal-content">
|
||||||
<text class="qrcode-modal-title">{{ $t('invite.qrcodeTitle') || '邀请好友,赢现金' }}</text>
|
<text class="qrcode-modal-title">{{ $t('invite.qrcodeTitle') || '邀请好友,赢现金' }}</text>
|
||||||
<view class="qrcode-box">
|
<view class="qrcode-box">
|
||||||
<u-qrcode v-if="showQRCodeContent" ref="uQrcode" :val="qrcodeValue" :size="200" unit="px" :load-make="true" :using-components="true"></u-qrcode>
|
<image v-if="wxQrcodeImage" :src="wxQrcodeImage" class="wx-qrcode-image" mode="aspectFit"></image>
|
||||||
|
<view v-else-if="qrcodeLoading" class="qrcode-loading">
|
||||||
|
<text>加载中...</text>
|
||||||
|
</view>
|
||||||
|
<text v-else class="qrcode-error">{{ qrcodeError || '加载失败' }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="qrcode-actions">
|
<view class="qrcode-actions">
|
||||||
<!-- #ifdef MP-WEIXIN -->
|
<!-- #ifdef MP-WEIXIN -->
|
||||||
|
|
@ -291,6 +295,9 @@
|
||||||
showQRCodeContent: false, // 控制二维码内容显示
|
showQRCodeContent: false, // 控制二维码内容显示
|
||||||
qrcodeDataUrl: '',
|
qrcodeDataUrl: '',
|
||||||
qrcodeValue: '', // 二维码内容
|
qrcodeValue: '', // 二维码内容
|
||||||
|
wxQrcodeImage: '', // 微信小程序码图片(base64)
|
||||||
|
qrcodeLoading: false, // 小程序码加载状态
|
||||||
|
qrcodeError: '', // 小程序码错误信息
|
||||||
appLogo: '', // 应用logo,用于分享图片
|
appLogo: '', // 应用logo,用于分享图片
|
||||||
applyStep: 1,
|
applyStep: 1,
|
||||||
withdrawAmount: '',
|
withdrawAmount: '',
|
||||||
|
|
@ -404,9 +411,9 @@
|
||||||
this.commissionStats = res.data
|
this.commissionStats = res.data
|
||||||
// 更新提现记录显示
|
// 更新提现记录显示
|
||||||
this.updateWithdrawDisplay()
|
this.updateWithdrawDisplay()
|
||||||
// 预先设置二维码内容,用于分享
|
// 预先设置二维码内容,用于分享(使用小程序路径)
|
||||||
const inviteCode = res.data.invitationCode || ''
|
const inviteCode = res.data.invitationCode || ''
|
||||||
this.qrcodeValue = `https://your-domain.com/invite?code=${inviteCode}`
|
this.qrcodeValue = `/pages/index/index?inviteCode=${inviteCode}`
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取佣金统计失败:', error)
|
console.error('获取佣金统计失败:', error)
|
||||||
|
|
@ -491,26 +498,36 @@
|
||||||
showCancel: false
|
showCancel: false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
generateQRCode() {
|
async generateQRCode() {
|
||||||
// 构建邀请链接,包含邀请码
|
// 如果已有小程序码,直接显示
|
||||||
const inviteCode = this.commissionStats.invitationCode || ''
|
if (this.wxQrcodeImage) {
|
||||||
// 设置二维码内容为邀请链接
|
|
||||||
this.qrcodeValue = `https://your-domain.com/invite?code=${inviteCode}`
|
|
||||||
// 先隐藏二维码内容
|
|
||||||
this.showQRCodeContent = false
|
|
||||||
// 打开二维码弹窗
|
|
||||||
this.showQRCodeModal = true
|
this.showQRCodeModal = true
|
||||||
},
|
return
|
||||||
onQRCodePopupOpen() {
|
}
|
||||||
// 弹窗打开后延迟显示二维码
|
|
||||||
setTimeout(() => {
|
// 显示弹窗并开始加载
|
||||||
this.showQRCodeContent = true
|
this.showQRCodeModal = true
|
||||||
}, 100)
|
this.qrcodeLoading = true
|
||||||
|
this.qrcodeError = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 调用后端API获取微信小程序码
|
||||||
|
const res = await appServer.GetMiniProgramQRCode({ width: 430 })
|
||||||
|
|
||||||
|
if (res && res.code === 0 && res.data && res.data.qrcodeUrl) {
|
||||||
|
this.wxQrcodeImage = res.data.qrcodeUrl
|
||||||
|
} else {
|
||||||
|
this.qrcodeError = res?.message || '生成小程序码失败'
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('生成小程序码失败:', error)
|
||||||
|
this.qrcodeError = '网络错误,请重试'
|
||||||
|
} finally {
|
||||||
|
this.qrcodeLoading = false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
closeQRCodeModal() {
|
closeQRCodeModal() {
|
||||||
this.showQRCodeContent = false
|
|
||||||
this.showQRCodeModal = false
|
this.showQRCodeModal = false
|
||||||
this.qrcodeDataUrl = ''
|
|
||||||
},
|
},
|
||||||
shareQRCodeToFriend() {
|
shareQRCodeToFriend() {
|
||||||
// 分享给好友
|
// 分享给好友
|
||||||
|
|
@ -524,12 +541,27 @@
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
saveQRCodeImage() {
|
saveQRCodeImage() {
|
||||||
// 使用 u-qrcode 组件的 toTempFilePath 方法获取图片并保存
|
if (!this.wxQrcodeImage) {
|
||||||
if (this.$refs.uQrcode) {
|
uni.showToast({
|
||||||
this.$refs.uQrcode.toTempFilePath({
|
title: '小程序码加载中,请稍候',
|
||||||
success: (res) => {
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将 base64 图片保存到相册
|
||||||
|
// 先将 base64 转为临时文件
|
||||||
|
const base64Data = this.wxQrcodeImage.replace(/^data:image\/\w+;base64,/, '')
|
||||||
|
const filePath = `${wx.env.USER_DATA_PATH}/qrcode_${Date.now()}.png`
|
||||||
|
|
||||||
|
const fs = uni.getFileSystemManager()
|
||||||
|
fs.writeFile({
|
||||||
|
filePath: filePath,
|
||||||
|
data: base64Data,
|
||||||
|
encoding: 'base64',
|
||||||
|
success: () => {
|
||||||
uni.saveImageToPhotosAlbum({
|
uni.saveImageToPhotosAlbum({
|
||||||
filePath: res.tempFilePath,
|
filePath: filePath,
|
||||||
success: () => {
|
success: () => {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: this.$t('invite.saveSuccess') || '保存成功',
|
title: this.$t('invite.saveSuccess') || '保存成功',
|
||||||
|
|
@ -558,19 +590,13 @@
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
fail: (err) => {
|
fail: (err) => {
|
||||||
console.error('获取二维码图片失败:', err)
|
console.error('写入文件失败:', err)
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '获取二维码失败',
|
title: '保存失败',
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
|
||||||
uni.showToast({
|
|
||||||
title: '二维码生成中,请稍候',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
shareToFriend() {
|
shareToFriend() {
|
||||||
uni.share({
|
uni.share({
|
||||||
|
|
@ -1295,21 +1321,25 @@
|
||||||
margin-bottom: 30rpx;
|
margin-bottom: 30rpx;
|
||||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: visible;
|
overflow: hidden;
|
||||||
|
|
||||||
// 修复 u-qrcode 组件的 canvas 定位问题
|
.wx-qrcode-image {
|
||||||
:deep(.u-qrcode) {
|
width: 400rpx;
|
||||||
position: static !important;
|
height: 400rpx;
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.u-qrcode__content) {
|
.qrcode-loading {
|
||||||
position: static !important;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
color: #999;
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qrcode-error {
|
||||||
|
color: #ff4d4f;
|
||||||
|
font-size: 26rpx;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.u-qrcode__canvas) {
|
:deep(.u-qrcode__canvas) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user