This commit is contained in:
2026-06-29 14:09:46 +08:00
parent 4f041ffa37
commit 7e73312838
23 changed files with 365 additions and 1250 deletions

12
src/auto-imports.d.ts vendored
View File

@@ -10,8 +10,10 @@ declare global {
const ZoomAdapter: typeof import('./utils/zoomAdapter.js')['ZoomAdapter']
const aesDecrypt: typeof import('./utils/crypto.js')['aesDecrypt']
const aesEncrypt: typeof import('./utils/crypto.js')['aesEncrypt']
const appendAppAuthQueryParams: typeof import('./composables/useReportWebview')['appendAppAuthQueryParams']
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
const buildAgentVipApplyH5Url: typeof import('./composables/useAgentVipH5')['buildAgentVipApplyH5Url']
const calculatePromotionPricing: typeof import('./utils/promotionPricing.js')['calculatePromotionPricing']
const chatCrypto: typeof import('./utils/chatCrypto.js')['default']
const chatEncrypt: typeof import('./utils/chatEncrypt.js')['default']
@@ -78,6 +80,7 @@ declare global {
const markRaw: typeof import('vue')['markRaw']
const navigateLogin: typeof import('./utils/navigate')['navigateLogin']
const navigateTo: typeof import('./utils/navigate')['navigateTo']
const navigateToAgentVipApplyH5: typeof import('./composables/useAgentVipH5')['navigateToAgentVipApplyH5']
const nextTick: typeof import('vue')['nextTick']
const onActivated: typeof import('vue')['onActivated']
const onAddToFavorites: typeof import('@dcloudio/uni-app')['onAddToFavorites']
@@ -122,7 +125,9 @@ declare global {
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
const openAgentVipApplyInExternalBrowser: typeof import('./composables/useAgentVipH5')['openAgentVipApplyInExternalBrowser']
const openCustomerService: typeof import('./composables/useCustomerService')['openCustomerService']
const openExternalUrl: typeof import('./composables/openExternalUrl')['openExternalUrl']
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
const provide: typeof import('vue')['provide']
const provideLocal: typeof import('@vueuse/core')['provideLocal']
@@ -156,6 +161,7 @@ declare global {
const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const shouldSilencePrivacyBlockedRequest: typeof import('./composables/usePrivacyConsent')['shouldSilencePrivacyBlockedRequest']
const syncRef: typeof import('@vueuse/core')['syncRef']
const syncRefs: typeof import('@vueuse/core')['syncRefs']
const templateRef: typeof import('@vueuse/core')['templateRef']
@@ -408,8 +414,10 @@ declare module 'vue' {
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
readonly aesDecrypt: UnwrapRef<typeof import('./utils/crypto.js')['aesDecrypt']>
readonly aesEncrypt: UnwrapRef<typeof import('./utils/crypto.js')['aesEncrypt']>
readonly appendAppAuthQueryParams: UnwrapRef<typeof import('./composables/useReportWebview')['appendAppAuthQueryParams']>
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
readonly buildAgentVipApplyH5Url: UnwrapRef<typeof import('./composables/useAgentVipH5')['buildAgentVipApplyH5Url']>
readonly calculatePromotionPricing: UnwrapRef<typeof import('./utils/promotionPricing.js')['calculatePromotionPricing']>
readonly clearAuthStorage: UnwrapRef<typeof import('./utils/storage')['clearAuthStorage']>
readonly clearToken: UnwrapRef<typeof import('./utils/storage')['clearToken']>
@@ -474,6 +482,7 @@ declare module 'vue' {
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
readonly navigateLogin: UnwrapRef<typeof import('./utils/navigate')['navigateLogin']>
readonly navigateTo: UnwrapRef<typeof import('./utils/navigate')['navigateTo']>
readonly navigateToAgentVipApplyH5: UnwrapRef<typeof import('./composables/useAgentVipH5')['navigateToAgentVipApplyH5']>
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
readonly onAddToFavorites: UnwrapRef<typeof import('@dcloudio/uni-app')['onAddToFavorites']>
@@ -518,7 +527,9 @@ declare module 'vue' {
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
readonly onWatcherCleanup: UnwrapRef<typeof import('vue')['onWatcherCleanup']>
readonly openAgentVipApplyInExternalBrowser: UnwrapRef<typeof import('./composables/useAgentVipH5')['openAgentVipApplyInExternalBrowser']>
readonly openCustomerService: UnwrapRef<typeof import('./composables/useCustomerService')['openCustomerService']>
readonly openExternalUrl: UnwrapRef<typeof import('./composables/openExternalUrl')['openExternalUrl']>
readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
readonly provide: UnwrapRef<typeof import('vue')['provide']>
readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']>
@@ -552,6 +563,7 @@ declare module 'vue' {
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
readonly shouldSilencePrivacyBlockedRequest: UnwrapRef<typeof import('./composables/usePrivacyConsent')['shouldSilencePrivacyBlockedRequest']>
readonly syncRef: UnwrapRef<typeof import('@vueuse/core')['syncRef']>
readonly syncRefs: UnwrapRef<typeof import('@vueuse/core')['syncRefs']>
readonly templateRef: UnwrapRef<typeof import('@vueuse/core')['templateRef']>

1
src/components.d.ts vendored
View File

@@ -12,7 +12,6 @@ declare module 'vue' {
BindPhoneDialog: typeof import('./components/BindPhoneDialog.vue')['default']
EmptyState: typeof import('./components/EmptyState.vue')['default']
ImageSaveGuide: typeof import('./components/ImageSaveGuide.vue')['default']
InquireForm: typeof import('./components/InquireForm.vue')['default']
LoginDialog: typeof import('./components/LoginDialog.vue')['default']
Payment: typeof import('./components/Payment.vue')['default']
PriceInputPopup: typeof import('./components/PriceInputPopup.vue')['default']

View File

@@ -1,884 +0,0 @@
<script setup>
import { computed, nextTick, onMounted, onUnmounted, reactive, ref } from 'vue'
import BindPhoneDialog from '@/components/BindPhoneDialog.vue'
import LoginDialog from '@/components/LoginDialog.vue'
import Payment from '@/components/Payment.vue'
import SectionTitle from '@/components/SectionTitle.vue'
import { useRouter } from '@/composables/uni-router'
import { useAppConfig } from '@/composables/useAppConfig'
import { useDialogStore } from '@/stores/dialogStore'
import { useUserStore } from '@/stores/userStore'
import { aesEncrypt } from '@/utils/crypto'
import { setAuthSession } from '@/utils/storage'
// Props
const props = defineProps({
// 查询类型:'normal' | 'promotion'
type: {
type: String,
default: 'normal',
},
// 产品特征
feature: {
type: String,
required: true,
},
// 推广链接标识符(仅推广查询需要)
linkIdentifier: {
type: String,
default: '',
},
// 产品数据(从外部传入)
featureData: {
type: Object,
default: () => ({}),
},
})
// Emits
const emit = defineEmits(['submit-success'])
const PRODUCT_BACKGROUND_MAP = {
companyinfo: '/static/images/product/xwqy_inquire_bg.png',
preloanbackgroundcheck: '/static/images/product/dqfx_inquire_bg.png',
personalData: '/static/images/product/grdsj_inquire_bg.png',
marriage: '/static/images/product/marriage_inquire_bg.png',
homeservice: '/static/images/product/homeservice_inquire_bg.png',
backgroundcheck: '/static/images/product/backgroundcheck_inquire_bg.png',
rentalinfo: '/static/images/product/rentalinfo_inquire_bg.png',
}
const TRAPEZOID_BACKGROUND_MAP = {
marriage: '/static/images/report/title_inquire_bg_red.png',
homeservice: '/static/images/report/title_inquire_bg_green.png',
default: '/static/images/report/title_inquire_bg.png',
}
function showToast(options) {
const message = typeof options === 'string' ? options : (options?.message || options?.title || '')
if (!message)
return
uni.showToast({
title: message,
icon: options?.type === 'success' ? 'success' : 'none',
})
}
const { feature } = toRefs(props)
function loadProductBackground(productType) {
return PRODUCT_BACKGROUND_MAP[productType] || ''
}
const router = useRouter()
const dialogStore = useDialogStore()
const userStore = useUserStore()
const isWeChat = ref(false)
const { appConfig, loadAppConfig } = useAppConfig()
// 响应式数据
const showPayment = ref(false)
const pendingPayment = ref(false)
const queryId = ref(null)
const productBackground = ref('')
const productMainImage = ref('')
const trapezoidBgImage = ref('')
const isCountingDown = ref(false)
const countdown = ref(60)
const verificationCodeInputRef = ref(null)
// 使用传入的featureData或创建响应式引用
const featureData = computed(() => props.featureData || {})
// 表单数据
const formData = reactive({
name: '',
idCard: '',
mobile: '',
verificationCode: '',
agreeToTerms: false,
})
// 计算属性
const isPhoneNumberValid = computed(() => {
return /^1[3-9]\d{9}$/.test(formData.mobile)
})
const isIdCardValid = computed(() => /^\d{17}[\dX]$/i.test(formData.idCard))
// 小微企业(companyinfo)暂不需要验证码
const needVerificationCode = computed(() => props.feature !== 'companyinfo')
const isLoggedIn = computed(() => userStore.isLoggedIn)
const buttonText = computed(() => {
return isLoggedIn.value ? '立即查询' : '前往登录'
})
const hasHeroImage = computed(() => Boolean(productBackground.value))
const queryRetentionDaysText = computed(() => `${appConfig.value.query.retention_days}`)
function loadTrapezoidBackground() {
trapezoidBgImage.value = TRAPEZOID_BACKGROUND_MAP[props.feature] || TRAPEZOID_BACKGROUND_MAP.default
}
// 牌匾背景图片样式
const trapezoidBgStyle = computed(() => {
if (trapezoidBgImage.value) {
return {
backgroundImage: `url(${trapezoidBgImage.value})`,
}
}
return {}
})
// 牌匾文字样式
const trapezoidTextStyle = computed(() => {
// homeservice 和 marriage 使用白色文字
if (props.feature === 'homeservice' || props.feature === 'marriage') {
return {
color: 'white',
}
}
// 其他情况使用默认字体色(不设置 color使用浏览器默认或继承
return {}
})
// 获取功能图标
function getFeatureIcon(apiId) {
const iconMap = {
JRZQ4AA8: '/static/inquire_icons/huankuanyali.svg', // 还款压力
QCXG7A2B: '/static/inquire_icons/mingxiacheliang.svg', // 名下车辆
BehaviorRiskScan: '/static/inquire_icons/fengxianxingwei.svg', // 风险行为扫描
IVYZ5733: '/static/inquire_icons/hunyinzhuangtai.svg', // 婚姻状态
PersonEnterprisePro: '/static/inquire_icons/renqiguanxi.svg', // 人企关系加强版
JRZQ0A03: '/static/inquire_icons/jiedaishenqing.svg', // 借贷申请记录
FLXG3D56: '/static/inquire_icons/jiedaiweiyue.svg', // 借贷违约失信
FLXG0V4B: '/static/inquire_icons/sifasheyu.svg', // 司法涉诉
JRZQ8203: '/static/inquire_icons/jiedaixingwei.svg', // 借贷行为记录
JRZQ09J8: '/static/inquire_icons/beijianguanrenyuan.svg', // 收入评估
JRZQ4B6C: '/static/inquire_icons/fengxianxingwei.svg', // 探针C风险评估
}
return iconMap[apiId] || '/static/inquire_icons/default.svg'
}
// 处理图标加载错误
function handleIconError(event) {
event.target.style.display = 'none'
}
// 获取卡片样式类
function getCardClass(index) {
const colorIndex = index % 4
const colorClasses = [
'bg-gradient-to-br from-blue-50 via-blue-25 to-white border-2 border-blue-200',
'bg-gradient-to-br from-green-50 via-green-25 to-white border-2 border-green-200',
'bg-gradient-to-br from-purple-50 via-purple-25 to-white border-2 border-purple-200',
'bg-gradient-to-br from-orange-50 via-orange-25 to-white border-2 border-orange-200',
]
return colorClasses[colorIndex]
}
// 方法
function validateField(field, value, validationFn, errorMessage) {
if (isHasInput(field) && !validationFn(value)) {
showToast({ message: errorMessage })
return false
}
return true
}
const defaultInput = ['name', 'idCard', 'mobile', 'verificationCode']
function isHasInput(input) {
return defaultInput.includes(input)
}
// 处理绑定手机号成功的回调
function handleBindSuccess() {
if (pendingPayment.value) {
pendingPayment.value = false
submitRequest()
}
}
// 处理登录成功的回调
function handleLoginSuccess() {
if (pendingPayment.value) {
pendingPayment.value = false
submitRequest()
}
}
// 处理输入框点击事件
async function handleInputClick() {
if (!isLoggedIn.value) {
// 非微信浏览器环境:未登录用户提示打开登录弹窗
if (!isWeChat.value) {
const { confirm } = await uni.showModal({
title: '提示',
content: '您需要登录后才能进行查询,是否立即登录?',
confirmText: '立即登录',
cancelText: '取消',
})
if (confirm)
dialogStore.openLogin()
}
}
else {
// 微信浏览器环境:已登录但检查是否需要绑定手机号
if (isWeChat.value && !userStore.mobile) {
dialogStore.openBindPhone()
}
}
}
function handleSubmit() {
// 非微信浏览器环境:检查登录状态
if (!isWeChat.value && !isLoggedIn.value) {
dialogStore.openLogin()
return
}
// 基本协议验证
if (!formData.agreeToTerms) {
showToast({ message: `请阅读并同意用户协议和隐私政策` })
return
}
if (
!validateField('name', formData.name, value => value, '请输入姓名')
|| !validateField(
'mobile',
formData.mobile,
() => isPhoneNumberValid.value,
'请输入有效的手机号',
)
|| !validateField(
'idCard',
formData.idCard,
() => isIdCardValid.value,
'请输入有效的身份证号码',
)
|| (needVerificationCode.value
&& !validateField(
'verificationCode',
formData.verificationCode,
value => value,
'请输入验证码',
))
) {
return
}
// 检查是否需要绑定手机号
if (!userStore.mobile) {
pendingPayment.value = true
dialogStore.openBindPhone()
}
else {
submitRequest()
}
}
async function submitRequest() {
const req = {
name: formData.name,
id_card: formData.idCard,
mobile: formData.mobile,
}
if (needVerificationCode.value) {
req.code = formData.verificationCode
}
const reqStr = JSON.stringify(req)
const inquireKey = import.meta.env.VITE_INQUIRE_AES_KEY
if (!inquireKey) {
throw new Error('缺少环境变量: VITE_INQUIRE_AES_KEY')
}
const encodeData = aesEncrypt(reqStr, inquireKey)
let apiUrl = ''
const requestData = { data: encodeData }
if (props.type === 'promotion') {
apiUrl = `/query/service_agent/${props.feature}`
requestData.agent_identifier = props.linkIdentifier
}
else {
apiUrl = `/query/service/${props.feature}`
}
const { data } = await useApiFetch(apiUrl)
.post(requestData)
.json()
if (data.value.code === 200) {
queryId.value = data.value.data.id
// 推广查询需要保存token
if (props.type === 'promotion') {
setAuthSession(data.value.data)
}
showPayment.value = true
emit('submit-success', data.value.data)
}
}
async function sendVerificationCode() {
if (isCountingDown.value || !isPhoneNumberValid.value)
return
if (!isPhoneNumberValid.value) {
showToast({ message: '请输入有效的手机号' })
return
}
const { data, error } = await useApiFetch('/auth/sendSms')
.post({ mobile: formData.mobile, actionType: 'query', captchaVerifyParam: '' })
.json()
if (!error.value && data.value?.code === 200) {
showToast({ message: '验证码发送成功', type: 'success' })
startCountdown()
nextTick(() => {
verificationCodeInputRef.value?.focus?.()
})
}
else {
showToast({ message: data.value?.msg || '验证码发送失败,请重试' })
}
}
let timer = null
function startCountdown() {
isCountingDown.value = true
countdown.value = 60
timer = setInterval(() => {
if (countdown.value > 0) {
countdown.value--
}
else {
clearInterval(timer)
isCountingDown.value = false
}
}, 1000)
}
function toUserAgreement() {
router.push(`/userAgreement`)
}
function toPrivacyPolicy() {
router.push(`/privacyPolicy`)
}
function toAuthorization() {
router.push(`/authorization`)
}
function toExample() {
router.push(`/example?feature=${props.feature}`)
}
function toHistory() {
router.push('/historyQuery')
}
// 生命周期
onMounted(async () => {
loadBackgroundImage()
loadTrapezoidBackground()
await loadAppConfig()
})
// 加载背景图片
async function loadBackgroundImage() {
productBackground.value = loadProductBackground(props.feature)
}
onUnmounted(() => {
if (timer) {
clearInterval(timer)
}
})
watch(feature, async () => {
loadBackgroundImage()
loadTrapezoidBackground()
})
</script>
<template>
<view class="inquire-bg relative min-h-screen">
<view v-if="hasHeroImage" class="hero-banner">
<image :src="productBackground" class="hero-banner-image" mode="widthFix" />
<view class="hero-banner-mask" />
<view class="hero-badge-wrap">
<view class="trapezoid-bg-image flex items-center justify-center" :style="trapezoidBgStyle">
<view class="whitespace-nowrap text-xl" :style="trapezoidTextStyle">
{{ featureData.product_name }}
</view>
</view>
</view>
</view>
<view class="content-wrap relative mx-4 min-h-screen pb-12" :class="{ 'with-hero': hasHeroImage }">
<view class="card-container">
<!-- 基本信息标题 -->
<view class="mb-6 flex items-center">
<SectionTitle title="基本信息" />
<view class="ml-auto flex cursor-pointer items-center text-gray-600" @click="toExample">
<image src="/static/images/report/slbg_inquire_icon.png" alt="示例报告" class="mr-1 h-4 w-4" />
<text class="">
示例报告
</text>
</view>
</view>
<!-- 表单输入区域 -->
<view class="mb-6 space-y-4">
<view class="flex items-center border-b border-gray-100 py-3">
<text class="w-20 text-gray-700 font-medium">
姓名
</text>
<wd-input
v-model="formData.name"
class="inquire-wd-input flex-1"
type="text"
placeholder="请输入正确的姓名"
no-border
clearable
@focus="handleInputClick"
/>
</view>
<view class="flex items-center border-b border-gray-100 py-3">
<text class="w-20 text-gray-700 font-medium">
身份证号
</text>
<wd-input
v-model="formData.idCard"
class="inquire-wd-input flex-1"
type="text"
placeholder="请输入准确的身份证号"
no-border
clearable
@focus="handleInputClick"
/>
</view>
<view class="flex items-center border-b border-gray-100 py-3">
<text class="w-20 text-gray-700 font-medium">
手机号
</text>
<wd-input
v-model="formData.mobile"
class="inquire-wd-input flex-1"
type="number"
placeholder="请输入手机号"
maxlength="11"
no-border
clearable
@focus="handleInputClick"
/>
</view>
<!-- 小微企业(companyinfo)暂不展示验证码 -->
<view v-if="needVerificationCode" class="flex items-center border-b border-gray-100 py-3">
<text class="w-20 text-gray-700 font-medium">
验证码
</text>
<wd-input
ref="verificationCodeInputRef"
v-model="formData.verificationCode"
class="inquire-wd-input flex-1"
placeholder="请输入验证码"
maxlength="6"
no-border
clearable
@focus="handleInputClick"
>
<template #suffix>
<wd-button
class="captcha-wd-btn"
size="small"
type="primary"
plain
:disabled="isCountingDown || !isPhoneNumberValid"
@click="sendVerificationCode"
>
{{ isCountingDown ? `${countdown}s` : '获取验证码' }}
</wd-button>
</template>
</wd-input>
</view>
</view>
<!-- 协议同意 -->
<view class="agreement-wrap mb-6">
<wd-checkbox v-model="formData.agreeToTerms" shape="square" size="18px" />
<view class="agreement-text">
<text class="text-sm text-gray-500">
我已阅读并同意
</text>
<text class="agreement-link" @click="toUserAgreement">
用户协议
</text>
<text class="agreement-link" @click="toPrivacyPolicy">
隐私政策
</text>
<text class="agreement-link" @click="toAuthorization">
授权书
</text>
</view>
</view>
<!-- 查询按钮 -->
<wd-button class="submit-wd-btn mb-4 mt-10" block type="primary" @click="handleSubmit">
<view class="w-full flex items-center justify-center">
<text>{{ buttonText }}</text>
<text class="ml-4">
¥{{ featureData.sell_price }}
</text>
</view>
</wd-button>
<!-- <view class="text-xs text-gray-500 leading-relaxed mt-8" v-html="featureData.description">
</view> -->
<!-- 免责声明 -->
<view class="mt-2 text-center text-xs text-gray-500 leading-relaxed">
为保证用户的隐私及数据安全查询结果生成{{ queryRetentionDaysText }}后将自动删除
</view>
</view>
<!-- 报告包含内容 -->
<view v-if="featureData.features && featureData.features.length > 0" class="card mt-3">
<view class="mb-3 flex items-center text-base font-semibold" style="color: var(--color-text-primary);">
<view class="mr-2 h-5 w-1 rounded-full"
style="background: linear-gradient(to bottom, var(--color-primary), var(--color-primary-700));" />
报告包含内容
</view>
<view class="grid grid-cols-4 items-stretch gap-2">
<template v-for="(item, index) in featureData.features" :key="item.id">
<!-- FLXG0V4B 特殊处理显示8个独立的案件类型 -->
<template v-if="item.api_id === 'FLXG0V4B'">
<view v-for="(caseType, caseIndex) in [
{ name: '管辖案件', icon: 'beijianguanrenyuan.svg' },
{ name: '刑事案件', icon: 'xingshi.svg' },
{ name: '民事案件', icon: 'minshianjianguanli.svg' },
{ name: '失信被执行', icon: 'shixinren.svg' },
{ name: '行政案件', icon: 'xingzhengfuwu.svg' },
{ name: '赔偿案件', icon: 'yuepeichang.svg' },
{ name: '执行案件', icon: 'zhixinganjian.svg' },
{ name: '限高被执行', icon: 'xianzhigaoxiaofei.svg' },
]" :key="`${item.id}-${caseIndex}`"
class="h-full min-h-0 w-full flex flex-col items-center rounded-xl p-2 text-center text-sm text-gray-700 font-medium shadow-lg"
:class="getCardClass(index + caseIndex)">
<view class="mb-1 shrink-0">
<image :src="`/static/inquire_icons/${caseType.icon}`" :alt="caseType.name"
class="mx-auto h-6 w-6 drop-shadow-sm" @error="handleIconError" />
</view>
<view
class="w-full flex flex-1 items-center justify-center break-all px-0.5 text-xs font-medium leading-snug">
{{ caseType.name }}
</view>
</view>
</template>
<!-- DWBG8B4D 特殊处理:显示拆分模块 -->
<template v-else-if="item.api_id === 'DWBG8B4D'">
<view v-for="(module, moduleIndex) in [
{ name: '要素核查', icon: 'beijianguanrenyuan.svg' },
{ name: '运营商核验', icon: 'mingxiacheliang.svg' },
{ name: '公安重点人员检验', icon: 'xingshi.svg' },
{ name: '逾期风险综述', icon: 'huankuanyali.svg' },
{ name: '法院曝光台信息', icon: 'sifasheyu.svg' },
{ name: '借贷评估', icon: 'jiedaishenqing.svg' },
{ name: '租赁风险评估', icon: 'jiedaixingwei.svg' },
{ name: '关联风险监督', icon: 'renqiguanxi.svg' },
{ name: '规则风险提示', icon: 'fengxianxingwei.svg' },
]" :key="`${item.id}-${moduleIndex}`"
class="h-full min-h-0 w-full flex flex-col items-center rounded-xl p-2 text-center text-sm text-gray-700 font-medium shadow-lg"
:class="getCardClass(index + moduleIndex)">
<view class="mb-1 flex shrink-0 items-center justify-center text-xl">
<image :src="`/static/inquire_icons/${module.icon}`" :alt="module.name" class="h-6 w-6 drop-shadow-sm"
@error="handleIconError" />
</view>
<view
class="w-full flex flex-1 items-center justify-center break-all px-1 text-xs font-medium leading-snug">
{{ module.name }}
</view>
</view>
</template>
<!-- CJRZQ5E9F 特殊处理:显示拆分模块 -->
<template v-else-if="item.api_id === 'JRZQ5E9F'">
<view v-for="(module, moduleIndex) in [
{ name: '信用评分', icon: 'huankuanyali.svg' },
{ name: '贷款行为分析', icon: 'jiedaixingwei.svg' },
{ name: '机构分析', icon: 'jiedaishenqing.svg' },
{ name: '时间趋势分析', icon: 'zhixinganjian.svg' },
{ name: '风险指标详情', icon: 'fengxianxingwei.svg' },
{ name: '专业建议', icon: 'yuepeichang.svg' },
]" :key="`${item.id}-${moduleIndex}`"
class="h-full min-h-0 w-full flex flex-col items-center rounded-xl p-2 text-center text-sm text-gray-700 font-medium shadow-lg"
:class="getCardClass(index + moduleIndex)">
<view class="mb-1 flex shrink-0 items-center justify-center text-xl">
<image :src="`/static/inquire_icons/${module.icon}`" :alt="module.name" class="h-6 w-6 drop-shadow-sm"
@error="handleIconError" />
</view>
<view
class="w-full flex flex-1 items-center justify-center break-all px-1 text-xs font-medium leading-snug">
{{ module.name }}
</view>
</view>
</template>
<!-- PersonEnterprisePro/CQYGL3F8E 特殊处理:显示拆分模块 -->
<template v-else-if="item.api_id === 'PersonEnterprisePro' || item.api_id === 'QYGL3F8E'">
<view v-for="(module, moduleIndex) in [
{ name: '投资企业记录', icon: 'renqiguanxi.svg' },
{ name: '高管任职记录', icon: 'beijianguanrenyuan.svg' },
{ name: '涉诉风险', icon: 'sifasheyu.svg' },
{ name: '对外投资历史', icon: 'renqiguanxi.svg' },
{ name: '融资历史', icon: 'huankuanyali.svg' },
{ name: '行政处罚', icon: 'xingzhengfuwu.svg' },
{ name: '经营异常', icon: 'fengxianxingwei.svg' },
]" :key="`${item.id}-${moduleIndex}`"
class="h-full min-h-0 w-full flex flex-col items-center rounded-xl p-2 text-center text-sm text-gray-700 font-medium shadow-lg"
:class="getCardClass(index + moduleIndex)">
<view class="mb-1 flex shrink-0 items-center justify-center text-xl">
<image :src="`/static/inquire_icons/${module.icon}`" :alt="module.name" class="h-6 w-6 drop-shadow-sm"
@error="handleIconError" />
</view>
<view
class="w-full flex flex-1 items-center justify-center break-all px-1 text-xs font-medium leading-snug">
{{ module.name }}
</view>
</view>
</template>
<!-- DWBG6A2C 特殊处理:显示拆分模块 -->
<template v-else-if="item.api_id === 'DWBG6A2C'">
<view v-for="(module, moduleIndex) in [
{ name: '命中风险标注', icon: 'fengxianxingwei.svg' },
{ name: '公安重点人员核验', icon: 'beijianguanrenyuan.svg' },
{ name: '涉赌涉诈人员核验', icon: 'xingshi.svg' },
{ name: '风险名单', icon: 'jiedaiweiyue.svg' },
{ name: '历史借贷行为', icon: 'jiedaixingwei.svg' },
{ name: '近24个月放款情况', icon: 'jiedaishenqing.svg' },
{ name: '履约情况', icon: 'huankuanyali.svg' },
{ name: '历史逾期记录', icon: 'jiedaiweiyue.svg' },
{ name: '授信详情', icon: 'huankuanyali.svg' },
{ name: '租赁行为', icon: 'mingxiacheliang.svg' },
{ name: '关联风险监督', icon: 'renqiguanxi.svg' },
{ name: '法院风险信息', icon: 'sifasheyu.svg' },
]" :key="`${item.id}-${moduleIndex}`"
class="h-full min-h-0 w-full flex flex-col items-center rounded-xl p-2 text-center text-sm text-gray-700 font-medium shadow-lg"
:class="getCardClass(index + moduleIndex)">
<view class="mb-1 flex shrink-0 items-center justify-center text-xl">
<image :src="`/static/inquire_icons/${module.icon}`" :alt="module.name" class="h-6 w-6 drop-shadow-sm"
@error="handleIconError" />
</view>
<view
class="w-full flex flex-1 items-center justify-center break-all px-1 text-xs font-medium leading-snug">
{{ module.name }}
</view>
</view>
</template>
<!-- 其他功能正常显示 -->
<view v-else
class="h-full min-h-0 w-full flex flex-col items-center rounded-xl p-2 text-center text-sm text-gray-700 font-medium shadow-lg"
:class="getCardClass(index)">
<view class="mb-1 flex shrink-0 items-center justify-center">
<image :src="getFeatureIcon(item.api_id)" :alt="item.name" class="h-6 w-6 drop-shadow-sm"
@error="handleIconError" />
</view>
<view
class="w-full flex flex-1 items-center justify-center break-all px-1 text-xs font-medium leading-snug">
{{ item.name }}
</view>
</view>
</template>
</view>
<view class="mt-3 text-center">
<view class="inline-flex items-center border rounded-full px-3 py-1.5 transition-all"
style="background: linear-gradient(135deg, var(--color-primary-light), rgba(255,255,255,0.8)); border-color: var(--color-primary);">
<view class="mr-1.5 h-1.5 w-1.5 rounded-full" style="background-color: var(--color-primary);" />
<text class="text-xs font-medium" style="color: var(--color-primary);">
更多信息请解锁报告
</text>
</view>
</view>
</view>
<!-- 产品详情卡片 -->
<view class="card mt-4">
<view class="mb-4 text-xl font-bold" style="color: var(--color-text-primary);">
{{ featureData.product_name }}
</view>
<view class="mb-4 flex items-start justify-between">
<view class="text-lg" style="color: var(--color-text-secondary);">
价格:
</view>
<view>
<view class="text-danger text-2xl font-semibold">
¥{{ featureData.sell_price }}
</view>
</view>
</view>
<image v-if="productMainImage" :src="productMainImage" alt="产品详情主图" class="mb-4 w-full rounded-lg" />
<view class="mb-4 leading-relaxed" style="color: var(--color-text-secondary);" v-html="featureData.description" />
<view class="text-danger mb-2 text-xs italic">
为保证用户的隐私以及数据安全,查询的结果生成{{ queryRetentionDaysText }}之后将自动清除。
</view>
</view>
</view>
<!-- 支付组件 -->
<Payment :id="queryId" v-model="showPayment" :data="featureData" type="query" @close="showPayment = false" />
<BindPhoneDialog @bind-success="handleBindSuccess" />
<LoginDialog @login-success="handleLoginSuccess" />
<!-- 历史查询按钮 - 仅推广查询且已登录时显示 -->
<view v-if="props.type === 'promotion' && isLoggedIn"
class="bg-primary fixed right-2 top-3/4 cursor-pointer rounded-xl px-4 py-2 text-sm text-white font-bold shadow active:bg-blue-500"
@click="toHistory">
历史查询
</view>
</view>
</template>
<style scoped>
/* 背景样式 */
.inquire-bg {
background-color: var(--color-primary-50);
min-height: 100vh;
position: relative;
}
.hero-banner {
position: relative;
overflow: hidden;
border-bottom-left-radius: 24px;
border-bottom-right-radius: 24px;
background: linear-gradient(180deg, #eef4ff 0%, #dfe9ff 100%);
}
.hero-banner-image {
display: block;
width: 100%;
margin-top: -2px;
}
.hero-banner-mask {
position: absolute;
inset: 0;
background: linear-gradient(180deg, rgba(20, 32, 64, 0.04) 0%, rgba(20, 32, 64, 0.1) 100%);
pointer-events: none;
}
.hero-badge-wrap {
position: absolute;
left: 50%;
bottom: 34px;
transform: translateX(-50%);
width: 160px;
display: flex;
justify-content: center;
z-index: 3;
}
.content-wrap.with-hero {
margin-top: -68px;
padding-top: 0;
z-index: 2;
}
/* 卡片样式优化 */
.card {
@apply shadow-lg rounded-xl p-6 transition-all hover:shadow-xl;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06),
0 0 0 1px rgba(255, 255, 255, 0.05);
}
/* 梯形背景图片样式 */
.trapezoid-bg-image {
background-size: contain;
background-repeat: no-repeat;
background-position: center;
height: 44px;
width: 100%;
}
/* 卡片容器样式 */
.card-container {
background: white;
border-radius: 20px;
padding: 32px 16px;
box-shadow: 0px 0px 24px 0px #3F3F3F0F;
border: 1px solid rgba(255, 255, 255, 0.75);
}
.card-container input::placeholder {
color: #DDDDDD;
}
.captcha-wd-btn {
margin-left: 12px;
}
:deep(.inquire-wd-input .wd-input__inner) {
text-align: left !important;
}
:deep(.submit-wd-btn.wd-button) {
border-radius: 48px;
min-height: 48px;
}
:deep(.captcha-wd-btn.wd-button) {
min-width: 96px;
}
:deep(.captcha-wd-btn.wd-button.is-small) {
padding: 0 10px;
}
.agreement-wrap {
display: flex;
align-items: flex-start;
gap: 10px;
}
:deep(.agreement-wrap .wd-checkbox) {
margin-top: 1px;
}
.agreement-text {
flex: 1;
font-size: 13px;
line-height: 1.7;
word-break: break-all;
}
.agreement-link {
color: #2563eb;
}
/* 功能标签样式 */
.feature-tag {
background-color: var(--color-primary-light);
color: var(--color-primary);
padding: 6px 12px;
border-radius: 9999px;
font-size: 12px;
font-weight: 500;
display: flex;
align-items: center;
}
/* 功能标签圆点 */
.feature-dot {
width: 6px;
height: 6px;
background-color: var(--color-primary);
border-radius: 50%;
margin-right: 8px;
}
</style>

View File

@@ -1,6 +1,8 @@
<script setup>
import { openAgentVipApplyInExternalBrowser } from '@/composables/useAgentVipH5'
function toAgentVip() {
uni.navigateTo({ url: '/pages/agent-vip-apply' })
openAgentVipApplyInExternalBrowser()
}
</script>

View File

@@ -0,0 +1,46 @@
/** 在系统默认浏览器中打开链接App 用 plusH5 开发调试用 window.open */
function redactUrlForLog(url: string): string {
try {
const u = new URL(url)
for (const key of ['token', 'refreshAfter', 'accessExpire']) {
if (u.searchParams.has(key))
u.searchParams.set(key, '***')
}
return u.toString()
}
catch {
return '[invalid-url]'
}
}
export function openExternalUrl(url: string): boolean {
if (!url)
return false
// #ifdef APP-PLUS
try {
if (typeof plus !== 'undefined' && plus.runtime?.openURL) {
plus.runtime.openURL(url, (err: unknown) => {
console.error('[openExternalUrl] 打开失败', err, redactUrlForLog(url))
uni.showToast({ title: '无法打开浏览器', icon: 'none' })
})
return true
}
}
catch (e) {
console.error('[openExternalUrl] 异常', e)
}
uni.showToast({ title: '当前环境无法打开外部浏览器', icon: 'none' })
return false
// #endif
// #ifdef H5
window.open(url, '_blank')
return true
// #endif
// #ifndef APP-PLUS || H5
uni.showToast({ title: '请使用 App 打开', icon: 'none' })
return false
// #endif
}

View File

@@ -6,7 +6,7 @@ import { pages } from 'virtual:uni-pages'
const pathToPage: Record<string, string> = {
'/': '/pages/index',
'/login': '/pages/login',
'/historyQuery': '/pages/history-query',
'/historyQuery': '/pages/index',
'/help': '/pages/help',
'/help/detail': '/pages/help-detail',
'/help/guide': '/pages/help-guide',
@@ -35,7 +35,7 @@ const nameToPage: Record<string, string> = {
invitation: '/pages/invitation',
promote: '/pages/promote',
agent: '/pages/agent',
history: '/pages/history-query',
history: '/pages/index',
help: '/pages/help',
helpDetail: '/pages/help-detail',
helpGuide: '/pages/help-guide',
@@ -70,7 +70,7 @@ export function resolveWebToUni(to: string | { name?: string, path?: string, que
if (pathOnly.startsWith('/inquire/')) {
const feature = pathOnly.replace(/^\/inquire\//, '')
const base = `/pages/inquire?feature=${encodeURIComponent(feature)}`
const base = `/pages/promote?feature=${encodeURIComponent(feature)}`
return queryPart ? `${base}&${queryPart}` : base
}
if (pathOnly.startsWith('/agent/invitationAgentApply/')) {
@@ -142,12 +142,31 @@ const pageMetaByRoute = new Map(
(pages as UniPageMeta[]).map(page => [normalizeUniRoute(page.path), page]),
)
function resolvePageMetaByRoute(route: string): UniPageMeta | undefined {
const normalized = normalizeUniRoute(route)
const candidates = [
normalized,
normalized.startsWith('pages/') ? normalized.slice('pages/'.length) : `pages/${normalized}`,
]
for (const key of candidates) {
const meta = pageMetaByRoute.get(key)
if (meta)
return meta
}
return undefined
}
export function getCurrentPageMeta(): UniPageMeta | undefined {
return pageMetaByRoute.get(getCurrentUniRoute())
return resolvePageMetaByRoute(getCurrentUniRoute())
}
export function getPageTitleByRoute(route = getCurrentUniRoute()): string {
return pageMetaByRoute.get(normalizeUniRoute(route))?.style?.navigationBarTitleText || 'BDRP'
const meta = resolvePageMetaByRoute(route)
const raw = meta?.style?.navigationBarTitleText
if (typeof raw === 'string' && raw.trim())
return raw.trim()
const appName = import.meta.env.VITE_APP_NAME
return (typeof appName === 'string' && appName.trim()) ? appName.trim() : 'BDRP'
}
export function getLayoutPageTitle(): string {
@@ -163,7 +182,6 @@ const UNI_TO_WEB_NOTIFY_PATHS: Record<string, string[]> = {
'pages/agent': ['/agent'],
'pages/me': ['/me'],
'pages/promote': ['/agent/promote'],
'pages/history-query': ['/historyQuery'],
'pages/help': ['/help'],
'pages/help-detail': ['/help/detail'],
'pages/help-guide': ['/help/guide'],
@@ -176,7 +194,6 @@ const UNI_TO_WEB_NOTIFY_PATHS: Record<string, string[]> = {
'pages/agent-service-agreement': ['/agentSerivceAgreement'],
'pages/authorization': ['/authorization'],
'pages/payment-result': ['/payment/result'],
'pages/inquire': ['/inquire'],
'pages/login': ['/login'],
'pages/invitation': ['/agent/invitation'],
'pages/agent-promote-details': ['/agent/promoteDetails'],
@@ -202,8 +219,8 @@ export function getWebPathsForNotification(): string[] {
const q: Record<string, string> = { ...(page?.options || {}) }
// 动态路由:带参数的路径
if (r === 'pages/inquire' && q.feature)
return [`/inquire/${q.feature}`]
if (r === 'pages/promote' && q.feature)
return [`/inquire/${q.feature}`, '/agent/promote']
if (r === 'pages/subordinate-detail' && q.id)
return [`/agent/subordinateDetail/${q.id}`]
if (r === 'pages/invitation-agent-apply' && q.linkIdentifier)

View File

@@ -0,0 +1,58 @@
import { openExternalUrl } from '@/composables/openExternalUrl'
import { useReportWebview } from '@/composables/useReportWebview'
import { getToken } from '@/utils/storage'
const VIP_APPLY_H5_PATH = '/agent/vipApply'
const CONFIRM_TITLE = '前往浏览器完成升级'
const CONFIRM_CONTENT = '代理会员开通/续费将在系统浏览器中完成,与 App 支付完全分离。是否继续打开?'
export function buildAgentVipApplyH5Url(params: Record<string, string> = {}) {
const { buildSitePathUrl } = useReportWebview()
return buildSitePathUrl(VIP_APPLY_H5_PATH, params)
}
function buildTypeQuery(options?: { type?: 'vip' | 'svip' | 'VIP' | 'SVIP' }) {
const query: Record<string, string> = {}
const rawType = options?.type
if (rawType) {
const normalized = String(rawType).toUpperCase()
if (normalized === 'VIP' || normalized === 'SVIP')
query.type = normalized.toLowerCase()
}
return query
}
/**
* 在系统浏览器打开 H5 会员页(带 App 登录态),打开前需用户确认。
*/
export function openAgentVipApplyInExternalBrowser(options?: { type?: 'vip' | 'svip' | 'VIP' | 'SVIP' }) {
if (!getToken()) {
uni.showToast({ title: '请先登录', icon: 'none' })
uni.navigateTo({ url: '/pages/login' })
return false
}
const h5Url = buildAgentVipApplyH5Url(buildTypeQuery(options))
if (!h5Url) {
uni.showToast({ title: '未配置 H5 站点地址', icon: 'none' })
return false
}
uni.showModal({
title: CONFIRM_TITLE,
content: CONFIRM_CONTENT,
confirmText: '继续',
cancelText: '取消',
success: (res) => {
if (res.confirm)
openExternalUrl(h5Url)
},
})
return true
}
/** @deprecated 请使用 openAgentVipApplyInExternalBrowser */
export function navigateToAgentVipApplyH5(options?: { type?: 'vip' | 'svip' | 'VIP' | 'SVIP' }) {
return openAgentVipApplyInExternalBrowser(options)
}

View File

@@ -18,7 +18,6 @@ const HOME_URL = '/pages/index'
*/
const ROUTE_PERMISSION_MAP: Record<string, PagePermission> = {
'pages/promote': { requiresAuth: true, requiresAgent: true },
'pages/history-query': { requiresAuth: true },
'pages/withdraw': { requiresAuth: true, requiresAgent: true },
'pages/payment-result': { requiresAuth: true },
'pages/report-result-webview': { requiresAuth: true },

View File

@@ -3,6 +3,27 @@ import { getToken } from '@/utils/storage'
declare function getCurrentPages(): any[]
/** 与 bdrp-webview `VITE_TOKEN_VERSION` 保持一致,供外部浏览器 / WebView 带参登录 */
function getAppTokenVersionForH5(): string {
return String(import.meta.env.VITE_TOKEN_VERSION || '1.0')
}
/** 外部浏览器打开 H5 时携带 App 登录态(须与 webview `applyAppWebViewTokenFromRoute` 对齐) */
export function appendAppAuthQueryParams(params: Record<string, string> = {}): Record<string, string> {
const merged: Record<string, string> = { ...params, source: 'app' }
const token = getToken()
if (token)
merged.token = token
const refreshAfter = uni.getStorageSync('refreshAfter')
const accessExpire = uni.getStorageSync('accessExpire')
if (refreshAfter != null && refreshAfter !== '')
merged.refreshAfter = String(refreshAfter)
if (accessExpire != null && accessExpire !== '')
merged.accessExpire = String(accessExpire)
merged.tokenVersion = getAppTokenVersionForH5()
return merged
}
/** App 端部分 WebView 无 URLSearchParams用手动编码保证兼容 */
function buildQueryString(params: Record<string, string>): string {
const parts: string[] = []
@@ -20,46 +41,37 @@ export function useReportWebview() {
}
const buildReportUrl = (page: 'example' | 'report', params: Record<string, string> = {}) => {
const token = getToken() || ''
const base = resolveBase()
if (!base) {
console.warn('[report webview] 请配置 VITE_SITE_ORIGIN 或 VITE_REPORT_BASE_URL')
return ''
}
const merged: Record<string, string> = { source: 'app', ...params }
if (token)
merged.token = token
const merged = appendAppAuthQueryParams(params)
const search = buildQueryString(merged)
/** 使用 H5 无顶栏的 App 专用路由,避免 PageLayout 与 App 原生导航栏重复 */
return `${base}/app/${page === 'example' ? 'example' : 'report'}?${search}`
}
const buildReportShareUrl = (linkIdentifier: string, params: Record<string, string> = {}) => {
const token = getToken() || ''
const base = resolveBase()
if (!base) {
console.warn('[report webview] 请配置 VITE_SITE_ORIGIN 或 VITE_REPORT_BASE_URL')
return ''
}
const merged: Record<string, string> = { source: 'app', ...params }
if (token)
merged.token = token
const merged = appendAppAuthQueryParams(params)
const search = buildQueryString(merged)
const encodedLink = encodeURIComponent(linkIdentifier)
return `${base}/report/share/${encodedLink}${search ? `?${search}` : ''}`
}
const buildSitePathUrl = (path: string, params: Record<string, string> = {}) => {
const token = getToken() || ''
const base = resolveBase()
if (!base) {
console.warn('[site webview] 请配置 VITE_SITE_ORIGIN 或 VITE_REPORT_BASE_URL')
return ''
}
const normalizedPath = path.startsWith('/') ? path : `/${path}`
const merged: Record<string, string> = { source: 'app', ...params }
if (token)
merged.token = token
const merged = appendAppAuthQueryParams(params)
const search = buildQueryString(merged)
return `${base}${normalizedPath}${search ? `?${search}` : ''}`
}

View File

@@ -1,6 +1,6 @@
<script setup>
import { onShow } from '@dcloudio/uni-app'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { computed, nextTick, onMounted, ref } from 'vue'
import {
getCurrentUniRoute,
getLayoutPageTitle,
@@ -11,10 +11,13 @@ import { ensurePrivacyConsentIfNeeded } from '@/composables/usePrivacyConsent'
import { useGlobalNotification } from '@/composables/useGlobalNotification'
const router = useRouter()
const pageTitle = ref('赤眉')
const currentRoute = ref('')
const safeAreaTop = ref(0)
const immersiveNavbarSolid = ref(false)
/** 用于在 onShow 时触发标题 computed 重新读取页面栈 */
const routeRevision = ref(0)
const currentRoute = computed(() => {
routeRevision.value
return getCurrentUniRoute()
})
const pageTitle = computed(() => getLayoutPageTitle())
const {
showPopup,
@@ -25,24 +28,10 @@ const {
onClosePopup,
} = useGlobalNotification()
const immersiveRoutes = new Set([
'pages/inquire',
])
const isImmersivePage = computed(() => immersiveRoutes.has(currentRoute.value))
const immersiveNavbarClass = computed(() => (
immersiveNavbarSolid.value ? 'immersive-wd-navbar is-solid' : 'immersive-wd-navbar'
))
const immersiveNavbarStyle = computed(() => (
immersiveNavbarSolid.value
? 'background: rgba(255,255,255,0.96); box-shadow: 0 2px 12px rgba(15, 23, 42, 0.08);'
: 'background: transparent; box-shadow: none;'
))
function syncTitle() {
currentRoute.value = getCurrentUniRoute()
pageTitle.value = getLayoutPageTitle()
immersiveNavbarSolid.value = false
nextTick(() => {
routeRevision.value += 1
})
}
function buildCurrentPageUrl() {
@@ -60,15 +49,6 @@ function ensureAuthIfNeeded() {
ensurePageAccessByUrl(buildCurrentPageUrl(), 'redirect')
}
function getSafeAreaTop() {
uni.getSystemInfo({
success: (res) => {
if (res.safeArea)
safeAreaTop.value = res.safeArea.top
},
})
}
function onClickOverlay() {
onClosePopup()
}
@@ -77,17 +57,9 @@ function onClickLeft() {
router.back()
}
function onImmersiveNavbarChange(payload) {
if (!payload || payload.route !== currentRoute.value || !immersiveRoutes.has(payload.route))
return
immersiveNavbarSolid.value = Boolean(payload.solid)
}
onMounted(() => {
getSafeAreaTop()
syncTitle()
loadShownNotificationKeys()
console.log('[通知DEBUG] default layout onMounted, 即将请求通知')
getGlobalNotify()
})
@@ -95,35 +67,29 @@ onShow(() => {
ensurePrivacyConsentIfNeeded()
syncTitle()
ensureAuthIfNeeded()
console.log('[通知DEBUG] default layout onShow')
checkNotification()
})
onMounted(() => {
uni.$on('immersive-navbar-change', onImmersiveNavbarChange)
})
onUnmounted(() => {
uni.$off('immersive-navbar-change', onImmersiveNavbarChange)
onReady(() => {
syncTitle()
})
</script>
<template>
<view
class="page-layout min-h-screen from-sky-100/20 to-white bg-gradient-to-b"
:class="{ 'transparent-layout': isImmersivePage }"
>
<view class="page-layout min-h-screen from-sky-100/20 to-white bg-gradient-to-b">
<wd-navbar
v-if="isImmersivePage"
:class="immersiveNavbarClass"
:title="pageTitle"
:custom-style="immersiveNavbarStyle"
:key="currentRoute"
safe-area-inset-top
left-arrow
placeholder
fixed
bordered
@click-left="onClickLeft"
/>
<wd-navbar v-else :title="pageTitle" safe-area-inset-top left-arrow placeholder fixed @click-left="onClickLeft" />
>
<template #title>
<text class="layout-navbar-title">{{ pageTitle }}</text>
</template>
</wd-navbar>
<slot />
<wd-popup v-model="showPopup" round :close-on-click-modal="true" @close="onClickOverlay">
<view class="popup-content p-8 text-center" style="width: 85vw;">
@@ -142,28 +108,16 @@ onUnmounted(() => {
</template>
<style lang="scss" scoped>
.transparent-layout {
background: transparent;
}
:deep(.immersive-wd-navbar) {
z-index: 1000;
transition: background-color 0.2s ease, box-shadow 0.2s ease;
}
:deep(.immersive-wd-navbar .wd-navbar__title),
:deep(.immersive-wd-navbar .wd-navbar__left),
:deep(.immersive-wd-navbar .wd-navbar__left .wd-icon) {
color: #fff !important;
text-shadow: 0 1px 6px rgba(0, 0, 0, 0.22);
transition: color 0.2s ease, text-shadow 0.2s ease;
}
:deep(.immersive-wd-navbar.is-solid .wd-navbar__title),
:deep(.immersive-wd-navbar.is-solid .wd-navbar__left),
:deep(.immersive-wd-navbar.is-solid .wd-navbar__left .wd-icon) {
color: #111827 !important;
text-shadow: none;
:deep(.layout-navbar-title) {
display: block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #111827;
font-size: 18px;
font-weight: 600;
line-height: 1.2;
}
.popup-content {

View File

@@ -1,986 +0,0 @@
<script setup>
import { storeToRefs } from 'pinia'
import { computed, onMounted, reactive, ref } from 'vue'
import { openCustomerService } from '@/composables/useCustomerService'
import { useAgentStore } from '@/stores/agentStore'
definePage({ layout: 'default', auth: true })
/** API 与初始状态可能为 undefined/字符串,避免参与运算出现 NaN */
function toFiniteNumber(v, fallback = 0) {
if (v === null || v === undefined || v === '')
return fallback
const n = typeof v === 'number' ? v : Number(String(v).replace(/,/g, ''))
return Number.isFinite(n) ? n : fallback
}
// 获取代理状态
const agentStore = useAgentStore()
const { level, ExpiryTime } = storeToRefs(agentStore)
const selectedType = ref('vip') // 默认选择 VIP供下方 computed 使用)
// 计算是否已经是VIP或SVIP
const isVipOrSvip = computed(() => ['VIP', 'SVIP'].includes(level.value))
const isVip = computed(() => level.value === 'VIP')
const isSvip = computed(() => level.value === 'SVIP')
// 计算续费后的到期时间
const renewalExpiryTime = computed(() => {
if (!ExpiryTime.value)
return '未知'
// 从格式化字符串中提取日期部分
const dateStr = ExpiryTime.value.split(' ')[0] // 假设格式是 "YYYY-MM-DD HH:MM:SS"
const parts = dateStr.split('-').map(p => Number.parseInt(p, 10))
const [year, month, day] = parts
if (parts.length < 3 || [year, month, day].some(n => !Number.isFinite(n)))
return '未知'
// 创建日期对象并加一年
const expiryDate = new Date(year, month - 1, day) // 月份从0开始所以要-1
expiryDate.setFullYear(expiryDate.getFullYear() + 1)
// 返回格式化的日期字符串
return `${expiryDate.getFullYear()}-${String(expiryDate.getMonth() + 1).padStart(2, '0')}-${String(expiryDate.getDate()).padStart(2, '0')}`
})
// 按钮文字 - 根据当前状态显示不同文案
const buttonText = computed(() => {
if (!isVipOrSvip.value)
return '立即开通' // 非会员状态
if (selectedType.value === 'vip') {
if (isVip.value)
return '续费VIP代理' // VIP续费VIP
return '降级不可用' // SVIP不能降级到VIP
}
else {
if (isSvip.value)
return '续费SVIP代理' // SVIP续费SVIP
return '升级SVIP代理' // VIP升级SVIP
}
})
// 是否可以操作按钮
const canPerformAction = computed(() => {
// 非会员可以开通任何会员
if (!isVipOrSvip.value)
return true
// VIP不能降级到普通会员
if (isVip.value && selectedType.value === '')
return false
// SVIP不能降级到VIP
if (isSvip.value && selectedType.value === 'vip')
return false
return true
})
// 计算按钮类名
const buttonClass = computed(() => {
const baseClass
= 'w-full py-4 rounded-lg font-bold text-lg shadow-lg transform transition-transform active:scale-98 relative overflow-hidden'
if (!canPerformAction.value) {
return `${baseClass} bg-gray-400 text-white cursor-not-allowed`
}
if (isVip.value && selectedType.value === 'svip') {
return `${baseClass} bg-gradient-to-r from-purple-500 to-indigo-600 text-white`
}
return `${baseClass} bg-gradient-to-r from-amber-500 to-amber-600 active:from-amber-600 active:to-amber-700 text-white`
})
// VIP价格配置展示用单位数值在拉取 API 后写入)
const vipConfig = reactive({ priceUnit: '元' })
// 计算得出的收益数据
const revenueData = computed(() => {
const price = toFiniteNumber(vipConfig.price)
const svipPrice = toFiniteNumber(vipConfig.svipPrice)
const vipCommission = toFiniteNumber(vipConfig.vipCommission)
const svipCommission = toFiniteNumber(vipConfig.svipCommission)
const vipFloatingRate = toFiniteNumber(vipConfig.vipFloatingRate)
const svipFloatingRate = toFiniteNumber(vipConfig.svipFloatingRate)
const vipConversionBonus = toFiniteNumber(vipConfig.vipConversionBonus)
const svipConversionBonus = toFiniteNumber(vipConfig.svipConversionBonus)
const withdrawRatio = toFiniteNumber(vipConfig.withdrawRatio)
const baseOrders = 300 // 基础订单数
const pricePerOrder = 50 // 每单价格
const baseRevenue = baseOrders * pricePerOrder // 基础推广收益
const vipCommissionRevenue = baseOrders * vipCommission // VIP下级贡献收益
const svipCommissionRevenue = baseOrders * svipCommission // SVIP下级贡献收益
const vipFloatingRevenue = 100 * 100 * (vipFloatingRate / 100) // VIP浮动收益
const svipFloatingRevenue = 200 * 100 * (svipFloatingRate / 100) // SVIP浮动收益
const vipConversionRevenue = vipConversionBonus * 2 // VIP转化奖励
const svipConversionRevenue = svipConversionBonus * 2 // SVIP转化奖励
const vipExtraRevenue = 3000 // VIP额外收益估计
const svipExtraRevenue = 6000 // SVIP额外收益估计
// 平级提现奖励(只有SVIP才有)
const withdrawReward = 20000 * (withdrawRatio / 100)
// 计算月总收益
const vipMonthlyTotal
= baseRevenue
+ vipCommissionRevenue
+ vipFloatingRevenue
+ vipConversionRevenue
+ vipExtraRevenue
const svipMonthlyTotal
= baseRevenue
+ svipCommissionRevenue
+ svipFloatingRevenue
+ withdrawReward
+ svipConversionRevenue
+ svipExtraRevenue
// 计算VIP和SVIP之间的差额
const monthlyDifference = svipMonthlyTotal - vipMonthlyTotal
const priceDifference = svipPrice - price
const vipRate = price > 0 ? Math.round(vipMonthlyTotal / price) : 0
const svipRate = svipPrice > 0 ? Math.round(svipMonthlyTotal / svipPrice) : 0
// 月增收对应到「多少天回本」:避免除 0 或月差为 0 时出现 Infinity / NaN
let recoverDays = 0
if (priceDifference > 0 && monthlyDifference > 0) {
const perDay = monthlyDifference / 30
if (perDay > 0)
recoverDays = Math.ceil(priceDifference / perDay)
}
return {
vipMonthly: Math.round(toFiniteNumber(vipMonthlyTotal)),
svipMonthly: Math.round(toFiniteNumber(svipMonthlyTotal)),
vipYearly: Math.round(toFiniteNumber(vipMonthlyTotal) * 12),
svipYearly: Math.round(toFiniteNumber(svipMonthlyTotal) * 12),
monthlyDifference: Math.round(toFiniteNumber(monthlyDifference)),
yearlyDifference: Math.round(toFiniteNumber(monthlyDifference) * 12),
vipRate,
svipRate,
priceDifference: Math.round(toFiniteNumber(priceDifference)),
recoverDays,
withdrawReward: Math.round(toFiniteNumber(withdrawReward)),
}
})
// 加载价格配置
onMounted(async () => {
// 从API获取会员配置信息
try {
const { data, error } = await useApiFetch('/agent/membership/info', { silent: true }).get().json()
if (data.value && !error.value && data.value.code === 200) {
const configData = data.value.data
// 更新VIP配置
if (configData.vip_config) {
const vipData = configData.vip_config
vipConfig.price = toFiniteNumber(vipData.price)
vipConfig.vipCommission = toFiniteNumber(vipData.report_commission) // 直接显示,就是几元/单
vipConfig.vipFloatingRate = toFiniteNumber(vipData.price_ratio) * 100 // 转换为百分比
vipConfig.vipConversionBonus = toFiniteNumber(vipData.lower_convert_vip_reward)
vipConfig.vipWithdrawalLimit = toFiniteNumber(vipData.exemption_amount)
}
// 更新SVIP配置
if (configData.svip_config) {
const svipData = configData.svip_config
vipConfig.svipPrice = toFiniteNumber(svipData.price)
vipConfig.svipCommission = toFiniteNumber(svipData.report_commission) // 直接显示,就是几元/单
vipConfig.svipFloatingRate = toFiniteNumber(svipData.price_ratio) * 100 // 转换为百分比
vipConfig.withdrawRatio = toFiniteNumber(svipData.lower_withdraw_reward_ratio) * 100 // 转换为百分比
vipConfig.svipConversionBonus = toFiniteNumber(svipData.lower_convert_svip_reward)
vipConfig.svipWithdrawalLimit = toFiniteNumber(svipData.exemption_amount)
}
}
else {
console.error('获取会员配置信息失败', data.value?.msg || '未知错误')
}
}
catch (error) {
console.error('加载会员配置信息失败', error)
}
})
const showPayment = ref(false)
const payID = ref('')
const payData = computed(() => ({
product_name: `${selectedType.value === 'vip' ? 'VIP' : 'SVIP'}代理`,
sell_price: selectedType.value === 'vip'
? toFiniteNumber(vipConfig.price)
: toFiniteNumber(vipConfig.svipPrice),
}))
function showToast(message) {
if (!message)
return
uni.showToast({
title: message,
icon: 'none',
})
}
// 选择代理类型
function selectType(type) {
selectedType.value = type
}
// 申请VIP或SVIP
async function applyVip() {
// 如果是SVIP要降级到VIP提示不能降级
if (isSvip.value && selectedType.value === 'vip') {
showToast('SVIP会员不能降级到VIP会员')
return
}
if (toFiniteNumber(payData.value.sell_price) <= 0) {
showToast('会员价格加载中,请稍后重试')
return
}
const { data, error } = await useApiFetch('/agent/membership/activate')
.post({
type: selectedType.value.toUpperCase(),
})
.json()
if (data.value && !error.value) {
if (data.value.code === 200) {
if (data.value.data.id) {
payID.value = data.value.data.id
showPayment.value = true
}
}
}
}
// 联系客服
function contactService() {
openCustomerService()
}
function formatExpiryTime(expiryTimeStr) {
if (!expiryTimeStr)
return '未知'
// 从格式化字符串中提取日期部分
return expiryTimeStr.split(' ')[0] // 假设格式是 "YYYY-MM-DD HH:MM:SS"
}
</script>
<template>
<view class="agent-VIP-apply min-h-screen w-full from-amber-50 via-amber-100 to-amber-50 bg-gradient-to-b pb-24">
<!-- 装饰元素 -->
<view
class="absolute right-0 top-0 h-32 w-32 rounded-bl-full from-amber-300 to-amber-500 bg-gradient-to-br opacity-20" />
<view
class="absolute left-0 top-40 h-16 w-16 rounded-tr-full from-amber-400 to-amber-600 bg-gradient-to-tr opacity-20" />
<view
class="absolute bottom-60 right-0 h-24 w-24 rounded-tl-full from-amber-300 to-amber-500 bg-gradient-to-bl opacity-20" />
<!-- 顶部标题区域 -->
<view class="header relative px-4 pb-6 pt-8 text-center">
<view
class="absolute left-1/2 h-1 w-24 animate-pulse rounded-full from-amber-300 via-amber-500 to-amber-300 bg-gradient-to-r -top-2 -translate-x-1/2" />
<view class="mb-1 text-3xl text-amber-800 font-bold">
{{ isVipOrSvip ? '代理会员续费' : 'VIP代理申请' }}
</view>
<view class="mx-auto mt-2 max-w-xs text-sm text-amber-700">
<template v-if="isVipOrSvip">
您的会员有效期至 {{ formatExpiryTime(ExpiryTime) }}续费后有效期至
{{ renewalExpiryTime }}
</template>
<template v-else>
平台为疯狂推广者定制的赚买计划助您收益<text class="text-red-500 font-bold">翻倍增升</text>
</template>
</view>
<!-- 装饰性金币图标 -->
<view class="absolute left-4 top-6 transform -rotate-12">
<view
class="h-8 w-8 flex items-center justify-center rounded-full from-yellow-300 to-yellow-500 bg-gradient-to-br shadow-lg">
<text class="text-xs text-white font-bold">¥</text>
</view>
</view>
<view class="absolute right-6 top-10 rotate-12 transform">
<view
class="h-6 w-6 flex items-center justify-center rounded-full from-yellow-400 to-yellow-600 bg-gradient-to-br shadow-lg">
<text class="text-xs text-white font-bold">¥</text>
</view>
</view>
</view>
<!-- 选择代理类型 -->
<view class="card-container mb-8 px-4">
<view class="transform overflow-hidden border border-amber-100 rounded-xl bg-white shadow-lg transition-all">
<view
class="relative overflow-hidden from-amber-500 to-amber-600 bg-gradient-to-r px-4 py-3 text-center text-white font-bold">
<view class="relative z-10">选择代理类型</view>
<view class="absolute inset-0 bg-amber-500 opacity-30">
<view
class="animate-shimmer absolute left-0 top-0 h-full w-full translate-x-full transform from-transparent via-white to-transparent bg-gradient-to-r opacity-20 -skew-x-30" />
</view>
</view>
<view class="flex gap-4 p-6">
<view
class="relative flex-1 transform cursor-pointer border-2 rounded-lg p-4 text-center transition-all duration-300 hover:-translate-y-1"
:class="[
selectedType === 'vip'
? 'border-amber-500 bg-amber-50 shadow-md'
: 'border-gray-200 hover:border-amber-300',
]" @click="selectType('vip')">
<view class="text-xl text-amber-700 font-bold">
VIP代理
</view>
<view class="mt-1 text-lg text-amber-600 font-bold">
{{ vipConfig.price }}{{ vipConfig.priceUnit }}
</view>
<view class="mt-2 text-sm text-gray-600">
标准VIP权益
</view>
<view v-if="selectedType === 'vip'"
class="absolute h-6 w-6 flex items-center justify-center rounded-full from-amber-500 to-amber-600 bg-gradient-to-br shadow-md -right-2 -top-2">
<wd-icon name="check" custom-style="color:#fff;font-size:14px;" />
</view>
</view>
<view
class="relative flex-1 transform cursor-pointer border-2 rounded-lg p-4 text-center transition-all duration-300 hover:-translate-y-1"
:class="[
selectedType === 'svip'
? 'border-amber-500 bg-amber-50 shadow-md'
: 'border-gray-200 hover:border-amber-300',
]" @click="selectType('svip')">
<view class="text-xl text-amber-700 font-bold">
SVIP代理
</view>
<view class="mt-1 text-lg text-amber-600 font-bold">
{{ vipConfig.svipPrice }}{{ vipConfig.priceUnit }}
</view>
<view class="mt-2 text-sm text-gray-600">
超级VIP权益
</view>
<view v-if="selectedType === 'svip'"
class="absolute h-6 w-6 flex items-center justify-center rounded-full from-amber-500 to-amber-600 bg-gradient-to-br shadow-md -right-2 -top-2">
<wd-icon name="check" custom-style="color:#fff;font-size:14px;" />
</view>
</view>
</view>
</view>
</view>
<!-- 六大超值权益 -->
<view class="card-container mb-8 px-4">
<view class="overflow-hidden border border-amber-100 rounded-xl bg-white shadow-lg">
<view
class="relative overflow-hidden from-amber-500 to-amber-600 bg-gradient-to-r px-4 py-3 text-center text-white font-bold">
<view class="relative z-10">六大超值权益</view>
<view class="absolute inset-0 bg-amber-500 opacity-30">
<view
class="animate-shimmer absolute left-0 top-0 h-full w-full translate-x-full transform from-transparent via-white to-transparent bg-gradient-to-r opacity-20 -skew-x-30" />
</view>
</view>
<view class="grid grid-cols-2 gap-4 p-4">
<!-- 权益1 -->
<view
class="border border-amber-200 rounded-lg from-amber-50 to-amber-100 bg-gradient-to-br p-3 transition-all duration-300 hover:border-amber-300 hover:shadow-md">
<view class="mb-2 flex items-center text-amber-800 font-bold">
<text
class="mr-2 h-6 w-6 flex items-center justify-center rounded-full from-amber-500 to-amber-600 bg-gradient-to-br text-xs text-white">1</text>
下级贡献收益
</view>
<text class="text-sm text-gray-600">
下级完全收益您来定涨多少赚多少一单最高收益<text class="text-red-500 font-bold">10</text>
</text>
</view>
<!-- 权益2 -->
<view
class="border border-amber-200 rounded-lg from-amber-50 to-amber-100 bg-gradient-to-br p-3 transition-all duration-300 hover:border-amber-300 hover:shadow-md">
<view class="mb-2 flex items-center text-amber-800 font-bold">
<text
class="mr-2 h-6 w-6 flex items-center justify-center rounded-full from-amber-500 to-amber-600 bg-gradient-to-br text-xs text-white">2</text>
下级提现收益
</view>
<text class="text-sm text-gray-600">
下级定价标准由您定超过标准部分收益更丰厚一单最高多赚<text class="text-red-500 font-bold">10</text>
</text>
</view>
<!-- 权益3 -->
<view
class="border border-amber-200 rounded-lg from-amber-50 to-amber-100 bg-gradient-to-br p-3 transition-all duration-300 hover:border-amber-300 hover:shadow-md">
<view class="mb-2 flex items-center text-amber-800 font-bold">
<text
class="mr-2 h-6 w-6 flex items-center justify-center rounded-full from-amber-500 to-amber-600 bg-gradient-to-br text-xs text-white">3</text>
转换高额奖励
</view>
<text class="text-sm text-gray-600">
下级成为VIPSVIP高额奖励立马发放<text class="text-red-500 font-bold">399</text>
</text>
</view>
<!-- 权益4 -->
<view
class="border border-amber-200 rounded-lg from-amber-50 to-amber-100 bg-gradient-to-br p-3 transition-all duration-300 hover:border-amber-300 hover:shadow-md">
<view class="mb-2 flex items-center text-amber-800 font-bold">
<text
class="mr-2 h-6 w-6 flex items-center justify-center rounded-full from-amber-500 to-amber-600 bg-gradient-to-br text-xs text-white">4</text>
下级提现奖励
</view>
<text class="text-sm text-gray-600">
下级成为SVIP每次提现都奖励1%坐享被动收入
</text>
</view>
<!-- 权益6 -->
<view
class="border border-amber-200 rounded-lg from-amber-50 to-amber-100 bg-gradient-to-br p-3 transition-all duration-300 hover:border-amber-300 hover:shadow-md">
<view class="mb-2 flex items-center text-amber-800 font-bold">
<text
class="mr-2 h-6 w-6 flex items-center justify-center rounded-full from-amber-500 to-amber-600 bg-gradient-to-br text-xs text-white">6</text>
平台专项扶持
</view>
<text class="text-sm text-gray-600">
一对一专属客服服务为合作伙伴提供全方位成长赋能
</text>
</view>
</view>
</view>
</view>
<!-- 权益对比表 -->
<view v-if="selectedType" class="card-container mb-8 px-4">
<view class="overflow-hidden border border-amber-100 rounded-xl bg-white shadow-lg">
<view
class="relative overflow-hidden from-amber-500 to-amber-600 bg-gradient-to-r px-4 py-3 text-center text-white font-bold">
<view class="relative z-10">{{ selectedType === 'vip' ? 'VIP' : 'SVIP' }}代理权益对比</view>
<view class="absolute inset-0 bg-amber-500 opacity-30">
<view
class="animate-shimmer absolute left-0 top-0 h-full w-full translate-x-full transform from-transparent via-white to-transparent bg-gradient-to-r opacity-20 -skew-x-30" />
</view>
</view>
<view class="overflow-x-auto p-4">
<table class="w-full border-collapse">
<thead>
<tr class="from-amber-100 to-amber-200 bg-gradient-to-r">
<th class="border border-amber-200 p-2 text-left text-amber-800">
权益项目
</th>
<th class="border border-amber-200 p-2 text-center text-amber-800">
普通代理
</th>
<th class="border border-amber-200 p-2 text-center text-amber-800"
:class="{ 'bg-amber-200': selectedType === 'vip' }">
VIP代理
</th>
<th class="border border-amber-200 p-2 text-center text-amber-800"
:class="{ 'bg-amber-200': selectedType === 'svip' }">
SVIP代理
</th>
</tr>
</thead>
<tbody>
<tr class="transition-colors hover:bg-amber-50">
<td class="border border-amber-200 p-2 font-medium">
会员权益
</td>
<td class="border border-amber-200 p-2 text-center">
普通代理<br>免费
</td>
<td class="border border-amber-200 p-2 text-center font-bold" :class="{
'text-amber-700 bg-amber-50': selectedType === 'vip',
}">
{{ vipConfig.price }}{{ vipConfig.priceUnit }}
</td>
<td class="border border-amber-200 p-2 text-center font-bold" :class="{
'text-amber-700 bg-amber-50': selectedType === 'svip',
}">
{{ vipConfig.svipPrice }}{{ vipConfig.priceUnit }}
</td>
</tr>
<tr class="transition-colors hover:bg-amber-50">
<td class="border border-amber-200 p-2 font-medium">
下级贡献收益
</td>
<td class="border border-amber-200 p-2 text-center">
1/
</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'vip',
}">
{{ vipConfig.vipCommission }}/
</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'svip',
}">
{{ vipConfig.svipCommission }}/
</td>
</tr>
<tr class="transition-colors hover:bg-amber-50">
<td class="border border-amber-200 p-2 font-medium">
自定义设置下级成本
</td>
<td class="border border-amber-200 p-2 text-center">
</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'vip',
}">
</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'svip',
}">
</td>
</tr>
<tr class="transition-colors hover:bg-amber-50">
<td class="border border-amber-200 p-2 font-medium">
下级价格浮动收益
</td>
<td class="border border-amber-200 p-2 text-center">
</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'vip',
}">
最高{{ vipConfig.vipFloatingRate }}%
</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'svip',
}">
最高{{ vipConfig.svipFloatingRate }}%
</td>
</tr>
<tr class="transition-colors hover:bg-amber-50">
<td class="border border-amber-200 p-2 font-medium">
下级提现奖励
</td>
<td class="border border-amber-200 p-2 text-center">
</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'vip',
}">
</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'svip',
}">
{{ vipConfig.withdrawRatio }}%
</td>
</tr>
<tr class="transition-colors hover:bg-amber-50">
<td class="border border-amber-200 p-2 font-medium">
下级转化奖励
</td>
<td class="border border-amber-200 p-2 text-center">
</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'vip',
}">
{{ vipConfig.vipConversionBonus }}*10
</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'svip',
}">
{{ vipConfig.svipConversionBonus }}*10
</td>
</tr>
<tr class="transition-colors hover:bg-amber-50">
<td class="border border-amber-200 p-2 font-medium">
提现次数额度
</td>
<td class="border border-amber-200 p-2 text-center">
800/
</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'vip',
}">
{{ vipConfig.vipWithdrawalLimit }}/
</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'svip',
}">
{{ vipConfig.svipWithdrawalLimit }}/
</td>
</tr>
<tr class="transition-colors hover:bg-amber-50">
<td class="border border-amber-200 p-2 font-medium">
提现次数
</td>
<td class="border border-amber-200 p-2 text-center">
1/
</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'vip',
}">
1/
</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'svip',
}">
2/
</td>
</tr>
</tbody>
</table>
</view>
</view>
</view>
<!-- 收益预估 -->
<view v-if="selectedType" class="card-container mb-8 px-4">
<view class="overflow-hidden border border-amber-100 rounded-xl bg-white shadow-lg">
<view
class="relative overflow-hidden from-amber-500 to-amber-600 bg-gradient-to-r px-4 py-3 text-center text-white font-bold">
<view class="relative z-10">收益预估对比</view>
<view class="absolute inset-0 bg-amber-500 opacity-30">
<view
class="animate-shimmer absolute left-0 top-0 h-full w-full translate-x-full transform from-transparent via-white to-transparent bg-gradient-to-r opacity-20 -skew-x-30" />
</view>
</view>
<view class="p-4">
<!-- 顶部收益概览 -->
<view class="mb-6 overflow-hidden border border-amber-200 rounded-lg">
<view class="from-amber-100 to-amber-200 bg-gradient-to-r px-4 py-2 text-center text-amber-800 font-bold">
VIP与SVIP代理收益对比
</view>
<view class="grid grid-cols-2 divide-x divide-amber-200">
<view class="p-4 text-center" :class="{ 'bg-amber-50': selectedType === 'vip' }">
<view class="mb-1 text-sm text-gray-600">
VIP月预计收益
</view>
<view class="text-xl text-amber-600 font-bold">
{{ revenueData.vipMonthly }}
</view>
<view class="mt-1 text-xs text-gray-500">
年收益{{ revenueData.vipYearly }}
</view>
</view>
<view class="p-4 text-center" :class="{ 'bg-amber-50': selectedType === 'svip' }">
<view class="mb-1 text-sm text-gray-600">
SVIP月预计收益
</view>
<view class="text-xl text-red-500 font-bold">
{{ revenueData.svipMonthly }}
</view>
<view class="mt-1 text-xs text-gray-500">
年收益{{ revenueData.svipYearly }}
</view>
</view>
</view>
<view class="from-red-50 to-red-100 bg-gradient-to-r px-4 py-2 text-center text-red-600 font-medium">
选择SVIP相比VIP月增收益<text class="font-bold">{{ revenueData.monthlyDifference }}</text>
</view>
</view>
<!-- 详细收益表格 -->
<view class="overflow-x-auto">
<table class="w-full border-collapse">
<thead>
<tr class="from-amber-100 to-amber-200 bg-gradient-to-r">
<th class="border border-amber-200 p-2 text-left text-amber-800">
收益来源
</th>
<th class="w-1/3 border border-amber-200 p-2 text-center text-amber-800"
:class="{ 'bg-amber-200': selectedType === 'vip' }">
VIP代理
</th>
<th class="w-1/3 border border-amber-200 p-2 text-center text-amber-800"
:class="{ 'bg-amber-200': selectedType === 'svip' }">
SVIP代理
</th>
</tr>
</thead>
<tbody>
<tr class="transition-colors hover:bg-amber-50">
<td class="border border-amber-200 p-2 font-medium">
推广收益()
</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'vip',
}">
300×50=15,000
</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'svip',
}">
300×50=15,000
</td>
</tr>
<tr class="transition-colors hover:bg-amber-50">
<td class="border border-amber-200 p-2 font-medium">
下级贡献收益()
</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'vip',
}">
300×{{ vipConfig.vipCommission }}=360
</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'svip',
}">
300×{{ vipConfig.svipCommission }}=450
</td>
</tr>
<tr class="transition-colors hover:bg-amber-50">
<td class="border border-amber-200 p-2 font-medium">
下级价格浮动收益()
</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'vip',
}">
100×100×{{ vipConfig.vipFloatingRate }}%=500
</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'svip',
}">
200×100×{{ vipConfig.svipFloatingRate }}%=2,000
</td>
</tr>
<tr class="transition-colors hover:bg-amber-50">
<td class="border border-amber-200 p-2 font-medium">
下级提现奖励()
</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'vip',
}">
-
</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'svip',
}">
{{ revenueData.withdrawReward }}
</td>
</tr>
<tr class="transition-colors hover:bg-amber-50">
<td class="border border-amber-200 p-2 font-medium">
下级转化奖励()
</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'vip',
}">
{{ vipConfig.vipConversionBonus }}×2=598
</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'svip',
}">
{{ vipConfig.svipConversionBonus }}×2=798
</td>
</tr>
<tr class="transition-colors hover:bg-amber-50">
<td class="border border-amber-200 p-2 font-medium">
额外业务收益()
</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'vip',
}">
约3,000
</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'svip',
}">
约6,000
</td>
</tr>
<tr class="from-amber-50 to-amber-100 bg-gradient-to-r font-bold">
<td class="border border-amber-200 p-3">
月计收益
</td>
<td class="border border-amber-200 p-3 text-center text-amber-700" :class="{
'bg-amber-50 border-amber-300': selectedType === 'vip',
}">
{{ revenueData.vipMonthly }}
</td>
<td class="border border-amber-200 p-3 text-center text-red-500" :class="{
'bg-amber-50 border-amber-300': selectedType === 'svip',
}">
{{ revenueData.svipMonthly }}
</td>
</tr>
<tr class="from-amber-50 to-amber-100 bg-gradient-to-r font-bold">
<td class="border border-amber-200 p-3">
年计收益
</td>
<td class="border border-amber-200 p-3 text-center text-amber-700" :class="{
'bg-amber-50 border-amber-300': selectedType === 'vip',
}">
{{ revenueData.vipYearly }}
</td>
<td class="border border-amber-200 p-3 text-center text-red-500" :class="{
'bg-amber-50 border-amber-300': selectedType === 'svip',
}">
{{ revenueData.svipYearly }}
</td>
</tr>
</tbody>
</table>
</view>
<!-- 投资回报率 -->
<view class="mt-6 border border-amber-200 rounded-lg from-amber-50 to-amber-100 bg-gradient-to-r p-4">
<view class="mb-3 text-center text-amber-800 font-bold">
投资收益分析
</view>
<view class="grid grid-cols-1 gap-4">
<view class="rounded-lg bg-white p-3 shadow-sm">
<view class="flex items-center justify-between">
<view class="flex-1 border-r border-amber-100 pr-3">
<view class="mb-1 text-center text-amber-700 font-medium">
VIP方案
</view>
<view class="text-center">
<view class="text-sm text-amber-600">
投资{{ vipConfig.price }}
</view>
<view class="text-sm text-gray-600">
月收益{{ revenueData.vipMonthly }}
</view>
</view>
</view>
<view class="flex-1 pl-3">
<view class="mb-1 text-center text-red-500 font-medium">
SVIP方案
</view>
<view class="text-center">
<view class="text-sm text-red-500">
投资{{ vipConfig.svipPrice }}
</view>
<view class="text-sm text-gray-600">
月收益{{ revenueData.svipMonthly }}
</view>
</view>
</view>
</view>
</view>
<!-- 升级收益对比 -->
<view class="rounded-lg from-red-50 to-amber-50 bg-gradient-to-r p-3 shadow-sm">
<view class="mb-2 text-center text-red-700 font-medium">
SVIP升级优势分析
</view>
<view class="flex items-center justify-center gap-3">
<view class="text-center">
<view class="text-sm text-gray-600">
额外投资
</view>
<view class="text-red-600 font-bold">
{{ revenueData.priceDifference }}
</view>
</view>
<view
class="h-6 w-6 flex flex-shrink-0 items-center justify-center rounded-full bg-red-500 text-white">
<view class="transform -translate-y-px">
</view>
</view>
<view class="text-center">
<view class="text-sm text-gray-600">
每月额外收益
</view>
<view class="text-red-600 font-bold">
{{ revenueData.monthlyDifference }}
</view>
</view>
<view
class="h-6 w-6 flex flex-shrink-0 items-center justify-center rounded-full bg-red-500 text-white">
<text class="transform -translate-y-px"></text>
</view>
<view class="text-center">
<view class="text-sm text-gray-600">
投资回收时间
</view>
<view class="text-red-600 font-bold">
{{ revenueData.recoverDays }}
</view>
</view>
</view>
<view class="mt-3 text-center text-red-500 font-medium">
额外投资{{ revenueData.priceDifference }}<text class="text-red-600 font-bold">年多赚{{
revenueData.yearlyDifference }}</text>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 申请按钮固定在底部 -->
<view
class="fixed bottom-0 left-0 right-0 z-30 from-amber-100 to-transparent bg-gradient-to-t px-4 py-3 backdrop-blur-sm">
<view class="flex flex-col gap-2">
<wd-button :class="buttonClass" block :disabled="!canPerformAction" @click="applyVip">
<text class="relative z-10">{{ buttonText }}</text>
<view
class="animate-shimmer absolute left-0 top-0 h-full w-full translate-x-full transform from-transparent via-white to-transparent bg-gradient-to-r opacity-20 -skew-x-30" />
</wd-button>
<wd-button custom-class="contact-wd-btn" block plain @click="contactService">
<view class="flex items-center justify-center text-amber-700 font-medium">
<wd-icon name="service" custom-class="mr-1" />
<text>联系客服咨询</text>
</view>
</wd-button>
<!-- 最终解释权声明 -->
<view class="py-1 text-center text-xs text-gray-400">
最终解释权归戎行技术有限公司所有
</view>
</view>
</view>
</view>
<Payment :id="payID" v-model="showPayment" :data="payData" type="agent_vip" @close="showPayment = false" />
</template>
<style scoped>
.agent-VIP-apply {
font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
sans-serif;
}
@keyframes shimmer {
0% {
transform: translateX(-100%) skewX(-30deg);
}
100% {
transform: translateX(200%) skewX(-30deg);
}
}
.animate-shimmer {
animation: shimmer 3s infinite;
}
.active\:scale-98:active {
transform: scale(0.98);
}
:deep(.contact-wd-btn.wd-button) {
border-color: #fbbf24;
background-color: #fff;
box-shadow: 0 4px 10px rgba(120, 53, 15, 0.08);
}
</style>

