diff --git a/api/index.js b/api/index.js index 304632b..b80323a 100644 --- a/api/index.js +++ b/api/index.js @@ -46,6 +46,7 @@ export const commonApi = { getConfig: (params) => request({ url: API.INTERFACE_COMMON_GET_CONFIG, method: 'POST', params }), getHomeConfig: (params) => request({ url: API.INTERFACE_HOME_GET_CONFIG, method: 'POST', params }), getHtml: (params) => request({ url: API.INTERFACE_COMMON_GET_HTML, method: 'POST', params }), + pvcv: (params) => request({ url: API.INTERFACE_PVCV_ADD, method: 'POST', params }), feedback: (params) => request({ url: API.INTERFACE_COMMON_POST_FEEDBACK, method: 'POST', params }) } diff --git a/main.js b/main.js index 380100e..bfd3d6d 100644 --- a/main.js +++ b/main.js @@ -3,6 +3,7 @@ import App from './App.vue' import { createPinia } from 'pinia' import uviewPlus from '@/uni_modules/uview-plus' import * as filters from '@/utils/filter.js' +import { reportPagePv } from '@/utils/pvcv.js' function lockLightTheme() { if (typeof uni === 'undefined') return @@ -27,6 +28,12 @@ export function createApp() { // 全局过滤器/工具(Vue3 已移除 filter,这里挂到 globalProperties 供模板使用) app.config.globalProperties.$f = filters + app.mixin({ + onShow() { + reportPagePv() + } + }) + // #ifdef H5-DEV || H5-TEST // 开发/测试环境可按需引入 vconsole // #endif diff --git a/pages/login/login.vue b/pages/login/login.vue index d202b09..9aeaeed 100644 --- a/pages/login/login.vue +++ b/pages/login/login.vue @@ -69,6 +69,7 @@ import { useUserStore } from '@/store/user.js' import { useApp } from '@/utils/useApp.js' import { isInWechat } from '@/utils/util.js' import { rememberLoginSource, returnAfterLogin } from '@/utils/login.js' +import { reportCv } from '@/utils/pvcv.js' import CONFIG from '@/utils/config.js' const userStore = useUserStore() @@ -86,6 +87,7 @@ isIOS = uni.getSystemInfoSync().platform === 'ios' // #endif onLoad((options = {}) => { + reportCv('login_page_show') type.value = options.type || '1' source.value = options.source || '' const redirect = options.redirect || options.url || '' @@ -251,6 +253,7 @@ async function postLogin(code) { function afterLogin(res) { const inviteCode = uni.getStorageSync('invite_code') userStore.setLogin({ token: res.data.token, userInfo: res.data }) + reportCv('login_success') if (res.data.is_register) { toast(inviteCode ? '登录并领取成功!' : '登录成功!') diff --git a/pages/recharge/recharge.vue b/pages/recharge/recharge.vue index 8a86dbb..f21bf07 100644 --- a/pages/recharge/recharge.vue +++ b/pages/recharge/recharge.vue @@ -65,6 +65,7 @@ import { onShow } from '@dcloudio/uni-app' import { useUserStore } from '@/store/user.js' import { useApp } from '@/utils/useApp.js' import { pointApi } from '@/api/index.js' +import { reportCv } from '@/utils/pvcv.js' const userStore = useUserStore() const { toast } = useApp() @@ -174,6 +175,7 @@ async function pollOrder(orderNo, times = 5) { async function handleBuy() { if (!ensureLogin()) return if (!current.value || paying.value) return + reportCv('point_recharge_click') paying.value = true uni.showLoading({ mask: true, title: '发起支付...' }) @@ -181,6 +183,7 @@ async function handleBuy() { const code = await getWxLoginCode() const res = await pointApi.createOrder({ package_id: current.value.id, code }) const order = res.data + reportCv('point_recharge_order') uni.hideLoading() // 打印后端返回的下单结果(含 env,1=沙箱不扣费 / 0=现网真实扣费) @@ -197,6 +200,7 @@ async function handleBuy() { const ok = await pollOrder(order.order_no) uni.hideLoading() if (ok) { + reportCv('point_recharge_paid') toast('充值成功') } else { toast('支付成功,点数到账处理中') diff --git a/pages/tool/portrait-photo.vue b/pages/tool/portrait-photo.vue index 870764d..ff64895 100644 --- a/pages/tool/portrait-photo.vue +++ b/pages/tool/portrait-photo.vue @@ -179,19 +179,30 @@ - + 生成数量 {{ unitPoints }}点/张,共{{ totalPoints }}点 - - + - {{ quantity }} - + + + 按单张收费,一次最多生成{{ maxCount }}张,失败的不扣点 + + + 生成数量 + {{ unitPoints }}点/张 + + + 固定 1 张 + + 后台已关闭多张生成,本次只生成1张 + + @@ -285,6 +296,7 @@ const activeRatio = ref('') const options = reactive({}) const quantity = ref(1) const maxCount = ref(4) +const quantityEnabled = ref(true) const unitPoints = ref(0) const extraPrompt = ref('') const localImage = ref('') @@ -306,7 +318,8 @@ const hasDemo = computed(() => Boolean(demoPair.value.original && demoPair.value const currentStyle = computed(() => styles.value.find((item) => item.key === activeStyle.value) || {}) const currentGroups = computed(() => currentStyle.value.groups || []) const currentPoints = computed(() => Number(balance.value.points || balance.value.point_balance || 0)) -const totalPoints = computed(() => unitPoints.value * quantity.value) +const effectiveQuantity = computed(() => (quantityEnabled.value ? quantity.value : 1)) +const totalPoints = computed(() => unitPoints.value * effectiveQuantity.value) const isPending = computed(() => [10, 20].includes(Number(job.value.status))) const canEdit = computed(() => !job.value.photo_open_id || Number(job.value.status) < 0) const resultUrls = computed(() => job.value.result_urls || []) @@ -359,19 +372,50 @@ watch([activeModel, quantity], () => { if (!configLoading.value) loadConfig(true) }) +function pickFirstDefined(values) { + return values.find((value) => value !== undefined && value !== null && value !== '') +} + +function normalizeQuantityEnabled(data = {}) { + const raw = pickFirstDefined([ + data.quantity_enabled, + data.quantity_supported, + data.product && data.product.quantity_enabled, + data.billing && data.billing.product && data.billing.product.quantity_enabled, + data.billing && data.billing.quote && data.billing.quote.product && data.billing.quote.product.quantity_enabled + ]) + if (raw === undefined) return Number(data.max_count || 1) > 1 + if (typeof raw === 'boolean') return raw + return !['0', 'false', 'no', 'off', 'unsupported', 'not_support', '不支持'].includes(String(raw).toLowerCase()) +} + +function applyQuantityConfig(data = {}) { + const enabled = normalizeQuantityEnabled(data) + const rawMax = pickFirstDefined([ + data.max_count, + data.product && data.product.max_quantity, + data.billing && data.billing.product && data.billing.product.max_quantity, + data.billing && data.billing.quote && data.billing.quote.product && data.billing.quote.product.max_quantity + ]) + const nextMax = enabled ? Math.max(1, Number(rawMax || maxCount.value || 1)) : 1 + quantityEnabled.value = enabled + maxCount.value = nextMax + quantity.value = enabled ? Math.min(Math.max(1, quantity.value), nextMax) : 1 +} + async function loadConfig(silent = false) { try { const res = await portraitApi.getConfig({ style: activeStyle.value || undefined, model: activeModel.value || undefined, - count: quantity.value + count: effectiveQuantity.value }) const data = res.data || {} models.value = data.models || [] styles.value = data.styles || [] ratios.value = data.ratios || [] tips.value = data.tips || [] - maxCount.value = Number(data.max_count || 4) + applyQuantityConfig(data) unitPoints.value = Number(data.unit_points || 0) if (!activeModel.value) activeModel.value = data.default_model || (models.value[0] || {}).key || '' if (!activeRatio.value) { @@ -506,7 +550,7 @@ async function submit() { style: activeStyle.value, model: activeModel.value, ratio: activeRatio.value, - count: quantity.value, + count: effectiveQuantity.value, extra_prompt: extraPrompt.value, request_id: guid(), ...options @@ -640,10 +684,12 @@ function goRecharge() { } function decreaseQuantity() { + if (!quantityEnabled.value || quantity.value <= 1) return quantity.value = Math.max(1, quantity.value - 1) } function increaseQuantity() { + if (!quantityEnabled.value || quantity.value >= maxCount.value) return quantity.value = Math.min(maxCount.value, quantity.value + 1) } @@ -962,7 +1008,23 @@ function increaseQuantity() { text-align: center; background: #eef7ff; } +.quantity-btn.disabled { + color: #a8bfd6; + background: #f3f7fb; +} .quantity-value { min-width: 60rpx; color: #172033; font-size: 32rpx; font-weight: 900; text-align: center; } +.single-quantity { + margin-top: 22rpx; + height: 76rpx; + padding: 0 26rpx; + border-radius: 20rpx; + color: #0b8cff; + font-size: 28rpx; + font-weight: 900; + line-height: 76rpx; + background: #eef7ff; + box-sizing: border-box; +} .note { display: block; margin-top: 16rpx; color: #8a95a6; font-size: 22rpx; } .textarea { diff --git a/utils/pvcv.js b/utils/pvcv.js new file mode 100644 index 0000000..f10a87e --- /dev/null +++ b/utils/pvcv.js @@ -0,0 +1,72 @@ +import request from '@/utils/request.js' +import API from '@/api/config.js' +import { getUA } from '@/utils/util.js' +import { getUser } from '@/utils/auth.js' + +let lastReportKey = '' +let lastReportAt = 0 + +function stringifyQuery(query = {}) { + return Object.keys(query) + .filter(key => query[key] !== undefined && query[key] !== null && query[key] !== '') + .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(query[key])}`) + .join('&') +} + +function getCurrentPageInfo() { + const pages = getCurrentPages() + const page = pages[pages.length - 1] + if (!page) return null + + const route = page.route || '' + const query = page.options || {} + const queryString = stringifyQuery(query) + const path = route ? `/${route}` : '/' + const url = queryString ? `${path}?${queryString}` : path + + return { + route, + path, + url + } +} + +export function reportPagePv(extra = {}) { + const page = getCurrentPageInfo() + if (!page) return + + const now = Date.now() + const key = extra.key ? `${page.url}::${extra.key}` : page.url + if (key === lastReportKey && now - lastReportAt < 800) return + lastReportKey = key + lastReportAt = now + + const user = getUser() || {} + const referrer = uni.getStorageSync('pvcv_last_url') || '' + uni.setStorageSync('pvcv_last_url', page.url) + + request({ + url: API.INTERFACE_PVCV_ADD, + method: 'POST', + params: { + source: uni.getStorageSync('source') || '', + referrer, + url: page.url, + path: page.path, + page_key: page.route, + user_id: user.user_id || 0, + user_agent: getUA(), + ...extra + } + }).catch(() => { + // 统计失败不能影响页面主流程 + }) +} + +export function reportCv(key, extra = {}) { + if (!key) return + reportPagePv({ + ...extra, + key + }) +}