feat: 增加裂变页面
parent
0d59ff4f74
commit
021f65a7f9
|
|
@ -2,7 +2,7 @@ VITE_APP_TITLE = '六纬中考通'
|
||||||
VITE_APP_PORT = 9000
|
VITE_APP_PORT = 9000
|
||||||
|
|
||||||
VITE_UNI_APPID = 'H57F2ACE4'
|
VITE_UNI_APPID = 'H57F2ACE4'
|
||||||
VITE_WX_APPID = 'wxc48ad15d58a3e417'
|
VITE_WX_APPID = 'wx4b925e36c17dd54a'
|
||||||
|
|
||||||
# h5部署网站的base,配置到 manifest.config.ts 里的 h5.router.base
|
# h5部署网站的base,配置到 manifest.config.ts 里的 h5.router.base
|
||||||
# https://uniapp.dcloud.net.cn/collocation/manifest.html#h5-router
|
# https://uniapp.dcloud.net.cn/collocation/manifest.html#h5-router
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,7 @@
|
||||||
"pinia": "2.0.36",
|
"pinia": "2.0.36",
|
||||||
"pinia-plugin-persistedstate": "3.2.1",
|
"pinia-plugin-persistedstate": "3.2.1",
|
||||||
"sard-uniapp": "^1.22.1",
|
"sard-uniapp": "^1.22.1",
|
||||||
|
"uqrcodejs": "^4.0.7",
|
||||||
"vue": "^3.4.21",
|
"vue": "^3.4.21",
|
||||||
"z-paging": "^2.8.8"
|
"z-paging": "^2.8.8"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,14 @@
|
||||||
</text>
|
</text>
|
||||||
<view class="mt-[20rpx] text-center">
|
<view class="mt-[20rpx] text-center">
|
||||||
<view class="text-[#000] text-[26rpx] font-700">学习风格表现</view>
|
<view class="text-[#000] text-[26rpx] font-700">学习风格表现</view>
|
||||||
<view class="mt-[10rpx]" v-for="(item, index) in item.learning_performance" :key="index">
|
<view class="mt-[10rpx]" v-for="(sonItem, index) in item.learning_performance" :key="index">
|
||||||
{{ item }}
|
{{ sonItem }}
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="mt-[20rpx] text-center">
|
<view class="mt-[20rpx] text-center">
|
||||||
<view class="text-[#000] text-[26rpx] font-700">学习风格特点</view>
|
<view class="text-[#000] text-[26rpx] font-700">学习风格特点</view>
|
||||||
<view class="mt-[10rpx]" v-for="(item, index) in item.features" :key="index">
|
<view class="mt-[10rpx]" v-for="(sonItem, index) in item.features" :key="index">
|
||||||
{{ item }}
|
{{ sonItem }}
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
|
||||||
|
|
@ -16,37 +16,6 @@
|
||||||
避免过度放松,保持适度的学习节奏。
|
避免过度放松,保持适度的学习节奏。
|
||||||
</view>
|
</view>
|
||||||
</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>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
</template>
|
</template>
|
||||||
</Navbar>
|
</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="flex flex-col flex-1 overflow-auto pb-[20rpx]">
|
||||||
<!-- 顶部卡片 -->
|
<!-- 顶部卡片 -->
|
||||||
<view class="mt-[30rpx] mx-[24rpx]">
|
<view class="mt-[30rpx] mx-[24rpx]">
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
</template>
|
</template>
|
||||||
</Navbar>
|
</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="flex flex-col flex-1 overflow-auto pb-[20rpx]">
|
||||||
<!-- 顶部卡片 -->
|
<!-- 顶部卡片 -->
|
||||||
<view class="mt-[30rpx] mx-[24rpx]">
|
<view class="mt-[30rpx] mx-[24rpx]">
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,37 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
// code here
|
|
||||||
import RequestComp from './components/request.vue'
|
|
||||||
|
|
||||||
definePage({
|
definePage({
|
||||||
style: {
|
style: {
|
||||||
navigationBarTitleText: '分包页面',
|
navigationBarTitleText: '分包页面',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
function gotoScroll() {
|
function navigateToVideoFn() {
|
||||||
uni.navigateTo({
|
uni.openChannelsLive({
|
||||||
url: '/pages-sub/demo/scroll',
|
finderUserName: 'sphju9MCfZetYHP',
|
||||||
|
success: () => {
|
||||||
|
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
console.error('跳转失败:', err)
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onLoad(() => {
|
||||||
|
uni.getChannelsLiveInfo({
|
||||||
|
finderUserName: 'sphju9MCfZetYHP',
|
||||||
|
success: (res) => {
|
||||||
|
console.log('res', res)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<view class="text-center">
|
<view class="text-center">
|
||||||
<view class="m-8">
|
<button @click="navigateToVideoFn">
|
||||||
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>
|
</button>
|
||||||
<view>
|
|
||||||
<RequestComp />
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -101,24 +101,27 @@ const handleChange = () => {
|
||||||
|
|
||||||
onShow(() => {
|
onShow(() => {
|
||||||
|
|
||||||
|
Promise.all([
|
||||||
getAreaList().then(resp => {
|
getAreaList().then(resp => {
|
||||||
if (resp.code === 200) {
|
if (resp.code === 200) {
|
||||||
areaList.value = [{ value: '', label: '不限' }, ...resp.result]
|
areaList.value = [{ value: '', label: '不限' }, ...resp.result]
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
|
|
||||||
getSchoolNature().then(resp => {
|
getSchoolNature().then(resp => {
|
||||||
if (resp.code === 200) {
|
if (resp.code === 200) {
|
||||||
natureList.value = [{ value: '', label: '不限' }, ...resp.result]
|
natureList.value = [{ value: '', label: '不限' }, ...resp.result]
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
getHistoryYearList().then(resp => {
|
getHistoryYearList().then(resp => {
|
||||||
if (resp.code === 200) {
|
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()
|
handleChange()
|
||||||
})
|
})
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
|
||||||
|
|
@ -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 './info';
|
||||||
|
|
||||||
export * from "./requestApi"
|
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 default store
|
||||||
|
|
||||||
// 模块统一导出
|
// 模块统一导出
|
||||||
|
export * from './invite'
|
||||||
export * from './token'
|
export * from './token'
|
||||||
export * from './user'
|
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