View File

@@ -1,9 +1,10 @@
<script setup>
import { openCustomerService } from '@/composables/useCustomerService'
import { openAgentVipApplyInExternalBrowser } from '@/composables/useAgentVipH5'
definePage({ layout: 'default', auth: true })
function toVipApply() {
uni.navigateTo({ url: '/pages/agent-vip-apply' })
openAgentVipApplyInExternalBrowser()
}
function toService() {

View File

@@ -1,167 +0,0 @@
<script setup>
import { onMounted, ref } from 'vue'
import { onReachBottom } from '@dcloudio/uni-app'
definePage({ layout: 'default', auth: true })
const page = ref(1)
const pageSize = ref(10)
const total = ref(0)
const reportList = ref([])
const loading = ref(false)
const hasMore = ref(true)
const loadMoreState = ref('loading')
// 初始加载数据
function mergeUniqueById(oldList, newList) {
const map = new Map()
const resolveKey = (item, index) => String(item?.id ?? item?.order_id ?? `${item?.create_time || ''}_${item?.query_state || ''}_${index}`)
oldList.forEach((item, index) => {
map.set(resolveKey(item, index), item)
})
newList.forEach((item, index) => {
map.set(resolveKey(item, oldList.length + index), item)
})
return Array.from(map.values())
}
async function fetchData(reset = false) {
if (loading.value)
return
if (!reset && !hasMore.value)
return
loading.value = true
if (reset) {
page.value = 1
hasMore.value = true
loadMoreState.value = 'loading'
reportList.value = []
}
const { data, error } = await useApiFetch(`query/list?page=${page.value}&page_size=${pageSize.value}`, { silent: true })
.get()
.json()
if (data.value && !error.value) {
if (data.value.code === 200) {
total.value = data.value.data.total
const incoming = data.value.data.list || []
reportList.value = reset ? incoming : mergeUniqueById(reportList.value, incoming)
const isLastPage = incoming.length < pageSize.value || reportList.value.length >= total.value
hasMore.value = !isLastPage
loadMoreState.value = isLastPage ? 'finished' : 'loading'
if (!isLastPage)
page.value += 1
}
else {
loadMoreState.value = 'error'
}
}
else {
loadMoreState.value = 'error'
}
loading.value = false
}
// 初始加载
onMounted(() => {
fetchData(true)
})
onReachBottom(() => {
fetchData()
})
function toDetail(item) {
if (item.query_state !== 'success')
return
uni.navigateTo({
url: `/pages/report-result-webview?order_id=${encodeURIComponent(String(item.order_id || ''))}`,
})
}
// 状态文字映射
function stateText(state) {
switch (state) {
case 'pending':
return '查询中'
case 'success':
return '查询成功'
case 'failed':
return '查询失败'
case 'refunded':
return '已退款'
default:
return '未知状态'
}
}
// 状态颜色映射
function statusClass(state) {
switch (state) {
case 'pending':
return 'status-pending'
case 'success':
return 'status-success'
case 'failed':
return 'status-failed'
case 'refunded':
return 'status-refunded'
default:
return ''
}
}
</script>
<template>
<view class="flex flex-col gap-4 p-4">
<view class="history-scroll">
<view
v-for="item in reportList" :key="item.id" class="relative mb-4 cursor-pointer rounded-lg bg-white p-4 shadow-sm"
@click="toDetail(item)"
>
<view class="flex flex-col">
<view class="mb-1 text-xl text-black">
{{ item.product_name }}
</view>
<view class="text-sm text-[#999999]">
{{ item.create_time }}
</view>
</view>
<view
class="absolute right-0 top-0 rounded-bl-lg rounded-tr-lg px-2 py-[1px] text-sm text-white font-medium"
:class="[statusClass(item.query_state)]"
>
{{ stateText(item.query_state) }}
</view>
</view>
<view v-if="loading" class="py-4 text-center text-sm text-gray-400">
加载中...
</view>
<EmptyState v-else-if="!reportList.length" text="暂无查询记录" />
</view>
<wd-loadmore v-if="reportList.length > 0" :state="loadMoreState" @reload="fetchData" />
</view>
</template>
<style scoped>
.history-scroll {
min-height: calc(100vh - 120px);
}
.status-pending {
background-color: #1976d2;
color: white;
}
.status-success {
background-color: #1FBE5D;
color: white;
}
.status-failed {
background-color: #EB3C3C;
color: white;
}
.status-refunded {
background-color: #999999;
color: white;
}
</style>

View File

@@ -1,22 +1,10 @@
<script setup>
import { computed } from 'vue'
import bgIcon from '/static/images/bg_icon.png'
import bannerImg from '/static/images/index/banner_1.png'
import bannerImg2 from '/static/images/index/banner_2.png'
import bannerImg3 from '/static/images/index/banner_3.png'
import honestyBanner from '/static/images/index/banner_B.png'
import companyIcon from '/static/images/index/company_bg.png'
import housekeepingRiskIcon from '/static/images/index/housekeeping_risk_bg.png'
import loanCheckIcon from '/static/images/index/loan_check_bg.png'
import marriageRiskIcon from '/static/images/index/marriage_risk_bg.png'
import personalDataIcon from '/static/images/index/personal_data_bg.png'
import preLoanRiskIcon from '/static/images/index/preloan_risk_bg.png'
import rentalInfoBg from '/static/images/index/rentalinfo_bg.png'
import rightIcon from '/static/images/index/right.png'
import indexPromoteIcon from '/static/images/index/tgbg.png'
import indexMyReportIcon from '/static/images/index/wdbg.png'
import indexInvitationIcon from '/static/images/index/yqhy.png'
import { useAppConfig } from '@/composables/useAppConfig'
import { ensurePrivacyConsentIfNeeded } from '@/composables/usePrivacyConsent'
definePage({ layout: 'home' })
@@ -27,28 +15,6 @@ onShow(() => {
const banners = [bannerImg, bannerImg2, bannerImg3]
const services = [
{ title: '个人大数据', name: 'personalData', bg: personalDataIcon },
{ title: '婚恋风险', name: 'marriage', bg: marriageRiskIcon },
{ title: '入职背调', name: 'backgroundcheck', bg: preLoanRiskIcon },
]
const riskServices = [
{ title: '小微', name: 'companyinfo', bg: companyIcon },
{ title: '家政', name: 'homeservice', bg: housekeepingRiskIcon },
{ title: '贷前', name: 'preloanbackgroundcheck', bg: loanCheckIcon },
{ title: '诚信租赁', name: 'rentalinfo', bg: rentalInfoBg },
]
const { appConfig, loadAppConfig } = useAppConfig()
const queryRetentionDaysText = computed(() => `${appConfig.value.query.retention_days}`)
void loadAppConfig()
function toInquire(name) {
uni.navigateTo({
url: `/pages/inquire?feature=${encodeURIComponent(name)}`,
})
}
function toInvitation() {
uni.navigateTo({ url: '/pages/invitation' })
}
@@ -56,112 +22,70 @@ function toInvitation() {
function toPromote() {
uni.navigateTo({ url: '/pages/promote' })
}
function toHistory() {
uni.navigateTo({ url: '/pages/history-query' })
}
</script>
<template>
<view class="box-border min-h-screen">
<view class="relative p-4">
<swiper class="banner-swiper overflow-hidden rounded-xl" circular autoplay :interval="3000" indicator-dots
indicator-color="rgba(255,255,255,0.5)" indicator-active-color="#ffffff">
<swiper-item v-for="(item, index) in banners" :key="index">
<view class="box-border min-h-screen bg-[#f5faff] pb-4">
<view class="m-4 overflow-hidden rounded-2xl">
<swiper
class="banner-swiper w-full"
circular
autoplay
:interval="3000"
indicator-dots
indicator-color="rgba(255,255,255,0.45)"
indicator-active-color="#ffffff"
>
<swiper-item v-for="(item, index) in banners" :key="index" class="h-full w-full">
<image :src="item" class="h-full w-full" mode="aspectFill" />
</swiper-item>
</swiper>
</view>
<view class="px-6">
<view class="grid grid-cols-3 gap-3">
<view class="flex flex-col items-center justify-center text-center" @click="toPromote">
<view
class="box-content h-16 w-16 flex items-center justify-center rounded-full from-white to-blue-100/10 bg-gradient-to-b p-1 shadow-lg">
<image :src="indexPromoteIcon" class="h-12 w-12" mode="aspectFit" />
</view>
<text class="mt-1 text-center font-bold">
推广报告
</text>
</view>
<view class="flex flex-col items-center justify-center text-center" @click="toInvitation">
<view
class="box-content h-16 w-16 flex items-center justify-center rounded-full from-white to-blue-100/10 bg-gradient-to-b p-1 shadow-lg">
<image :src="indexInvitationIcon" class="h-12 w-12" mode="aspectFit" />
</view>
<text class="mt-1 text-center font-bold">
邀请下级
</text>
</view>
<view class="flex flex-col items-center justify-center text-center" @click="toHistory">
<view
class="box-content h-16 w-16 flex items-center justify-center rounded-full from-white to-blue-100/10 bg-gradient-to-b p-1 shadow-lg">
<image :src="indexMyReportIcon" class="h-12 w-12" mode="aspectFit" />
</view>
<text class="mt-1 text-center font-bold">
我的报告
</text>
</view>
</view>
</view>
<view class="relative p-4 pt-0">
<view class="grid grid-cols-2 my-4 gap-4" style="grid-template-rows: repeat(2, 1fr);">
<view v-for="(service, index) in services" :key="index"
class="relative min-h-18 flex flex-col rounded-xl px-4 py-2 shadow-lg"
:class="index === 0 ? 'row-span-2' : ''"
:style="`background: url(${service.bg}) no-repeat; background-size: 100% 100%; background-position: center;`"
@click="toInquire(service.name)">
<view class="min-h-18 flex items-end">
<!-- <text class="text-base text-gray-700 font-semibold">
{{ service.title }}
</text> -->
</view>
</view>
<view class="mt-2 px-4">
<view class="mb-2 text-base text-gray-800 font-bold">
即刻赚佣金
</view>
<scroll-view scroll-x class="risk-scroll my-4 px-1 pb-4 pt-2 -mx-1">
<view class="inline-flex gap-2">
<view v-for="(service, index) in riskServices" :key="index"
class="relative h-24 w-[107px] flex-shrink-0 rounded-xl shadow-lg"
:style="`background: url(${service.bg}) no-repeat; background-size: 100% 100%; background-position: center;`"
@click="toInquire(service.name)">
<view class="h-full flex items-end px-2 py-2">
<!-- <text class="text-sm text-gray-700 font-semibold">
{{ service.title }}
</text> -->
</view>
<view
class="card-gradient-1 mb-4 flex items-center rounded-2xl p-0 shadow-xl"
@click="toPromote"
>
<view class="flex flex-1 flex-col items-start justify-center py-4 pl-4">
<view class="mb-1 text-[20px] text-blue-700 font-bold">
直推报告
</view>
<view class="mb-4 text-xs text-gray-700">
选择所需报告类型灵活定价一键分享客户客户下单即结算佣金实时到账
</view>
<wd-button plain>
立即推广
</wd-button>
</view>
</scroll-view>
<view class="mb-3 mt-6 flex items-center">
<view class="bg-primary h-5 w-1.5 rounded-xl" />
<text class="ml-2 text-lg text-gray-800">
诚信专栏
</text>
<image class="mx-4 my-4 h-20 w-20" :src="indexPromoteIcon" mode="aspectFit" />
</view>
<view class="mt-4 overflow-hidden rounded-xl bg-white shadow-xl">
<image :src="honestyBanner" class="block w-full" mode="widthFix" />
<view
class="card-gradient-2 flex items-center rounded-2xl p-0 shadow-xl"
@click="toInvitation"
>
<view class="flex flex-1 flex-col items-start justify-center py-4 pl-4">
<view class="mb-1 text-[20px] text-teal-700 font-bold">
邀请下级代理
</view>
<view class="mb-4 text-xs text-gray-700">
邀请好友成为代理好友推广获客客户支付即返佣团队收益轻松到手
</view>
<wd-button plain>
立即邀请
</wd-button>
</view>
<image class="mx-4 my-4 h-20 w-20" :src="indexInvitationIcon" mode="aspectFit" />
</view>
<view class="mt-4 box-border h-14 w-full flex items-center rounded-lg bg-white px-4 text-gray-700 shadow-lg"
@click="toHistory">
<view class="mr-4 h-full flex items-center justify-center">
<image class="h-10 w-10" :src="bgIcon" mode="aspectFit" />
</view>
<view class="flex-1">
<view class="text-gray-800">
我的历史查询记录
</view>
<view class="text-xs text-gray-500">
查询记录有效期为{{ queryRetentionDaysText }}
</view>
</view>
<image :src="rightIcon" class="h-6 w-6" mode="aspectFit" />
<view class="relative mt-4 h-[120px] overflow-hidden rounded-2xl shadow-xl">
<image class="absolute inset-0 h-full w-full" :src="honestyBanner" mode="aspectFill" />
<view class="absolute inset-0 bg-black/5" />
</view>
</view>
</view>
@@ -172,7 +96,17 @@ function toHistory() {
height: 160px;
}
.risk-scroll {
white-space: nowrap;
.card-gradient-1 {
background: linear-gradient(135deg, #e3f0ff 0%, #fafdff 100%);
box-shadow:
0 6px 24px 0 rgba(60, 120, 255, 0.1),
0 1.5px 4px 0 rgba(60, 120, 255, 0.08);
}
.card-gradient-2 {
background: linear-gradient(135deg, #e6f7fa 0%, #fafdff 100%);
box-shadow:
0 6px 24px 0 rgba(0, 200, 180, 0.1),
0 1.5px 4px 0 rgba(0, 200, 180, 0.08);
}
</style>

View File

@@ -1,54 +0,0 @@
<script setup>
import { ref } from 'vue'
import InquireForm from '@/components/InquireForm.vue'
definePage({ layout: 'default' })
const feature = ref('')
const IMMERSIVE_NAVBAR_ROUTE = 'pages/inquire'
// 获取产品信息
const featureData = ref({})
onLoad(async (query) => {
const q = query || {}
feature.value = String(q.feature || '')
if (q.out_trade_no) {
uni.navigateTo({
url: `/pages/report-result-webview?out_trade_no=${encodeURIComponent(String(q.out_trade_no))}`,
})
return
}
await getProduct()
})
async function getProduct() {
if (!feature.value)
return
const { data } = await useApiFetch(`/product/en/${feature.value}`, { silent: true })
.get()
.json()
if (data.value) {
featureData.value = data.value.data
}
}
onPageScroll((event) => {
uni.$emit('immersive-navbar-change', {
route: IMMERSIVE_NAVBAR_ROUTE,
solid: event.scrollTop > 120,
})
})
onUnload(() => {
uni.$emit('immersive-navbar-change', {
route: IMMERSIVE_NAVBAR_ROUTE,
solid: false,
})
})
</script>
<template>
<InquireForm type="normal" :feature="feature" :feature-data="featureData" />
</template>

View File

@@ -2,6 +2,7 @@
import { storeToRefs } from 'pinia'
import { computed, onBeforeMount, ref, watch } from 'vue'
import { openCustomerService } from '@/composables/useCustomerService'
import { openAgentVipApplyInExternalBrowser } from '@/composables/useAgentVipH5'
import { useAgentStore } from '@/stores/agentStore'
import { useDialogStore } from '@/stores/dialogStore'
import { useUserStore } from '@/stores/userStore'
@@ -75,9 +76,7 @@ function maskName(name) {
return `${name.substring(0, 3)}****${name.substring(7)}`
}
function toHistory() {
uni.navigateTo({ url: '/pages/history-query' })
}
function toPromote() {
uni.navigateTo({ url: '/pages/promote' })
}
@@ -127,7 +126,7 @@ function toVipConfig() {
}
function toVipRenewal() {
uni.navigateTo({ url: '/pages/agent-vip-apply' })
openAgentVipApplyInExternalBrowser()
}
function formatExpiryTime(expiryTimeStr) {
@@ -298,17 +297,6 @@ onBeforeMount(() => {
</view>
<image src="/static/images/me/right.png" class="h-4 w-4" alt="右箭头" />
</view>
<view
class="w-full flex items-center justify-between border-b border-gray-100 px-6 py-4 transition-colors hover:bg-blue-50"
@click="toHistory">
<view class="flex items-center gap-3">
<image src="/static/images/index/wdbg.png" class="h-6 w-6" alt="我的报告" />
<text class="text-gray-700 font-medium">
我的报告
</text>
</view>
<image src="/static/images/me/right.png" class="h-4 w-4" alt="右箭头" />
</view>
<view
class="w-full flex items-center justify-between border-b border-gray-100 px-6 py-4 transition-colors hover:bg-blue-50"
@click="toUserAgreement">

View File

@@ -105,13 +105,13 @@ onShow(() => {
</view>
<view class="permissions">
<view class="permission-item">
1. 网络权限用于连接服务加载业务数据与提交操作请求
1. 网络权限用于连接服务加载页面与提交查询支付等操作
</view>
<view class="permission-item">
2. 存储权限用于缓存页面数据保存登录状态与图片文件
2. 相册/存储写入仅在您点击保存推广海报或二维码图片时将应用内生成的图片保存到手机相册
</view>
<view class="permission-item">
3. 设备信息权限用于运行稳定性安全风控与异常排查
3. 支付相关能力在您主动发起支付宝支付时由支付服务完成交易具体权限以支付 SDK 为准
</view>
</view>
<view class="hint">

View File

@@ -19,6 +19,9 @@ const formData = ref({
clientPrice: null,
})
/** 从首页报告卡片带入的 product_en配置加载后预选 */
const initialFeature = ref('')
const availableReportTypes = computed(() => {
if (!productConfig.value?.length)
return []
@@ -84,6 +87,17 @@ function openPricePicker() {
showPricePicker.value = true
}
function toExampleReport() {
const feature = formData.value.productType
if (!feature) {
uni.showToast({ title: '请先选择报告类型', icon: 'none' })
return
}
uni.navigateTo({
url: `/pages/report-example-webview?feature=${encodeURIComponent(feature)}`,
})
}
async function getPromoteConfig() {
loadingConfig.value = true
try {
@@ -91,7 +105,10 @@ async function getPromoteConfig() {
if (data.value && !error.value && data.value.code === 200) {
const list = data.value.data.AgentProductConfig || []
productConfig.value = list
const availableType = availableReportTypes.value[0]
const preset = initialFeature.value
? availableReportTypes.value.find(item => item.value === initialFeature.value)
: null
const availableType = preset || availableReportTypes.value[0]
if (availableType) {
selectProductType(availableType.value)
}
@@ -143,6 +160,12 @@ async function generatePromotionCode() {
}
}
onLoad((options) => {
const feature = options?.feature
if (feature)
initialFeature.value = decodeURIComponent(String(feature))
})
onMounted(() => {
getPromoteConfig()
})
@@ -163,8 +186,14 @@ onMounted(() => {
</view>
<VipBanner />
<view class="card mb-4">
<view class="mb-2 text-xl font-semibold">
<view class="card mb-4 relative">
<text
class="absolute right-4 top-4 z-10 text-sm text-blue-600 active:opacity-70"
@click="toExampleReport"
>
示例报告
</text>
<view class="mb-2 pr-20 text-xl font-semibold">
生成推广码
</view>
<wd-form ref="promotionForm" :model="formData">

3
src/uni-pages.d.ts vendored
View File

@@ -9,7 +9,6 @@ type _LocationUrl =
"/pages/agent-promote-details" |
"/pages/agent-rewards-details" |
"/pages/agent-service-agreement" |
"/pages/agent-vip-apply" |
"/pages/agent-vip-config" |
"/pages/agent-vip" |
"/pages/agent" |
@@ -18,8 +17,6 @@ type _LocationUrl =
"/pages/help-detail" |
"/pages/help-guide" |
"/pages/help" |
"/pages/history-query" |
"/pages/inquire" |
"/pages/invitation-agent-apply" |
"/pages/invitation" |
"/pages/launch" |