feat:小程序首页白屏问题

This commit is contained in:
dzc committed 2026-06-17 20:21:33 +08:00
1 parent b22de4d6b2
commit 6412d3be5c
10 files changed
+209 -84

No files matched your search

+30 -2
View File
@@ -1,5 +1,5 @@
<template>
<crud-layout title="通知公告" :bg="filePath(SERVICE_IMAGE.blueBg)">
<crud-layout mainIsScroll title="通知公告" :bg="filePath(SERVICE_IMAGE.blueBg)">
<view class="notice-detail">
<view class="detail-header">
<view class="title">
@@ -66,8 +66,36 @@ const goGetCoupon = () => {
uni.navigateTo({ url: `/pages/merchant/merchant-activity?actId=${actDetail.value.actId}&actType=${actDetail.value.actType}` })
}
const normalizeRichText = (html: string) => {
const origin = "https://stgl.pj.gov.cn"
return html.replace(/<img\b[^>]*>/gi, (imgTag) => {
let next = imgTag
next = next.replace(/\bsrc\s*=\s*(['"])(.*?)\1/i, (_m, quote: string, src: string) => {
let resolved = src
if (resolved?.startsWith("/api")) resolved = `${origin}${resolved}`
if (resolved?.startsWith("//")) resolved = `https:${resolved}`
return `src=${quote}${resolved}${quote}`
})
const styleMatch = next.match(/\bstyle\s*=\s*(['"])([\s\S]*?)\1/i)
const required = "max-width: 100%; height: auto; display: block;"
if (styleMatch) {
const quote = styleMatch[1]
const styleValue = styleMatch[2] ?? ""
if (styleValue.includes("max-width") || styleValue.includes("width")) return next
const merged = styleValue.trim().length ? `${styleValue.replace(/;?\s*$/, "; ")}${required}` : required
return next.replace(/\bstyle\s*=\s*(['"])([\s\S]*?)\1/i, `style=${quote}${merged}${quote}`)
}
return next.replace(/<img\b/i, `<img style="${required}"`)
})
}
const nodes = computed(() => {
return detail.value.description?.replaceAll(`<img src="/api`, `<img src="https://stgl.pj.gov.cn/api`)
const html = detail.value.description
if (!html) return ""
return normalizeRichText(html)
})
function getTypeLabel(type: string) {
+14 -22
View File
@@ -1,22 +1,24 @@
<template>
<view class="banner-detail">
<view class="detail-header">
<view class="title">{{ detail.title || "加载中..." }}</view>
<crud-layout title="详情" mainIsScroll :bg="filePath(SERVICE_IMAGE.blueBg)" ref="crudLayoutRef">
<view class="banner-detail">
<view class="detail-header">
<view class="title">{{ detail.title || "加载中..." }}</view>
</view>
<view v-if="loading" class="loading-tip">加载中...</view>
<view v-else-if="error" class="error-tip">{{ error }}</view>
<view v-else class="detail-content">
<rich-text :nodes="richTextNodes || ''" />
</view>
</view>
<view v-if="loading" class="loading-tip">加载中...</view>
<view v-else-if="error" class="error-tip">{{ error }}</view>
<view v-else class="detail-content">
<rich-text :nodes="richTextNodes || ''" />
</view>
</view>
</crud-layout>
</template>
<script setup lang="ts">
import { computed, ref } from "vue"
import { onLoad } from "@dcloudio/uni-app"
import sysBannerApi from "@/sheep/api/sys-banner"
import { filePath } from "@/sheep/utils/file"
import { filePath, parseHtml } from "@/sheep/utils/file"
import { SERVICE_IMAGE } from "@@/constant/index"
const detail = ref<{ title?: string; richTextContent?: string }>({})
const loading = ref(false)
@@ -31,11 +33,7 @@ const toAbsoluteUrl = (url?: string | null) => {
const richTextNodes = computed(() => {
const html = detail.value.richTextContent
if (!html) return ""
return html.replace(/<img\s+[^>]*src=["']([^"']+)["']/gi, (match, src) => {
const fullSrc = toAbsoluteUrl(src)
if (!fullSrc) return match
return match.replace(src, fullSrc)
})
return parseHtml(html)
})
onLoad(async (options: any) => {
@@ -66,12 +64,6 @@ onLoad(async (options: any) => {
</script>
<style lang="scss" scoped>
.banner-detail {
padding: 32rpx;
background: #f5f5f5;
min-height: 100vh;
}
.detail-header {
background: #fff;
border-radius: 16rpx;
+27 -12
View File
@@ -1,6 +1,6 @@
<template>
<crud-layout mainIsScroll bg="/static/page/blue-bg.png">
<view v-if="state.actId" class="px-16rpx py-32rpx">
<crud-layout title="商户活动" mainIsScroll :bg="filePath(SERVICE_IMAGE.blueBg)" ref="crudLayoutRef">
<view v-if="state.actId" class="py-32rpx">
<view v-if="state.actType == 'CANCEL_ACT'"
class="card rounded-lb-0! rounded-rb-0! w-full h-400rpx relative activity-card overflow-hidden">
<view class="ribbon">兑换券</view>
@@ -12,10 +12,11 @@
</text>
</view>
<view class="w-4/5 px-64rpx pt-32rpx text-28rpx c-#fff">{{ state.actDetail.actTitle }}</view>
<view class="absolute bottom-100 left-60 text-40 italic">
<view class="absolute bottom-100rpx left-60rpx text-40rpx italic">
<text class="color-red mr-16rpx">¥{{ hooks.useMoney(state.actDetail.activeCancelPrice) }}</text>
<text class="text-30rpx line-through color-red">¥{{ hooks.useMoney(state.actDetail.activeOrgPrice)
}}</text>
<text class="text-30rpx line-through color-red">
¥{{ hooks.useMoney(state.actDetail.activeOrgPrice) }}
</text>
</view>
<view class="absolute bottom-40 left-60 text-28rpx bg-#eeeeee44 c-#fff px-16rpx rounded-full">限量领取 先领先得
</view>
@@ -69,10 +70,6 @@
<text>天内使用</text>
</view>
</view>
<view v-if="state.actDetail.description"
class="m-l-20rpx m-y-20rpx w-55% text-28rpx bg-#eeeeee44 c-#fff p-16rpx rounded-16rpx">
{{ state.actDetail.description }}
</view>
</view>
<view v-if="state.couponInfo.actStatus == '2'"
class="text-center card rounded-lt-0! rounded-rt-0! p-24rpx sub-text-30">
@@ -87,6 +84,9 @@
</view>
</template>
</view>
<view class="detail-content">
<rich-text :nodes="nodes || ''"></rich-text>
</view>
<template #main-bottom>
<view class="w-full flex-col-center">
<tm-button class="px-256rpx m-16rpx" width="90%" @click="doQrcode">我的礼券</tm-button>
@@ -98,12 +98,14 @@
</template>
<script setup lang="ts">
import { reactive, ref, nextTick } from "vue"
import { reactive, ref, nextTick, computed } from "vue"
import { onLoad } from "@dcloudio/uni-app"
import { actCancel, actComApi, actCouponApi, userApi } from "@/sheep/api"
import { hooks, router } from "@/sheep"
import { serviceEnum, storageConstant } from "@@/constant"
import { serviceEnum, storageConstant } from "@@/constant";
import { SERVICE_IMAGE } from "@@/constant/index"
import { filePath } from "@@/utils/file"
import ModifyUser from "@@/components/modify-user.vue"
@@ -119,6 +121,10 @@ const state = reactive<any>({
userDetail: {},
})
const nodes = computed(() => {
return state.actDetail.description?.replaceAll(`<img src="/api`, `<img src="https://stgl.pj.gov.cn/api`)
})
onLoad((option: any) => {
const { actId, actType } = option
if (!actId || !actType) return uni.showToast({ title: "参数错误", icon: "error" })
@@ -258,8 +264,17 @@ const modifyUserClose = () => {
</script>
<style scoped>
.detail-content {
background: #fff;
border-radius: 16rpx;
padding: 32rpx;
font-size: 24rpx;
color: #333;
line-height: 1.6;
overflow: auto;
}
.activity-card {
background-image: url("../../static/page/activity-card-bg.png");
background-repeat: no-repeat;
background-size: contain;
background-position: 130%;
+3 -2
View File
@@ -9,7 +9,8 @@ import * as CryptoJS from "crypto-js"
import { actCancel, actComApi, actCouponApi, applyApi } from "@@/api"
import { hooks } from "@/sheep"
import { serviceEnum } from "@@/constant"
import { serviceEnum, SERVICE_IMAGE } from "@@/constant"
import { filePath } from "@@/utils/file"
import zTabs from "@@/components/other/z-tabs/z-tabs.vue"
@@ -159,7 +160,7 @@ const genVerificationCode = async (params: Record<string, any>) => {
</script>
<template>
<crud-layout mainIsScroll bg="/static/page/blue-bg.png">
<crud-layout title="我的礼券" mainIsScroll :bg="filePath(SERVICE_IMAGE.blueBg)">
<z-paging ref="pageRef" height="100%" :fixed="false" v-model="dynamicPageHook.list" :default-page-size="10"
safe-area-inset-bottom :loading-more-enabled="true" :show-scrollbar="false" @query="query"
:auto-clean-list-when-reload="false" :auto-scroll-to-top-when-reload="false"
+32 -5
View File
@@ -2,7 +2,7 @@
import { ref, reactive, computed, nextTick, onMounted } from "vue"
import { onHide, onShow } from "@dcloudio/uni-app"
import { router, constant } from "@@/index"
import { router } from "@@/index"
import { noticeApi, appApi } from "@@/api/index"
import { getAppInfo } from "@@/utils/app"
import { SERVICE_IMAGE } from "@@/constant/index"
@@ -48,7 +48,9 @@ onShow(() => {
if (!ceheckLogin()) {
uni.showToast({ title: "登录已过期,请重新登录", icon: "none" })
uni.navigateTo({ url: "/pages/root/login" })
return
}
getSwiperList()
getNoticeList()
getAppMenuList()
})
@@ -72,6 +74,20 @@ const doMenu = async (v: any) => {
tabbarStore.jumpFirstTabbar()
}
// 获取轮播图
const getSwiperList = async () => {
try {
const { data, code }: any = await noticeApi.getSwiperList()
console.log("获取轮播图,code:", code);
if (code != 0) {
throw new Error()
}
state.swiperList = (data || []).filter(Boolean)
} catch (error) {
console.log("获取轮播图意外的错误error:", error);
}
}
// 获取通知公告列表
const getNoticeList = async () => {
try {
@@ -79,6 +95,7 @@ const getNoticeList = async () => {
current: 1,
size: 9
})
console.log("获取通知公告列表,code:", code);
if (code != 0) {
throw new Error()
}
@@ -97,7 +114,10 @@ const getAppMenuList = async () => {
platformEnum: platform
}
const { data, code }: any = await appApi.appList(params)
if (code != 0) {
console.log("获取应用菜单列表,code:", code);
if (code == 424) {
} else if (code != 0) {
throw new Error()
}
state.appMenuList = (data || []).filter(Boolean)
@@ -105,6 +125,12 @@ const getAppMenuList = async () => {
console.log("获取应用菜单列表意外的错误error:", error);
}
}
const doSwiper = async (v: any) => {
if (!v) return
const target = (v.jumpValue || "").replace(/\s+/g, "")
uni.navigateTo({ url: target })
}
</script>
<template>
@@ -112,8 +138,8 @@ const getAppMenuList = async () => {
<view class="h-20rpx" />
<swiper class="w-full h-280rpx rounded-16rpx overflow-hidden" circular :indicator-dots="true"
indicator-active-color="#0779FD" indicator-color="#ffffff" :autoplay="true">
<swiper-item v-for="(v, i) in state?.swiperList" :key="i">
<image class="w-full h-full" :src="v.image" mode="aspectFill" />
<swiper-item v-for="(v, i) in state?.swiperList" :key="v.id" @tap="doSwiper(v)">
<image class="w-full h-full" :src="v.imageUrl" mode="aspectFill" />
</swiper-item>
</swiper>
<view class="h-20rpx" />
@@ -135,7 +161,8 @@ const getAppMenuList = async () => {
<view class="w-full">
<view
class="h-100rpx flex justify-between items-center p-x-16rpx p-y-20rpx border-b-1 border-color-#f0f0f0"
v-for="(v, idx) in group" :key="idx">
v-for="(v, idx) in group" :key="idx"
@tap="() => router.jump('/pages/integrated/notice/detail', { id: v?.id })">
<view class="flex-1 sub-text-26 font-normal! truncate">{{ v.title }}</view>
<view class="w-250rpx second-text-24 m-l-10rpx">{{ v.createTime }}</view>
</view>
+10
View File
@@ -1,6 +1,16 @@
import request from "@/sheep/utils/request"
export default {
// ========== 轮播图相关接口 ==========
/**
* 获取轮播图列表
*/
getSwiperList: () =>
request({
url: "/integrated/sysBanner/app/slides",
method: "GET",
}),
// 公告分页列表
getNoticePage: (params: { current: number; size: number }) =>
request({
+16
View File
@@ -1,5 +1,17 @@
import { throttle } from "radash"
function normalizePath(path = "") {
if (!path) return ""
const purePath = path.split("?")[0] || ""
if (!purePath) return ""
return purePath.startsWith("/") ? purePath : `/${purePath}`
}
function getCurrentPagePath() {
const currentPage = getCurrentPage()
return normalizePath(currentPage?.route || "")
}
function paramsToQuery(params: { [key: string]: string }) {
let query = []
for (let key in params) {
@@ -14,6 +26,10 @@ function _jump(path = "", params = {}, type = 1) {
if (!path) {
console.error("router::jump>>path 不能为空")
}
const targetPath = normalizePath(path)
if (targetPath && getCurrentPagePath() === targetPath) {
return
}
if (hasTabbar(path)) {
uni.switchTab({ url: path })
return
+26
View File
@@ -22,3 +22,29 @@ export const filePath = (path?: string | null) => {
return `${base}${ensureLeadingSlash(rawPath)}`
}
export const parseHtml = (html: string) => {
const origin = "https://stgl.pj.gov.cn"
return html.replace(/<img\b[^>]*>/gi, (imgTag) => {
let next = imgTag
next = next.replace(/\bsrc\s*=\s*(['"])(.*?)\1/i, (_m, quote: string, src: string) => {
let resolved = src
if (resolved?.startsWith("/api")) resolved = `${origin}${resolved}`
if (resolved?.startsWith("//")) resolved = `https:${resolved}`
return `src=${quote}${resolved}${quote}`
})
const styleMatch = next.match(/\bstyle\s*=\s*(['"])([\s\S]*?)\1/i)
const required = "max-width: 100%; height: auto; display: block;"
if (styleMatch) {
const quote = styleMatch[1]
const styleValue = styleMatch[2] ?? ""
if (styleValue.includes("max-width") || styleValue.includes("width")) return next
const merged = styleValue.trim().length ? `${styleValue.replace(/;?\s*$/, "; ")}${required}` : required
return next.replace(/\bstyle\s*=\s*(['"])([\s\S]*?)\1/i, `style=${quote}${merged}${quote}`)
}
return next.replace(/<img\b/i, `<img style="${required}"`)
})
}
+51 -41
View File
@@ -4,7 +4,7 @@ import { isEmpty } from "radash"
import * as router from "@@/router"
import * as constant from "@@/constant"
import { createCooldown, getBaseApiUrl } from "@@/utils/app"
import { getBaseApiUrl } from "@@/utils/app"
const TOKEN_KEY = constant.storageConstant.TOKEN
const USER_INFO_KEY = constant.storageConstant.USERINFO
@@ -18,8 +18,13 @@ const SKIP_AUTH_URLS = [
const SKIP_REFRESH_TOKEN = ["/sys/auth/refresh-token", "/auth/token/check_token"]
let refreshTokenLock = true
const runLoginRedirect = createCooldown(1500)
let refreshTokenPromise: Promise<any> | null = null
const BASIC_AUTH = "Basic " + base64.encode(import.meta.env.VITE_OAUTH2_APP_CLIENT)
const buildErrorResponse = (error: any, fallbackMsg: string = "请求失败") => ({
code: error?.statusCode || -1,
data: error?.data ?? null,
msg: error?.data?.msg || error?.errMsg || error?.message || fallbackMsg,
})
const https = new Request({
baseURL: getBaseApiUrl(),
@@ -58,13 +63,21 @@ https.interceptors.request.use(async (config) => {
// 过期,刷新token
const newTokenStorage = await refreshToken()
if (newTokenStorage) {
const tokenValue = getTokenValue(newTokenStorage)
tokenValue = getTokenValue(newTokenStorage)
if (tokenValue) config_header.Authorization = tokenValue
} else {
return Promise.reject({
statusCode: 424,
errMsg: "refresh token failed",
data: { msg: "登录已过期,请重新登录" },
config,
})
}
}
}
config_header["Authorization"] = tokenValue
}
config.header = config_header
return config
}, Promise.reject)
@@ -79,7 +92,6 @@ https.interceptors.response.use(
msg: null,
})
}
return Promise.resolve(response.data)
},
async (error) => {
@@ -87,23 +99,16 @@ https.interceptors.response.use(
// 处理 424 错误(token过期)
if (error.statusCode === 424) {
// 判断是否在登录页面
const currentPage = router.getCurrentPage()
const currentRoute = currentPage?.route
if (currentRoute === "pages/root/login" || currentRoute === "/pages/root/login") {
return
}
uni.removeStorageSync(TOKEN_KEY)
uni.removeStorageSync(USER_INFO_KEY)
runLoginRedirect(() => {
uni.showToast({ title: "登录已过期,请重新登录", icon: "none" })
uni.navigateTo({ url: "/pages/root/login" })
})
uni.showToast({ title: "登录已过期,请重新登录", icon: "none" })
router.jump("/pages/root/login")
}
// 显示错误信息
else if (error?.data?.msg) {
uni.showToast({ title: error.data.msg, icon: "none", mask: true })
}
return Promise.resolve(error.data)
return Promise.resolve(buildErrorResponse(error))
},
)
@@ -120,34 +125,39 @@ const isTokenExpires = (tokenStorage: GlobalSetting.TokenStorage): boolean => {
}
async function refreshToken(): Promise<any> {
refreshTokenLock = false
try {
const tokenStorage = uni.getStorageSync(TOKEN_KEY) as GlobalSetting.TokenStorage
const res: any = await https.middleware({
url: "/auth/oauth2/token",
header: {
skipToken: "true",
Authorization: BASIC_AUTH,
"Content-Type": "application/x-www-form-urlencoded",
},
method: "POST",
params: { refresh_token: tokenStorage.refreshToken, grant_type: "refresh_token", scope: "server" },
})
const { data } = res || {}
if (data) {
tokenStorage.accessToken = data?.accessToken?.tokenValue
tokenStorage.refreshToken = data?.refreshToken?.tokenValue
tokenStorage.accessTokenExpiresIn = data?.accessToken?.expiresAt
tokenStorage.refreshTokenExpiresIn = data?.refreshToken?.expiresAt
uni.setStorageSync(TOKEN_KEY, tokenStorage)
return tokenStorage
if (refreshTokenPromise) return refreshTokenPromise
refreshTokenPromise = (async () => {
refreshTokenLock = false
try {
const tokenStorage = uni.getStorageSync(TOKEN_KEY) as GlobalSetting.TokenStorage
const res: any = await https.middleware({
url: "/auth/oauth2/token",
header: {
skipToken: "true",
Authorization: BASIC_AUTH,
"Content-Type": "application/x-www-form-urlencoded",
},
method: "POST",
params: { refresh_token: tokenStorage.refreshToken, grant_type: "refresh_token", scope: "server" },
})
const { data } = res || {}
if (data) {
tokenStorage.accessToken = data?.accessToken?.tokenValue
tokenStorage.refreshToken = data?.refreshToken?.tokenValue
tokenStorage.accessTokenExpiresIn = data?.accessToken?.expiresAt
tokenStorage.refreshTokenExpiresIn = data?.refreshToken?.expiresAt
uni.setStorageSync(TOKEN_KEY, tokenStorage)
return tokenStorage
}
return null
} catch (error) {
return false
} finally {
refreshTokenLock = true
refreshTokenPromise = null
}
return null
} catch (error) {
return false
} finally {
refreshTokenLock = true
}
})()
return refreshTokenPromise
}
// 请求函数
Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB