feat: 增加裂变页面
parent
0d59ff4f74
commit
021f65a7f9
|
|
@ -2,7 +2,7 @@ VITE_APP_TITLE = '六纬中考通'
|
|||
VITE_APP_PORT = 9000
|
||||
|
||||
VITE_UNI_APPID = 'H57F2ACE4'
|
||||
VITE_WX_APPID = 'wxc48ad15d58a3e417'
|
||||
VITE_WX_APPID = 'wx4b925e36c17dd54a'
|
||||
|
||||
# h5部署网站的base,配置到 manifest.config.ts 里的 h5.router.base
|
||||
# https://uniapp.dcloud.net.cn/collocation/manifest.html#h5-router
|
||||
|
|
|
|||
|
|
@ -107,6 +107,7 @@
|
|||
"pinia": "2.0.36",
|
||||
"pinia-plugin-persistedstate": "3.2.1",
|
||||
"sard-uniapp": "^1.22.1",
|
||||
"uqrcodejs": "^4.0.7",
|
||||
"vue": "^3.4.21",
|
||||
"z-paging": "^2.8.8"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -9,14 +9,14 @@
|
|||
</text>
|
||||
<view class="mt-[20rpx] text-center">
|
||||
<view class="text-[#000] text-[26rpx] font-700">学习风格表现</view>
|
||||
<view class="mt-[10rpx]" v-for="(item, index) in item.learning_performance" :key="index">
|
||||
{{ item }}
|
||||
<view class="mt-[10rpx]" v-for="(sonItem, index) in item.learning_performance" :key="index">
|
||||
{{ sonItem }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="mt-[20rpx] text-center">
|
||||
<view class="text-[#000] text-[26rpx] font-700">学习风格特点</view>
|
||||
<view class="mt-[10rpx]" v-for="(item, index) in item.features" :key="index">
|
||||
{{ item }}
|
||||
<view class="mt-[10rpx]" v-for="(sonItem, index) in item.features" :key="index">
|
||||
{{ sonItem }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
|
|
|||
|
|
@ -16,37 +16,6 @@
|
|||
避免过度放松,保持适度的学习节奏。
|
||||
</view>
|
||||
</view>
|
||||
<view class="flex flex-col gap-[12rpx]">
|
||||
<view class="flex items-center gap-[10rpx]">
|
||||
<view class="w-[38rpx] h-[38rpx]">
|
||||
<image
|
||||
src="https://api.static.ycymedu.com/src/images/home/diet-icon.png"
|
||||
mode="scaleToFill"
|
||||
class="w-[38rpx] h-[38rpx]"
|
||||
/>
|
||||
</view>
|
||||
<text class="text-[32rpx] text-[#000] font-700">饮食建议</text>
|
||||
</view>
|
||||
<view class="text-[26rpx] text-[#666] font-400">
|
||||
保持规律作息,早睡早起,避免熬夜。 每天适当运动(如散步、跑步),保持精力充沛。
|
||||
避免过度放松,保持适度的学习节奏。
|
||||
</view>
|
||||
</view>
|
||||
<view class="flex flex-col gap-[12rpx]">
|
||||
<view class="flex items-center gap-[10rpx]">
|
||||
<view class="w-[38rpx] h-[38rpx]">
|
||||
<image
|
||||
src="https://api.static.ycymedu.com/src/images/home/learn-icon.png"
|
||||
mode="scaleToFill"
|
||||
class="w-[38rpx] h-[38rpx]"
|
||||
/>
|
||||
</view>
|
||||
<text class="text-[32rpx] text-[#000] font-700">学习建议</text>
|
||||
</view>
|
||||
<view class="text-[26rpx] text-[#666] font-400">
|
||||
保持规律作息,早睡早起,避免熬夜。 每天适当运动(如散步、跑步),保持精力充沛。
|
||||
避免过度放松,保持适度的学习节奏。
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
</template>
|
||||
</Navbar>
|
||||
|
||||
<view class="flex-1 overflow-auto relative">
|
||||
<view class="flex-1 overflow-auto relative flex flex-col">
|
||||
<view class="flex flex-col flex-1 overflow-auto pb-[20rpx]">
|
||||
<!-- 顶部卡片 -->
|
||||
<view class="mt-[30rpx] mx-[24rpx]">
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
</template>
|
||||
</Navbar>
|
||||
|
||||
<view class="flex-1 overflow-auto relative">
|
||||
<view class="flex-1 overflow-auto relative flex flex-col">
|
||||
<view class="flex flex-col flex-1 overflow-auto pb-[20rpx]">
|
||||
<!-- 顶部卡片 -->
|
||||
<view class="mt-[30rpx] mx-[24rpx]">
|
||||
|
|
|
|||
|
|
@ -1,37 +1,37 @@
|
|||
<script lang="ts" setup>
|
||||
// code here
|
||||
import RequestComp from './components/request.vue'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '分包页面',
|
||||
},
|
||||
})
|
||||
|
||||
function gotoScroll() {
|
||||
uni.navigateTo({
|
||||
url: '/pages-sub/demo/scroll',
|
||||
function navigateToVideoFn() {
|
||||
uni.openChannelsLive({
|
||||
finderUserName: 'sphju9MCfZetYHP',
|
||||
success: () => {
|
||||
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('跳转失败:', err)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
onLoad(() => {
|
||||
uni.getChannelsLiveInfo({
|
||||
finderUserName: 'sphju9MCfZetYHP',
|
||||
success: (res) => {
|
||||
console.log('res', res)
|
||||
},
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="text-center">
|
||||
<view class="m-8">
|
||||
http://localhost:9000/#/pages-sub/demo/index
|
||||
</view>
|
||||
<view class="my-4 text-green-500">
|
||||
分包页面demo
|
||||
</view>
|
||||
<view class="text-blue-500">
|
||||
分包页面里面的components示例
|
||||
</view>
|
||||
<button class="my-4" type="primary" size="mini" @click="gotoScroll">
|
||||
跳转到上拉刷新和下拉加载更多
|
||||
<button @click="navigateToVideoFn">
|
||||
点击跳视频号
|
||||
</button>
|
||||
<view>
|
||||
<RequestComp />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -101,23 +101,26 @@ const handleChange = () => {
|
|||
|
||||
onShow(() => {
|
||||
|
||||
Promise.all([
|
||||
getAreaList().then(resp => {
|
||||
if (resp.code === 200) {
|
||||
areaList.value = [{ value: '', label: '不限' }, ...resp.result]
|
||||
}
|
||||
})
|
||||
|
||||
}),
|
||||
getSchoolNature().then(resp => {
|
||||
if (resp.code === 200) {
|
||||
natureList.value = [{ value: '', label: '不限' }, ...resp.result]
|
||||
}
|
||||
})
|
||||
}),
|
||||
getHistoryYearList().then(resp => {
|
||||
if (resp.code === 200) {
|
||||
yearList.value = [{ value: '', label: '不限' }, ...resp.result]
|
||||
yearList.value = [...resp.result]
|
||||
searchParams.value.year = yearList.value[yearList.value.length - 1]?.value
|
||||
}
|
||||
})
|
||||
]).then(() => {
|
||||
handleChange()
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,92 @@
|
|||
<script lang="ts" setup>
|
||||
import { trackPromoterRedirect } from '@/service'
|
||||
import { useInviteStore } from '@/store/invite'
|
||||
|
||||
const inviteStore = useInviteStore()
|
||||
let weChatLiveId = ''
|
||||
let douyinLiveUrl = ''
|
||||
|
||||
onLoad(() => {
|
||||
const referralCode = inviteStore.currentPromoter?.referralCode
|
||||
if (!referralCode) {
|
||||
return
|
||||
}
|
||||
|
||||
trackPromoterRedirect(referralCode).then((resp: any) => {
|
||||
weChatLiveId = resp?.liveLinks?.wechatLiveUrl || 'sphju9MCfZetYHP'
|
||||
douyinLiveUrl = resp?.liveLinks?.douyinLiveUrl || ''
|
||||
}).catch((error) => {
|
||||
console.error('[redirect]', error)
|
||||
})
|
||||
})
|
||||
|
||||
function navigateToVideoFn() {
|
||||
if (!weChatLiveId) {
|
||||
uni.showToast({ title: '直播链接获取中,请稍后再试', icon: 'none' })
|
||||
return
|
||||
}
|
||||
uni.openChannelsLive({
|
||||
finderUserName: weChatLiveId,
|
||||
success: () => {
|
||||
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('跳转失败:', err)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function navigateToDouyinFn() {
|
||||
if (!douyinLiveUrl) {
|
||||
uni.showToast({ title: '直播链接获取中,请稍后再试', icon: 'none' })
|
||||
return
|
||||
}
|
||||
uni.setClipboardData({
|
||||
data: douyinLiveUrl,
|
||||
success: () => {
|
||||
uni.hideToast()
|
||||
uni.showModal({
|
||||
title: '链接已复制',
|
||||
content: '请打开抖音 App 粘贴查看直播',
|
||||
showCancel: false,
|
||||
confirmText: '我知道了',
|
||||
})
|
||||
},
|
||||
fail: () => {
|
||||
uni.showToast({ title: '复制失败,请稍后重试', icon: 'none' })
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="h-screen w-screen flex flex-col bg-[#F4F6FA]">
|
||||
<image
|
||||
src="https://lw-zk.oss-cn-hangzhou.aliyuncs.com/liebian/ZZBG.png"
|
||||
mode="widthFix"
|
||||
class="w-full"
|
||||
/>
|
||||
|
||||
<view class="mt-[80rpx] px-[54rpx]">
|
||||
<view class="w-full flex items-center justify-center gap-[16rpx] rounded-[210rpx] bg-[#1580FF] py-[30rpx] text-white font-500" @click="navigateToVideoFn">
|
||||
<view class="h-[39rpx] w-[46rpx] rounded-[210rpx] text-[32rpx]">
|
||||
<image
|
||||
src="https://lw-zk.oss-cn-hangzhou.aliyuncs.com/liebian/shipin.png"
|
||||
mode="widthFix"
|
||||
/>
|
||||
</view>
|
||||
视频号查看
|
||||
</view>
|
||||
|
||||
<view class="mt-[48rpx] w-full flex items-center justify-center gap-[16rpx] border-1 border-[#1580FF] rounded-[210rpx] border-solid bg-white py-[30rpx] text-[#1580FF] font-500" @click="navigateToDouyinFn">
|
||||
<view class="h-[39rpx] w-[46rpx] rounded-[210rpx] text-[32rpx]">
|
||||
<image
|
||||
src="https://lw-zk.oss-cn-hangzhou.aliyuncs.com/liebian/douyin.png"
|
||||
mode="widthFix"
|
||||
/>
|
||||
</view>
|
||||
抖音查看
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,171 @@
|
|||
<template>
|
||||
<view class="relative min-h-screen flex flex-col bg-[#F6F8FB]">
|
||||
<image
|
||||
src="https://lw-zk.oss-cn-hangzhou.aliyuncs.com/liebian/fenxiang.png"
|
||||
mode="widthFix"
|
||||
class="absolute left-0 top-0 aspect-[1.75/1] w-full"
|
||||
/>
|
||||
|
||||
<form
|
||||
class="relative z-1 mx-[30rpx] mt-[280rpx] h-max flex flex-col rounded-[20rpx] bg-[#fff] px-[30rpx] pb-[60rpx] pt-[30rpx]"
|
||||
@submit="handleSubmit"
|
||||
>
|
||||
<label class="block">
|
||||
<view class="mb-[12rpx] block text-[28rpx] text-[#111111] font-500 leading-[1]">
|
||||
<text class="text-[#EB5241]">*</text>
|
||||
<text>教师姓名</text>
|
||||
</view>
|
||||
<view class="h-[88rpx] flex items-center gap-[18rpx] border border-[#D9D9D9] rounded-[10rpx] border-solid px-[20rpx]">
|
||||
<input
|
||||
v-model.trim="form.name"
|
||||
class="min-w-0 flex-1 select-text border-none bg-transparent p-0 text-[30rpx] text-[#1F2329] outline-none"
|
||||
type="text"
|
||||
placeholder="请填写教师姓名"
|
||||
confirm-type="next"
|
||||
placeholder-style="color:#A6AFBD;font-size:30rpx;text-align:left;"
|
||||
>
|
||||
</view>
|
||||
</label>
|
||||
|
||||
<label class="mt-[50rpx] block">
|
||||
<view class="mb-[12rpx] block text-[30rpx] text-[#111111] font-500 leading-[1]">
|
||||
<text class="text-[#EB5241]">*</text>
|
||||
<text>教师手机号</text>
|
||||
</view>
|
||||
<view class="h-[88rpx] flex items-center gap-[18rpx] border border-[#D9D9D9] rounded-[10rpx] border-solid px-[20rpx]">
|
||||
<input
|
||||
v-model.trim="form.phone"
|
||||
class="min-w-0 flex-1 select-text border-none bg-transparent p-0 text-[30rpx] text-[#1F2329] outline-none"
|
||||
type="number"
|
||||
:maxlength="11"
|
||||
inputmode="numeric"
|
||||
placeholder="请填写教师手机号"
|
||||
confirm-type="done"
|
||||
placeholder-style="color:#A6AFBD;font-size:30rpx;text-align:left;"
|
||||
@input="handlePhoneInput"
|
||||
>
|
||||
</view>
|
||||
</label>
|
||||
|
||||
<button
|
||||
form-type="submit"
|
||||
class="mx-auto mt-[60rpx] h-[88rpx] w-[480rpx] rounded-[12rpx] border-none bg-[#1580FF] text-[32rpx] text-[#fff] font-500 active:bg-[#1580FF] disabled:bg-[#A9D0FF]"
|
||||
:disabled="!canSubmit || loading"
|
||||
:loading="loading"
|
||||
>
|
||||
{{ loading ? '生成中...' : '生成' }}
|
||||
</button>
|
||||
|
||||
<view
|
||||
v-if="errorMessage"
|
||||
class="mb-0 mt-[24rpx] text-center text-[24rpx] text-[#EB5241] leading-[1.4]"
|
||||
>
|
||||
{{ errorMessage }}
|
||||
</view>
|
||||
</form>
|
||||
|
||||
<view class="mt-[50rpx] px-[30rpx] text-[26rpx] text-[#666] leading-[36rpx]">
|
||||
填写教师信息后,系统将生成专属直播二维码。教师可将二维码分享给学生或家长,学生扫码进入直播间后,系统会自动统计进入人数。
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Promoter } from '@/service/invite'
|
||||
import { createPromoter } from '@/service'
|
||||
import { useInviteStore } from '@/store/invite'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '教师直播码',
|
||||
},
|
||||
})
|
||||
|
||||
const inviteStore = useInviteStore()
|
||||
|
||||
const loading = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const promoter = ref<Promoter | null>(null)
|
||||
const form = reactive({
|
||||
name: '',
|
||||
phone: '',
|
||||
referralCode: '',
|
||||
})
|
||||
|
||||
onLoad((options) => {
|
||||
console.log('canshu', options)
|
||||
const referralCode = options?.referralCode || options?.code
|
||||
if (referralCode) {
|
||||
form.referralCode = decodeURIComponent(referralCode)
|
||||
}
|
||||
})
|
||||
|
||||
onShareAppMessage(() => {
|
||||
return {
|
||||
title: '六纬中考通',
|
||||
path: '/pages-sub/invite/login',
|
||||
imageUrl: 'https://lw-zk.oss-cn-hangzhou.aliyuncs.com/liebian/zhuanshumafenxiangtu.png',
|
||||
}
|
||||
})
|
||||
onShareTimeline(() => {
|
||||
return {
|
||||
title: '六纬中考通',
|
||||
}
|
||||
})
|
||||
|
||||
const canSubmit = computed(() => {
|
||||
return form.name.trim().length > 0 && /^1[3-9]\d{9}$/.test(form.phone)
|
||||
})
|
||||
|
||||
function handlePhoneInput(event: { detail?: { value?: string } }) {
|
||||
form.phone = (event.detail?.value || '').replace(/\D/g, '').slice(0, 11)
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (loading.value) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!form.name.trim()) {
|
||||
uni.showToast({ title: '请填写教师姓名', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
if (!/^1[3-9]\d{9}$/.test(form.phone)) {
|
||||
uni.showToast({ title: '请填写正确手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
const result = await createPromoter({
|
||||
name: form.name.trim(),
|
||||
phone: form.phone,
|
||||
referralCode: form.referralCode || undefined,
|
||||
})
|
||||
|
||||
promoter.value = result
|
||||
inviteStore.setPromoter(result)
|
||||
|
||||
uni.showToast({ title: '生成成功', icon: 'success' })
|
||||
uni.navigateTo({ url: '/pages-sub/invite/qrcode' })
|
||||
}
|
||||
catch (error) {
|
||||
const message = error instanceof Error ? error.message : '提交失败,请稍后重试'
|
||||
errorMessage.value = message
|
||||
uni.showToast({ title: message, icon: 'none' })
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* uni-app 中 button 默认带边框,使用原子化 border-none 已覆盖;如有需要可在此处补充覆盖样式 */
|
||||
button::after {
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,247 @@
|
|||
<template>
|
||||
<view class="min-h-screen flex flex-col items-center bg-[#F6F8FB]">
|
||||
<canvas
|
||||
id="poster"
|
||||
canvas-id="poster"
|
||||
class="block"
|
||||
:style="{ width: `${canvasWidth}px`, height: `${canvasHeight}px` }"
|
||||
/>
|
||||
<view v-if="errorMessage" class="mt-[40rpx] px-[40rpx] text-center text-[26rpx] text-[#EB5241] leading-[1.5]">
|
||||
{{ errorMessage }}
|
||||
</view>
|
||||
<view class="grid grid-cols-2 mb-[30rpx] mt-auto w-full gap-[20rpx] bg-[#fff] pt-[12rpx] pb-safe">
|
||||
<button
|
||||
class="ml-[30rpx] mr-0 h-[88rpx] rounded-[12rpx] border-none bg-[#F5F5F5] text-[32rpx] text-[#333]"
|
||||
@click="handleRegenerate"
|
||||
>
|
||||
重新生成
|
||||
</button>
|
||||
<button
|
||||
class="ml-0 mr-[30rpx] h-[88rpx] rounded-[12rpx] border-none bg-[#1580FF] text-[32rpx] text-[#fff]"
|
||||
@click="handleSave"
|
||||
>
|
||||
保存到手机
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import UQRCode from 'uqrcodejs'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '海报测试',
|
||||
},
|
||||
})
|
||||
|
||||
const POSTER_IMAGE_URL = 'https://lw-zk.oss-cn-hangzhou.aliyuncs.com/liebian/erweima.png'
|
||||
const POSTER_DESIGN_HEIGHT_RPX = 1171
|
||||
|
||||
// 二维码在海报中的位置(设计稿单位 rpx)
|
||||
const QR_OFFSET_LEFT_RPX = 40
|
||||
const QR_OFFSET_TOP_RPX = 991
|
||||
const QR_WIDTH_RPX = 140
|
||||
const QR_HEIGHT_RPX = 140
|
||||
const QR_CONTENT = '你好'
|
||||
|
||||
const { windowWidth } = uni.getSystemInfoSync()
|
||||
|
||||
const canvasWidth = ref(windowWidth)
|
||||
const canvasHeight = ref(uni.upx2px(POSTER_DESIGN_HEIGHT_RPX))
|
||||
const errorMessage = ref('')
|
||||
const saving = ref(false)
|
||||
const posterReady = ref(false)
|
||||
|
||||
onReady(() => {
|
||||
renderPoster()
|
||||
})
|
||||
|
||||
async function renderPoster() {
|
||||
errorMessage.value = ''
|
||||
posterReady.value = false
|
||||
try {
|
||||
await nextTick()
|
||||
const ctx = uni.createCanvasContext('poster')
|
||||
|
||||
// 1. 背景海报
|
||||
const localPath = await getLocalImagePath(POSTER_IMAGE_URL)
|
||||
ctx.drawImage(localPath, 0, 0, canvasWidth.value, canvasHeight.value)
|
||||
|
||||
// 2. 叠加二维码
|
||||
drawQrCode(ctx, QR_CONTENT, {
|
||||
x: uni.upx2px(QR_OFFSET_LEFT_RPX),
|
||||
y: uni.upx2px(QR_OFFSET_TOP_RPX),
|
||||
width: uni.upx2px(QR_WIDTH_RPX),
|
||||
height: uni.upx2px(QR_HEIGHT_RPX),
|
||||
})
|
||||
|
||||
// 3. 一次性提交
|
||||
await new Promise<void>(resolve => ctx.draw(false, () => resolve()))
|
||||
posterReady.value = true
|
||||
}
|
||||
catch (error) {
|
||||
const message = error instanceof Error ? error.message : '海报绘制失败'
|
||||
errorMessage.value = message
|
||||
console.error('[poster]', error)
|
||||
}
|
||||
}
|
||||
|
||||
function handleRegenerate() {
|
||||
uni.showLoading({ title: '重新生成中...', mask: true })
|
||||
renderPoster().finally(() => uni.hideLoading())
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (saving.value) {
|
||||
return
|
||||
}
|
||||
saving.value = true
|
||||
try {
|
||||
const tempFilePath = await canvasToTempFile('poster')
|
||||
|
||||
// #ifdef H5
|
||||
downloadFileH5(tempFilePath, 'poster.png')
|
||||
uni.showToast({ title: '已下载到本地', icon: 'success' })
|
||||
// #endif
|
||||
|
||||
// #ifndef H5
|
||||
await ensureAlbumAuth()
|
||||
await saveToAlbum(tempFilePath)
|
||||
uni.showToast({ title: '已保存到相册', icon: 'success' })
|
||||
// #endif
|
||||
}
|
||||
catch (error) {
|
||||
const message = error instanceof Error ? error.message : '保存失败,请稍后重试'
|
||||
uni.showToast({ title: message, icon: 'none' })
|
||||
console.error('[save]', error)
|
||||
}
|
||||
finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
interface QrRect {
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
function drawQrCode(ctx: UniApp.CanvasContext, data: string, rect: QrRect) {
|
||||
const qr = new UQRCode()
|
||||
qr.data = data
|
||||
qr.size = Math.max(rect.width, rect.height) // 仅用于内部生成,实际尺寸由 rect 控制
|
||||
qr.margin = 0
|
||||
qr.make()
|
||||
|
||||
const moduleCount = qr.moduleCount
|
||||
const cellW = rect.width / moduleCount
|
||||
const cellH = rect.height / moduleCount
|
||||
|
||||
// 白底
|
||||
ctx.setFillStyle('#FFFFFF')
|
||||
ctx.fillRect(rect.x, rect.y, rect.width, rect.height)
|
||||
|
||||
// 黑色模块(多 0.5px 重叠避免低像素下出现白色细缝)
|
||||
ctx.setFillStyle('#000000')
|
||||
for (let row = 0; row < moduleCount; row++) {
|
||||
for (let col = 0; col < moduleCount; col++) {
|
||||
if (qr.modules[row][col]?.isBlack) {
|
||||
ctx.fillRect(
|
||||
rect.x + col * cellW,
|
||||
rect.y + row * cellH,
|
||||
cellW + 0.5,
|
||||
cellH + 0.5,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getLocalImagePath(src: string) {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
uni.getImageInfo({
|
||||
src,
|
||||
success: res => resolve(res.path),
|
||||
fail: err => reject(new Error(err?.errMsg || '海报背景图加载失败')),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function canvasToTempFile(canvasId: string) {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
uni.canvasToTempFilePath({
|
||||
canvasId,
|
||||
fileType: 'png',
|
||||
quality: 1,
|
||||
success: res => resolve(res.tempFilePath),
|
||||
fail: err => reject(new Error(err?.errMsg || '生成图片失败')),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function saveToAlbum(filePath: string) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
uni.saveImageToPhotosAlbum({
|
||||
filePath,
|
||||
success: () => resolve(),
|
||||
fail: err => reject(new Error(err?.errMsg || '保存到相册失败')),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 小程序场景下首次保存需用户授权
|
||||
function ensureAlbumAuth() {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
uni.getSetting({
|
||||
success: (res) => {
|
||||
const authorized = res.authSetting['scope.writePhotosAlbum']
|
||||
if (authorized || authorized === undefined) {
|
||||
// 已授权 或 还没询问过(直接走 saveImageToPhotosAlbum 会触发系统弹窗)
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '需要您授权保存到相册',
|
||||
success: (modalRes) => {
|
||||
if (!modalRes.confirm) {
|
||||
reject(new Error('已取消保存'))
|
||||
return
|
||||
}
|
||||
uni.openSetting({
|
||||
success: (settingRes) => {
|
||||
if (settingRes.authSetting['scope.writePhotosAlbum']) {
|
||||
resolve()
|
||||
}
|
||||
else {
|
||||
reject(new Error('未授权保存到相册'))
|
||||
}
|
||||
},
|
||||
fail: () => reject(new Error('授权失败')),
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
fail: () => resolve(), // 拿不到设置时直接尝试保存
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// #ifdef H5
|
||||
function downloadFileH5(url: string, filename: string) {
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = filename
|
||||
link.style.display = 'none'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
}
|
||||
// #endif
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
//
|
||||
</style>
|
||||
|
|
@ -6,3 +6,4 @@ export * from './listAll';
|
|||
export * from './info';
|
||||
|
||||
export * from "./requestApi"
|
||||
export * from './invite'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
export interface CreatePromoterParams {
|
||||
name: string
|
||||
phone: string
|
||||
referralCode?: string
|
||||
}
|
||||
|
||||
export interface Promoter {
|
||||
_id?: string
|
||||
id: string
|
||||
name: string
|
||||
phone: string
|
||||
referralCode: string
|
||||
promotionCount: number
|
||||
promotionUrl: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
type InviteErrorData = {
|
||||
message?: string
|
||||
msg?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
const INVITE_API_URL = 'https://liveroom.ycymedu.com/api/h5/promoters'
|
||||
const INVITE_API_KEY = '28fd3c9a8739424ff5f38'
|
||||
|
||||
export const createPromoter = (params: CreatePromoterParams) => {
|
||||
return new Promise<Promoter>((resolve, reject) => {
|
||||
uni.request({
|
||||
url: INVITE_API_URL,
|
||||
method: 'POST',
|
||||
data: params,
|
||||
header: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': INVITE_API_KEY,
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
resolve(res.data as Promoter)
|
||||
return
|
||||
}
|
||||
|
||||
reject(new Error(getInviteErrorMessage(res.data)))
|
||||
},
|
||||
fail: () => {
|
||||
reject(new Error('提交失败,请稍后重试'))
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const getInviteErrorMessage = (data: unknown) => {
|
||||
const errorData = data as InviteErrorData | undefined
|
||||
return errorData?.message || errorData?.msg || errorData?.error || '提交失败,请稍后重试'
|
||||
}
|
||||
|
||||
const REDIRECT_API_URL = 'https://liveroom.ycymedu.com/api/h5/promotion-visits'
|
||||
|
||||
export const trackPromoterRedirect = (referralCode: string) => {
|
||||
return new Promise<unknown>((resolve, reject) => {
|
||||
uni.request({
|
||||
url: REDIRECT_API_URL,
|
||||
method: 'POST',
|
||||
data: { from: referralCode },
|
||||
header: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': INVITE_API_KEY,
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
resolve(res.data)
|
||||
return
|
||||
}
|
||||
|
||||
reject(new Error(getInviteErrorMessage(res.data)))
|
||||
},
|
||||
fail: () => {
|
||||
reject(new Error('请求失败,请稍后重试'))
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ setActivePinia(store)
|
|||
export default store
|
||||
|
||||
// 模块统一导出
|
||||
export * from './invite'
|
||||
export * from './token'
|
||||
export * from './user'
|
||||
export * from "./wishlist"
|
||||
export * from './wishlist'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
import type { Promoter } from '@/service/invite'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
export const useInviteStore = defineStore(
|
||||
'invite',
|
||||
() => {
|
||||
const promoter = ref<Promoter | null>(null)
|
||||
const currentPromoter = computed(() => promoter.value)
|
||||
|
||||
const setPromoter = (val: Promoter) => {
|
||||
promoter.value = val
|
||||
}
|
||||
|
||||
const clearPromoter = () => {
|
||||
promoter.value = null
|
||||
uni.removeStorageSync('invite')
|
||||
}
|
||||
|
||||
return {
|
||||
promoter,
|
||||
currentPromoter,
|
||||
setPromoter,
|
||||
clearPromoter,
|
||||
}
|
||||
},
|
||||
{
|
||||
persist: true,
|
||||
},
|
||||
)
|
||||
Loading…
Reference in New Issue