This commit is contained in:
2026-04-27 14:48:57 +08:00
parent f1ce86b5e8
commit cd7c4c8d3b
11 changed files with 176 additions and 107 deletions

View File

@@ -6,6 +6,7 @@ import { useUserStore } from "@/stores/userStore";
import { useDialogStore } from "@/stores/dialogStore";
import { useAuthStore } from "@/stores/authStore";
import { useWeixinShare } from "@/composables/useWeixinShare";
import { useAppConfig } from "@/composables/useAppConfig";
import WechatOverlay from "@/components/WechatOverlay.vue";
// import MaintenanceDialog from "@/components/MaintenanceDialog.vue";
@@ -15,8 +16,11 @@ const userStore = useUserStore();
const dialogStore = useDialogStore();
const authStore = useAuthStore();
const { configWeixinShare, setDynamicShare } = useWeixinShare();
const { loadAppConfig } = useAppConfig();
onMounted(() => {
void loadAppConfig();
// 检查token版本如果版本不匹配则清除旧token
checkTokenVersion()
@@ -218,7 +222,7 @@ const h5WeixinGetCode = () => {
<template>
<RouterView />
<WechatOverlay />
<!-- <WechatOverlay /> -->
<BindPhoneDialog />
<!-- <MaintenanceDialog /> -->
</template>

View File

@@ -120,6 +120,7 @@ declare global {
const useAliyunCaptcha: typeof import('./composables/useAliyunCaptcha.js')['default']
const useAnimate: typeof import('@vueuse/core')['useAnimate']
const useApiFetch: typeof import('./composables/useApiFetch.js')['default']
const useAppConfig: typeof import('./composables/useAppConfig.js')['useAppConfig']
const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference']
const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery']
const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']

View File

@@ -7,6 +7,23 @@ import { splitDWBG8B4DForTabs } from '@/ui/CDWBG8B4D/utils/simpleSplitter.js';
import { splitDWBG6A2CForTabs } from '@/ui/DWBG6A2C/utils/simpleSplitter.js';
import { splitCJRZQ5E9FForTabs } from '@/ui/CJRZQ5E9F/utils/simpleSplitter.js';
import { splitCQYGL3F8EForTabs } from '@/ui/CQYGL3F8E/utils/simpleSplitter.js';
import { useAppConfig } from '@/composables/useAppConfig';
import { useRoute } from 'vue-router';
// 与首页/查询表一致:报告保留天数由 App.vue 拉取的 /app/config 中 query.retention_days
const { appConfig } = useAppConfig();
const route = useRoute();
/**
* App 内嵌 WebView 走 /app/report、/app/example无 PageLayout 顶栏sticky 的 offset-top 若为 46
* 会与「为 van-nav-bar 预留」一致,在顶栏不存在时 Tab 上方会多出一块空白。
* 浏览器内 /report、/example 仍带顶栏,保持 46。
*/
const tabsStickyOffsetTop = computed(() => {
if (route.path.startsWith('/app/') || route.meta?.embedForApp)
return 0;
return 46;
});
// 动态导入产品背景图片的函数
const loadProductBackground = async (productType) => {
@@ -782,7 +799,7 @@ watch([reportData, componentRiskScores], () => {
</div>
<!-- Tabs 区域 -->
<StyledTabs v-model:active="active" scrollspy sticky :offset-top="46">
<StyledTabs v-model:active="active" scrollspy sticky :offset-top="tabsStickyOffsetTop">
<div class="flex flex-col gap-y-4 p-4">
<LEmpty v-if="isEmpty" />
<van-tab title="分析指数">
@@ -837,8 +854,7 @@ watch([reportData, componentRiskScores], () => {
1本份报告是在取得您个人授权后我们才向合法存有您以上个人信息的机构去调取相关内容我们不会以任何形式对您的报告进行存储除您和您授权的人外不会提供给任何人和机构进行查看
</p>
<p class="text-[#999999]">
&nbsp; &nbsp; 2本报告自生成之日起有效期 30
过期自动删除如果您对本份报告存有异议可能是合作机构数据有延迟或未能获取到您的相关数据出于合作平台数据隐私的保护本平台将不做任何解释
&nbsp; &nbsp; 2本报告自生成之日起有效期 {{ appConfig.query.retention_days }} 过期自动删除如果您对本份报告存有异议可能是合作机构数据有延迟或未能获取到您的相关数据出于合作平台数据隐私的保护本平台将不做任何解释
</p>
<p class="text-[#999999]">
&nbsp; &nbsp; 3若以上数据有错误请联系平台客服

View File

@@ -37,6 +37,8 @@
</template>
<script setup>
import { calculatePromotionPricing, safeTruncate } from '@/utils/promotionPricing'
const props = defineProps({
defaultPrice: {
type: Number,
@@ -58,31 +60,12 @@ watch(show, () => {
})
const costPrice = computed(() => {
if (!productConfig.value) return 0.00
// 平台定价成本
let platformPricing = 0
platformPricing += productConfig.value.cost_price
if (price.value > productConfig.value.p_pricing_standard) {
platformPricing += (price.value - productConfig.value.p_pricing_standard) * productConfig.value.p_overpricing_ratio
}
if (productConfig.value.a_pricing_standard > platformPricing && productConfig.value.a_pricing_end > platformPricing && productConfig.value.a_overpricing_ratio > 0) {
if (price.value > productConfig.value.a_pricing_standard) {
if (price.value > productConfig.value.a_pricing_end) {
platformPricing += (productConfig.value.a_pricing_end - productConfig.value.a_pricing_standard) * productConfig.value.a_overpricing_ratio
} else {
platformPricing += (price.value - productConfig.value.a_pricing_standard) * productConfig.value.a_overpricing_ratio
}
}
}
return safeTruncate(platformPricing)
const pricingResult = computed(() => {
return calculatePromotionPricing(price.value, productConfig.value)
})
const promotionRevenue = computed(() => {
return safeTruncate(price.value - costPrice.value)
});
const costPrice = computed(() => pricingResult.value.costPrice)
const promotionRevenue = computed(() => pricingResult.value.promotionRevenue)
// 价格校验与修正逻辑
const validatePrice = (currentPrice) => {
@@ -122,15 +105,6 @@ const validatePrice = (currentPrice) => {
console.log(newPrice, message)
return { newPrice, message };
}
function safeTruncate(num, decimals = 2) {
if (isNaN(num) || !isFinite(num)) return "0.00";
const factor = 10 ** decimals;
const scaled = Math.trunc(num * factor);
const truncated = scaled / factor;
return truncated.toFixed(decimals);
}
const isManualConfirm = ref(false)
const onConfirm = () => {
if (isManualConfirm.value) return

View File

@@ -0,0 +1,46 @@
export function safeTruncate(num, decimals = 2) {
if (Number.isNaN(num) || !Number.isFinite(num))
return '0.00'
const factor = 10 ** decimals
const scaled = Math.trunc(num * factor)
return (scaled / factor).toFixed(decimals)
}
function calculatePlatformOverpricingCost(price, config) {
if (price <= config.p_pricing_standard)
return 0
return (price - config.p_pricing_standard) * config.p_overpricing_ratio
}
function calculateSuperiorOverpricingCost(price, config) {
if (config.a_overpricing_ratio <= 0)
return 0
if (price <= config.a_pricing_standard)
return 0
if (config.a_pricing_end <= config.a_pricing_standard)
return 0
const superiorRangeAmount = Math.min(price, config.a_pricing_end) - config.a_pricing_standard
return Math.max(0, superiorRangeAmount) * config.a_overpricing_ratio
}
export function calculatePromotionPricing(priceInput, config) {
if (!config)
return { costPrice: '0.00', promotionRevenue: '0.00' }
const price = Number(priceInput)
if (!Number.isFinite(price))
return { costPrice: '0.00', promotionRevenue: '0.00' }
const baseCost = Number(config.cost_price) || 0
const platformOverpricingCost = calculatePlatformOverpricingCost(price, config)
const superiorOverpricingCost = calculateSuperiorOverpricingCost(price, config)
const totalCost = baseCost + platformOverpricingCost + superiorOverpricingCost
const revenue = price - totalCost
return {
costPrice: safeTruncate(totalCost),
promotionRevenue: safeTruncate(revenue),
}
}

View File

@@ -763,12 +763,6 @@ function selectType(type) {
// 申请VIP或SVIP
async function applyVip() {
// 如果是VIP想升级到SVIP提示联系客服
if (isVip.value && selectedType.value === 'svip') {
contactService()
return
}
// 如果是SVIP要降级到VIP提示不能降级
if (isSvip.value && selectedType.value === 'vip') {
showToast('SVIP会员不能降级到VIP会员')

View File

@@ -15,7 +15,7 @@
<div v-if="isAgent" class="absolute -bottom-2 -right-2">
<div class="flex items-center justify-center rounded-full px-3 py-1 text-xs font-bold text-white shadow-sm"
:class="levelGradient.badge">
{{ levelNames[level] }}
{{ levelNames[normalizedLevel] }}
</div>
</div>
</div>
@@ -39,15 +39,15 @@
</p>
</template>
<p v-if="isAgent" class="text-sm font-medium" :class="levelGradient.text">
🎖 {{ levelText[level] }}
🎖 {{ levelText[normalizedLevel] }}
</p>
</div>
</div>
<VipBanner v-if="isAgent && (level === 'normal' || level === '')" />
<VipBanner v-if="isAgent && normalizedLevel === 'NORMAL'" />
<!-- 功能菜单 -->
<div class="">
<div class="bg-white rounded-xl shadow-sm overflow-hidden">
<template v-if="isAgent && ['VIP', 'SVIP'].includes(level)">
<template v-if="isAgent && ['VIP', 'SVIP'].includes(normalizedLevel)">
<button
class="w-full flex items-center justify-between px-6 py-4 hover:bg-purple-50 transition-colors border-b border-gray-100"
@click="toVipConfig">
@@ -165,8 +165,17 @@ const { isAgent, level, ExpiryTime } = storeToRefs(agentStore);
const { userName, userAvatar, isLoggedIn, mobile } = storeToRefs(userStore);
const { isWeChat } = useEnv();
const normalizedLevel = computed(() => {
const raw = String(level.value || "").trim();
if (!raw || ["NORMAL", "NORNAL"].includes(raw.toUpperCase()) || raw === "normal" || raw === "nornal") return "NORMAL";
if (raw.toUpperCase().includes("SVIP")) return "SVIP";
if (raw.toUpperCase().includes("VIP")) return "VIP";
return "NORMAL";
});
const levelNames = {
normal: "普通代理",
NORMAL: "普通代理",
"": "普通代理",
VIP: "VIP代理",
SVIP: "SVIP代理",
@@ -174,6 +183,7 @@ const levelNames = {
const levelText = {
normal: "基础代理特权",
NORMAL: "基础代理特权",
"": "基础代理特权",
VIP: "高级代理特权",
SVIP: "尊享代理特权",
@@ -182,24 +192,27 @@ const levelText = {
const levelGradient = computed(() => ({
border: {
normal: "bg-gradient-to-r from-gray-300 to-gray-400",
NORMAL: "bg-gradient-to-r from-gray-300 to-gray-400",
"": "bg-gradient-to-r from-gray-300 to-gray-400",
VIP: "bg-gradient-to-r from-yellow-400 to-amber-500",
SVIP: "bg-gradient-to-r from-purple-400 to-pink-400 shadow-[0_0_15px_rgba(163,51,200,0.2)]",
}[level.value],
}[normalizedLevel.value],
badge: {
normal: "bg-gradient-to-r from-gray-500 to-gray-600",
NORMAL: "bg-gradient-to-r from-gray-500 to-gray-600",
"": "bg-gradient-to-r from-gray-500 to-gray-600",
VIP: "bg-gradient-to-r from-yellow-500 to-amber-600",
SVIP: "bg-gradient-to-r from-purple-500 to-pink-500",
}[level.value],
}[normalizedLevel.value],
text: {
normal: "text-gray-600",
NORMAL: "text-gray-600",
"": "text-gray-600",
VIP: "text-amber-600",
SVIP: "text-purple-600",
}[level.value],
}[normalizedLevel.value],
}));
const maskName = (name) => {
@@ -276,8 +289,9 @@ function formatExpiryTime(expiryTimeStr) {
const getDefaultAvatar = () => {
if (!isAgent.value) return headShot;
switch (level.value) {
switch (normalizedLevel.value) {
case "normal":
case "NORMAL":
case "":
return "/image/shot_nonal.png";
case "VIP":

View File

@@ -87,6 +87,7 @@
<script setup>
import PriceInputPopup from '@/components/PriceInputPopup.vue';
import VipBanner from '@/components/VipBanner.vue';
import { calculatePromotionPricing } from '@/utils/promotionPricing';
const showTypePicker = ref(false);
const showApplyPopup = ref(false); // 用来控制申请代理弹窗的显示
const showPricePicker = ref(false);
@@ -120,41 +121,16 @@ const reportTypes = computed(() => {
// return (pickerProductConfig.value.cost_price + platformPricing).toFixed(2)
// })
const costPrice = computed(() => {
if (!pickerProductConfig.value) return 0.00
// 平台定价成本
let platformPricing = 0
platformPricing += pickerProductConfig.value.cost_price
if (clientPrice.value > pickerProductConfig.value.p_pricing_standard) {
platformPricing += (clientPrice.value - pickerProductConfig.value.p_pricing_standard) * pickerProductConfig.value.p_overpricing_ratio
}
if (pickerProductConfig.value.a_pricing_standard > platformPricing && pickerProductConfig.value.a_pricing_end > platformPricing && pickerProductConfig.value.a_overpricing_ratio > 0) {
if (clientPrice.value > pickerProductConfig.value.a_pricing_standard) {
if (clientPrice.value > pickerProductConfig.value.a_pricing_end) {
platformPricing += (pickerProductConfig.value.a_pricing_end - pickerProductConfig.value.a_pricing_standard) * pickerProductConfig.value.a_overpricing_ratio
} else {
platformPricing += (clientPrice.value - pickerProductConfig.value.a_pricing_standard) * pickerProductConfig.value.a_overpricing_ratio
}
}
}
return safeTruncate(platformPricing)
const pricingResult = computed(() => {
return calculatePromotionPricing(clientPrice.value, pickerProductConfig.value)
})
const costPrice = computed(() => pricingResult.value.costPrice)
const promotionRevenue = computed(() => {
return safeTruncate(clientPrice.value - costPrice.value)
return pricingResult.value.promotionRevenue
});
const showQRcode = ref(false);
function safeTruncate(num, decimals = 2) {
if (isNaN(num) || !isFinite(num)) return "0.00";
const factor = 10 ** decimals;
const scaled = Math.trunc(num * factor);
const truncated = scaled / factor;
return truncated.toFixed(decimals);
}
const generatePromotionCode = async () => {
if (selectedReportType.value.length === 0) {
showToast({ message: '请选择报告类型' });

View File

@@ -27,7 +27,7 @@ const fetchRewardDetails = async () => {
// 更新用户信息
userInfo.value = {
createTime: data.value.data.create_time,
level: data.value.data.level_name || '普通',
level: data.value.data.level_name || data.value.data.level || '',
mobile: data.value.data.mobile,
}
// 更新汇总数据
@@ -148,6 +148,29 @@ const userInfo = ref({})
const summary = ref({})
const statistics = ref([])
const normalizeAgentLevel = (value) => {
const raw = String(value || '').trim()
if (!raw)
return 'NORMAL'
const cleaned = raw.replace(/代理$/, '')
if (cleaned === '普通')
return 'NORMAL'
const upper = cleaned.toUpperCase()
if (upper.includes('SVIP'))
return 'SVIP'
if (upper.includes('VIP'))
return 'VIP'
if (upper === 'NORMAL' || upper === 'NORNAL')
return 'NORMAL'
return 'NORMAL'
}
const getAgentLevelLabel = value => ({
NORMAL: '普通代理',
VIP: 'VIP代理',
SVIP: 'SVIP代理',
}[normalizeAgentLevel(value)] || '普通代理')
onMounted(() => {
fetchRewardDetails()
})
@@ -214,7 +237,7 @@ const formatNumber = num => {
<div class="flex items-center space-x-3">
<div class="text-xl font-semibold text-gray-800">{{ userInfo.mobile }}</div>
<span class="px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-600">
{{ userInfo.level }}代理
{{ getAgentLevelLabel(userInfo.level) }}
</span>
</div>
</div>

View File

@@ -64,16 +64,38 @@ const formatNumber = num => {
return Number(num).toFixed(2)
}
const getLevelText = item => {
return item?.level_name || item?.level || '普通'
const normalizeAgentLevel = (value) => {
const raw = String(value || '').trim()
if (!raw)
return 'NORMAL'
const cleaned = raw.replace(/代理$/, '')
if (cleaned === '普通')
return 'NORMAL'
const upper = cleaned.toUpperCase()
if (upper.includes('SVIP'))
return 'SVIP'
if (upper.includes('VIP'))
return 'VIP'
if (upper === 'NORMAL' || upper === 'NORNAL')
return 'NORMAL'
return 'NORMAL'
}
const getLevelText = (item) => {
const normalized = normalizeAgentLevel(item?.level_name || item?.level)
return {
NORMAL: '普通代理',
VIP: 'VIP代理',
SVIP: 'SVIP代理',
}[normalized] || '普通代理'
}
// 获取等级标签样式
const getLevelClass = level => {
switch (level) {
case 'SVIP':
const getLevelClass = (levelText) => {
switch (levelText) {
case 'SVIP代理':
return 'bg-purple-100 text-purple-600'
case 'VIP':
case 'VIP代理':
return 'bg-blue-100 text-blue-600'
default:
return 'bg-gray-100 text-gray-600'
@@ -122,7 +144,7 @@ onMounted(() => {
</div>
<div class="text-xl font-semibold text-gray-800">{{ item.mobile }}</div>
<span :class="['px-3 py-1 rounded-full text-sm font-medium', getLevelClass(getLevelText(item))]">
{{ getLevelText(item) }}代理
{{ getLevelText(item) }}
</span>
</div>
</div>

View File

@@ -23,7 +23,10 @@ function generateSeoTemplatesPlugin() {
if (!viteConfig || viteConfig.command !== "build") return;
const env = loadEnv(viteConfig.mode, viteConfig.root, "");
const mergedEnv = { ...env, ...process.env };
const script = path.join(viteConfig.root, "seo/generate-seo-templates.cjs");
const script = path.join(
viteConfig.root,
"seo/generate-seo-templates.cjs",
);
const result = spawnSync(process.execPath, [script], {
cwd: viteConfig.root,
env: mergedEnv,
@@ -46,16 +49,16 @@ export default defineConfig({
strictPort: true, // 如果端口被占用则抛出错误而不是使用下一个可用端口
proxy: {
"/api/v1": {
target: "http://127.0.0.1:8888", // 本地接口地址
// target: "https://www.tianyuandb.com", // 本地接口地址
// target: "http://127.0.0.1:8888", // 本地接口地址
target: "https://chimei.ronsafe.cn/", // 本地接口地址
changeOrigin: true,
},
},
},
build: {
// 构建优化
target: 'es2015', // 支持更多浏览器
minify: 'terser', // 使用terser进行压缩
target: "es2015", // 支持更多浏览器
minify: "terser", // 使用terser进行压缩
terserOptions: {
compress: {
drop_console: true, // 移除console.log
@@ -66,15 +69,15 @@ export default defineConfig({
output: {
// 代码分割策略
manualChunks: {
vendor: ['vue', 'vue-router', 'pinia'],
vant: ['vant'],
utils: ['axios', 'lodash', 'crypto-js'],
charts: ['echarts', 'vue-echarts'],
vendor: ["vue", "vue-router", "pinia"],
vant: ["vant"],
utils: ["axios", "lodash", "crypto-js"],
charts: ["echarts", "vue-echarts"],
},
// 文件名策略
chunkFileNames: 'assets/js/[name]-[hash].js',
entryFileNames: 'assets/js/[name]-[hash].js',
assetFileNames: 'assets/[ext]/[name]-[hash].[ext]',
chunkFileNames: "assets/js/[name]-[hash].js",
entryFileNames: "assets/js/[name]-[hash].js",
assetFileNames: "assets/[ext]/[name]-[hash].[ext]",
},
},
// 启用CSS代码分割
@@ -92,11 +95,7 @@ export default defineConfig({
"@vueuse/core", // 自动引入 VueUse 中的工具函数(可选)
],
dts: "src/auto-imports.d.ts", // 生成类型定义文件(可选)
dirs: [
"src/composables",
"src/stores",
"src/components",
],
dirs: ["src/composables", "src/stores", "src/components"],
resolvers: [VantResolver()],
}),
Components({
@@ -112,6 +111,6 @@ export default defineConfig({
},
// 优化依赖预构建
optimizeDeps: {
include: ['vue', 'vue-router', 'pinia', 'vant', 'axios'],
include: ["vue", "vue-router", "pinia", "vant", "axios"],
},
});