f
4
src/auto-imports.d.ts
vendored
@@ -48,6 +48,7 @@ declare global {
|
||||
const getApiPrefix: typeof import('./utils/runtimeEnv.js')['getApiPrefix']
|
||||
const getAppDebug: typeof import('./utils/runtimeEnv.js')['getAppDebug']
|
||||
const getAppName: typeof import('./utils/runtimeEnv.js')['getAppName']
|
||||
const getCachedMergedPosterWeixin: typeof import('./utils/posterQrWeixin.js')['getCachedMergedPosterWeixin']
|
||||
const getCompanyName: typeof import('./utils/runtimeEnv.js')['getCompanyName']
|
||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||
@@ -55,6 +56,7 @@ declare global {
|
||||
const getCustomerServiceUrl: typeof import('./utils/runtimeEnv.js')['getCustomerServiceUrl']
|
||||
const getInviteChannelKey: typeof import('./utils/runtimeEnv.js')['getInviteChannelKey']
|
||||
const getMeShareTitle: typeof import('./utils/runtimeEnv.js')['getMeShareTitle']
|
||||
const getPosterOrigin: typeof import('./utils/runtimeEnv.js')['getPosterOrigin']
|
||||
const getPosterSrcList: typeof import('./utils/posterQrWeixin.js')['getPosterSrcList']
|
||||
const getShareTitle: typeof import('./utils/runtimeEnv.js')['getShareTitle']
|
||||
const getSiteOrigin: typeof import('./utils/runtimeEnv.js')['getSiteOrigin']
|
||||
@@ -393,6 +395,7 @@ declare module 'vue' {
|
||||
readonly getApiPrefix: UnwrapRef<typeof import('./utils/runtimeEnv.js')['getApiPrefix']>
|
||||
readonly getAppDebug: UnwrapRef<typeof import('./utils/runtimeEnv.js')['getAppDebug']>
|
||||
readonly getAppName: UnwrapRef<typeof import('./utils/runtimeEnv.js')['getAppName']>
|
||||
readonly getCachedMergedPosterWeixin: UnwrapRef<typeof import('./utils/posterQrWeixin.js')['getCachedMergedPosterWeixin']>
|
||||
readonly getCompanyName: UnwrapRef<typeof import('./utils/runtimeEnv.js')['getCompanyName']>
|
||||
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
|
||||
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
|
||||
@@ -400,6 +403,7 @@ declare module 'vue' {
|
||||
readonly getCustomerServiceUrl: UnwrapRef<typeof import('./utils/runtimeEnv.js')['getCustomerServiceUrl']>
|
||||
readonly getInviteChannelKey: UnwrapRef<typeof import('./utils/runtimeEnv.js')['getInviteChannelKey']>
|
||||
readonly getMeShareTitle: UnwrapRef<typeof import('./utils/runtimeEnv.js')['getMeShareTitle']>
|
||||
readonly getPosterOrigin: UnwrapRef<typeof import('./utils/runtimeEnv.js')['getPosterOrigin']>
|
||||
readonly getPosterSrcList: UnwrapRef<typeof import('./utils/posterQrWeixin.js')['getPosterSrcList']>
|
||||
readonly getShareTitle: UnwrapRef<typeof import('./utils/runtimeEnv.js')['getShareTitle']>
|
||||
readonly getSiteOrigin: UnwrapRef<typeof import('./utils/runtimeEnv.js')['getSiteOrigin']>
|
||||
|
||||
@@ -3,52 +3,29 @@
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<view class="max-h-[calc(100vh-100px)] m-4">
|
||||
<!-- 单 canvas 离屏绘制,避免 swiper 未挂载项取不到节点 -->
|
||||
<canvas
|
||||
id="mpPosterCanvas"
|
||||
canvas-id="mpPosterCanvas"
|
||||
type="2d"
|
||||
class="mp-poster-canvas-hidden"
|
||||
/>
|
||||
<canvas id="mpPosterCanvas" canvas-id="mpPosterCanvas" type="2d" class="mp-poster-canvas-hidden" />
|
||||
<view class="p-2 flex justify-center">
|
||||
<swiper
|
||||
:key="swiperMountKey"
|
||||
class="mp-poster-swiper w-full"
|
||||
:style="{ height: swiperHeightPx + 'px' }"
|
||||
:duration="280"
|
||||
:easing-function="easeOutCubic"
|
||||
@change="onSwiperChange"
|
||||
>
|
||||
<swiper :key="swiperMountKey" class="mp-poster-swiper w-full" :style="{ height: swiperHeightPx + 'px' }"
|
||||
:current="currentSwiperIndex" :duration="280" :easing-function="easeOutCubic" @change="onSwiperChange">
|
||||
<swiper-item v-for="(_, idx) in posterSrcList" :key="idx" class="mp-swiper-item">
|
||||
<view class="mp-poster-item">
|
||||
<image
|
||||
v-if="renderedPaths[idx]"
|
||||
:src="renderedPaths[idx]"
|
||||
mode="aspectFit"
|
||||
class="rounded-xl shadow poster-preview-mp"
|
||||
:style="posterPreviewStyle"
|
||||
/>
|
||||
<image v-if="renderedPaths[idx]" :src="renderedPaths[idx]" mode="aspectFit"
|
||||
class="rounded-xl shadow poster-preview-mp" :style="posterPreviewStyle" />
|
||||
<view v-else class="text-gray-400 text-sm py-16">生成海报中…</view>
|
||||
</view>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
</view>
|
||||
<view
|
||||
v-if="mode === 'promote'"
|
||||
class="text-center text-gray-500 text-xs mb-2"
|
||||
>
|
||||
<view v-if="mode === 'promote'" class="text-center text-gray-500 text-xs mb-2">
|
||||
← 左右滑动切换海报 →
|
||||
</view>
|
||||
<view class="divider">分享与保存</view>
|
||||
<view class="mp-share-actions">
|
||||
<button
|
||||
class="share-mp-btn flex flex-col items-center justify-center"
|
||||
open-type="share"
|
||||
plain
|
||||
@tap="onShareFriendPrepare"
|
||||
>
|
||||
<!-- <button class="share-mp-btn flex flex-col items-center justify-center" open-type="share" plain
|
||||
@tap="onShareFriendPrepare">
|
||||
<image src="/static/image/icon_share_friends.svg" class="w-10 h-10 rounded-full" />
|
||||
<text class="text-center mt-1 text-gray-600 text-xs">分享给好友</text>
|
||||
</button>
|
||||
</button> -->
|
||||
<view class="flex flex-col items-center justify-center" @click="savePoster">
|
||||
<image src="/static/image/icon_share_img.svg" class="w-10 h-10 rounded-full" />
|
||||
<view class="text-center mt-1 text-gray-600 text-xs">保存图片</view>
|
||||
@@ -65,14 +42,9 @@
|
||||
<view class="max-h-[calc(100vh-100px)] m-4">
|
||||
<view class="p-4 flex justify-center">
|
||||
<view class="max-h-[70vh] rounded-xl overflow-hidden">
|
||||
<image
|
||||
:src="posterImageUrlRemote"
|
||||
class="rounded-xl shadow poster-image"
|
||||
:style="{ width: imageWidth + 'px', height: imageHeight + 'px' }"
|
||||
mode="aspectFit"
|
||||
@load="onImageLoad"
|
||||
@error="onImageError"
|
||||
/>
|
||||
<image :src="posterImageUrlRemote" class="rounded-xl shadow poster-image"
|
||||
:style="{ width: imageWidth + 'px', height: imageHeight + 'px' }" mode="aspectFit" @load="onImageLoad"
|
||||
@error="onImageError" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="divider">分享到好友</view>
|
||||
@@ -97,7 +69,7 @@ import { getApiBaseUrl, getAgentTabShareTitle, getShareTitle } from '@/utils/run
|
||||
import { buildPromotionH5Url } from '@/utils/promotionH5Url.js'
|
||||
import { setMiniPromotionShareFriend } from '@/utils/miniPromotionSharePayload.js'
|
||||
// #ifdef MP-WEIXIN
|
||||
import { getPosterSrcList, drawMergedPosterWeixin } from '@/utils/posterQrWeixin.js'
|
||||
import { getPosterSrcList, drawMergedPosterWeixin, getCachedMergedPosterWeixin } from '@/utils/posterQrWeixin.js'
|
||||
// #endif
|
||||
|
||||
const props = defineProps({
|
||||
@@ -184,12 +156,40 @@ function updateMpPosterLayout() {
|
||||
|
||||
/** 串行生成,避免多索引共用同一 canvas 竞态 */
|
||||
let mpGenSeq = Promise.resolve()
|
||||
const mpRenderStateCache = new Map()
|
||||
|
||||
function getRenderCacheKey() {
|
||||
return `${mode.value}::${linkIdentifier.value || ''}`
|
||||
}
|
||||
|
||||
function updateRenderCache(partial) {
|
||||
const key = getRenderCacheKey()
|
||||
const prev = mpRenderStateCache.get(key) || { paths: [], lastIndex: 0 }
|
||||
const next = {
|
||||
paths: Array.isArray(partial.paths) ? [...partial.paths] : prev.paths,
|
||||
lastIndex: Number.isFinite(partial.lastIndex) ? partial.lastIndex : prev.lastIndex,
|
||||
}
|
||||
mpRenderStateCache.set(key, next)
|
||||
}
|
||||
|
||||
function resetMpPosters() {
|
||||
mpGenSeq = Promise.resolve()
|
||||
const n = posterSrcList.value.length
|
||||
renderedPaths.value = Array.from({ length: n }, () => '')
|
||||
currentSwiperIndex.value = 0
|
||||
const cache = mpRenderStateCache.get(getRenderCacheKey())
|
||||
const initialPaths = Array.from({ length: n }, (_, idx) => {
|
||||
const saved = cache?.paths?.[idx]
|
||||
if (saved) return saved
|
||||
return getCachedMergedPosterWeixin({
|
||||
linkUrl: generalUrl(),
|
||||
posterSrc: posterSrcList.value[idx],
|
||||
mode: mode.value,
|
||||
index: idx,
|
||||
}) || ''
|
||||
})
|
||||
renderedPaths.value = initialPaths
|
||||
const cachedIndex = cache?.lastIndex ?? 0
|
||||
currentSwiperIndex.value = Math.min(Math.max(cachedIndex, 0), Math.max(n - 1, 0))
|
||||
updateRenderCache({ paths: initialPaths, lastIndex: currentSwiperIndex.value })
|
||||
}
|
||||
|
||||
function generatePosterMp(index) {
|
||||
@@ -198,7 +198,7 @@ function generatePosterMp(index) {
|
||||
if (!proxy) return Promise.resolve()
|
||||
|
||||
mpGenSeq = mpGenSeq
|
||||
.catch(() => {})
|
||||
.catch(() => { })
|
||||
.then(async () => {
|
||||
if (renderedPaths.value[index]) return
|
||||
await new Promise((r) => setTimeout(r, 80))
|
||||
@@ -224,6 +224,7 @@ function generatePosterMp(index) {
|
||||
const next = [...renderedPaths.value]
|
||||
next[index] = temp
|
||||
renderedPaths.value = next
|
||||
updateRenderCache({ paths: next })
|
||||
})
|
||||
return mpGenSeq
|
||||
}
|
||||
@@ -231,6 +232,7 @@ function generatePosterMp(index) {
|
||||
function onSwiperChange(e) {
|
||||
const cur = e.detail?.current ?? 0
|
||||
currentSwiperIndex.value = cur
|
||||
updateRenderCache({ lastIndex: cur })
|
||||
if (!renderedPaths.value[cur]) {
|
||||
// 等 swiper 切换动画走一部分再跑 canvas,减轻主线程卡顿
|
||||
setTimeout(() => {
|
||||
@@ -249,7 +251,7 @@ watch(show, (v) => {
|
||||
swiperMountKey.value += 1
|
||||
resetMpPosters()
|
||||
nextTick(() => {
|
||||
generatePosterMp(0).catch((err) => {
|
||||
generatePosterMp(currentSwiperIndex.value).catch((err) => {
|
||||
console.error('生成海报失败', err)
|
||||
uni.showToast({ title: '海报生成失败', icon: 'none' })
|
||||
})
|
||||
@@ -264,7 +266,7 @@ watch([mode, linkIdentifier], () => {
|
||||
swiperMountKey.value += 1
|
||||
resetMpPosters()
|
||||
nextTick(() => {
|
||||
generatePosterMp(0).catch(() => {})
|
||||
generatePosterMp(currentSwiperIndex.value).catch(() => { })
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -319,7 +321,7 @@ function onImageLoad() {
|
||||
imageWidth.value = size.width
|
||||
imageHeight.value = size.height
|
||||
},
|
||||
fail: () => {},
|
||||
fail: () => { },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -518,5 +520,6 @@ function copyUrl() {
|
||||
.share-mp-btn::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* #endif */
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"apiUrl": "http://127.0.0.1:8888",
|
||||
"apiPrefix": "/api/v1",
|
||||
"posterUrl": "http://127.0.0.1:8888/posts",
|
||||
"siteOrigin": "http://127.0.0.1:8888",
|
||||
"appName": "赤眉",
|
||||
"companyName": "戎行技术有限公司有限公司",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"apiUrl": "https://chimei.ronsafe.cn",
|
||||
"apiPrefix": "/api/v1",
|
||||
"posterUrl": "https://chimei.ronsafe.cn/posts",
|
||||
"siteOrigin": "https://chimei.ronsafe.cn",
|
||||
"appName": "赤眉",
|
||||
"companyName": "戎行技术有限公司有限公司",
|
||||
|
||||
1
src/env.d.ts
vendored
@@ -4,6 +4,7 @@
|
||||
interface BdrpRuntimeConfig {
|
||||
apiUrl: string
|
||||
apiPrefix: string
|
||||
posterUrl: string
|
||||
siteOrigin: string
|
||||
appName: string
|
||||
companyName: string
|
||||
|
||||
@@ -164,6 +164,5 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"__esModule": true,
|
||||
"subPackages": []
|
||||
}
|
||||
|
||||
@@ -111,21 +111,12 @@
|
||||
import { ref, reactive, computed, onMounted, watch } from 'vue'
|
||||
import { getAgentMembershipUserConfig, saveAgentMembershipUserConfig, getProductConfig } from '@/apis/agent'
|
||||
|
||||
// 报告类型选项 - 与 webview 保持一致,支持从后端动态加载
|
||||
const reportOptions = ref([
|
||||
{ label: '入职风险', value: 1 },
|
||||
{ label: '小微企业', value: 2 },
|
||||
{ label: '家政风险', value: 3 },
|
||||
{ label: '婚恋风险', value: 4 },
|
||||
{ label: '贷前风险', value: 5 },
|
||||
{ label: '租赁风险', value: 6 },
|
||||
{ label: '个人风险', value: 7 },
|
||||
{ label: '个人大数据', value: 27 },
|
||||
])
|
||||
// 报告类型选项:完全由后端返回
|
||||
const reportOptions = ref([])
|
||||
|
||||
// 状态管理
|
||||
const showPicker = ref(false)
|
||||
const selectedReportId = ref(1)
|
||||
const selectedReportId = ref(null)
|
||||
const selectedReportText = computed(() => {
|
||||
const opt = reportOptions.value.find((o) => o.value === selectedReportId.value)
|
||||
return opt ? opt.label : '请选择'
|
||||
@@ -248,6 +239,8 @@ const validateRatio = () => {
|
||||
|
||||
// 获取配置
|
||||
const getConfig = async () => {
|
||||
if (!selectedReportId.value)
|
||||
return
|
||||
try {
|
||||
const res = await getAgentMembershipUserConfig({ product_id: selectedReportId.value })
|
||||
if (res.code === 200) {
|
||||
@@ -357,37 +350,30 @@ const closeRangeError = () => {
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
// 从后端加载可选报告列表(可选,若失败则使用默认列表)
|
||||
// 从后端加载可选报告列表
|
||||
const loadReportOptions = async () => {
|
||||
try {
|
||||
const res = await getProductConfig()
|
||||
if (res.code !== 200 || !res.data) return
|
||||
if (res.code !== 200 || !res.data)
|
||||
return
|
||||
const list = res.data.agent_product_config || res.data.AgentProductConfig || []
|
||||
if (list.length) {
|
||||
const productNameMap = {
|
||||
1: '入职风险',
|
||||
2: '小微企业',
|
||||
3: '家政风险',
|
||||
4: '婚恋风险',
|
||||
5: '贷前风险',
|
||||
6: '租赁风险',
|
||||
7: '个人风险',
|
||||
27: '个人大数据',
|
||||
}
|
||||
reportOptions.value = list.map((p) => ({
|
||||
label: productNameMap[p.product_id] || `报告${p.product_id}`,
|
||||
reportOptions.value = list
|
||||
.map(p => ({
|
||||
label: p.product_name || `报告${p.product_id}`,
|
||||
value: p.product_id,
|
||||
}))
|
||||
if (!reportOptions.value.find((o) => o.value === selectedReportId.value)) {
|
||||
.filter(p => p.value != null)
|
||||
if (reportOptions.value.length > 0) {
|
||||
if (!reportOptions.value.find(o => o.value === selectedReportId.value))
|
||||
selectedReportId.value = reportOptions.value[0].value
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadReportOptions()
|
||||
getConfig()
|
||||
if (selectedReportId.value)
|
||||
getConfig()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -33,13 +33,13 @@
|
||||
<wd-form :model="formData" ref="promotionForm">
|
||||
<wd-cell-group border>
|
||||
<!-- 报告类型 -->
|
||||
<wd-picker label="报告类型" label-width="100px" v-model="formData.productType" :columns="[reportTypes]"
|
||||
<wd-picker label="报告类型" label-width="100px" v-model="formData.productType" :columns="reportTypes"
|
||||
title="选择报告类型" prop="productType" placeholder="请选择报告类型" @confirm="onConfirmType"
|
||||
:rules="[{ required: true, message: '请选择报告类型' }]" />
|
||||
|
||||
<!-- 定价 -->
|
||||
<wd-input label="客户查询价" label-width="100px" v-model="formData.clientPrice" placeholder="请输入价格" readonly
|
||||
clickable @click="showPricePicker = true" prop="clientPrice" suffix-icon="arrow-right"
|
||||
clickable @click="showPricePicker = true" prop="clientPrice" suffix-icon="arrow-right"
|
||||
:rules="[{ required: true, message: '请输入客户查询价' }]" />
|
||||
</wd-cell-group>
|
||||
|
||||
@@ -76,17 +76,6 @@ import GzhQrcode from '@/components/GzhQrcode.vue'
|
||||
|
||||
usePromotionShareHandlers({ defaultTitle: getAgentTabShareTitle() })
|
||||
|
||||
// 报告类型
|
||||
const reportTypes = [
|
||||
{ label: '人事背调', value: 'backgroundcheck', id: 1 },
|
||||
{ label: '老板企业报告', value: 'companyinfo', id: 2 },
|
||||
{ label: '家政风险', value: 'homeservice', id: 3 },
|
||||
{ label: '婚恋风险', value: 'marriage', id: 4 },
|
||||
{ label: '贷前背调', value: 'preloanbackgroundcheck', id: 5 },
|
||||
{ label: '租赁风险', value: 'rentalrisk', id: 6 },
|
||||
{ label: '个人风险', value: 'riskassessment', id: 7 }
|
||||
]
|
||||
|
||||
// 状态管理
|
||||
const promotionForm = ref(null)
|
||||
const showPricePicker = ref(false)
|
||||
@@ -102,6 +91,18 @@ const formData = ref({
|
||||
clientPrice: null
|
||||
})
|
||||
|
||||
const reportTypes = computed(() => {
|
||||
if (!productConfig.value?.length)
|
||||
return []
|
||||
return productConfig.value
|
||||
.map(item => ({
|
||||
id: item.product_id,
|
||||
label: item.product_name || `产品${item.product_id}`,
|
||||
value: item.product_en || '',
|
||||
}))
|
||||
.filter(item => !!item.value)
|
||||
})
|
||||
|
||||
// 计算成本价格
|
||||
const costPrice = computed(() => {
|
||||
if (!pickerProductConfig.value) return '0.00'
|
||||
@@ -153,16 +154,6 @@ const generatePromotionCode = async () => {
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取选中产品的完整信息
|
||||
const reportType = reportTypes.find(item => item.value === formData.value.productType)
|
||||
if (!reportType) {
|
||||
uni.showToast({
|
||||
title: '请选择有效的报告类型',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const res = await generatePromotionLink({
|
||||
product: formData.value.productType,
|
||||
price: formData.value.clientPrice
|
||||
@@ -187,20 +178,30 @@ const generatePromotionCode = async () => {
|
||||
|
||||
// 选择类型
|
||||
const selectProductType = (reportTypeValue) => {
|
||||
const reportType = reportTypes.find(item => item.id === reportTypeValue || item.value === reportTypeValue)
|
||||
const reportType = reportTypes.value.find(item => item.id === reportTypeValue || item.value === reportTypeValue)
|
||||
|
||||
if (!reportType) return
|
||||
if (!reportType) {
|
||||
pickerProductConfig.value = null
|
||||
formData.value.productType = ''
|
||||
formData.value.clientPrice = null
|
||||
return
|
||||
}
|
||||
|
||||
formData.value.productType = reportType.value
|
||||
|
||||
if (productConfig.value) {
|
||||
for (let i of productConfig.value) {
|
||||
if (i.product_id === reportType.id) {
|
||||
pickerProductConfig.value = i
|
||||
formData.value.clientPrice = i.p_pricing_standard.toString()
|
||||
}
|
||||
}
|
||||
if (!productConfig.value) {
|
||||
pickerProductConfig.value = null
|
||||
formData.value.clientPrice = null
|
||||
return
|
||||
}
|
||||
const matchedConfig = productConfig.value.find(item => item.product_id === reportType.id)
|
||||
if (!matchedConfig) {
|
||||
pickerProductConfig.value = null
|
||||
formData.value.clientPrice = null
|
||||
return
|
||||
}
|
||||
pickerProductConfig.value = matchedConfig
|
||||
formData.value.clientPrice = Number(matchedConfig.p_pricing_standard)
|
||||
}
|
||||
|
||||
// 获取产品配置
|
||||
@@ -209,9 +210,19 @@ const getPromoteConfig = async () => {
|
||||
const res = await getProductConfig()
|
||||
|
||||
if (res.code === 200) {
|
||||
productConfig.value = res.data.AgentProductConfig
|
||||
// 选择第一个报告类型
|
||||
selectProductType(1) // 使用ID 1选择第一个报告类型
|
||||
productConfig.value = res.data.AgentProductConfig || []
|
||||
const firstType = reportTypes.value[0]
|
||||
if (firstType) {
|
||||
selectProductType(firstType.value)
|
||||
} else {
|
||||
pickerProductConfig.value = null
|
||||
formData.value.productType = ''
|
||||
formData.value.clientPrice = null
|
||||
uni.showToast({
|
||||
title: '暂无可推广产品',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: res.msg || '获取配置失败',
|
||||
@@ -233,11 +244,9 @@ const onPriceChange = (price) => {
|
||||
|
||||
// 类型选择确认
|
||||
const onConfirmType = (e) => {
|
||||
// picker在单列模式下返回的是选中项的值
|
||||
if (e && e.value && e.value.length > 0) {
|
||||
const selectedValue = e.value[0]
|
||||
const selectedValue = e?.value?.[0] ?? e?.value ?? e?.selectedOptions?.[0]?.value
|
||||
if (selectedValue !== undefined && selectedValue !== null && selectedValue !== '')
|
||||
selectProductType(selectedValue)
|
||||
}
|
||||
}
|
||||
|
||||
// 显示公众号二维码
|
||||
|
||||
|
Before Width: | Height: | Size: 262 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 807 KiB |
|
Before Width: | Height: | Size: 409 KiB |
|
Before Width: | Height: | Size: 415 KiB |
|
Before Width: | Height: | Size: 572 KiB |
|
Before Width: | Height: | Size: 618 KiB |
|
Before Width: | Height: | Size: 392 KiB |
|
Before Width: | Height: | Size: 181 KiB |
|
Before Width: | Height: | Size: 297 KiB |
|
Before Width: | Height: | Size: 264 KiB |
|
Before Width: | Height: | Size: 585 KiB |
@@ -1,7 +1,8 @@
|
||||
/**
|
||||
* 微信小程序:本地海报底图 + uQRCode 合成(与 webview QRcode.vue 配置对齐)
|
||||
* 微信小程序:服务端海报底图 + uQRCode 合成(与 webview QRcode.vue 配置对齐)
|
||||
*/
|
||||
import UQRCode from 'uqrcodejs'
|
||||
import UQRCode from "uqrcodejs";
|
||||
import { getPosterOrigin } from "@/utils/runtimeEnv.js";
|
||||
|
||||
/** 与 webview src/components/QRcode.vue 中 qrCodePositions 一致 */
|
||||
export const POSTER_QR_POSITIONS = {
|
||||
@@ -16,43 +17,58 @@ export const POSTER_QR_POSITIONS = {
|
||||
{ x: 255, y: 940, size: 250 },
|
||||
],
|
||||
invitation: [{ x: 360, y: -1370, size: 360 }],
|
||||
};
|
||||
|
||||
function buildPosterUrl(filename) {
|
||||
return `${getPosterOrigin()}/${filename}`;
|
||||
}
|
||||
|
||||
export function getPosterSrcList(mode) {
|
||||
if (mode === 'invitation') {
|
||||
return ['/static/poster/yq_qrcode_1.png']
|
||||
if (mode === "invitation") {
|
||||
return [buildPosterUrl("yq_qrcode_1.png")];
|
||||
}
|
||||
return [
|
||||
'/static/poster/tg_qrcode_1.png',
|
||||
'/static/poster/tg_qrcode_2.png',
|
||||
'/static/poster/tg_qrcode_3.png',
|
||||
'/static/poster/tg_qrcode_4.png',
|
||||
'/static/poster/tg_qrcode_5.png',
|
||||
'/static/poster/tg_qrcode_6.png',
|
||||
'/static/poster/tg_qrcode_7.png',
|
||||
'/static/poster/tg_qrcode_8.png',
|
||||
]
|
||||
buildPosterUrl("tg_qrcode_1.png"),
|
||||
buildPosterUrl("tg_qrcode_2.png"),
|
||||
buildPosterUrl("tg_qrcode_3.png"),
|
||||
buildPosterUrl("tg_qrcode_4.png"),
|
||||
buildPosterUrl("tg_qrcode_5.png"),
|
||||
buildPosterUrl("tg_qrcode_6.png"),
|
||||
buildPosterUrl("tg_qrcode_7.png"),
|
||||
buildPosterUrl("tg_qrcode_8.png"),
|
||||
];
|
||||
}
|
||||
|
||||
function getWx() {
|
||||
return typeof wx !== 'undefined' ? wx : null
|
||||
return typeof wx !== "undefined" ? wx : null;
|
||||
}
|
||||
|
||||
const mergedPosterCache = new Map();
|
||||
|
||||
function buildMergedPosterCacheKey({ linkUrl, posterSrc, mode, index }) {
|
||||
return `${mode || "promote"}::${index || 0}::${posterSrc || ""}::${linkUrl || ""}`;
|
||||
}
|
||||
|
||||
export function getCachedMergedPosterWeixin(opts) {
|
||||
const key = buildMergedPosterCacheKey(opts || {});
|
||||
return mergedPosterCache.get(key) || "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 与 webview 一致:优先 png,不存在则尝试同名的 jpg(避免仅提交了 jpg 时小程序侧失败)
|
||||
* @param {string} posterSrc 如 /static/poster/tg_qrcode_2.png
|
||||
* @param {string} posterSrc 如 https://example.com/posts/tg_qrcode_2.png
|
||||
*/
|
||||
function getPosterSrcCandidates(posterSrc) {
|
||||
const base = posterSrc.replace(/\.(png|jpg|jpeg)$/i, '')
|
||||
return [`${base}.png`, `${base}.jpg`, `${base}.jpeg`]
|
||||
const base = posterSrc.replace(/\.(png|jpg|jpeg)$/i, "");
|
||||
return [`${base}.png`, `${base}.jpg`, `${base}.jpeg`];
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<{ info: Record<string, unknown>, resolvedSrc: string }>}
|
||||
*/
|
||||
async function resolvePosterImageInfo(posterSrc) {
|
||||
const candidates = getPosterSrcCandidates(posterSrc)
|
||||
let lastErr = ''
|
||||
const candidates = getPosterSrcCandidates(posterSrc);
|
||||
let lastErr = "";
|
||||
for (const src of candidates) {
|
||||
try {
|
||||
const info = await new Promise((resolve, reject) => {
|
||||
@@ -60,16 +76,16 @@ async function resolvePosterImageInfo(posterSrc) {
|
||||
src,
|
||||
success: resolve,
|
||||
fail: (e) => reject(e),
|
||||
})
|
||||
})
|
||||
return { info, resolvedSrc: src }
|
||||
});
|
||||
});
|
||||
return { info, resolvedSrc: src };
|
||||
} catch (e) {
|
||||
lastErr = (e && (e.errMsg || e.message)) || String(e)
|
||||
lastErr = (e && (e.errMsg || e.message)) || String(e);
|
||||
}
|
||||
}
|
||||
throw new Error(
|
||||
`海报底图无法加载(${lastErr})。请将 tg_qrcode_1~8、yq_qrcode_1 等底图放入小程序目录 src/static/poster/,与同仓库 webview 的 assets 命名一致(支持 .png 或 .jpg)。当前已尝试:${candidates.join('、')}`,
|
||||
)
|
||||
`海报底图无法加载(${lastErr})。请检查服务端 /posts/ 下是否存在 tg_qrcode_1~8、yq_qrcode_1 等底图,并确认小程序已配置该域名为合法下载域名(支持 .png 或 .jpg)。当前已尝试:${candidates.join("、")}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,21 +93,21 @@ async function resolvePosterImageInfo(posterSrc) {
|
||||
* 应用根路径 /static/... 作为 img.src 更可靠,失败再退回 info.path
|
||||
*/
|
||||
function loadCanvasImage(canvas, resolvedSrc, infoPath) {
|
||||
const img = canvas.createImage()
|
||||
const img = canvas.createImage();
|
||||
return new Promise((resolve, reject) => {
|
||||
const tryPath = (p, isFallback) => {
|
||||
img.onload = () => resolve(img)
|
||||
img.onload = () => resolve(img);
|
||||
img.onerror = () => {
|
||||
if (!isFallback && infoPath && p !== infoPath) {
|
||||
tryPath(infoPath, true)
|
||||
tryPath(infoPath, true);
|
||||
} else {
|
||||
reject(new Error(`canvas 图片加载失败: ${p}`))
|
||||
reject(new Error(`canvas 图片加载失败: ${p}`));
|
||||
}
|
||||
}
|
||||
img.src = p
|
||||
}
|
||||
tryPath(resolvedSrc, false)
|
||||
})
|
||||
};
|
||||
img.src = p;
|
||||
};
|
||||
tryPath(resolvedSrc, false);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -105,60 +121,67 @@ function loadCanvasImage(canvas, resolvedSrc, infoPath) {
|
||||
* @returns {Promise<string>} tempFilePath
|
||||
*/
|
||||
export async function drawMergedPosterWeixin(opts) {
|
||||
const { canvas, linkUrl, posterSrc, mode, index, componentInstance } = opts
|
||||
const ctx = canvas.getContext('2d')
|
||||
const { canvas, linkUrl, posterSrc, mode, index, componentInstance } = opts;
|
||||
const cacheKey = buildMergedPosterCacheKey({ linkUrl, posterSrc, mode, index });
|
||||
const cachedPath = mergedPosterCache.get(cacheKey);
|
||||
if (cachedPath) {
|
||||
return cachedPath;
|
||||
}
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) {
|
||||
throw new Error('canvas 2d 不可用')
|
||||
throw new Error("canvas 2d 不可用");
|
||||
}
|
||||
|
||||
const wxApi = getWx()
|
||||
const wxApi = getWx();
|
||||
if (!wxApi?.createOffscreenCanvas) {
|
||||
throw new Error('当前微信基础库不支持 createOffscreenCanvas,请升级基础库')
|
||||
throw new Error("当前微信基础库不支持 createOffscreenCanvas,请升级基础库");
|
||||
}
|
||||
|
||||
const { info, resolvedSrc } = await resolvePosterImageInfo(posterSrc)
|
||||
const infoPath = typeof info.path === 'string' ? info.path : ''
|
||||
const img = await loadCanvasImage(canvas, resolvedSrc, infoPath)
|
||||
const { info, resolvedSrc } = await resolvePosterImageInfo(posterSrc);
|
||||
const infoPath = typeof info.path === "string" ? info.path : "";
|
||||
const img = await loadCanvasImage(canvas, resolvedSrc, infoPath);
|
||||
|
||||
const w = img.width
|
||||
const h = img.height
|
||||
const sys = uni.getSystemInfoSync()
|
||||
const dpr = sys.pixelRatio || 2
|
||||
const w = img.width;
|
||||
const h = img.height;
|
||||
const dpr = 1;
|
||||
|
||||
canvas.width = w * dpr
|
||||
canvas.height = h * dpr
|
||||
ctx.scale(dpr, dpr)
|
||||
ctx.clearRect(0, 0, w, h)
|
||||
ctx.drawImage(img, 0, 0, w, h)
|
||||
canvas.width = w * dpr;
|
||||
canvas.height = h * dpr;
|
||||
ctx.scale(dpr, dpr);
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
ctx.drawImage(img, 0, 0, w, h);
|
||||
|
||||
const positions = POSTER_QR_POSITIONS[mode] || POSTER_QR_POSITIONS.promote
|
||||
const position = positions[index] || positions[0]
|
||||
const qrY = position.y < 0 ? h + position.y : position.y
|
||||
const s = position.size
|
||||
const positions = POSTER_QR_POSITIONS[mode] || POSTER_QR_POSITIONS.promote;
|
||||
const position = positions[index] || positions[0];
|
||||
const qrY = position.y < 0 ? h + position.y : position.y;
|
||||
const s = position.size;
|
||||
|
||||
const off = wxApi.createOffscreenCanvas({ type: '2d', width: s, height: s })
|
||||
const octx = off.getContext('2d')
|
||||
const qr = new UQRCode()
|
||||
qr.data = linkUrl
|
||||
qr.size = s
|
||||
qr.margin = 0
|
||||
qr.make()
|
||||
qr.canvasContext = octx
|
||||
await qr.drawCanvas()
|
||||
ctx.drawImage(off, position.x, qrY, s, s)
|
||||
const off = wxApi.createOffscreenCanvas({ type: "2d", width: s, height: s });
|
||||
const octx = off.getContext("2d");
|
||||
const qr = new UQRCode();
|
||||
qr.data = linkUrl;
|
||||
qr.size = s;
|
||||
qr.margin = 0;
|
||||
qr.make();
|
||||
qr.canvasContext = octx;
|
||||
await qr.drawCanvas();
|
||||
ctx.drawImage(off, position.x, qrY, s, s);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const conf = {
|
||||
canvas,
|
||||
fileType: 'png',
|
||||
fileType: "png",
|
||||
quality: 1,
|
||||
success: (res) => resolve(res.tempFilePath),
|
||||
success: (res) => {
|
||||
mergedPosterCache.set(cacheKey, res.tempFilePath);
|
||||
resolve(res.tempFilePath);
|
||||
},
|
||||
fail: reject,
|
||||
}
|
||||
};
|
||||
if (componentInstance) {
|
||||
uni.canvasToTempFilePath(conf, componentInstance)
|
||||
uni.canvasToTempFilePath(conf, componentInstance);
|
||||
} else {
|
||||
uni.canvasToTempFilePath(conf)
|
||||
uni.canvasToTempFilePath(conf);
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,6 +8,58 @@ const BASE_URL = getApiPrefix()
|
||||
// #ifndef H5
|
||||
const BASE_URL = getApiBaseUrl()
|
||||
// #endif
|
||||
const TEMP_USER_INVALID_CODE = 100012
|
||||
let wxMiniReauthPromise = null
|
||||
|
||||
const clearAuthData = () => {
|
||||
uni.removeStorageSync('token')
|
||||
uni.removeStorageSync('refreshAfter')
|
||||
uni.removeStorageSync('accessExpire')
|
||||
uni.removeStorageSync('userInfo')
|
||||
uni.removeStorageSync('agentInfo')
|
||||
}
|
||||
|
||||
const silentWxMiniReAuth = () => {
|
||||
if (wxMiniReauthPromise)
|
||||
return wxMiniReauthPromise
|
||||
|
||||
wxMiniReauthPromise = new Promise((resolve, reject) => {
|
||||
clearAuthData()
|
||||
uni.login({
|
||||
success: (loginRes) => {
|
||||
if (!loginRes.code) {
|
||||
reject(new Error('wx.login failed'))
|
||||
return
|
||||
}
|
||||
uni.request({
|
||||
url: `${BASE_URL}/user/wxMiniAuth`,
|
||||
method: 'POST',
|
||||
data: { code: loginRes.code },
|
||||
header: {
|
||||
'X-Platform': 'wxmini',
|
||||
},
|
||||
success: (authRes) => {
|
||||
if (authRes.statusCode === 200 && authRes.data?.code === 200 && authRes.data?.data) {
|
||||
uni.setStorageSync('token', authRes.data.data.accessToken)
|
||||
uni.setStorageSync('refreshAfter', authRes.data.data.refreshAfter)
|
||||
uni.setStorageSync('accessExpire', authRes.data.data.accessExpire)
|
||||
resolve(authRes.data)
|
||||
return
|
||||
}
|
||||
reject(authRes.data || new Error('wxMiniAuth failed'))
|
||||
},
|
||||
fail: reject,
|
||||
})
|
||||
},
|
||||
fail: reject,
|
||||
})
|
||||
}).finally(() => {
|
||||
wxMiniReauthPromise = null
|
||||
})
|
||||
|
||||
return wxMiniReauthPromise
|
||||
}
|
||||
|
||||
function request(options) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 请求拦截器逻辑
|
||||
@@ -38,6 +90,24 @@ function request(options) {
|
||||
success: (res) => {
|
||||
// 响应拦截器逻辑
|
||||
if (res.statusCode === 200) {
|
||||
if (res.data?.code === TEMP_USER_INVALID_CODE) {
|
||||
if (!options.__retriedAfterReauth) {
|
||||
silentWxMiniReAuth()
|
||||
.then(() => {
|
||||
request({
|
||||
...options,
|
||||
hideLoading: true,
|
||||
__retriedAfterReauth: true,
|
||||
}).then(resolve).catch(reject)
|
||||
})
|
||||
.catch(() => {
|
||||
reject(res.data)
|
||||
})
|
||||
return
|
||||
}
|
||||
reject(res.data)
|
||||
return
|
||||
}
|
||||
resolve(res.data)
|
||||
}
|
||||
else if (res.statusCode === 401) {
|
||||
|
||||
@@ -1,68 +1,72 @@
|
||||
/**
|
||||
* 最简配置:两份 json 用相对路径 import;import.meta.env.PROD 由 Vite 构建期替换成字面量。
|
||||
*/
|
||||
import dev from '../config/runtime.development.json'
|
||||
import prod from '../config/runtime.production.json'
|
||||
import dev from "../config/runtime.development.json";
|
||||
import prod from "../config/runtime.production.json";
|
||||
|
||||
const _e = import.meta.env.PROD ? prod : dev
|
||||
const _e = import.meta.env.PROD ? prod : dev;
|
||||
|
||||
function nonempty(name) {
|
||||
const v = _e[name]
|
||||
if (v === undefined || v === '') {
|
||||
throw new Error(`缺少运行时配置: ${name}(请检查 src/config/runtime.*.json)`)
|
||||
const v = _e[name];
|
||||
if (v === undefined || v === "") {
|
||||
throw new Error(
|
||||
`缺少运行时配置: ${name}(请检查 src/config/runtime.*.json)`,
|
||||
);
|
||||
}
|
||||
return String(v)
|
||||
return String(v);
|
||||
}
|
||||
|
||||
export function getApiOrigin() {
|
||||
return nonempty('apiUrl').replace(/\/$/, '')
|
||||
return nonempty("apiUrl").replace(/\/$/, "");
|
||||
}
|
||||
export function getPosterOrigin() {
|
||||
return nonempty("posterUrl").replace(/\/$/, "");
|
||||
}
|
||||
|
||||
export function getApiPrefix() {
|
||||
const p = nonempty('apiPrefix')
|
||||
return p.startsWith('/') ? p : `/${p}`
|
||||
const p = nonempty("apiPrefix");
|
||||
return p.startsWith("/") ? p : `/${p}`;
|
||||
}
|
||||
|
||||
export function getApiBaseUrl() {
|
||||
return `${getApiOrigin()}${getApiPrefix()}`
|
||||
return `${getApiOrigin()}${getApiPrefix()}`;
|
||||
}
|
||||
|
||||
export function getSiteOrigin() {
|
||||
return nonempty('siteOrigin').replace(/\/$/, '')
|
||||
return nonempty("siteOrigin").replace(/\/$/, "");
|
||||
}
|
||||
|
||||
export function getCustomerServiceUrl() {
|
||||
return nonempty('customerServiceUrl')
|
||||
return nonempty("customerServiceUrl");
|
||||
}
|
||||
|
||||
export function getCustomerServiceCorpId() {
|
||||
return String(_e.customerServiceCorpId || '')
|
||||
return String(_e.customerServiceCorpId || "");
|
||||
}
|
||||
|
||||
export function getInviteChannelKey() {
|
||||
return nonempty('inviteChannelKey')
|
||||
return nonempty("inviteChannelKey");
|
||||
}
|
||||
|
||||
export function getAppName() {
|
||||
return nonempty('appName')
|
||||
return nonempty("appName");
|
||||
}
|
||||
|
||||
export function getCompanyName() {
|
||||
return nonempty('companyName')
|
||||
return nonempty("companyName");
|
||||
}
|
||||
|
||||
export function getShareTitle() {
|
||||
return nonempty('shareTitle')
|
||||
return nonempty("shareTitle");
|
||||
}
|
||||
|
||||
export function getMeShareTitle() {
|
||||
return nonempty('shareTitleMe')
|
||||
return nonempty("shareTitleMe");
|
||||
}
|
||||
|
||||
export function getAgentTabShareTitle() {
|
||||
return nonempty('shareTitleAgent')
|
||||
return nonempty("shareTitleAgent");
|
||||
}
|
||||
|
||||
export function getAppDebug() {
|
||||
return Boolean(_e.appDebug)
|
||||
return Boolean(_e.appDebug);
|
||||
}
|
||||
|
||||