Compare commits

...
14 Commits
Author SHA1 Message Date
dzc 81ffd2dab8 feat:加密图片重复刷新问题 2026-10-29 18:47:31 +08:00
dzc 44444c3b1b feat:日志敏感信息过滤 2026-10-29 15:47:05 +08:00
dzc 3ea7c4e872 fix:图片访问
fix:权限修改
2026-10-29 14:25:54 +08:00
dzc 653b4688cd fix:图片访问
fix:权限修改
2026-10-29 14:25:46 +08:00
dzc 6d267bcfc5 feat:tabbar 2026-08-24 17:29:33 +08:00
dzc f4adf95e7a feat:小程序修改 2026-08-21 12:20:36 +08:00
dzc 5f267c1e76 11 2026-08-20 19:43:28 +08:00
dzc 223a199ae8 feat:密码加密 2026-08-09 15:11:30 +08:00
dzc 0398148db7 feat:小程序修改 2026-08-09 15:05:10 +08:00
dzc 6412d3be5c feat:小程序首页白屏问题 2026-06-17 20:21:33 +08:00
dzc b22de4d6b2 feat:跳转到宁波银行美好商城 2026-06-03 11:28:39 +08:00
dzc d7fbf72df4 feat:小程序新的指纹唯一编号 2026-05-27 15:19:42 +08:00
dzc 150b8b3ac7 feat:支付选择优惠券 2026-05-26 18:24:57 +08:00
dzc bce6da0a0f feat:bug 2026-05-18 19:06:06 +08:00
119 changed files with 2304 additions and 512 deletions

No files matched your search

+6 -6
View File
@@ -17,16 +17,16 @@ VITE_PWD_ENC_KEY='pigxpigxpigxpigx'
VITE_OAUTH2_APP_CLIENT='mini:mini' VITE_OAUTH2_APP_CLIENT='mini:mini'
# --------------------------- start 开发环境 start ---------------------------------- # --------------------------- start 开发环境 start ----------------------------------
VITE_APP_DEV_SERVICE = https://stgl.pj.gov.cn/api # VITE_APP_DEV_SERVICE = https://www.pjzhhq.cn/api
# VITE_APP_DEV_SERVICE = http://localhost:9999 # VITE_APP_DEV_SERVICE = http://localhost:9999
# VITE_APP_DEV_SERVICE = https://dzc-test-1.fy.takin.cc VITE_APP_DEV_SERVICE = https://dzc-test-1.fy.takin.cc
# VITE_IMAGE_DEV_PATH = http://localhost:9999 # VITE_IMAGE_DEV_PATH = http://localhost:9999
# VITE_IMAGE_DEV_PATH = https://dzc-test-1.fy.takin.cc/api VITE_IMAGE_DEV_PATH = https://dzc-test-1.fy.takin.cc
VITE_IMAGE_DEV_PATH = https://stgl.pj.gov.cn/api # VITE_IMAGE_DEV_PATH = https://www.pjzhhq.cn/api
# ----------------------------- end 开发环境 end ------------------------------------ # ----------------------------- end 开发环境 end ------------------------------------
# --------------------------- start 生产环境 start ---------------------------------- # --------------------------- start 生产环境 start ----------------------------------
VITE_APP_PROD_SERVICE = https://stgl.pj.gov.cn/api VITE_APP_PROD_SERVICE = https://www.pjzhhq.cn/api
VITE_APP_PROD_IMAGE = https://stgl.pj.gov.cn/api VITE_APP_PROD_IMAGE = https://www.pjzhhq.cn/api
# ----------------------------- end 生产环境 end ------------------------------------ # ----------------------------- end 生产环境 end ------------------------------------
+4 -1
View File
@@ -18,4 +18,7 @@ dist
*.ntvs* *.ntvs*
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
# Local file-access credentials
.env.*.local
+20 -18
View File
@@ -1,20 +1,22 @@
<!DOCTYPE html> <!doctype html>
<html> <html>
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<script> <script>
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') || var coverSupport =
CSS.supports('top: constant(a)')) "CSS" in window && typeof CSS.supports === "function" && (CSS.supports("top: env(a)") || CSS.supports("top: constant(a)"))
document.write( document.write(
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' + '<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
(coverSupport ? ', viewport-fit=cover' : '') + '" />') (coverSupport ? ", viewport-fit=cover" : "") +
</script> '" />',
<title></title> )
<!--preload-links--> </script>
<!--app-context--> <title></title>
</head> <!--preload-links-->
<body> <!--app-context-->
<div id="app"><!--app-html--></div> </head>
<script type="module" src="/src/main.ts"></script> <body>
</body> <div id="app"><!--app-html--></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html> </html>
+4
View File
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { startFileAccess, stopFileAccess } from "./sheep/utils/file-access"
import { getLaunchParam, getAppInfo } from "./sheep/utils/app" import { getLaunchParam, getAppInfo } from "./sheep/utils/app"
import { jump } from "./sheep/router/index" import { jump } from "./sheep/router/index"
import { useTabbarStore } from "./sheep/stores/tabbar" import { useTabbarStore } from "./sheep/stores/tabbar"
@@ -11,6 +12,7 @@ export default {
} }
}, },
onLaunch() { onLaunch() {
startFileAccess()
console.log("App Launch"); console.log("App Launch");
const _this = this; const _this = this;
// uni.hideTabBar() // uni.hideTabBar()
@@ -19,9 +21,11 @@ export default {
// #endif // #endif
}, },
onShow() { onShow() {
startFileAccess()
console.log("App Show"); console.log("App Show");
}, },
onHide() { onHide() {
stopFileAccess()
console.log("App Hide"); console.log("App Hide");
// const _this = this; // const _this = this;
// wx.offAppRoute(_this.watchRouter) // wx.offAppRoute(_this.watchRouter)
+2
View File
@@ -1,3 +1,4 @@
import { installFileAccess } from './sheep/utils/file-access'
import { createSSRApp } from "vue" import { createSSRApp } from "vue"
import "uno.css" import "uno.css"
@@ -8,6 +9,7 @@ import pinia from "@@/stores/index"
export function createApp() { export function createApp() {
const app = createSSRApp(App) const app = createSSRApp(App)
installFileAccess(app)
app.use(pinia) app.use(pinia)
app.use(tmUi) app.use(tmUi)
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "", "name": "",
"appid": "", "appid": "wx11771c7bf2913cb2",
"description": "", "description": "",
"versionName": "1.0.0", "versionName": "1.0.0",
"versionCode": "100", "versionCode": "100",
@@ -50,7 +50,7 @@
"quickapp": {}, "quickapp": {},
/* 小程序特有相关 */ /* 小程序特有相关 */
"mp-weixin": { "mp-weixin": {
"appid": "", "appid": "wx11771c7bf2913cb2",
"setting": { "setting": {
"urlCheck": false "urlCheck": false
}, },
+3 -1
View File
@@ -96,7 +96,9 @@
{ {
"path": "pages/root/web-view", "path": "pages/root/web-view",
"style": { "style": {
"navigationBarTitleText": "webview页面", "navigationBarBackgroundColor": "#ffffff",
"navigationBarTitleText": "美好商城",
"navigationStyle": "default",
"disableScroll": true "disableScroll": true
} }
}, },
+3 -3
View File
@@ -20,7 +20,7 @@ const state = reactive<any>({
// 重新设置图片路径 // 重新设置图片路径
const boxesList = computed(() => { const boxesList = computed(() => {
return pageHook.list.map((item: any) => { return pageHook.list.map((item: any) => {
return { ...item, images: filePath(item.images) } return { ...item, images: item.images }
}) })
}) })
@@ -66,7 +66,7 @@ onShow(() => {
</script> </script>
<template> <template>
<crud-layout title="餐桌列表" :bg="filePath(SERVICE_IMAGE.blueBg)"> <crud-layout title="餐桌列表" :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))">
<z-paging ref="pageRef" height="100%" :fixed="false" v-model="pageHook.list" :default-page-size="999" <z-paging ref="pageRef" height="100%" :fixed="false" v-model="pageHook.list" :default-page-size="999"
safe-area-inset-bottom :loading-more-enabled="true" :show-scrollbar="false" @query="query" 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" @scroll="pageHook.scroll"> :auto-clean-list-when-reload="false" :auto-scroll-to-top-when-reload="false" @scroll="pageHook.scroll">
@@ -80,7 +80,7 @@ onShow(() => {
<view class="boxes-grid"> <view class="boxes-grid">
<view v-for="item in boxesList" :key="item.id" class="box-card"> <view v-for="item in boxesList" :key="item.id" class="box-card">
<view class="card-image-section"> <view class="card-image-section">
<image v-if="item.images" :src="item.images" class="box-image" mode="aspectFill" /> <image v-if="item.images" :src="$fileUrl(item.images)" class="box-image" mode="aspectFill" @error="$fileRetry(item.images)" />
<view v-else class="image-placeholder"> <view v-else class="image-placeholder">
<text class="placeholder-icon">🍴</text> <text class="placeholder-icon">🍴</text>
<text class="placeholder-text">餐桌</text> <text class="placeholder-text">餐桌</text>
+2 -2
View File
@@ -49,8 +49,8 @@ const remove = () => {
<template> <template>
<view class="flex-row items-start card p-x-15rpx p-y-25rpx m-b-20rpx" <view class="flex-row items-start card p-x-15rpx p-y-25rpx m-b-20rpx"
@tap="props.productId && router.jump('/pages/canteen/product-detail', { id: props.productId })"> @tap="props.productId && router.jump('/pages/canteen/product-detail', { id: props.productId })">
<image v-if="props.mode == 'default'" class="size-180rpx rounded-12rpx" :src="props.pic" mode="aspectFill" /> <image v-if="props.mode == 'default'" class="size-180rpx rounded-12rpx" :src="$fileUrl(props.pic)" mode="aspectFill" @error="$fileRetry(props.pic)" />
<image v-if="props.mode == 'cart'" class="size-120rpx rounded-12rpx" :src="props.pic" mode="aspectFill" /> <image v-if="props.mode == 'cart'" class="size-120rpx rounded-12rpx" :src="$fileUrl(props.pic)" mode="aspectFill" @error="$fileRetry(props.pic)" />
<view class="flex-1 m-l-20rpx"> <view class="flex-1 m-l-20rpx">
<view class="sub-text-28 show-line">{{ props.name }}</view> <view class="sub-text-28 show-line">{{ props.name }}</view>
<view class="m-t-10rpx flex-row items-center"> <view class="m-t-10rpx flex-row items-center">
+4 -4
View File
@@ -236,12 +236,12 @@ const doFood = () => {
</script> </script>
<template> <template>
<crud-layout mainIsScroll tabBarType="just-flot-back" :bg="filePath(SERVICE_IMAGE.blueBg)"> <crud-layout mainIsScroll tabBarType="just-flot-back" :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))">
<tm-sticky> <tm-sticky>
<swiper class="w-100vw h-30vh" circular :indicator-dots="true" indicator-active-color="#0779FD" <swiper class="w-100vw h-30vh" circular :indicator-dots="true" indicator-active-color="#0779FD"
indicator-color="#ffffff" :autoplay="true"> indicator-color="#ffffff" :autoplay="true">
<swiper-item v-for="(v, i) in state.canteenDetail?.swiperPic?.split(',')" :key="i"> <swiper-item v-for="(v, i) in state.canteenDetail?.swiperPic?.split(',')" :key="i">
<image class="w-full h-full" :src="filePath(v)" mode="aspectFill" /> <image class="w-full h-full" :src="$fileUrl(filePath(v))" mode="aspectFill" @error="$fileRetry(filePath(v))" />
</swiper-item> </swiper-item>
</swiper> </swiper>
@@ -250,8 +250,8 @@ const doFood = () => {
<!-- start 简介 start --> <!-- start 简介 start -->
<view class="h-15rpx" /> <view class="h-15rpx" />
<view class="flex-row card p-20rpx"> <view class="flex-row card p-20rpx">
<image class="w-180rpx h-180rpx rounded-12rpx" :src="filePath(state.canteenDetail.logoPic)" <image class="w-180rpx h-180rpx rounded-12rpx" :src="$fileUrl(filePath(state.canteenDetail.logoPic))"
mode="aspectFill" /> mode="aspectFill" @error="$fileRetry(filePath(state.canteenDetail.logoPic))" />
<view class="flex-1 flex-col m-l-20rpx"> <view class="flex-1 flex-col m-l-20rpx">
<view class="flex-row items-center justify-between"> <view class="flex-row items-center justify-between">
<view class="flex-row-center sub-text-34"> <view class="flex-row-center sub-text-34">
+2 -2
View File
@@ -52,7 +52,7 @@ const query = (page: number, pagesize: number) => {
// 重新设置图片路径 // 重新设置图片路径
const foodList = computed(() => { const foodList = computed(() => {
return pageHook.list.map((item: any) => { return pageHook.list.map((item: any) => {
return { ...item, picUrl: filePath(item.picUrl) } return { ...item, picUrl: item.picUrl }
}) })
}) })
@@ -94,7 +94,7 @@ const doProduct = (v: any) => {
</script> </script>
<template> <template>
<crud-layout title="每日菜谱" navBgColor="#1777FF" navBgTitleColor="white" :bg="filePath(SERVICE_IMAGE.blueBg)"> <crud-layout title="每日菜谱" navBgColor="#1777FF" navBgTitleColor="white" :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))">
<z-paging ref="pageRef" height="100%" :fixed="false" v-model="pageHook.list" :default-page-size="8" <z-paging ref="pageRef" height="100%" :fixed="false" v-model="pageHook.list" :default-page-size="8"
safe-area-inset-bottom :loading-more-enabled="true" :show-scrollbar="false" @query="query" 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" @scroll="pageHook.scroll"> :auto-clean-list-when-reload="false" :auto-scroll-to-top-when-reload="false" @scroll="pageHook.scroll">
+3 -3
View File
@@ -34,9 +34,9 @@ const getDetail = async () => {
</script> </script>
<template> <template>
<crud-layout title="菜品详情" tabBarType="just-flot-back" :bg="filePath(SERVICE_IMAGE.blueBg)"> <crud-layout title="菜品详情" tabBarType="just-flot-back" :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))">
<view v-if="state.detail"> <view v-if="state.detail">
<image class="w-100vw" :src="filePath(state.detail.picUrl)" mode="widthFix" /> <image class="w-100vw" :src="$fileUrl(filePath(state.detail.picUrl))" mode="widthFix" @error="$fileRetry(filePath(state.detail.picUrl))" />
<view class="p-x-15rpx"> <view class="p-x-15rpx">
<view class="flex-col justify-between card p-y-20rpx p-x-30rpx"> <view class="flex-col justify-between card p-y-20rpx p-x-30rpx">
<view class="flex-row items-center justify-between"> <view class="flex-row items-center justify-between">
@@ -58,7 +58,7 @@ const getDetail = async () => {
<view v-if="state.detail.description" <view v-if="state.detail.description"
class="m-t-15rpx flex-col justify-between card p-y-20rpx p-x-30rpx"> class="m-t-15rpx flex-col justify-between card p-y-20rpx p-x-30rpx">
<view class="prefix_title">详情</view> <view class="prefix_title">详情</view>
<view class="m-t-15rpx" v-html="state.detail.description"></view> <view class="m-t-15rpx" v-html="$fileHtml(state.detail.description)"></view>
</view> </view>
</view> </view>
</view> </view>
+1 -1
View File
@@ -83,7 +83,7 @@ const doSubmit = async () => {
</script> </script>
<template> <template>
<crud-layout title="申请车辆" :bg="filePath(SERVICE_IMAGE.blueBg)"> <crud-layout title="申请车辆" :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))">
<view class="h-30rpx" /> <view class="h-30rpx" />
<view class="card p-x-30rpx p-y-40rpx"> <view class="card p-x-30rpx p-y-40rpx">
<tm-form ref="applyFormRef" v-model="applyForm" :rules="applyFormRules" direction="vertical"> <tm-form ref="applyFormRef" v-model="applyForm" :rules="applyFormRules" direction="vertical">
+1 -1
View File
@@ -39,7 +39,7 @@ const doApply = () => {
</script> </script>
<template> <template>
<crud-layout title="我的车辆" :bg="filePath(SERVICE_IMAGE.blueBg)"> <crud-layout title="我的车辆" :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))">
<z-paging ref="pageRef" height="100%" :fixed="false" v-model="pageHook.list" :default-page-size="10" <z-paging ref="pageRef" height="100%" :fixed="false" v-model="pageHook.list" :default-page-size="10"
safe-area-inset-bottom :loading-more-enabled="true" :show-scrollbar="false" @query="query" 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" @scroll="pageHook.scroll"> :auto-clean-list-when-reload="false" :auto-scroll-to-top-when-reload="false" @scroll="pageHook.scroll">
+2 -2
View File
@@ -35,8 +35,8 @@ const switchRich = (type: string) => {
</script> </script>
<template> <template>
<crud-layout mainIsScroll :title="state.title" :bg="filePath(SERVICE_IMAGE.blueBg)"> <crud-layout mainIsScroll :title="state.title" :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))">
<view class="h-30rpx" /> <view class="h-30rpx" />
<view class="sub-text-24" style="line-height: 1.6 !important;" v-html="switchRich(state.id)"></view> <view class="sub-text-24" style="line-height: 1.6 !important;" v-html="$fileHtml(switchRich(state.id))"></view>
</crud-layout> </crud-layout>
</template> </template>
+2 -2
View File
@@ -42,7 +42,7 @@ const getDetail = async () => {
if (!isEmpty(data)) { if (!isEmpty(data)) {
const img = filePath(data.faceImg) const img = filePath(data.faceImg)
applyForm.faceImg = [img] applyForm.faceImg = [img]
uploadFileUrl.value = img uploadFileUrl.value = data.faceImg
} }
} }
@@ -79,7 +79,7 @@ const doSubmit = async () => {
</script> </script>
<template> <template>
<crud-layout :title="state.detailId ? '修改人脸' : '人脸上传'" :bg="filePath(SERVICE_IMAGE.blueBg)"> <crud-layout :title="state.detailId ? '修改人脸' : '人脸上传'" :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))">
<view class="h-30rpx" /> <view class="h-30rpx" />
<view class="card px-30rpx py-40rpx"> <view class="card px-30rpx py-40rpx">
<tm-form ref="applyFormRef" v-model="applyForm" :rules="applyFormRules" direction="vertical"> <tm-form ref="applyFormRef" v-model="applyForm" :rules="applyFormRules" direction="vertical">
+3 -3
View File
@@ -49,14 +49,14 @@ const doDel = (id: string) => {
</script> </script>
<template> <template>
<crud-layout title="人脸管理" :bg="filePath(SERVICE_IMAGE.blueBg)"> <crud-layout title="人脸管理" :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))">
<z-paging ref="pageRef" height="100%" :fixed="false" v-model="pageHook.list" :default-page-size="10" <z-paging ref="pageRef" height="100%" :fixed="false" v-model="pageHook.list" :default-page-size="10"
safe-area-inset-bottom :loading-more-enabled="true" :show-scrollbar="false" @query="query" 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" @scroll="pageHook.scroll"> :auto-clean-list-when-reload="false" :auto-scroll-to-top-when-reload="false" @scroll="pageHook.scroll">
<view class="h-20rpx" /> <view class="h-20rpx" />
<view class="card flex-col-center p-20rpx mb-20rpx" v-for="(v, i) in pageHook.list" :key="v.id"> <view class="card flex-col-center p-20rpx mb-20rpx" v-for="(v, i) in pageHook.list" :key="v.id">
<image class="w-300rpx h-300rpx rounded-50rpx mb-16rpx" :src="filePath(v.faceImg)" mode="aspectFill" <image class="w-300rpx h-300rpx rounded-50rpx mb-16rpx" :src="$fileUrl(filePath(v.faceImg))" mode="aspectFill"
@click.stop="hooks.usePreviewImage(filePath(v.faceImg))" /> @click.stop="hooks.usePreviewImage(filePath(v.faceImg))" @error="$fileRetry(filePath(v.faceImg))" />
<view class="sub-text-30 mb-16rpx">{{ v.username }}·{{ v.deptName }}</view> <view class="sub-text-30 mb-16rpx">{{ v.username }}·{{ v.deptName }}</view>
<view v-if="false" class="flex w-full items-center justify-end gap-16rpx second-text-26"> <view v-if="false" class="flex w-full items-center justify-end gap-16rpx second-text-26">
<text class="botder_btn" @click="doEdit(v.id)">修改</text> <text class="botder_btn" @click="doEdit(v.id)">修改</text>
+2 -2
View File
@@ -1,5 +1,5 @@
<template> <template>
<crud-layout mainIsScroll title="意见反馈" :bg="filePath(SERVICE_IMAGE.blueBg)"> <crud-layout mainIsScroll title="意见反馈" :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))">
<view v-if="!detail.title" class="loading-tip">加载中...</view> <view v-if="!detail.title" class="loading-tip">加载中...</view>
<view v-else> <view v-else>
<view class="detail-header"> <view class="detail-header">
@@ -12,7 +12,7 @@
<view class="content-text">{{ detail.description || '无描述' }}</view> <view class="content-text">{{ detail.description || '无描述' }}</view>
<view v-if="imageList.length" class="image-list"> <view v-if="imageList.length" class="image-list">
<image v-for="(img, idx) in imageList" :key="idx" <image v-for="(img, idx) in imageList" :key="idx"
:src="img.startsWith('http') || img.startsWith('https') ? img : filePath(img)" class="feedback-img" :src="$fileUrl(img.startsWith('http') || img.startsWith('https') ? img : filePath(img))" class="feedback-img"
mode="aspectFill" @error="handleImageError(img)" @load="handleImageLoad(img)" /> mode="aspectFill" @error="handleImageError(img)" @load="handleImageLoad(img)" />
</view> </view>
</view> </view>
+1 -1
View File
@@ -1,5 +1,5 @@
<template> <template>
<crud-layout mainIsScroll title="意见反馈" :bg="filePath(SERVICE_IMAGE.blueBg)"> <crud-layout mainIsScroll title="意见反馈" :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))">
<view class="feedback-tabs"> <view class="feedback-tabs">
<view :class="['tab', { active: activeTab === 0 }]" @click="activeTab = 0">提交反馈</view> <view :class="['tab', { active: activeTab === 0 }]" @click="activeTab = 0">提交反馈</view>
<view :class="['tab', { active: activeTab === 1 }]" @click="activeTab = 1">我的反馈</view> <view :class="['tab', { active: activeTab === 1 }]" @click="activeTab = 1">我的反馈</view>
+9 -11
View File
@@ -1,5 +1,5 @@
<template> <template>
<crud-layout mainIsScroll title="失物招领" :bg="filePath(SERVICE_IMAGE.blueBg)"> <crud-layout mainIsScroll title="失物招领" :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))">
<view v-if="loading" class="loading">加载中...</view> <view v-if="loading" class="loading">加载中...</view>
<view v-else-if="detail"> <view v-else-if="detail">
<view class="detail-card"> <view class="detail-card">
@@ -12,13 +12,13 @@
</view> </view>
<view v-if="imageList.length" class="image-list"> <view v-if="imageList.length" class="image-list">
<image v-for="(src, index) in imageList" :key="index" class="detail-image" :src="src" mode="aspectFill" <image v-for="(src, index) in imageList" :key="index" class="detail-image" :src="$fileUrl(src)" mode="aspectFill"
@tap="previewImage(index)" /> @tap="previewImage(index)" @error="$fileRetry(src)" />
</view> </view>
<view class="detail-section"> <view class="detail-section">
<view class="section-title">物品描述</view> <view class="section-title">物品描述</view>
<rich-text v-if="descriptionNodes" :nodes="descriptionNodes"></rich-text> <rich-text v-if="descriptionNodes" :nodes="$fileNodes(descriptionNodes)"></rich-text>
<view v-else class="section-content">暂无描述信息</view> <view v-else class="section-content">暂无描述信息</view>
</view> </view>
@@ -37,6 +37,8 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { previewFiles } from '@/sheep/utils/file-access'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app' import { onLoad } from '@dcloudio/uni-app'
@@ -44,7 +46,7 @@ import dayjs from 'dayjs'
import lostAndFoundApi from '@/sheep/api/lost-and-found' import lostAndFoundApi from '@/sheep/api/lost-and-found'
import { filePath } from '@/sheep/utils/file' import { filePath, parseHtml } from '@/sheep/utils/file'
import { SERVICE_IMAGE } from "@@/constant/index" import { SERVICE_IMAGE } from "@@/constant/index"
interface LostAndFoundDetail { interface LostAndFoundDetail {
@@ -102,11 +104,7 @@ const imageList = computed(() => {
const descriptionNodes = computed(() => { const descriptionNodes = computed(() => {
const desc = detail.value?.description const desc = detail.value?.description
if (!desc) return '' if (!desc) return ''
return desc.replace(/<img\s+[^>]*src=["']([^"']+)["']/gi, (match, src) => { return parseHtml(desc)
const fullSrc = toAbsoluteUrl(src)
if (!fullSrc) return match
return match.replace(src, fullSrc)
})
}) })
const contactInfo = computed(() => { const contactInfo = computed(() => {
@@ -123,7 +121,7 @@ const formatDate = (date?: string) => {
const previewImage = (index: number) => { const previewImage = (index: number) => {
if (!imageList.value.length) return if (!imageList.value.length) return
uni.previewImage({ previewFiles({
current: index, current: index,
urls: imageList.value, urls: imageList.value,
}) })
+2 -2
View File
@@ -1,11 +1,11 @@
<template> <template>
<crud-layout mainIsScroll title="失物招领" :bg="filePath(SERVICE_IMAGE.blueBg)"> <crud-layout mainIsScroll title="失物招领" :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))">
<scroll-view scroll-y class="scroll-view" @scrolltolower="onLoadMore" refresher-enabled <scroll-view scroll-y class="scroll-view" @scrolltolower="onLoadMore" refresher-enabled
:refresher-triggered="refreshing" @refresherrefresh="onRefresh"> :refresher-triggered="refreshing" @refresherrefresh="onRefresh">
<view v-if="list.length > 0"> <view v-if="list.length > 0">
<view v-for="item in list" :key="item.id" class="item-card" @tap="viewDetail(item)"> <view v-for="item in list" :key="item.id" class="item-card" @tap="viewDetail(item)">
<view class="item-title">{{ item.title }}</view> <view class="item-title">{{ item.title }}</view>
<view class="item-description" v-html="item.description"></view> <view class="item-description" v-html="$fileHtml(item.description)"></view>
<view class="item-info"> <view class="item-info">
<text>地点:{{ item.location }}</text> <text>地点:{{ item.location }}</text>
<text>时间:{{ formatDate(item.dateFound) }}</text> <text>时间:{{ formatDate(item.dateFound) }}</text>
@@ -1,5 +1,5 @@
<template> <template>
<crud-layout mainIsScroll title="会议预定" :bg="filePath(SERVICE_IMAGE.blueBg)"> <crud-layout mainIsScroll title="会议预定" :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))">
<!-- 会议室信息卡片 --> <!-- 会议室信息卡片 -->
<view class="room-card"> <view class="room-card">
<view class="room-header"> <view class="room-header">
+2 -2
View File
@@ -1,9 +1,9 @@
<template> <template>
<crud-layout mainIsScroll title="预约会议" :bg="filePath(SERVICE_IMAGE.blueBg)"> <crud-layout mainIsScroll title="预约会议" :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))">
<!-- 会议室信息 --> <!-- 会议室信息 -->
<view class="room-info-box"> <view class="room-info-box">
<image :src="roomImage" class="room-img" mode="aspectFill" /> <image :src="$fileUrl(roomImage)" class="room-img" mode="aspectFill" @error="$fileRetry(roomImage)" />
<view class="room-info-main"> <view class="room-info-main">
<view class="room-title">{{ room.name }} <text class="room-capacity">{{ room.capacity }}人</text></view> <view class="room-title">{{ room.name }} <text class="room-capacity">{{ room.capacity }}人</text></view>
<view class="room-address"><text>📍</text> {{ room.address }}</view> <view class="room-address"><text>📍</text> {{ room.address }}</view>
+2 -2
View File
@@ -1,5 +1,5 @@
<template> <template>
<crud-layout mainIsScroll title="会议预定" :bg="filePath(SERVICE_IMAGE.blueBg)"> <crud-layout mainIsScroll title="会议预定" :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))">
<!-- 日期选择栏(简化版) --> <!-- 日期选择栏(简化版) -->
<view class="date-bar"> <view class="date-bar">
<text class="date-text">{{ todayText }}</text> <text class="date-text">{{ todayText }}</text>
@@ -38,7 +38,7 @@
</text> </text>
</view> </view>
</view> </view>
<image :src="getRoomImage(room.picUrl)" class="room-img" mode="aspectFill" /> <image :src="$fileUrl(getRoomImage(room.picUrl))" class="room-img" mode="aspectFill" @error="$fileRetry(getRoomImage(room.picUrl))" />
</view> </view>
<!-- 时间条 --> <!-- 时间条 -->
<view class="timeline-bar"> <view class="timeline-bar">
@@ -1,5 +1,5 @@
<template> <template>
<crud-layout mainIsScroll title="会议预定" :bg="filePath(SERVICE_IMAGE.blueBg)"> <crud-layout mainIsScroll title="会议预定" :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))">
<!-- 顶部tab --> <!-- 顶部tab -->
<view class="tabs"> <view class="tabs">
<view v-for="(tab, idx) in tabs" :key="tab" :class="['tab', idx === activeTab ? 'active' : '']" <view v-for="(tab, idx) in tabs" :key="tab" :class="['tab', idx === activeTab ? 'active' : '']"
+10 -5
View File
@@ -1,5 +1,5 @@
<template> <template>
<crud-layout title="通知公告" :bg="filePath(SERVICE_IMAGE.blueBg)"> <crud-layout mainIsScroll title="通知公告" :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))">
<view class="notice-detail"> <view class="notice-detail">
<view class="detail-header"> <view class="detail-header">
<view class="title"> <view class="title">
@@ -15,7 +15,7 @@
</view> </view>
<view class="detail-content"> <view class="detail-content">
<rich-text :nodes="nodes || ''"></rich-text> <rich-text :nodes="$fileNodes(nodes || '')"></rich-text>
</view> </view>
<view v-if="detail.attachments && detail.attachments.length > 0" class="attachments"> <view v-if="detail.attachments && detail.attachments.length > 0" class="attachments">
@@ -32,10 +32,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { downloadFile as downloadProtectedFile } from '@/sheep/utils/file-access'
import { computed, ref } from "vue" import { computed, ref } from "vue"
import { onLoad } from "@dcloudio/uni-app" import { onLoad } from "@dcloudio/uni-app"
import { filePath } from "@@/utils/file" import { filePath, parseHtml } from "@@/utils/file"
import { SERVICE_IMAGE } from "@@/constant/index" import { SERVICE_IMAGE } from "@@/constant/index"
import { actCancel, actComApi, noticeApi } from "@/sheep/api" import { actCancel, actComApi, noticeApi } from "@/sheep/api"
@@ -66,8 +68,11 @@ const goGetCoupon = () => {
uni.navigateTo({ url: `/pages/merchant/merchant-activity?actId=${actDetail.value.actId}&actType=${actDetail.value.actType}` }) uni.navigateTo({ url: `/pages/merchant/merchant-activity?actId=${actDetail.value.actId}&actType=${actDetail.value.actType}` })
} }
const nodes = computed(() => { 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 parseHtml(html)
}) })
function getTypeLabel(type: string) { function getTypeLabel(type: string) {
@@ -104,7 +109,7 @@ function formatFileSize(size: number) {
} }
function downloadFile(file: any) { function downloadFile(file: any) {
uni.downloadFile({ downloadProtectedFile({
url: file.url, url: file.url,
success: (res) => { success: (res) => {
if (res.statusCode === 200) { if (res.statusCode === 200) {
+2 -1
View File
@@ -1,5 +1,6 @@
<template> <template>
<crud-layout title="通知公告" :bg="filePath(SERVICE_IMAGE.blueBg)"> <crud-layout title="通知公告" :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))">
<view class="h-30rpx" />
<view class="notice-container"> <view class="notice-container">
<view class="notice-list"> <view class="notice-list">
<view v-if="noticeList.length === 0" class="empty-tip"> 暂无公告 </view> <view v-if="noticeList.length === 0" class="empty-tip"> 暂无公告 </view>
+7 -5
View File
@@ -1,5 +1,5 @@
<template> <template>
<crud-layout mainIsScroll title="会议预定" :bg="filePath(SERVICE_IMAGE.blueBg)"> <crud-layout mainIsScroll title="会议预定" :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))">
<view class="detail-header"> <view class="detail-header">
<view class="equipment-info"> <view class="equipment-info">
<text class="equipment-name">{{ detail.title || detail.equipmentName || '无标题' }}</text> <text class="equipment-name">{{ detail.title || detail.equipmentName || '无标题' }}</text>
@@ -28,7 +28,7 @@
<text class="description">{{ detail.description }}</text> <text class="description">{{ detail.description }}</text>
</view> </view>
<view v-if="imageList.length" class="image-list"> <view v-if="imageList.length" class="image-list">
<image v-for="(img, idx) in imageList" :key="idx" :src="filePath(img)" class="repair-img" mode="aspectFill" /> <image v-for="(img, idx) in imageList" :key="idx" :src="$fileUrl(filePath(img))" class="repair-img" mode="aspectFill" @error="$fileRetry(filePath(img))" />
</view> </view>
</view> </view>
@@ -49,8 +49,8 @@
<text class="reply-text">{{ detail.handleRemark }}</text> <text class="reply-text">{{ detail.handleRemark }}</text>
</view> </view>
<view v-if="handleImageList.length" class="handle-image-list"> <view v-if="handleImageList.length" class="handle-image-list">
<image v-for="(img, idx) in handleImageList" :key="idx" :src="filePath(img)" class="handle-img" <image v-for="(img, idx) in handleImageList" :key="idx" :src="$fileUrl(filePath(img))" class="handle-img"
mode="aspectFill" @click="previewHandleImage(idx)" /> mode="aspectFill" @click="previewHandleImage(idx)" @error="$fileRetry(filePath(img))" />
</view> </view>
<view class="reply-footer"> <view class="reply-footer">
<text class="handle-by" v-if="detail.handleBy">处理人:{{ detail.handleBy }}</text> <text class="handle-by" v-if="detail.handleBy">处理人:{{ detail.handleBy }}</text>
@@ -61,6 +61,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { previewFiles } from '@/sheep/utils/file-access'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app' import { onLoad } from '@dcloudio/uni-app'
@@ -181,7 +183,7 @@ const hasHandleInfo = computed(() => {
function previewHandleImage(index: number) { function previewHandleImage(index: number) {
if (handleImageList.value.length === 0) return if (handleImageList.value.length === 0) return
const urls = handleImageList.value.map(img => filePath(img)) const urls = handleImageList.value.map(img => filePath(img))
uni.previewImage({ previewFiles({
current: index, current: index,
urls: urls urls: urls
}) })
+1 -1
View File
@@ -1,5 +1,5 @@
<template> <template>
<crud-layout mainIsScroll title="会议预定" :bg="filePath(SERVICE_IMAGE.blueBg)"> <crud-layout mainIsScroll title="会议预定" :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))">
<view class="repair-tabs"> <view class="repair-tabs">
<view :class="['tab', { active: activeTab === 0 }]" @click="activeTab = 0">维修申请</view> <view :class="['tab', { active: activeTab === 0 }]" @click="activeTab = 0">维修申请</view>
<view :class="['tab', { active: activeTab === 1 }]" @click="activeTab = 1">维修记录</view> <view :class="['tab', { active: activeTab === 1 }]" @click="activeTab = 1">维修记录</view>
+14 -22
View File
@@ -1,22 +1,24 @@
<template> <template>
<view class="banner-detail"> <crud-layout title="详情" mainIsScroll :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))" ref="crudLayoutRef">
<view class="detail-header"> <view class="banner-detail">
<view class="title">{{ detail.title || "加载中..." }}</view> <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="$fileNodes(richTextNodes || '')" />
</view>
</view> </view>
</crud-layout>
<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>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from "vue" import { computed, ref } from "vue"
import { onLoad } from "@dcloudio/uni-app" import { onLoad } from "@dcloudio/uni-app"
import sysBannerApi from "@/sheep/api/sys-banner" 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 detail = ref<{ title?: string; richTextContent?: string }>({})
const loading = ref(false) const loading = ref(false)
@@ -31,11 +33,7 @@ const toAbsoluteUrl = (url?: string | null) => {
const richTextNodes = computed(() => { const richTextNodes = computed(() => {
const html = detail.value.richTextContent const html = detail.value.richTextContent
if (!html) return "" if (!html) return ""
return html.replace(/<img\s+[^>]*src=["']([^"']+)["']/gi, (match, src) => { return parseHtml(html)
const fullSrc = toAbsoluteUrl(src)
if (!fullSrc) return match
return match.replace(src, fullSrc)
})
}) })
onLoad(async (options: any) => { onLoad(async (options: any) => {
@@ -66,12 +64,6 @@ onLoad(async (options: any) => {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.banner-detail {
padding: 32rpx;
background: #f5f5f5;
min-height: 100vh;
}
.detail-header { .detail-header {
background: #fff; background: #fff;
border-radius: 16rpx; border-radius: 16rpx;
+3 -3
View File
@@ -1,5 +1,5 @@
<template> <template>
<crud-layout title="工作代办" :bg="filePath(SERVICE_IMAGE.blueBg)"> <crud-layout title="工作代办" :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))">
<view class="todo-page"> <view class="todo-page">
<view class="todo-tabs"> <view class="todo-tabs">
<view :class="['tab-item', activeTab === 0 ? 'active' : '']" @click="switchTab(0)">我的待办</view> <view :class="['tab-item', activeTab === 0 ? 'active' : '']" @click="switchTab(0)">我的待办</view>
@@ -112,7 +112,7 @@
<text v-if="selectedUser?.id === employee.id" class="check-icon">✓</text> <text v-if="selectedUser?.id === employee.id" class="check-icon">✓</text>
</view> </view>
<view class="user-icon employee-icon"> <view class="user-icon employee-icon">
<image v-if="employee.avatar" :src="employee.avatar" class="avatar-img" /> <image v-if="employee.avatar" :src="$fileUrl(employee.avatar)" class="avatar-img" @error="$fileRetry(employee.avatar)" />
<text v-else class="avatar-text">{{ employee.name.charAt(0) }}</text> <text v-else class="avatar-text">{{ employee.name.charAt(0) }}</text>
</view> </view>
<view class="user-info"> <view class="user-info">
@@ -226,7 +226,7 @@
<view v-if="flow.userVoList && flow.userVoList.length > 0" class="flow-users"> <view v-if="flow.userVoList && flow.userVoList.length > 0" class="flow-users">
<view v-for="user in flow.userVoList" :key="user.id" class="flow-user"> <view v-for="user in flow.userVoList" :key="user.id" class="flow-user">
<view class="user-avatar"> <view class="user-avatar">
<image v-if="user.avatar" :src="user.avatar" class="avatar-img" /> <image v-if="user.avatar" :src="$fileUrl(user.avatar)" class="avatar-img" @error="$fileRetry(user.avatar)" />
<text v-else class="avatar-text">{{ user.name?.charAt(0) || '?' }}</text> <text v-else class="avatar-text">{{ user.name?.charAt(0) || '?' }}</text>
</view> </view>
<text class="user-name">{{ user.name || '未知用户' }}</text> <text class="user-name">{{ user.name || '未知用户' }}</text>
+1 -1
View File
@@ -1,5 +1,5 @@
<template> <template>
<crud-layout title="详情" mainIsScroll :bg="filePath(SERVICE_IMAGE.blueBg)"> <crud-layout title="详情" mainIsScroll :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))">
<view v-if="detail.id"> <view v-if="detail.id">
<view class="h-20rpx" /> <view class="h-20rpx" />
<tm-alert v-if="alertListType2[detail.checkStatus] && detail.type == '2'" :showClose="false" skin="normal" <tm-alert v-if="alertListType2[detail.checkStatus] && detail.type == '2'" :showClose="false" skin="normal"
@@ -1,5 +1,5 @@
<template> <template>
<crud-layout title="访客记录" :bg="filePath(SERVICE_IMAGE.blueBg)"> <crud-layout title="访客记录" :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))">
<z-paging ref="pageRef" height="100%" :fixed="false" v-model="pageHook.list" :default-page-size="10" <z-paging ref="pageRef" height="100%" :fixed="false" v-model="pageHook.list" :default-page-size="10"
safe-area-inset-bottom :loading-more-enabled="true" :show-scrollbar="false" @query="query" 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" @scroll="pageHook.scroll"> :auto-clean-list-when-reload="false" :auto-scroll-to-top-when-reload="false" @scroll="pageHook.scroll">
+1 -1
View File
@@ -1,5 +1,5 @@
<template> <template>
<crud-layout title="访客邀约" mainIsScroll :bg="filePath(SERVICE_IMAGE.blueBg)"> <crud-layout title="访客邀约" mainIsScroll :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))">
<view class="h-20rpx" /> <view class="h-20rpx" />
<tm-sheet :margin="[0]"> <tm-sheet :margin="[0]">
<tm-alert :border="['2']" :borderColor="['#89c0ff']" :darkBorderColor="['#3b5066']">访客邀约请填写以下信息</tm-alert> <tm-alert :border="['2']" :borderColor="['#89c0ff']" :darkBorderColor="['#3b5066']">访客邀约请填写以下信息</tm-alert>
+3 -3
View File
@@ -67,14 +67,14 @@ const toDetail = async (v: any) => {
<swiper class="w-100vw h-30vh" circular :indicator-dots="true" indicator-active-color="#0779FD" <swiper class="w-100vw h-30vh" circular :indicator-dots="true" indicator-active-color="#0779FD"
indicator-color="#ffffff" :autoplay="true"> indicator-color="#ffffff" :autoplay="true">
<swiper-item v-for="(v, i) in state.detail?.swiperPic?.split(',')" :key="i"> <swiper-item v-for="(v, i) in state.detail?.swiperPic?.split(',')" :key="i">
<image class="w-full h-full" :src="filePath(v)" mode="aspectFill" /> <image class="w-full h-full" :src="$fileUrl(filePath(v))" mode="aspectFill" @error="$fileRetry(filePath(v))" />
</swiper-item> </swiper-item>
</swiper> </swiper>
</template> </template>
<view class="flex-row card p-20rpx m-b-20rpx" v-for="(v, i) in pageHook.list" :key="i" <view class="flex-row card p-20rpx m-b-20rpx" v-for="(v, i) in pageHook.list" :key="i"
@tap="toDetail(v)"> @tap="toDetail(v)">
<image class="w-180rpx h-180rpx rounded-12rpx my-auto" :src="filePath(v.logoPic)" <image class="w-180rpx h-180rpx rounded-12rpx my-auto" :src="$fileUrl(filePath(v.logoPic))"
mode="aspectFill" /> mode="aspectFill" @error="$fileRetry(filePath(v.logoPic))" />
<view class="flex-1 flex-col m-l-20rpx"> <view class="flex-1 flex-col m-l-20rpx">
<view class="sub-text-34">{{ v.name }}</view> <view class="sub-text-34">{{ v.name }}</view>
<view class="m-t-10rpx"> <view class="m-t-10rpx">
+3 -3
View File
@@ -55,11 +55,11 @@ const doViewQrcode = async () => {
</script> </script>
<template> <template>
<crud-layout tabBarType="just-flot-back" :bg="filePath(SERVICE_IMAGE.blueBg)"> <crud-layout tabBarType="just-flot-back" :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))">
<view class="h-30rpx"></view> <view class="h-30rpx"></view>
<view class="size-full relative"> <view class="size-full relative">
<image class="w-650rpx h-1030rpx p-10rpx rounded-12rpx" :src="filePath(SERVICE_IMAGE.birthdayCoupon)" <image class="w-650rpx h-1030rpx p-10rpx rounded-12rpx" :src="$fileUrl(filePath(SERVICE_IMAGE.birthdayCoupon))"
mode="aspectFit" /> mode="aspectFit" @error="$fileRetry(filePath(SERVICE_IMAGE.birthdayCoupon))" />
<view class="z-100 size-120px absolute left-20rpx top-600rpx flex-row-center bg-white p-20rpx rounded-12rpx" <view class="z-100 size-120px absolute left-20rpx top-600rpx flex-row-center bg-white p-20rpx rounded-12rpx"
@tap="doViewQrcode"> @tap="doViewQrcode">
<canvas canvas-id="qrcodeId" class="size-100px" /> <canvas canvas-id="qrcodeId" class="size-100px" />
+37 -13
View File
@@ -2,19 +2,25 @@
import { reactive, ref, computed } from "vue" import { reactive, ref, computed } from "vue"
import { onLoad, onShow } from "@dcloudio/uni-app" import { onLoad, onShow } from "@dcloudio/uni-app"
import * as hooks from "@@/hooks"
import { serviceEnum } from "@@/constant" import { serviceEnum } from "@@/constant"
const { actCouponTypeEnum } = serviceEnum const { actCouponTypeEnum } = serviceEnum
const emit = defineEmits(["doVisibleQrcode"]) const emit = defineEmits(["doVisibleQrcode", "doUse"])
const props = withDefaults(defineProps<{ detail: any }>(), { const props = withDefaults(defineProps<{ mode?: 'list' | 'select', detail: any }>(), {
mode: "list",
detail: {}, detail: {},
}) })
const doVisibleQrcode = () => { const doVisibleQrcode = () => {
emit("doVisibleQrcode", { type: 'ACT_COUPON', detail: props.detail }) emit("doVisibleQrcode", { type: 'ACT_COUPON', detail: props.detail })
} }
const doUse = () => {
emit("doUse", { type: 'ACT_COUPON', detail: props.detail })
}
</script> </script>
<template> <template>
@@ -31,20 +37,28 @@ const doVisibleQrcode = () => {
class="iconfont icon-weishiyong1 absolute font-size-80rpx! color-#cccccc" /> class="iconfont icon-weishiyong1 absolute font-size-80rpx! color-#cccccc" />
<!-- 已过期 --> <!-- 已过期 -->
<view v-if="detail.status == 3" class="iconfont icon-yiguoqi absolute font-size-80rpx! color-#cccccc" /> <view v-if="detail.status == 3" class="iconfont icon-yiguoqi absolute font-size-80rpx! color-#cccccc" />
<!-- 已核销 -->
<view v-if="detail.status == 4" class="iconfont icon-yiguoqi absolute font-size-80rpx! color-#cccccc" />
</view> </view>
<view class="info-box"> <view class="info-box">
<view class="price-row"> <view class="price-row">
<view v-if="detail.faceValue" class="flex items-center"> <view v-if="detail.faceValue">
<text class="symbol">¥</text> <view v-if="detail.couponType == '2'" class="flex items-center">
<text class="amount">{{ detail.faceValue }}</text> <text class="symbol">立减</text>
<text class="amount">{{ detail.faceValue }}%</text>
</view>
<view v-else class="flex items-center">
<text class="symbol">¥</text>
<text class="amount">{{ hooks.useMoney(detail.faceValue) }}</text>
</view>
</view> </view>
<text class="name">{{ detail.couponName }}</text> <text class="name">{{ detail.couponName }}</text>
</view> </view>
<view v-if="detail.minPoint" class="condition"> <view v-if="detail.minPoint" class="condition">
满{{ detail.minPoint }}可用 <view>满{{ hooks.useMoney(detail.minPoint) }}可用</view>
<view class="ml-10rpx">{{ actCouponTypeEnum[detail.couponType] }}</view>
</view> </view>
<view class="scope-text flex-col"> <view class="scope-text flex-col">
<text>{{ actCouponTypeEnum[detail.couponType] }}</text>
<text>在{{ detail.startTime }}至{{ detail.endTime }}时间段使用</text> <text>在{{ detail.startTime }}至{{ detail.endTime }}时间段使用</text>
</view> </view>
</view> </view>
@@ -56,10 +70,18 @@ const doVisibleQrcode = () => {
<view class="circle-bottom"></view> <view class="circle-bottom"></view>
</view> </view>
<!-- 右侧按钮 --> <!-- 右侧按钮 -->
<view class="coupon-right"> <view class="flex-row-center">
<view v-if="detail.status == 1" class="action-btn" @tap="doVisibleQrcode">核销码</view> <view v-if="mode == 'list'" class="coupon-right">
<view v-if="detail.status == 2" class="action-btn disabled">已使用</view> <view v-if="detail.status == 1">
<view v-if="detail.status == 3" class="action-btn disabled">已过期</view> <view v-if="detail.couponType == '4'" class="action-btn" @tap="doVisibleQrcode">核销码</view>
<view v-else class="action-btn" @tap="doUse">去使用</view>
</view>
<view v-if="detail.status == 2" class="action-btn disabled">已使用</view>
<view v-if="detail.status == 3" class="action-btn disabled">已过期</view>
</view>
<view v-else-if="mode == 'select'">
<view class="action-btn">使用</view>
</view>
</view> </view>
</view> </view>
</template> </template>
@@ -117,7 +139,7 @@ const doVisibleQrcode = () => {
} }
.amount { .amount {
font-size: 48rpx; font-size: 28rpx;
font-weight: bold; font-weight: bold;
margin: 0 8rpx 0 2rpx; margin: 0 8rpx 0 2rpx;
line-height: 1; line-height: 1;
@@ -134,6 +156,8 @@ const doVisibleQrcode = () => {
} }
.condition { .condition {
display: flex;
align-items: center;
font-size: 24rpx; font-size: 24rpx;
color: #0094ff; color: #0094ff;
opacity: 0.8; opacity: 0.8;
@@ -188,7 +212,7 @@ const doVisibleQrcode = () => {
/* 右侧按钮区 */ /* 右侧按钮区 */
.coupon-right { .coupon-right {
width: 160rpx; width: 140rpx;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
+1 -1
View File
@@ -1,5 +1,5 @@
<template> <template>
<crud-layout mainIsScroll :bg="filePath(SERVICE_IMAGE.blueBg)"> <crud-layout mainIsScroll :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))">
<z-paging ref="pageRef" height="100%" :fixed="false" v-model="pageHook.list" :default-page-size="10" <z-paging ref="pageRef" height="100%" :fixed="false" v-model="pageHook.list" :default-page-size="10"
safe-area-inset-bottom :loading-more-enabled="true" :show-scrollbar="false" @query="query" 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" @scroll="pageHook.scroll"> :auto-clean-list-when-reload="false" :auto-scroll-to-top-when-reload="false" @scroll="pageHook.scroll">
+4 -4
View File
@@ -1,10 +1,10 @@
<template> <template>
<crud-layout mainIsScroll tabBarType="just-flot-back" :bg="filePath(SERVICE_IMAGE.blueBg)"> <crud-layout mainIsScroll tabBarType="just-flot-back" :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))">
<tm-sticky> <tm-sticky>
<swiper class="w-100vw h-30vh" circular :indicator-dots="true" indicator-active-color="#0779FD" <swiper class="w-100vw h-30vh" circular :indicator-dots="true" indicator-active-color="#0779FD"
indicator-color="#ffffff" :autoplay="true"> indicator-color="#ffffff" :autoplay="true">
<swiper-item v-for="(v, i) in state.detail?.swiperPic?.split(',')" :key="i"> <swiper-item v-for="(v, i) in state.detail?.swiperPic?.split(',')" :key="i">
<image class="w-full h-full" :src="filePath(v)" mode="aspectFill" /> <image class="w-full h-full" :src="$fileUrl(filePath(v))" mode="aspectFill" @error="$fileRetry(filePath(v))" />
</swiper-item> </swiper-item>
</swiper> </swiper>
@@ -13,8 +13,8 @@
<view class="h-15rpx" /> <view class="h-15rpx" />
<!-- start 简介 start --> <!-- start 简介 start -->
<view class="flex-row card p-20rpx m-b-20rpx"> <view class="flex-row card p-20rpx m-b-20rpx">
<image class="w-180rpx h-180rpx rounded-12rpx my-auto" :src="filePath(state.detail.logoPic)" <image class="w-180rpx h-180rpx rounded-12rpx my-auto" :src="$fileUrl(filePath(state.detail.logoPic))"
mode="aspectFill" /> mode="aspectFill" @error="$fileRetry(filePath(state.detail.logoPic))" />
<view class="flex-1 flex-col m-l-20rpx"> <view class="flex-1 flex-col m-l-20rpx">
<view class="sub-text-34">{{ state.detail.name }}</view> <view class="sub-text-34">{{ state.detail.name }}</view>
<view class="m-t-10rpx"> <view class="m-t-10rpx">
+102 -39
View File
@@ -1,23 +1,24 @@
<template> <template>
<crud-layout title="活动详情" mainIsScroll :bg="filePath(SERVICE_IMAGE.blueBg)"> <crud-layout title="商户活动" mainIsScroll :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))" ref="crudLayoutRef">
<view v-if="state.actId" class="px-16rpx py-32rpx"> <view v-if="state.actId" class="py-32rpx">
<view v-if="state.actType == 'CANCEL_ACT'" <view v-if="state.actType == 'CANCEL_ACT'"
class="card rounded-lb-0! rounded-rb-0! w-full h-400rpx relative activity-card overflow-hidden"> class="card rounded-lb-0! rounded-rb-0! w-full h-400rpx relative activity-card overflow-hidden">
<view class="ribbon">兑换券</view> <view class="ribbon">兑换券</view>
<view class="px-64rpx pt-40rpx flex justify-between items-center font-bold"> <view class="px-64rpx pt-40rpx flex justify-between items-center font-bold">
<text class="text-32 c-#FF4F3F">限时领取</text> <text class="text-32rpx c-#FF4F3F">限时领取</text>
<text class="text-30 c-#FF4F3F px-8rpx rounded-full bg-#eeeeee44"> <text class="text-30rpx c-#FF4F3F px-8rpx rounded-full bg-#eeeeee44">
{{ (state.actDetail.startTime?.split(" ")[0] || "") + "~" + {{ (state.actDetail.startTime?.split(" ")[0] || "") + "~" +
(state.actDetail.expireTime?.split(" ")[0] || "") }} (state.actDetail.expireTime?.split(" ")[0] || "") }}
</text> </text>
</view> </view>
<view class="w-4/5 px-64rpx pt-32rpx text-28 c-#fff">{{ state.actDetail.actTitle }}</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="color-red mr-16rpx">¥{{ hooks.useMoney(state.actDetail.activeCancelPrice) }}</text>
<text class="text-30 line-through color-red">¥{{ hooks.useMoney(state.actDetail.activeOrgPrice) <text class="text-30rpx line-through color-red">
}}</text> ¥{{ hooks.useMoney(state.actDetail.activeOrgPrice) }}
</text>
</view> </view>
<view class="absolute bottom-40 left-60 text-28 bg-#eeeeee44 c-#fff px-16rpx rounded-full">限量领取 先领先得 <view class="absolute bottom-40 left-60 text-28rpx bg-#eeeeee44 c-#fff px-16rpx rounded-full">限量领取 先领先得
</view> </view>
</view> </view>
<view class="card relative rounded-lb-0! rounded-rb-0! w-full activity-sr-card overflow-hidden" <view class="card relative rounded-lb-0! rounded-rb-0! w-full activity-sr-card overflow-hidden"
@@ -31,33 +32,44 @@
class="card relative rounded-lb-0! rounded-rb-0! w-full activity-card overflow-hidden"> class="card relative rounded-lb-0! rounded-rb-0! w-full activity-card overflow-hidden">
<view class="ribbon">{{ actCouponTypeEnum[state.actDetail.type] }}</view> <view class="ribbon">{{ actCouponTypeEnum[state.actDetail.type] }}</view>
<view class="px-64rpx pt-40rpx flex justify-between items-center font-bold"> <view class="px-64rpx pt-40rpx flex justify-between items-center font-bold">
<text class="text-32 c-#FF4F3F">{{ state.actDetail.name }}</text> <text class="text28 c-#FF4F3F">{{ state.actDetail.name }}</text>
<text class="text-30 c-#FF4F3F px-8rpx rounded-full bg-#eeeeee44"> <view v-if="state.actDetail.totalCount" class="coupon-stock-badge">
<view v-if="state.actDetail.validType == 1" class="flex items-center"> <view class="coupon-stock-dot" />
<text>{{ state.actDetail.validStartTime }}</text> <text class="coupon-stock-label">剩余</text>
<text>~</text> <text class="coupon-stock-value">{{ state.actDetail.surplusCount }}</text>
<text>{{ state.actDetail.validEndTime }}</text> <text class="coupon-stock-split">/</text>
</view> <text class="coupon-stock-total">{{ state.actDetail.totalCount }}</text>
<text class="coupon-stock-unit">张</text>
</view>
</view>
<view class="px-32rpx pt-32rpx text-26rpx c-#fff indent-1em">
<view v-if="state.actDetail.minPoint">
<text>满{{ hooks.useMoney(state.actDetail.minPoint) }}元,</text>
<text v-if="['2'].includes(state.actDetail.type)">
立减{{ state.actDetail.faceValue }}%
</text>
</view>
<text v-else-if="['1'].includes(state.actDetail.type)">
满减{{ hooks.useMoney(state.actDetail.faceValue) }}元
</text>
<text v-else-if="['3'].includes(state.actDetail.type)">
立减{{ hooks.useMoney(state.actDetail.faceValue) }}元
</text> </text>
</view> </view>
<view class="w-4/5 px-64rpx pt-32rpx text-28 c-#fff"> <view class="px-32rpx pb-32rpx pt-8rpx text-26rpx c-#fff">
<view v-if="state.actDetail.validType == '1'"> <view v-if="state.actDetail.validType == '1'" class="indent-1em">
<text>领取后请在</text> <text>领取后请在</text>
<text>{{ state.actDetail.validStartTime }}</text> <text>{{ state.actDetail.validStartTime }}</text>
<text>~</text> <text>~</text>
<text>{{ state.actDetail.validEndTime }}</text> <text>{{ state.actDetail.validEndTime }}</text>
<text>时间段内使用</text> <text>时间段内使用</text>
</view> </view>
<view v-if="state.actDetail.validType == '2'"> <view v-if="state.actDetail.validType == '2'" class="indent-1em">
<text>领取后请在</text> <text>领取后请在</text>
<text>{{ state.actDetail.validDays }}</text> <text>{{ state.actDetail.validDays }}</text>
<text>天内使用</text> <text>天内使用</text>
</view> </view>
</view> </view>
<view v-if="state.actDetail.description"
class="m-l-20rpx m-y-20rpx w-55% text-28 bg-#eeeeee44 c-#fff p-16rpx rounded-16rpx">
{{ state.actDetail.description }}
</view>
</view> </view>
<view v-if="state.couponInfo.actStatus == '2'" <view v-if="state.couponInfo.actStatus == '2'"
class="text-center card rounded-lt-0! rounded-rt-0! p-24rpx sub-text-30"> class="text-center card rounded-lt-0! rounded-rt-0! p-24rpx sub-text-30">
@@ -72,10 +84,13 @@
</view> </view>
</template> </template>
</view> </view>
<view class="detail-content">
<rich-text :nodes="$fileNodes(nodes || '')"></rich-text>
</view>
<template #main-bottom> <template #main-bottom>
<view class="w-full flex-col-center"> <view class="w-full flex-col-center">
<tm-button class="px-256rpx m-16rpx" width="90%" @click="doQrcode">我的礼券</tm-button> <tm-button class="px-256rpx m-16rpx" width="90%" @click="doQrcode">我的礼券</tm-button>
<view class="h-10rpx"></view> <view class="h-30rpx" />
</view> </view>
</template> </template>
</crud-layout> </crud-layout>
@@ -83,14 +98,14 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { reactive, ref, nextTick } from "vue" import { reactive, ref, nextTick, computed } from "vue"
import { onLoad } from "@dcloudio/uni-app" import { onLoad } from "@dcloudio/uni-app"
import { actCancel, actComApi, actCouponApi, userApi } from "@/sheep/api" import { actCancel, actComApi, actCouponApi, userApi } from "@/sheep/api"
import * as hooks from "@@/hooks" import { hooks, router } from "@/sheep"
import * as router from "@@/router" import { serviceEnum, storageConstant } from "@@/constant";
import { serviceEnum, storageConstant, SERVICE_IMAGE } from "@@/constant" import { SERVICE_IMAGE } from "@@/constant/index"
import { filePath } from "@@/utils/file" import { filePath, parseHtml } from "@@/utils/file"
import ModifyUser from "@@/components/modify-user.vue" import ModifyUser from "@@/components/modify-user.vue"
@@ -106,6 +121,10 @@ const state = reactive<any>({
userDetail: {}, userDetail: {},
}) })
const nodes = computed(() => {
return parseHtml(state.actDetail.description || '')
})
onLoad((option: any) => { onLoad((option: any) => {
const { actId, actType } = option const { actId, actType } = option
if (!actId || !actType) return uni.showToast({ title: "参数错误", icon: "error" }) if (!actId || !actType) return uni.showToast({ title: "参数错误", icon: "error" })
@@ -245,20 +264,21 @@ const modifyUserClose = () => {
</script> </script>
<style scoped> <style scoped>
.activity-card { .detail-content {
background-image: url("../../static/page/activity-card-bg.png"); background: #fff;
background-repeat: no-repeat; border-radius: 16rpx;
background-size: contain; padding: 32rpx;
background-position: 150%; font-size: 24rpx;
background-color: #1879ff77; color: #333;
line-height: 1.6;
overflow: auto;
} }
.activity-sr-card { .activity-card {
/* background-image: url("../../static/page/act_li_quan.jpg");
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: contain; background-size: contain;
background-position: 150%; background-position: 130%;
background-color: red; */ background-color: #1879ff77;
} }
.ribbon { .ribbon {
@@ -273,4 +293,47 @@ const modifyUserClose = () => {
color: #fff; color: #fff;
font-size: 24rpx; font-size: 24rpx;
} }
.coupon-stock-badge {
display: flex;
align-items: center;
padding: 8rpx 18rpx;
border-radius: 9999rpx;
background: rgba(238, 238, 238, 0.28);
backdrop-filter: blur(2px);
color: #fff;
}
.coupon-stock-dot {
width: 12rpx;
height: 12rpx;
margin-right: 10rpx;
border-radius: 9999rpx;
background: #0094ff;
}
.coupon-stock-label,
.coupon-stock-unit,
.coupon-stock-split,
.coupon-stock-total {
font-size: 24rpx;
line-height: 1;
}
.coupon-stock-value {
margin: 0 4rpx 0 8rpx;
font-size: 24rpx;
font-weight: 700;
line-height: 1;
}
.coupon-stock-total {
margin-left: 4rpx;
opacity: 0.9;
}
.coupon-stock-unit {
margin-left: 6rpx;
opacity: 0.9;
}
</style> </style>
+24 -7
View File
@@ -2,16 +2,15 @@
import { reactive, ref, computed } from "vue" import { reactive, ref, computed } from "vue"
import { onLoad, onShow } from "@dcloudio/uni-app" import { onLoad, onShow } from "@dcloudio/uni-app"
import * as router from "@@/router" import { router } from "@/sheep"
import { SERVICE_IMAGE } from "@@/constant/index"
import { filePath } from "@@/utils/file"
import uqrcode from "@@/utils/uqrcode" import uqrcode from "@@/utils/uqrcode"
import * as CryptoJS from "crypto-js" import * as CryptoJS from "crypto-js"
import { actCancel, actComApi, actCouponApi, applyApi } from "@@/api" import { actCancel, actComApi, actCouponApi, applyApi } from "@@/api"
import * as hooks from "@@/hooks" 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" import zTabs from "@@/components/other/z-tabs/z-tabs.vue"
@@ -20,6 +19,8 @@ import CouponCard from "./components/coupon-card.vue"
const { actCouponTypeEnum } = serviceEnum const { actCouponTypeEnum } = serviceEnum
const { scanCodeAction, processUrlScanResult, processBase64ScanResult } = hooks.useScanCode(false)
const pageRef = ref<any>(null) const pageRef = ref<any>(null)
const showQrCodeDialog = ref(false) const showQrCodeDialog = ref(false)
@@ -72,6 +73,22 @@ const doTab = (e: any) => {
getUserTotal() getUserTotal()
} }
// 去使用
const doUse = async (detail: any) => {
const { type, detail: actDetail } = detail
console.log("去使用:", type, actDetail);
const scanResult = await scanCodeAction()
if (!scanResult) return uni.showToast({ title: "扫码错误", icon: "none" })
// 判断扫码结果
if (scanResult.startsWith("http")) {
// 扫码结果是url链接
processUrlScanResult(scanResult.replace("/#", ""))
} else {
// 扫码结果是base64字符串
processBase64ScanResult(scanResult)
}
}
const doVisibleQrcode = async (detail: any) => { const doVisibleQrcode = async (detail: any) => {
const { type, detail: actDetail } = detail const { type, detail: actDetail } = detail
console.log("生成二维码:", type, actDetail); console.log("生成二维码:", type, actDetail);
@@ -143,7 +160,7 @@ const genVerificationCode = async (params: Record<string, any>) => {
</script> </script>
<template> <template>
<crud-layout title="我参加的活动" mainIsScroll :bg="filePath(SERVICE_IMAGE.blueBg)"> <crud-layout title="我的礼券" mainIsScroll :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))">
<z-paging ref="pageRef" height="100%" :fixed="false" v-model="dynamicPageHook.list" :default-page-size="10" <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" 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" :auto-clean-list-when-reload="false" :auto-scroll-to-top-when-reload="false"
@@ -157,7 +174,7 @@ const genVerificationCode = async (params: Record<string, any>) => {
<view class="h-20rpx" /> <view class="h-20rpx" />
<view v-if="state.tabId == 1"> <view v-if="state.tabId == 1">
<coupon-card v-for="item in dynamicPageHook.list" :key="item.id" :detail="item" <coupon-card v-for="item in dynamicPageHook.list" :key="item.id" :detail="item"
@do-visible-qrcode="doVisibleQrcode" /> @do-visible-qrcode="doVisibleQrcode" @doUse="doUse" />
</view> </view>
<view v-if="state.tabId == 2"> <view v-if="state.tabId == 2">
<cancel-card v-for="item in dynamicPageHook.list" :key="item.id" :detail="item" <cancel-card v-for="item in dynamicPageHook.list" :key="item.id" :detail="item"
+4 -4
View File
@@ -70,12 +70,12 @@ const doCancelRefund = async () => {
</script> </script>
<template> <template>
<crud-layout title="订单详情" mainIsScroll :bg="filePath(SERVICE_IMAGE.blueBg)"> <crud-layout title="订单详情" mainIsScroll :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))">
<view class="flex-row-center m-t-30rpx" <view class="flex-row-center m-t-30rpx"
v-if="state.detail.orderStatus === '5' && ['2', '7'].includes(state.detail.orderType)"> v-if="state.detail.orderStatus === '5' && ['2', '7'].includes(state.detail.orderType)">
<view class="flex-row-center bg-white w-550rpx rounded-16rpx"> <view class="flex-row-center bg-white w-550rpx rounded-16rpx">
<image class="w-500rpx" :src="filePath(state.detail.reserveCode)" mode="widthFix" <image class="w-500rpx" :src="$fileUrl(filePath(state.detail.reserveCode))" mode="widthFix"
@click.stop="hooks.usePreviewImage(filePath(state.detail.reserveCode))" /> @click.stop="hooks.usePreviewImage(filePath(state.detail.reserveCode))" @error="$fileRetry(filePath(state.detail.reserveCode))" />
</view> </view>
</view> </view>
<view class="h-30rpx" /> <view class="h-30rpx" />
@@ -118,7 +118,7 @@ const doCancelRefund = async () => {
<view class="m-t-15rpx"> <view class="m-t-15rpx">
<view class="flex-row items-start justify-between" v-for="(v, i) in state.shopCarList" :key="i"> <view class="flex-row items-start justify-between" v-for="(v, i) in state.shopCarList" :key="i">
<view class="flex-row items-center"> <view class="flex-row items-center">
<image class="w-160rpx h-160rpx rounded-8rpx" :src="filePath(v.productPic)" mode="aspectFill" /> <image class="w-160rpx h-160rpx rounded-8rpx" :src="$fileUrl(filePath(v.productPic))" mode="aspectFill" @error="$fileRetry(filePath(v.productPic))" />
<view class="flex-col m-l-15rpx second-text-26"> <view class="flex-col m-l-15rpx second-text-26">
<view class="sub-text-28">{{ v.productName }}</view> <view class="sub-text-28">{{ v.productName }}</view>
<view class="m-t-5rpx color-red">¥ {{ hooks.useMoney(v.productPrice) }}</view> <view class="m-t-5rpx color-red">¥ {{ hooks.useMoney(v.productPrice) }}</view>
@@ -0,0 +1,193 @@
<script setup lang="ts">
import * as hooks from "@@/hooks"
const emit = defineEmits(["doSelect"])
const props = withDefaults(defineProps<{ selectId?: any, detail: any, disabled?: boolean }>(), {
selectId: null,
detail: {},
disabled: false,
})
const doSelect = () => {
if (props.disabled) return
emit("doSelect", { type: 'ACT_COUPON', detail: props.detail })
}
</script>
<template>
<view class="border-4rpx border-solid border-#efefef coupon-item"
:class="{ 'border-4rpx border-solid border-blue!': selectId == detail.id && !disabled, 'coupon-item-disabled': disabled }"
@tap="doSelect">
<!-- 左侧主体 -->
<view class="coupon-left relative">
<image class="size-60rpx rounded-8rpx flex-shrink-0 m-r-10rpx bg-#f0f0f0" src="/static/icon/act_coupon.png"
mode="aspectFill" />
<view class="absolute top-0rpx right-80rpx">
<!-- 未使用 -->
<view v-if="detail.status == 1" class="iconfont icon-weishiyong absolute font-size-80rpx! color-blue" />
<!-- 已使用 -->
<view v-if="detail.status == 2"
class="iconfont icon-weishiyong1 absolute font-size-80rpx! color-#cccccc" />
<!-- 已过期 -->
<view v-if="detail.status == 3" class="iconfont icon-yiguoqi absolute font-size-80rpx! color-#cccccc" />
<!-- 已核销 -->
<view v-if="detail.status == 4" class="iconfont icon-yiguoqi absolute font-size-80rpx! color-#cccccc" />
</view>
<view class="info-box">
<view class="price-row">
<view v-if="detail.faceValue">
<view v-if="detail.couponType == '2'" class="flex items-center">
<text class="symbol">立减</text>
<text class="amount">{{ detail.faceValue }}%</text>
</view>
<view v-else class="flex items-center">
<text class="symbol">¥</text>
<text class="amount">{{ hooks.useMoney(detail.faceValue) }}</text>
</view>
</view>
<text class="name">{{ detail.couponName }}</text>
</view>
<view v-if="detail.minPoint" class="condition">
满{{ hooks.useMoney(detail.minPoint) }}可用
</view>
</view>
</view>
<!-- 虚线分割与缺口 -->
<view class="split-line">
<view class="circle-top"></view>
<view class="dashed-line"></view>
<view class="circle-bottom"></view>
</view>
<!-- 右侧按钮 -->
<view class="flex-row-center">
<view class="action-btn" :class="{ disabled }">{{ disabled ? "不可用" : "使用" }}</view>
</view>
</view>
</template>
<style scoped lang="scss">
.coupon-item {
display: flex;
align-items: stretch;
margin: 8rpx 0;
border-radius: 16rpx;
overflow: hidden;
position: relative;
}
.coupon-item-disabled {
opacity: 0.6;
}
/* 左侧布局 */
.coupon-left {
flex: 1;
display: flex;
align-items: center;
padding: 12rpx 0 12rpx 12rpx;
background: radial-gradient(circle at right top, transparent 12rpx, #fff 0) top left / 100% 51% no-repeat,
radial-gradient(circle at right bottom, transparent 12rpx, #fff 0) bottom left / 100% 51% no-repeat;
/* 备用背景色,防止渐变失效 */
background-color: #fff;
}
.goods-img {
width: 60rpx;
height: 60rpx;
border-radius: 8rpx;
background-color: #f0f0f0;
margin-right: 20rpx;
flex-shrink: 0;
}
.info-box {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
height: 80rpx;
}
.price-row {
display: flex;
align-items: center;
color: #0094ff;
}
.symbol {
font-size: 28rpx;
font-weight: 600;
}
.amount {
font-size: 28rpx;
font-weight: bold;
margin: 0 8rpx 0 2rpx;
line-height: 1;
}
.name {
font-size: 28rpx;
color: #333;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 200rpx;
}
.condition {
font-size: 24rpx;
color: #0094ff;
opacity: 0.8;
margin-top: 4rpx;
}
.scope-text {
font-size: 22rpx;
color: #999;
margin-top: auto;
}
/* 中间分割线区域 */
.split-line {
width: 30rpx;
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #fff;
}
.dashed-line {
width: 0;
height: 80%;
border-left: 2rpx dashed #0094ff44;
}
/* 右侧按钮区 */
.coupon-right {
width: 140rpx;
display: flex;
align-items: center;
justify-content: center;
background: radial-gradient(circle at left top, transparent 12rpx, #fff 0) top left / 100% 51% no-repeat,
radial-gradient(circle at left bottom, transparent 12rpx, #fff 0) bottom left / 100% 51% no-repeat;
background-color: #fff;
}
.action-btn {
background: #0094ff;
color: #fff;
font-size: 26rpx;
padding: 10rpx 24rpx;
border-radius: 30rpx;
font-weight: 500;
&.disabled {
background: #cccccc;
}
}
</style>
+4 -5
View File
@@ -1,6 +1,6 @@
<template> <template>
<crud-layout title="支付成功" :bg="filePath(SERVICE_IMAGE.blueBg)" ref="crudLayoutRef"> <crud-layout title="支付成功" :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))" ref="crudLayoutRef">
<view class="flex-col-center p-30rpx"> <view class="flex-col-center p-30rpx m-t-30rpx card">
<view class="flex-col-center m-y-50rpx"> <view class="flex-col-center m-y-50rpx">
<svg t="1742978804614" class="icon" viewBox="0 0 1024 1024" version="1.1" <svg t="1742978804614" class="icon" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="1708" width="64" height="64"> xmlns="http://www.w3.org/2000/svg" p-id="1708" width="64" height="64">
@@ -27,9 +27,8 @@
<text class="sub-text-30">扫一扫</text> <text class="sub-text-30">扫一扫</text>
</view> </view>
</view> </view>
</view> <div class="h-100rpx" />
<view class="p-40rpx fixed bottom-30rpx left-0 right-0 font-bold"> <tm-button :block="true" class="w-full p-40rpx" @tap="goBack">完成</tm-button>
<tm-button :block="true" @click="goBack">完成</tm-button>
</view> </view>
</crud-layout> </crud-layout>
</template> </template>
+132 -13
View File
@@ -1,19 +1,32 @@
<template> <template>
<crud-layout :title="scanPayState.mchName" :bg="filePath(SERVICE_IMAGE.blueBg)" ref="crudLayoutRef"> <crud-layout mainIsScroll :title="scanPayState.mchName" :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))" ref="crudLayoutRef">
<view class="h-screen flex flex-col"> <view class="h-screen flex flex-col">
<tm-sheet :margin="[0, 10]" class="flex flex-row"> <tm-sheet :margin="[0, 10]" class="flex flex-col ">
<span class="text-2xl text-[#333] mr-8rpx line-height-normal">¥</span> <view class="flex flex-row items-center">
<span class="text-3xl font-medium text-[#333]">{{ displayAmount }}</span> <span class="text-48rpx text-[#333] mr-8rpx line-height-normal">¥</span>
<span class="text-72rpx font-medium text-[#333]">{{ displayAmount }}</span>
</view>
<view v-if="couponDiscountAmount > 0" class="mt-10rpx flex items-center text-24rpx">
<view class="px-12rpx py-4rpx rounded-full bg-#0094ff1a c-#0094ff font-600">
已优惠 ¥{{ couponDiscountDisplay }}
</view>
<view class="ml-12rpx c-#666">
券后实付
<text class="c-#0094ff font-600">¥{{ couponPayableDisplay }}</text>
</view>
</view>
<view v-else-if="selectCoupon.id && couponUnavailableText" class="mt-10rpx text-24rpx c-#999">
{{ couponUnavailableText }}
</view>
</tm-sheet> </tm-sheet>
<tm-sheet v-if="walletList.length > 0" :margin="[0, 10]" class="flex flex-col"> <tm-sheet v-if="walletList.length > 0" :margin="[0, 10]" class="flex flex-col">
<view class="text-gray-700 font-bold text-28rpx mb-8rpx">选择支付钱包:</view> <view class="text-gray-700 font-bold text-28rpx mb-8rpx">选择支付钱包:</view>
<view v-for="(v, index) in walletList" :key="index" <view v-for="(v, index) in walletList" :key="index"
class="border-6 border-solid border-#efefef my-16rpx duration-200 flex justify-between items-center p-24rpx rounded-2" class="border-6 border-solid border-#efefef my-8rpx duration-200 flex justify-between items-center p-18rpx rounded-2"
:class="{ 'border-6 border-solid border-blue!': selectWallet.id == v.id }" :class="{ 'border-6 border-solid border-blue!': selectWallet.id == v.id }"
@click="selectWallet = v"> @click="selectWallet = v">
<view class="flex items-center"> <view class="flex items-center">
<image class="size-70rpx mr-16rpx" :src="filePath(v.walletTypeIcon)" /> <image class="size-50rpx mr-8rpx" :src="$fileUrl(filePath(v.walletTypeIcon))" @error="$fileRetry(filePath(v.walletTypeIcon))" />
<view class="text-32rpx">{{ v.walletTypeName }}</view> <view class="text-32rpx">{{ v.walletTypeName }}</view>
</view> </view>
<view class="text-28rpx flex items-center"> <view class="text-28rpx flex items-center">
@@ -22,29 +35,36 @@
</view> </view>
</view> </view>
</tm-sheet> </tm-sheet>
<tm-sheet v-if="actCouponList.length > 0" :margin="[0, 10]" class="flex flex-col">
<view class="text-gray-700 font-bold text-28rpx mb-8rpx">选择优惠券:</view>
<select-coupon-card :selectId="selectCoupon.id" v-for="item in actCouponList" :key="item.id"
:detail="item" :disabled="!isCouponAvailable(item)" @do-select="doSelectActCoupon" />
</tm-sheet>
<view class="h-30rpx" />
<tm-button class="mt-30rpx" :block="true" @click="handlePay()" :disabled="loading">支 付</tm-button> <tm-button class="mt-30rpx" :block="true" @click="handlePay()" :disabled="loading">支 付</tm-button>
<view class="h-30rpx" />
<number-keyboard v-if="numberKeyboardShow" v-model="amount" /> <number-keyboard v-if="numberKeyboardShow" v-model="amount" />
</view> </view>
</crud-layout> </crud-layout>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, reactive, onMounted, nextTick } from "vue" import { ref, computed, reactive, onMounted, nextTick, watch } from "vue"
import { onLoad, onShow } from "@dcloudio/uni-app" import { onLoad, onShow } from "@dcloudio/uni-app"
import payApi from "@@/api/pay" import payApi from "@@/api/pay"
import { merchantApi, orderApi, walletApi } from "@/sheep/api" import { merchantApi, orderApi, walletApi, actCouponApi } from "@/sheep/api"
import * as hooks from "@@/hooks" import * as hooks from "@@/hooks"
import * as router from "@@/router" import * as router from "@@/router"
import { filePath } from "@/sheep/utils/file" import { filePath } from "@/sheep/utils/file"
import { displayPrice, storePrice } from "@/sheep/utils/format-money" import { displayPrice, storePrice } from "@/sheep/utils/format-money"
import { usePayStore } from "@/sheep/stores/pay" import { usePayStore } from "@/sheep/stores/pay"
import { isEmpty } from "radash" import { isEmpty } from "radash"
import NumberKeyboard from "@/sheep/components/number-keyboard.vue"
import { SERVICE_IMAGE } from "@@/constant/index" import { SERVICE_IMAGE } from "@@/constant/index"
import NumberKeyboard from "@/sheep/components/number-keyboard.vue"
import SelectCouponCard from "./components/select-coupon-card.vue"
const crudLayoutRef = ref<any>(null) const crudLayoutRef = ref<any>(null)
const loading = ref(false) const loading = ref(false)
const scanPayState = reactive({ const scanPayState = reactive({
@@ -54,7 +74,9 @@ const scanPayState = reactive({
action: "", // action === "harQrCodeScanPay":扫收款机上生成的码支付 action: "", // action === "harQrCodeScanPay":扫收款机上生成的码支付
}) })
const walletList = ref<any>([]) // 钱包列表 const walletList = ref<any>([]) // 钱包列表
const actCouponList = ref<any>([]) // 优惠券列表
const selectWallet = ref<any>({}) // 当前选中的钱包 const selectWallet = ref<any>({}) // 当前选中的钱包
const selectCoupon = ref<any>({}) // 当前选中的优惠券
const orderInfo = ref<any>({}) // 订单信息 const orderInfo = ref<any>({}) // 订单信息
const amount = ref("") // 键盘输入的金额 const amount = ref("") // 键盘输入的金额
const numberKeyboardShow = ref(true) const numberKeyboardShow = ref(true)
@@ -87,6 +109,7 @@ onLoad(async (options: any) => {
onShow(async () => { onShow(async () => {
try { try {
await getWalletList() await getWalletList()
await getActCouponApi()
} finally { } finally {
uni.hideLoading() uni.hideLoading()
} }
@@ -104,6 +127,36 @@ const getMchDetail = async (mchId: string) => {
scanPayState.mchName = data.name scanPayState.mchName = data.name
} }
const doSelectActCoupon = (e: any) => {
const { detail } = e
if (!isCouponAvailable(detail)) {
uni.showToast({ title: `当前优惠券满${priceCentToYuan(detail.minPoint).toFixed(2)}元可用`, icon: "none" })
return
}
// 如果id一样就取消选择
if (selectCoupon.value?.id == detail.id) {
selectCoupon.value = {}
return
}
selectCoupon.value = detail
}
// 获取优惠券
const getActCouponApi = async () => {
try {
const { code, data }: any = await actCouponApi.mchList({
scopeId: scanPayState.mchId
})
if (code !== 0) return
actCouponList.value = data ?? []
if (actCouponList.value.length > 0) {
selectCoupon.value = actCouponList.value.find((item: any) => isCouponAvailable(item)) || {}
}
} catch (error) {
console.log("获取优惠券意外的错误error:", error);
}
}
// 获取钱包列表 // 获取钱包列表
const getWalletList = async () => { const getWalletList = async () => {
const { code, data } = await walletApi.all() const { code, data } = await walletApi.all()
@@ -127,6 +180,71 @@ const displayAmount = computed(() => {
return amount.value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") return amount.value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")
}) })
const amountNumber = computed(() => {
const value = Number(String(amount.value || 0).replace(/,/g, ""))
return Number.isNaN(value) ? 0 : value
})
const priceCentToYuan = (price: any) => {
return Number(displayPrice(Number(price || 0), "", 2))
}
const isCouponAvailable = (coupon: any) => {
if (!coupon) return false
const minPoint = priceCentToYuan(coupon.minPoint)
if (minPoint > 0 && amountNumber.value < minPoint) return false
return true
}
const couponPayableAmount = computed(() => {
if (!selectCoupon.value?.id || amountNumber.value <= 0) return amountNumber.value
const couponType = String(selectCoupon.value.couponType || "")
const minPoint = priceCentToYuan(selectCoupon.value.minPoint)
const useAmount = priceCentToYuan(selectCoupon.value.useAmount)
// 满减券: 不足门槛不优惠, 否则按 useAmount 作为券后金额
if (couponType === "1") {
const faceValue = priceCentToYuan(selectCoupon.value.faceValue)
if (minPoint > 0 && amountNumber.value < minPoint) return amountNumber.value
const payable = useAmount > 0 ? useAmount : amountNumber.value - faceValue
return Math.max(0, Number(payable.toFixed(2)))
}
// 折扣券: 不足门槛不优惠, totalPrice * faceValue / 100
if (couponType === "2") {
const faceValue = Number(selectCoupon.value.faceValue || 0)
if (minPoint > 0 && amountNumber.value < minPoint) return amountNumber.value
const _p = Math.max(0, Number(((amountNumber.value * (100 - faceValue)) / 100).toFixed(2)))
return _p
}
// 立减券: 按 useAmount 作为券后金额
if (couponType === "3") {
const faceValue = priceCentToYuan(selectCoupon.value.faceValue)
const payable = useAmount > 0 ? useAmount : amountNumber.value - faceValue
return Math.max(0, Number(payable.toFixed(2)))
}
return amountNumber.value
})
const couponDiscountAmount = computed(() => {
return Math.max(0, Number((amountNumber.value - couponPayableAmount.value).toFixed(2)))
})
const couponDiscountDisplay = computed(() => couponDiscountAmount.value.toFixed(2))
const couponPayableDisplay = computed(() => couponPayableAmount.value.toFixed(2))
const couponUnavailableText = computed(() => {
if (!selectCoupon.value?.id || amountNumber.value <= 0) return ""
const minPoint = priceCentToYuan(selectCoupon.value.minPoint)
if (minPoint > 0 && amountNumber.value < minPoint) {
return `当前优惠券满${minPoint.toFixed(2)}元可用`
}
return ""
})
watch(amountNumber, () => {
if (selectCoupon.value?.id && !isCouponAvailable(selectCoupon.value)) {
selectCoupon.value = {}
}
})
// 点击支付 // 点击支付
const handlePay = () => { const handlePay = () => {
if (!scanPayState.mchId) return uni.showToast({ title: "扫码参数不存在", icon: "none" }) if (!scanPayState.mchId) return uni.showToast({ title: "扫码参数不存在", icon: "none" })
@@ -158,6 +276,7 @@ const pay = async () => {
merchantId: scanPayState.mchId, merchantId: scanPayState.mchId,
customPrice: storePrice(Number(amount.value)), customPrice: storePrice(Number(amount.value)),
walletId: selectWallet.value.id, walletId: selectWallet.value.id,
actCouponRecordId: selectCoupon.value?.id || "",
}) })
if (code != 0) return uni.showToast({ title: msg || "支付失败", icon: "error" }) if (code != 0) return uni.showToast({ title: msg || "支付失败", icon: "error" })
} }
@@ -166,7 +285,7 @@ const pay = async () => {
usePayStore().setScanPayData({ usePayStore().setScanPayData({
mchId: scanPayState.mchId, mchId: scanPayState.mchId,
mchName: scanPayState.mchName, mchName: scanPayState.mchName,
payPrice: Number(amount.value).toFixed(2), payPrice: couponPayableDisplay.value ?? Number(amount.value).toFixed(2),
}) })
router.jump("/pages/pay/payComplete", {}, 2) router.jump("/pages/pay/payComplete", {}, 2)
} catch (error: any) { } catch (error: any) {
+3 -4
View File
@@ -1,9 +1,9 @@
<template> <template>
<crud-layout title="转账" mainIsScroll :bg="filePath(SERVICE_IMAGE.blueBg)"> <crud-layout title="转账" mainIsScroll :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))">
<view class="h-20rpx"></view> <view class="h-20rpx"></view>
<view class="p-32rpx card flex items-center justify-between"> <view class="p-32rpx card flex items-center justify-between">
<view class="flex items-center"> <view class="flex items-center">
<image class="w-64rpx h-64rpx mr-16rpx" :src="filePath(payStore.sourceWalletData.walletTypeIcon)" /> <image class="w-64rpx h-64rpx mr-16rpx" :src="$fileUrl(filePath(payStore.sourceWalletData.walletTypeIcon))" @error="$fileRetry(filePath(payStore.sourceWalletData.walletTypeIcon))" />
<view>{{ payStore.sourceWalletData.walletTypeName }}</view> <view>{{ payStore.sourceWalletData.walletTypeName }}</view>
</view> </view>
<view class="flex items-center c-#0094ff! font-bold"> <view class="flex items-center c-#0094ff! font-bold">
@@ -40,7 +40,6 @@
<view class="font-bold mt-48rpx"> <view class="font-bold mt-48rpx">
<tm-button block @click="confirmTransfer" v-if="payStore.sourceWalletData.walletTypeId">确定转账</tm-button> <tm-button block @click="confirmTransfer" v-if="payStore.sourceWalletData.walletTypeId">确定转账</tm-button>
</view> </view>
<number-keyboard v-model="amount" /> <number-keyboard v-model="amount" />
</crud-layout> </crud-layout>
@@ -51,7 +50,7 @@
:class="{ 'border-4 border-solid border-blue!': targetWalletType.id == v.walletTypeId }" :class="{ 'border-4 border-solid border-blue!': targetWalletType.id == v.walletTypeId }"
@click="selectTargetWalletType(v)"> @click="selectTargetWalletType(v)">
<view class="flex items-center"> <view class="flex items-center">
<image class="w-70rpx h-70rpx mr-16rpx" :src="filePath(v.walletTypeIcon)" /> <image class="w-70rpx h-70rpx mr-16rpx" :src="$fileUrl(filePath(v.walletTypeIcon))" @error="$fileRetry(filePath(v.walletTypeIcon))" />
<view class="flex flex-col"> <view class="flex flex-col">
<view class="main-text-32">{{ v.walletTypeName }}</view> <view class="main-text-32">{{ v.walletTypeName }}</view>
<view class="text-gray-500 text-sm">{{ v.cardNumber }}</view> <view class="text-gray-500 text-sm">{{ v.cardNumber }}</view>
@@ -1,9 +1,9 @@
<template> <template>
<crud-layout mainIsScroll :bg="filePath(SERVICE_IMAGE.blueBg)"> <crud-layout mainIsScroll :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))">
<view class="h-40rpx"></view> <view class="h-40rpx"></view>
<view class="card flex justify-between items-center mb-16rpx mx-20rpx p-32rpx" v-for="item in categoryList" <view class="card flex justify-between items-center mb-16rpx mx-20rpx p-32rpx" v-for="item in categoryList"
:key="item.id"> :key="item.id">
<image :src="filePath(item.picUrl)" class="size-100rpx mr-32rpx" /> <image :src="$fileUrl(filePath(item.picUrl))" class="size-100rpx mr-32rpx" @error="$fileRetry(filePath(item.picUrl))" />
<view class="flex-1 flex-col"> <view class="flex-1 flex-col">
<view class="flex justify-between items-center"> <view class="flex justify-between items-center">
<text class="sub-text-30">{{ item.name }}</text> <text class="sub-text-30">{{ item.name }}</text>
@@ -2,8 +2,8 @@
<tm-drawer z-index="800" v-model:show="modelValue" showClose title="购物车" size="80%" bgColor="#ECF4F9" <tm-drawer z-index="800" v-model:show="modelValue" showClose title="购物车" size="80%" bgColor="#ECF4F9"
@close="doClose"> @close="doClose">
<view v-if="totPrice > 0" class="flex-row card px-15rpx py-25rpx mb-20rpx" v-for="(v, i) in cartList" :key="i"> <view v-if="totPrice > 0" class="flex-row card px-15rpx py-25rpx mb-20rpx" v-for="(v, i) in cartList" :key="i">
<image class="size-160rpx rounded-12rpx mr-20rpx bg-#eeeeee" :src="filePath(v.productPic)" <image class="size-160rpx rounded-12rpx mr-20rpx bg-#eeeeee" :src="$fileUrl(filePath(v.productPic))"
mode="aspectFill" /> mode="aspectFill" @error="$fileRetry(filePath(v.productPic))" />
<view class="flex-1 overflow-hidden"> <view class="flex-1 overflow-hidden">
<view class="sub-text-30 truncate">{{ v.productName }}</view> <view class="sub-text-30 truncate">{{ v.productName }}</view>
<view class="second-text-26 show-line">单位 {{ v.unit }}</view> <view class="second-text-26 show-line">单位 {{ v.unit }}</view>
@@ -30,7 +30,7 @@
</view> </view>
<view v-else class="flex-col justify-center items-center pt-300rpx"> <view v-else class="flex-col justify-center items-center pt-300rpx">
<image :src="filePath(SERVICE_IMAGE.carEmpty)" class="size-350rpx" /> <image :src="$fileUrl(filePath(SERVICE_IMAGE.carEmpty))" class="size-350rpx" @error="$fileRetry(filePath(SERVICE_IMAGE.carEmpty))" />
<text class="sub-text-32 font-bold">购物车空空如也</text> <text class="sub-text-32 font-bold">购物车空空如也</text>
</view> </view>
@@ -4,9 +4,9 @@
<view class="flex-col card py-20rpx px-30rpx mb-20rpx"> <view class="flex-col card py-20rpx px-30rpx mb-20rpx">
<view class="prefix_title mb-15rpx">商品信息</view> <view class="prefix_title mb-15rpx">商品信息</view>
<view class="flex items-center"> <view class="flex items-center">
<image class="size-160rpx bg-#eeeeee rounded-1 mr-16rpx" :src="filePath(goodsInfo.coverImage)" <image class="size-160rpx bg-#eeeeee rounded-1 mr-16rpx" :src="$fileUrl(filePath(goodsInfo.coverImage))"
mode="aspectFill" mode="aspectFill"
@tap.stop="goodsInfo.coverImage && usePreviewImage(filePath(goodsInfo.coverImage))" /> @tap.stop="goodsInfo.coverImage && usePreviewImage(filePath(goodsInfo.coverImage))" @error="$fileRetry(filePath(goodsInfo.coverImage))" />
<view class="flex-1 overflow-hidden"> <view class="flex-1 overflow-hidden">
<view class="sub-text-32 truncate">{{ goodsInfo.title }}</view> <view class="sub-text-32 truncate">{{ goodsInfo.title }}</view>
<view class="second-text-32 c-red!"> <view class="second-text-32 c-red!">
+5 -5
View File
@@ -1,9 +1,9 @@
<template> <template>
<crud-layout title="共富商城" mainIsScroll :bg="filePath(SERVICE_IMAGE.blueBg)"> <crud-layout title="共富商城" mainIsScroll :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))">
<swiper class="w-full h-500rpx" circular :indicator-dots="true" indicator-active-color="#0779FD" <swiper class="w-full h-500rpx" circular :indicator-dots="true" indicator-active-color="#0779FD"
indicator-color="#ffffff" :autoplay="true"> indicator-color="#ffffff" :autoplay="true">
<swiper-item v-for="(v, i) in goodsDetail.carouselImages?.split(',')" :key="i"> <swiper-item v-for="(v, i) in goodsDetail.carouselImages?.split(',')" :key="i">
<image class="w-full h-full" :src="filePath(v)" mode="aspectFill" /> <image class="w-full h-full" :src="$fileUrl(filePath(v))" mode="aspectFill" @error="$fileRetry(filePath(v))" />
</swiper-item> </swiper-item>
</swiper> </swiper>
@@ -66,7 +66,7 @@
<view class="mt-20rpx card p-20rpx mx-20rpx"> <view class="mt-20rpx card p-20rpx mx-20rpx">
<view class="prefix_title">详情</view> <view class="prefix_title">详情</view>
<view class="w-full mt-20rpx overflow-hidden text-26rpx!" v-html="description"></view> <view class="w-full mt-20rpx overflow-hidden text-26rpx!" v-html="$fileHtml(description)"></view>
</view> </view>
<view class="h-30rpx" /> <view class="h-30rpx" />
@@ -85,7 +85,7 @@
import { onLoad } from "@dcloudio/uni-app" import { onLoad } from "@dcloudio/uni-app"
import { computed, reactive } from "vue" import { computed, reactive } from "vue"
import { prosperityShopApi } from "@/sheep/api" import { prosperityShopApi } from "@/sheep/api"
import { filePath } from "@/sheep/utils/file" import { filePath, parseHtml } from "@/sheep/utils/file"
import { displayPrice } from "@/sheep/utils/format-money" import { displayPrice } from "@/sheep/utils/format-money"
import { SERVICE_IMAGE } from "@@/constant/index" import { SERVICE_IMAGE } from "@@/constant/index"
@@ -105,7 +105,7 @@ const state = reactive<any>({
}) })
const goodsDetail = computed(() => state.goodsDetail) const goodsDetail = computed(() => state.goodsDetail)
const description = computed(() => { const description = computed(() => {
return goodsDetail.value.description?.replaceAll(`<img`, `<img style="width: 100%;"`) return parseHtml(goodsDetail.value.description || '')
}) })
onLoad(async (option: any) => { onLoad(async (option: any) => {
+1 -1
View File
@@ -1,5 +1,5 @@
<template> <template>
<crud-layout title="" mainIsScroll :bg="filePath(SERVICE_IMAGE.blueBg)"> <crud-layout title="" mainIsScroll :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))">
<view class="h-30rpx" /> <view class="h-30rpx" />
<tm-form @submit="onsubmit" v-model="form" :rules="rules" direction="horizontal"> <tm-form @submit="onsubmit" v-model="form" :rules="rules" direction="horizontal">
<view class="card p-20rpx mb-20rpx"> <view class="card p-20rpx mb-20rpx">
+3 -3
View File
@@ -1,5 +1,5 @@
<template> <template>
<crud-layout mainIsScroll :bg="filePath(SERVICE_IMAGE.blueBg)"> <crud-layout mainIsScroll :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))">
<z-paging ref="pageRef" height="100%" :fixed="false" v-model="pageHook.list" :default-page-size="10" <z-paging ref="pageRef" height="100%" :fixed="false" v-model="pageHook.list" :default-page-size="10"
safe-area-inset-bottom :loading-more-enabled="true" :show-scrollbar="false" @query="query" 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" @scroll="pageHook.scroll"> :auto-clean-list-when-reload="false" :auto-scroll-to-top-when-reload="false" @scroll="pageHook.scroll">
@@ -27,8 +27,8 @@
<view v-for="v in pageHook.list" :key="v.id" class="card mb-16rpx px-32rpx py-16rpx"> <view v-for="v in pageHook.list" :key="v.id" class="card mb-16rpx px-32rpx py-16rpx">
<view class="flex"> <view class="flex">
<image :src="filePath(v.coverImage)" class="size-180rpx rounded-12rpx bg-#eeeeee" mode="aspectFill" <image :src="$fileUrl(filePath(v.coverImage))" class="size-180rpx rounded-12rpx bg-#eeeeee" mode="aspectFill"
@click.stop="hooks.usePreviewImage(filePath(v.coverImage))" /> @click.stop="hooks.usePreviewImage(filePath(v.coverImage))" @error="$fileRetry(filePath(v.coverImage))" />
<view class="flex-1 ml-16rpx overflow-hidden"> <view class="flex-1 ml-16rpx overflow-hidden">
<view class="truncate sub-text-32">{{ v.title }}</view> <view class="truncate sub-text-32">{{ v.title }}</view>
<view class="sub-text-24 my-6rpx"> <view class="sub-text-24 my-6rpx">
+2 -2
View File
@@ -1,5 +1,5 @@
<template> <template>
<crud-layout title="共富商城" mainIsScroll :bg="filePath(SERVICE_IMAGE.blueBg)"> <crud-layout title="共富商城" mainIsScroll :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))">
<z-paging ref="pageRef" height="100%" :fixed="false" v-model="pageHook.list" :default-page-size="10" <z-paging ref="pageRef" height="100%" :fixed="false" v-model="pageHook.list" :default-page-size="10"
safe-area-inset-bottom :loading-more-enabled="true" :show-scrollbar="false" @query="query" 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" @scroll="pageHook.scroll"> :auto-clean-list-when-reload="false" :auto-scroll-to-top-when-reload="false" @scroll="pageHook.scroll">
@@ -20,7 +20,7 @@
<view v-for="v in pageHook.list" :key="v.id" class="card flex mb-16rpx p-16rpx" <view v-for="v in pageHook.list" :key="v.id" class="card flex mb-16rpx p-16rpx"
@click="doGoodsDetail(v.id)"> @click="doGoodsDetail(v.id)">
<image :src="filePath(v.coverImage)" class="size-160rpx rounded-12rpx bg-#eeeeee" mode="aspectFill" /> <image :src="$fileUrl(filePath(v.coverImage))" class="size-160rpx rounded-12rpx bg-#eeeeee" mode="aspectFill" @error="$fileRetry(filePath(v.coverImage))" />
<view class="flex-1 ml-16rpx overflow-hidden"> <view class="flex-1 ml-16rpx overflow-hidden">
<view class="truncate sub-text-30">{{ v.title }}</view> <view class="truncate sub-text-30">{{ v.title }}</view>
<view> <view>
+6 -6
View File
@@ -1,5 +1,5 @@
<template> <template>
<crud-layout title="结算" mainIsScroll :bg="filePath(SERVICE_IMAGE.blueBg)"> <crud-layout title="结算" mainIsScroll :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))">
<view class="h-20rpx" /> <view class="h-20rpx" />
<view class="bg-#FFA61Ecc color-#fff px-16rpx py-8rpx text-28rpx rounded-2">温馨提示:当天的订单预计明天开始配送</view> <view class="bg-#FFA61Ecc color-#fff px-16rpx py-8rpx text-28rpx rounded-2">温馨提示:当天的订单预计明天开始配送</view>
@@ -9,7 +9,7 @@
<view class="prefix_title text-36rpx mb-16rpx">自提点</view> <view class="prefix_title text-36rpx mb-16rpx">自提点</view>
<view class="flex-row items-center justify-between" @click="onPickupPoint"> <view class="flex-row items-center justify-between" @click="onPickupPoint">
<view v-if="curPickup.id" class="sub-text-30 mb-16rpx flex items-center"> <view v-if="curPickup.id" class="sub-text-30 mb-16rpx flex items-center">
<image class="size-80rpx rounded-1 mr-16rpx" :src="filePath(curPickup.logo)" /> <image class="size-80rpx rounded-1 mr-16rpx" :src="$fileUrl(filePath(curPickup.logo))" @error="$fileRetry(filePath(curPickup.logo))" />
<view> <view>
<view>{{ curPickup.name }}</view> <view>{{ curPickup.name }}</view>
<view class="sub-text-26">{{ curPickup.phone }}</view> <view class="sub-text-26">{{ curPickup.phone }}</view>
@@ -29,8 +29,8 @@
<view class="card p-20rpx mb-20rpx"> <view class="card p-20rpx mb-20rpx">
<view class="prefix_title text-36rpx mb-16rpx">我的购买</view> <view class="prefix_title text-36rpx mb-16rpx">我的购买</view>
<view v-if="totalCount > 0" class="flex-row mb-20rpx" v-for="(v, i) in cartList" :key="i"> <view v-if="totalCount > 0" class="flex-row mb-20rpx" v-for="(v, i) in cartList" :key="i">
<image class="size-140rpx rounded-12rpx mr-20rpx bg-#eeeeee" :src="filePath(v.productPic)" <image class="size-140rpx rounded-12rpx mr-20rpx bg-#eeeeee" :src="$fileUrl(filePath(v.productPic))"
mode="aspectFill" /> mode="aspectFill" @error="$fileRetry(filePath(v.productPic))" />
<view class="flex-1 overflow-hidden"> <view class="flex-1 overflow-hidden">
<view class="sub-text-30 truncate">{{ v.productName }}</view> <view class="sub-text-30 truncate">{{ v.productName }}</view>
<view class="flex justify-between items-center"> <view class="flex justify-between items-center">
@@ -69,8 +69,8 @@
<tm-radio v-for="(v, i) in pageHook.list" :key="i" class="w-full h-full px-16rpx" :value="v.id"> <tm-radio v-for="(v, i) in pageHook.list" :key="i" class="w-full h-full px-16rpx" :value="v.id">
<template v-slot:label="checked"> <template v-slot:label="checked">
<view class="flex items-center mb-8rpx"> <view class="flex items-center mb-8rpx">
<image class="size-140rpx rounded-12rpx mr-16rpx" :src="filePath(v.logo)" <image class="size-140rpx rounded-12rpx mr-16rpx" :src="$fileUrl(filePath(v.logo))"
mode="aspectFill" /> mode="aspectFill" @error="$fileRetry(filePath(v.logo))" />
<view class="flex-1 overflow-hidden"> <view class="flex-1 overflow-hidden">
<view class="sub-text-30 truncate">{{ v.name }}</view> <view class="sub-text-30 truncate">{{ v.name }}</view>
<view class="sub-text-26">{{ v.phone }}</view> <view class="sub-text-26">{{ v.phone }}</view>
+4 -4
View File
@@ -1,7 +1,7 @@
<template> <template>
<crud-layout title="共富商城" mainIsScroll :bg="filePath(SERVICE_IMAGE.blueBg)" @scroll="onScroll"> <crud-layout title="共富商城" mainIsScroll :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))" @scroll="onScroll">
<view class="w-full h-400rpx relative"> <view class="w-full h-400rpx relative">
<image :src="filePath(mchDetail.images)" class="w-full h-full bg-#eeeeee" mode="aspectFill" /> <image :src="$fileUrl(filePath(mchDetail.images))" class="w-full h-full bg-#eeeeee" mode="aspectFill" @error="$fileRetry(filePath(mchDetail.images))" />
<view class="absolute w-full h-full top-0 left-0" :style="filterStyle"></view> <view class="absolute w-full h-full top-0 left-0" :style="filterStyle"></view>
</view> </view>
<view v-if="visible" <view v-if="visible"
@@ -37,8 +37,8 @@
style="height: calc(100vh - 420rpx)"> style="height: calc(100vh - 420rpx)">
<view v-for="v in state.goodsList" :key="v.id" class="card flex mx-15rpx mb-16rpx p-8rpx" <view v-for="v in state.goodsList" :key="v.id" class="card flex mx-15rpx mb-16rpx p-8rpx"
@click="doGoodsDetail(v.id)"> @click="doGoodsDetail(v.id)">
<image :src="filePath(v.coverImage)" class="size-170rpx rounded-12rpx bg-#eeeeee" <image :src="$fileUrl(filePath(v.coverImage))" class="size-170rpx rounded-12rpx bg-#eeeeee"
mode="aspectFill" /> mode="aspectFill" @error="$fileRetry(filePath(v.coverImage))" />
<view class="flex-1 ml-16rpx overflow-hidden"> <view class="flex-1 ml-16rpx overflow-hidden">
<view class="truncate sub-text-30">{{ v.title }}</view> <view class="truncate sub-text-30">{{ v.title }}</view>
<view> <view>
+43 -8
View File
@@ -2,7 +2,7 @@
import { ref, reactive, computed, nextTick, onMounted } from "vue" import { ref, reactive, computed, nextTick, onMounted } from "vue"
import { onHide, onShow } from "@dcloudio/uni-app" import { onHide, onShow } from "@dcloudio/uni-app"
import { router, constant } from "@@/index" import { router } from "@@/index"
import { noticeApi, appApi } from "@@/api/index" import { noticeApi, appApi } from "@@/api/index"
import { getAppInfo } from "@@/utils/app" import { getAppInfo } from "@@/utils/app"
import { SERVICE_IMAGE } from "@@/constant/index" import { SERVICE_IMAGE } from "@@/constant/index"
@@ -48,7 +48,9 @@ onShow(() => {
if (!ceheckLogin()) { if (!ceheckLogin()) {
uni.showToast({ title: "登录已过期,请重新登录", icon: "none" }) uni.showToast({ title: "登录已过期,请重新登录", icon: "none" })
uni.navigateTo({ url: "/pages/root/login" }) uni.navigateTo({ url: "/pages/root/login" })
return
} }
getSwiperList()
getNoticeList() getNoticeList()
getAppMenuList() getAppMenuList()
}) })
@@ -68,10 +70,32 @@ const doMenu = async (v: any) => {
router.jump("/pages/third-app/happyMall") router.jump("/pages/third-app/happyMall")
return return
} }
// 是否是具体页面
const routePath = v.page.split("?")[0]
if (v.page.startsWith("/") && __ROUTES__.some((route) => route.path === routePath)) {
// 直接页面入口也要先组装所属业务 Tabbar,再进入目标页面。
await tabbarStore.componetTabbarList(v.page)
router.jump(v.page)
return
}
await tabbarStore.componetTabbarList(v.page) await tabbarStore.componetTabbarList(v.page)
tabbarStore.jumpFirstTabbar() 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 () => { const getNoticeList = async () => {
try { try {
@@ -79,6 +103,7 @@ const getNoticeList = async () => {
current: 1, current: 1,
size: 9 size: 9
}) })
console.log("获取通知公告列表,code:", code);
if (code != 0) { if (code != 0) {
throw new Error() throw new Error()
} }
@@ -97,7 +122,10 @@ const getAppMenuList = async () => {
platformEnum: platform platformEnum: platform
} }
const { data, code }: any = await appApi.appList(params) 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() throw new Error()
} }
state.appMenuList = (data || []).filter(Boolean) state.appMenuList = (data || []).filter(Boolean)
@@ -105,15 +133,21 @@ const getAppMenuList = async () => {
console.log("获取应用菜单列表意外的错误error:", error); console.log("获取应用菜单列表意外的错误error:", error);
} }
} }
const doSwiper = async (v: any) => {
if (!v) return
const target = (v.jumpValue || "").replace(/\s+/g, "")
uni.navigateTo({ url: target })
}
</script> </script>
<template> <template>
<crud-layout title="智慧后勤系统" mainIsScroll :bg="filePath(SERVICE_IMAGE.blueBg)" ref="crudLayoutRef"> <crud-layout title="智慧后勤系统" mainIsScroll :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))" ref="crudLayoutRef">
<view class="h-20rpx" /> <view class="h-20rpx" />
<swiper class="w-full h-280rpx rounded-16rpx overflow-hidden" circular :indicator-dots="true" <swiper class="w-full h-280rpx rounded-16rpx overflow-hidden" circular :indicator-dots="true"
indicator-active-color="#0779FD" indicator-color="#ffffff" :autoplay="true"> indicator-active-color="#0779FD" indicator-color="#ffffff" :autoplay="true">
<swiper-item v-for="(v, i) in state?.swiperList" :key="i"> <swiper-item v-for="(v, i) in state?.swiperList" :key="v.id" @tap="doSwiper(v)">
<image class="w-full h-full" :src="v.image" mode="aspectFill" /> <image class="w-full h-full" :src="$fileUrl(v.imageUrl)" mode="aspectFill" @error="$fileRetry(v.imageUrl)" />
</swiper-item> </swiper-item>
</swiper> </swiper>
<view class="h-20rpx" /> <view class="h-20rpx" />
@@ -135,7 +169,8 @@ const getAppMenuList = async () => {
<view class="w-full"> <view class="w-full">
<view <view
class="h-100rpx flex justify-between items-center p-x-16rpx p-y-20rpx border-b-1 border-color-#f0f0f0" 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="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 class="w-250rpx second-text-24 m-l-10rpx">{{ v.createTime }}</view>
</view> </view>
@@ -159,7 +194,7 @@ const getAppMenuList = async () => {
<view class="flex flex-col items-center justify-center" v-for="(v, i) in appMenuListSafe" :key="i" <view class="flex flex-col items-center justify-center" v-for="(v, i) in appMenuListSafe" :key="i"
@tap="doMenu(v)"> @tap="doMenu(v)">
<view class="flex-col-center"> <view class="flex-col-center">
<image class="size-100rpx" :src="filePath(v?.icon)" mode="aspectFill" /> <image class="size-100rpx" :src="$fileUrl(filePath(v?.icon))" mode="aspectFill" @error="$fileRetry(filePath(v?.icon))" />
<view class="m-t-10rpx sub-text-26">{{ v?.title || "" }}</view> <view class="m-t-10rpx sub-text-26">{{ v?.title || "" }}</view>
</view> </view>
</view> </view>
@@ -169,7 +204,7 @@ const getAppMenuList = async () => {
<empty text="暂无App" /> <empty text="暂无App" />
</view> </view>
</view> </view>
<view class="h-30rpx" /> <view class="h-80rpx" />
</crud-layout> </crud-layout>
</template> </template>
+10 -3
View File
@@ -97,12 +97,12 @@ const affirmConfirm = () => {
</script> </script>
<template> <template>
<crud-layout title="登录" :bg="filePath(SERVICE_IMAGE.blueBg)"> <crud-layout title="登录" :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))">
<view class="h-30rpx" /> <view class="h-30rpx" />
<view class="flex-col-center m-t-160rpx"> <view class="flex-col-center m-t-160rpx">
<view class="size-220rpx bg-white rounded-full flex-row-center"> <view class="size-220rpx bg-white rounded-full flex-row-center">
<image class="size-200rpx rounded-full" :src="filePath(SERVICE_IMAGE.logo)" mode="aspectFit" /> <image class="size-200rpx rounded-full" :src="$fileUrl(filePath(SERVICE_IMAGE.logo))" mode="aspectFit" @error="$fileRetry(filePath(SERVICE_IMAGE.logo))" />
</view> </view>
<view class="m-t-20rpx sub-text-28">浦江县机关事务智慧后勤一体化平台</view> <view class="m-t-20rpx sub-text-28">浦江县机关事务智慧后勤一体化平台</view>
</view> </view>
@@ -120,7 +120,14 @@ const affirmConfirm = () => {
</view> </view>
</view> </view>
<affirm-check v-model="state.isAgree" class="absolute bottom-30rpx" /> <view class="absolute bottom-60rpx">
<view class="sub-text-28 color-red! indent-2em">
<text>小程序只提供给有"浦江县机关事务智慧后勤一体化平台"账号的用户使用,其余用户不能使用该小程序</text>
</view>
<view class="h-10rpx" />
<affirm-check v-model="state.isAgree" />
</view>
<affirm-modal ref="affirmModalRef" @confirm="affirmConfirm" /> <affirm-modal ref="affirmModalRef" @confirm="affirmConfirm" />
<service-text ref="serviceTextRef" :showPhoneText="false" /> <service-text ref="serviceTextRef" :showPhoneText="false" />
<tm-modal :show="state.loginErrorVisible" title="登录错误" cancelText="取消" confirmText="联系客服" height="auto" <tm-modal :show="state.loginErrorVisible" title="登录错误" cancelText="取消" confirmText="联系客服" height="auto"
+10 -3
View File
@@ -63,7 +63,8 @@ const doSubmit = async () => {
uni.setStorageSync(tokenKey, { uni.setStorageSync(tokenKey, {
accessToken: res.data.access_token, accessToken: res.data.access_token,
refreshToken: res.data.refresh_token, refreshToken: res.data.refresh_token,
accessTokenExpiresIn: new Date(res.data.expires_in + Date.now()).toISOString(), // OAuth expires_in 的单位是秒,Date.now() 的单位是毫秒,必须先乘 1000。
accessTokenExpiresIn: new Date(Date.now() + Number(res.data.expires_in) * 1000).toISOString(),
refreshTokenExpiresIn: "", refreshTokenExpiresIn: "",
} as GlobalSetting.TokenStorage) } as GlobalSetting.TokenStorage)
@@ -111,7 +112,7 @@ const affirmModalConfirm = () => {
</script> </script>
<template> <template>
<crud-layout title="账号密码登录" :bg="filePath(SERVICE_IMAGE.blueBg)"> <crud-layout title="账号密码登录" :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))">
<view class="h-30rpx" /> <view class="h-30rpx" />
<view class="card p-x-30rpx p-y-40rpx"> <view class="card p-x-30rpx p-y-40rpx">
<tm-form ref="applyFormRef" v-model="applyForm" :rules="applyFormRules" direction="vertical"> <tm-form ref="applyFormRef" v-model="applyForm" :rules="applyFormRules" direction="vertical">
@@ -133,7 +134,13 @@ const affirmModalConfirm = () => {
<text></text> <text></text>
<text class="color-blue font-size-sm" @click="router.jump('/pages/root/login')">手机号一键登录</text> <text class="color-blue font-size-sm" @click="router.jump('/pages/root/login')">手机号一键登录</text>
</view> </view>
<affirm-check v-model="state.isAgree" class="absolute bottom-30rpx" /> <view class="absolute bottom-60rpx">
<view class="sub-text-28 color-red! indent-2em">
<text>小程序只提供给有"浦江县机关事务智慧后勤一体化平台"账号的用户使用,其余用户不能使用该小程序</text>
</view>
<view class="h-10rpx" />
<affirm-check v-model="state.isAgree" />
</view>
<affirm-modal ref="affirmModalRef" @confirm="affirmModalConfirm" /> <affirm-modal ref="affirmModalRef" @confirm="affirmModalConfirm" />
</crud-layout> </crud-layout>
</template> </template>
+40 -9
View File
@@ -1,24 +1,55 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive } from "vue" import { reactive } from "vue"
import { onLoad, onShow } from "@dcloudio/uni-app" import { onLoad } from "@dcloudio/uni-app"
const state = reactive({ const state = reactive({
url: "" url: "",
}) })
onLoad((options: any) => { onLoad((options: any) => {
if (options.url) { if (options.url) {
state.url = decodeURIComponent(options.url) state.url = decodeURIComponent(options.url)
// 只记录地址结构,不输出 token,便于定位微信 WebView 域名校验问题。
try {
const url = new URL(state.url)
console.log("[web-view] 加载地址:", {
origin: url.origin,
pathname: url.pathname,
queryKeys: Array.from(url.searchParams.keys()),
})
} catch (error) {
console.error("[web-view] 地址解析失败:", error)
}
} }
}) })
const handleWebViewLoad = (event: any) => {
console.log("[web-view] 加载完成:", event?.detail?.src || state.url)
}
const handleWebViewError = (event: any) => {
console.error("[web-view] 加载失败:", {
url: state.url,
detail: event?.detail,
})
uni.showToast({ title: "第三方页面加载失败,请检查业务域名配置", icon: "none" })
}
</script> </script>
<template> <template>
<crud-layout mainIsScroll tabBarType="just-flot-back"> <crud-layout mainIsScroll tabBarType="just-flot-back">
<web-view :update-title="false" :fullscreen="false" :webview-styles="{ <web-view class="mall-web-view" :update-title="false" :fullscreen="false" @load="handleWebViewLoad"
width: '100%', @error="handleWebViewError" :webview-styles="{
height: '100%', width: '100%',
backgroundColor: '#fff' height: '100%',
}" :src="state.url" /> backgroundColor: '#fff'
}" :src="state.url" />
</crud-layout> </crud-layout>
</template> </template>
<style lang="scss" scoped>
.mall-web-view {
width: 100%;
height: 100%;
}
</style>
+1 -1
View File
@@ -1,5 +1,5 @@
<template> <template>
<crud-layout :bg="filePath(SERVICE_IMAGE.blueBg)" ref="crudLayoutRef"> <crud-layout :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))" ref="crudLayoutRef">
<z-paging ref="pageRef" height="100%" :fixed="false" v-model="pageHook.list" :default-page-size="10" <z-paging ref="pageRef" height="100%" :fixed="false" v-model="pageHook.list" :default-page-size="10"
safe-area-inset-bottom :loading-more-enabled="true" :show-scrollbar="false" @query="query" 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" @scroll="pageHook.scroll"> :auto-clean-list-when-reload="false" :auto-scroll-to-top-when-reload="false" @scroll="pageHook.scroll">
+5 -2
View File
@@ -22,6 +22,9 @@ onLoad(() => {
}) })
onShow(async () => { onShow(async () => {
console.log("systemInfoSync:", uni.getSystemInfoSync());
console.log("menuButtonBoundingClientRect:", uni.getMenuButtonBoundingClientRect());
if (pageRef.value) { if (pageRef.value) {
pageRef.value.reload(false) pageRef.value.reload(false)
} }
@@ -52,13 +55,13 @@ const toDetail = async (v: any) => {
</script> </script>
<template> <template>
<crud-layout title="食堂" :bg="filePath(SERVICE_IMAGE.blueBg)" ref="crudLayoutRef"> <crud-layout title="食堂" :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))" ref="crudLayoutRef">
<z-paging ref="pageRef" height="100%" :fixed="false" v-model="pageHook.list" :default-page-size="10" <z-paging ref="pageRef" height="100%" :fixed="false" v-model="pageHook.list" :default-page-size="10"
safe-area-inset-bottom :loading-more-enabled="true" :show-scrollbar="false" @query="query" 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" @scroll="pageHook.scroll"> :auto-clean-list-when-reload="false" :auto-scroll-to-top-when-reload="false" @scroll="pageHook.scroll">
<view class="h-20rpx" /> <view class="h-20rpx" />
<view class="flex-row card p-20rpx m-b-20rpx" v-for="(v, i) in pageHook.list" :key="i" @tap="toDetail(v)"> <view class="flex-row card p-20rpx m-b-20rpx" v-for="(v, i) in pageHook.list" :key="i" @tap="toDetail(v)">
<image class="w-180rpx h-180rpx rounded-12rpx" :src="filePath(v.logoPic)" mode="aspectFill" /> <image class="w-180rpx h-180rpx rounded-12rpx" :src="$fileUrl(filePath(v.logoPic))" mode="aspectFill" @error="$fileRetry(filePath(v.logoPic))" />
<view class="flex-1 flex-col m-l-20rpx"> <view class="flex-1 flex-col m-l-20rpx">
<view class="flex-row items-center justify-between"> <view class="flex-row items-center justify-between">
<view class="flex-row-center sub-text-34">{{ v.name }}</view> <view class="flex-row-center sub-text-34">{{ v.name }}</view>
+1 -1
View File
@@ -1,5 +1,5 @@
<template> <template>
<crud-layout title="喜好分析" :bg="filePath(SERVICE_IMAGE.blueBg)" ref="crudLayoutRef"> <crud-layout title="喜好分析" :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))" ref="crudLayoutRef">
<z-paging ref="pageRef" height="100%" :fixed="false" safe-area-inset-bottom :loading-more-enabled="true" <z-paging ref="pageRef" height="100%" :fixed="false" safe-area-inset-bottom :loading-more-enabled="true"
:show-scrollbar="false" @query="query" :auto-clean-list-when-reload="false" :show-scrollbar="false" @query="query" :auto-clean-list-when-reload="false"
:auto-scroll-to-top-when-reload="false" hide-empty-view> :auto-scroll-to-top-when-reload="false" hide-empty-view>
+2 -2
View File
@@ -70,7 +70,7 @@ const toDetail = async (v: any) => {
</script> </script>
<template> <template>
<crud-layout title="合作商家" :bg="filePath(SERVICE_IMAGE.blueBg)" ref="crudLayoutRef"> <crud-layout title="合作商家" :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))" ref="crudLayoutRef">
<z-paging ref="pageRef" height="100%" :fixed="false" v-model="pageHook.list" :default-page-size="10" <z-paging ref="pageRef" height="100%" :fixed="false" v-model="pageHook.list" :default-page-size="10"
safe-area-inset-bottom :loading-more-enabled="true" :show-scrollbar="false" @query="query" 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" @scroll="pageHook.scroll"> :auto-clean-list-when-reload="false" :auto-scroll-to-top-when-reload="false" @scroll="pageHook.scroll">
@@ -93,7 +93,7 @@ const toDetail = async (v: any) => {
</template> </template>
<view class="flex-row card p-20rpx m-b-20rpx" v-for="(v, i) in pageHook.list" :key="i" @tap="toDetail(v)"> <view class="flex-row card p-20rpx m-b-20rpx" v-for="(v, i) in pageHook.list" :key="i" @tap="toDetail(v)">
<image class="w-180rpx h-180rpx rounded-12rpx my-auto" :src="filePath(v.logoPic)" mode="aspectFill" /> <image class="w-180rpx h-180rpx rounded-12rpx my-auto" :src="$fileUrl(filePath(v.logoPic))" mode="aspectFill" @error="$fileRetry(filePath(v.logoPic))" />
<view class="flex-1 flex-col m-l-20rpx"> <view class="flex-1 flex-col m-l-20rpx">
<view class="sub-text-34"> <view class="sub-text-34">
<text class="mr-16rpx">{{ v.name }}</text> <text class="mr-16rpx">{{ v.name }}</text>
+26 -26
View File
@@ -1,5 +1,5 @@
<template> <template>
<crud-layout :bg="filePath(SERVICE_IMAGE.blueBg)" ref="crudLayoutRef"> <crud-layout :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))" ref="crudLayoutRef">
<div class="integrated-title-card"> <div class="integrated-title-card">
<div class="integrated-title">综合服务</div> <div class="integrated-title">综合服务</div>
</div> </div>
@@ -8,7 +8,7 @@
<div class="service-grid"> <div class="service-grid">
<div v-for="item in services" :key="item.name" class="service-item" @click="goPage(item)"> <div v-for="item in services" :key="item.name" class="service-item" @click="goPage(item)">
<div class="service-icon-wrapper"> <div class="service-icon-wrapper">
<image :src="item.icon" class="service-icon" /> <image :src="$fileUrl(item.icon)" class="service-icon" @error="$fileRetry(item.icon)" />
<view v-if="item.name === 'todo' && todoCount > 0" class="badge"> <view v-if="item.name === 'todo' && todoCount > 0" class="badge">
{{ todoCount > 99 ? "99+" : todoCount }} {{ todoCount > 99 ? "99+" : todoCount }}
</view> </view>
@@ -42,36 +42,36 @@ const todoCount = ref(0)
const crudLayoutRef = ref<any>(null) const crudLayoutRef = ref<any>(null)
const services = [ const services = [
{ // {
name: "todo", // name: "todo",
label: "工作待办", // label: "工作待办",
icon: "/static/icon/todo.png", // icon: "/static/icon/todo.png",
url: "/pages/integrated/todo/index", // url: "/pages/integrated/todo/index",
}, // },
{ // {
name: "meeting-booking", // name: "meeting-booking",
label: "会议预定", // label: "会议预定",
icon: "/static/icon/meeting-booking.png", // icon: "/static/icon/meeting-booking.png",
url: "/pages/integrated/meeting/index", // url: "/pages/integrated/meeting/index",
}, // },
{ // {
name: "lost-and-found", // name: "lost-and-found",
label: "失物招领", // label: "失物招领",
icon: "/static/icon/service_1.png", // icon: "/static/icon/service_1.png",
url: "/pages/integrated/lostAndFound/index", // url: "/pages/integrated/lostAndFound/index",
}, // },
{ {
name: "feedback", name: "feedback",
label: "意见反馈", label: "意见反馈",
icon: "/static/icon/feedback.png", icon: "/static/icon/feedback.png",
url: "/pages/integrated/feedback/index", url: "/pages/integrated/feedback/index",
}, },
{ // {
name: "repair-report", // name: "repair-report",
label: "物业维修", // label: "物业维修",
icon: "/static/icon/service_2.png", // icon: "/static/icon/service_2.png",
url: "/pages/integrated/repair/index", // url: "/pages/integrated/repair/index",
}, // },
{ {
name: "notice", name: "notice",
label: "通知公告", label: "通知公告",
+19 -7
View File
@@ -1,10 +1,10 @@
<template> <template>
<crud-layout title="我的" mainIsScroll :bg="filePath(SERVICE_IMAGE.blueBg)" ref="crudLayoutRef"> <crud-layout title="我的" mainIsScroll :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))" ref="crudLayoutRef">
<template #main-top> <template #main-top>
<view class="flex items-center py-24rpx px-40rpx" @tap="doUserArea"> <view class="flex items-center py-24rpx px-40rpx" @tap="doUserArea">
<view class="flex items-center" v-if="state.userInfo?.sysUser?.userId"> <view class="flex items-center" v-if="state.userInfo?.sysUser?.userId">
<image v-if="state.userInfo?.sysUser?.userId" class="size-100rpx rounded-full" <image v-if="state.userInfo?.sysUser?.userId" class="size-100rpx rounded-full"
:src="filePath(state.userInfo.sysUser.avatar)" mode="aspectFill" /> :src="$fileUrl(filePath(state.userInfo.sysUser.avatar))" mode="aspectFill" @error="$fileRetry(filePath(state.userInfo.sysUser.avatar))" />
<image v-else class="w-100rpx h-100rpx rounded-full" src="/static/icon/default-avatar.png" <image v-else class="w-100rpx h-100rpx rounded-full" src="/static/icon/default-avatar.png"
mode="aspectFill" /> mode="aspectFill" />
<view class="ml-30rpx"> <view class="ml-30rpx">
@@ -32,7 +32,7 @@
<view class="service_item flex items-center justify-between" v-for="(v, i) in state.menuList" :key="i" <view class="service_item flex items-center justify-between" v-for="(v, i) in state.menuList" :key="i"
@tap="onTabMenu(v)"> @tap="onTabMenu(v)">
<view class="flex-row-center"> <view class="flex-row-center">
<image class="size-40rpx" :src="v.icon" mode="aspectFill" /> <image class="size-40rpx" :src="$fileUrl(v.icon)" mode="aspectFill" @error="$fileRetry(v.icon)" />
<text class="sub-text-28 m-l-20rpx">{{ v.text }}</text> <text class="sub-text-28 m-l-20rpx">{{ v.text }}</text>
</view> </view>
<view class="iconfont icon-youjiantou" /> <view class="iconfont icon-youjiantou" />
@@ -82,6 +82,7 @@ import { isEmpty } from "radash"
import dayjs from "dayjs" import dayjs from "dayjs"
import { filePath } from "@@/utils/file" import { filePath } from "@@/utils/file"
import { getAppInfo } from "@@/utils/app" import { getAppInfo } from "@@/utils/app"
import { normalizeTokenStorage } from "@@/utils/request"
import * as hooks from "@@/hooks" import * as hooks from "@@/hooks"
import * as router from "@@/router" import * as router from "@@/router"
import * as constant from "@@/constant" import * as constant from "@@/constant"
@@ -101,7 +102,7 @@ const state = reactive<any>({
{ text: "我参加的活动", icon: "/static/icon/act.png", path: "/pages/merchant/my-act-list" }, { text: "我参加的活动", icon: "/static/icon/act.png", path: "/pages/merchant/my-act-list" },
{ text: "我的钱包", icon: "/static/icon/service_3.png", path: "/pages/wallet/list" }, { text: "我的钱包", icon: "/static/icon/service_3.png", path: "/pages/wallet/list" },
{ text: "我的人脸", icon: "/static/icon/service_5.png", path: "/pages/human-face/list" }, { text: "我的人脸", icon: "/static/icon/service_5.png", path: "/pages/human-face/list" },
{ text: "车辆管理", icon: "/static/icon/service_6.png", path: "/pages/car/user-car/list" }, // { text: "车辆管理", icon: "/static/icon/service_6.png", path: "/pages/car/user-car/list" },
{ text: "清除缓存", icon: "/static/icon/clear-storage.png", path: "" }, { text: "清除缓存", icon: "/static/icon/clear-storage.png", path: "" },
], ],
appInfo: "", appInfo: "",
@@ -136,7 +137,11 @@ const storage = {
// 保存token到存储 // 保存token到存储
saveToken: (tokenData: any) => { saveToken: (tokenData: any) => {
uni.setStorageSync(STORAGE_KEYS.TOKEN, tokenData) // SSO 接口可能返回嵌套 token,统一落成请求层使用的扁平结构。
const normalizedToken = normalizeTokenStorage(tokenData)
if (!normalizedToken) return false
uni.setStorageSync(STORAGE_KEYS.TOKEN, normalizedToken)
return true
}, },
// 清除用户登录相关的存储 // 清除用户登录相关的存储
@@ -202,7 +207,11 @@ const login = {
const response = await userApi.authsh({ accountId, sessionId }) const response = await userApi.authsh({ accountId, sessionId })
if (response.code == 0) { if (response.code == 0) {
storage.saveToken({ accessToken: response.data.accessToken }) if (!storage.saveToken(response.data)) {
uni.hideLoading()
uni.showToast({ title: "登录返回数据异常", icon: "none" })
return
}
uni.hideLoading() uni.hideLoading()
uni.showToast({ title: "登录成功", icon: "none" }) uni.showToast({ title: "登录成功", icon: "none" })
@@ -226,7 +235,10 @@ const login = {
const res = await userApi.auth(token) const res = await userApi.auth(token)
if (res.code == 0) { if (res.code == 0) {
storage.saveToken(res.data) if (!storage.saveToken(res.data)) {
uni.showToast({ title: "登录返回数据异常", icon: "none" })
return
}
uni.hideLoading() uni.hideLoading()
uni.showToast({ title: "登录成功", icon: "none" }) uni.showToast({ title: "登录成功", icon: "none" })
+13 -6
View File
@@ -86,7 +86,7 @@ const doRefund = (v: any) => {
</script> </script>
<template> <template>
<crud-layout title="订单" :bg="filePath(SERVICE_IMAGE.blueBg)" ref="crudLayoutRef"> <crud-layout title="订单" :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))" ref="crudLayoutRef">
<z-paging ref="pageRef" height="100%" :fixed="false" v-model="pageHook.list" :default-page-size="10" <z-paging ref="pageRef" height="100%" :fixed="false" v-model="pageHook.list" :default-page-size="10"
safe-area-inset-bottom :loading-more-enabled="true" :show-scrollbar="false" @query="query" 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" @scroll="pageHook.scroll"> :auto-clean-list-when-reload="false" :auto-scroll-to-top-when-reload="false" @scroll="pageHook.scroll">
@@ -98,7 +98,7 @@ const doRefund = (v: any) => {
<view class="flex-col justify-center card p-24rpx m-b-20rpx" v-for="(v, i) in pageHook.list" :key="i"> <view class="flex-col justify-center card p-24rpx m-b-20rpx" v-for="(v, i) in pageHook.list" :key="i">
<view class="flex-row"> <view class="flex-row">
<image class="size-130rpx rounded-8rpx" :src="filePath(v.logoPic)" mode="aspectFill" /> <image class="size-130rpx rounded-8rpx" :src="$fileUrl(filePath(v.logoPic))" mode="aspectFill" @error="$fileRetry(filePath(v.logoPic))" />
<view class="flex-1 ml-20rpx"> <view class="flex-1 ml-20rpx">
<view class="flex-row items-center justify-between"> <view class="flex-row items-center justify-between">
<view class="sub-text-30">{{ v.merchantName }}</view> <view class="sub-text-30">{{ v.merchantName }}</view>
@@ -123,7 +123,14 @@ const doRefund = (v: any) => {
class="second-text-30 mr-16rpx"> class="second-text-30 mr-16rpx">
共 {{ v.carList.length }} 件 共 {{ v.carList.length }} 件
</text> </text>
<text class="main-text-30 color-red!">¥ {{ hooks.useMoney(v.totalPrice) }}</text> <view class="flex-col items-center">
<text v-if="v.couponPrice || v.extraPrice" class="main-text-26 mr-10rpx line-through">
¥{{ hooks.useMoney(v.totalPrice) }}
</text>
<text class="main-text-30 color-red!">
¥{{ hooks.useMoney(v.payPrice) }}
</text>
</view>
</view> </view>
</view> </view>
</view> </view>
@@ -138,9 +145,9 @@ const doRefund = (v: any) => {
<view class="flex items-center w-6/10"> <view class="flex items-center w-6/10">
<view class="size-100rpx"> <view class="size-100rpx">
<image v-if="v.payType == '9'" class="size-100rpx rounded-8rpx" <image v-if="v.payType == '9'" class="size-100rpx rounded-8rpx"
:src="pV.productPic" mode="aspectFill" /> :src="$fileUrl(pV.productPic)" mode="aspectFill" @error="$fileRetry(pV.productPic)" />
<image v-else class="size-100rpx rounded-8rpx" :src="filePath(pV.productPic)" <image v-else class="size-100rpx rounded-8rpx" :src="$fileUrl(filePath(pV.productPic))"
mode="aspectFill" /> mode="aspectFill" @error="$fileRetry(filePath(pV.productPic))" />
</view> </view>
<view class="ml-16rpx overflow-hidden"> <view class="ml-16rpx overflow-hidden">
<view class="sub-text-26 truncate">{{ pV.productName }}</view> <view class="sub-text-26 truncate">{{ pV.productName }}</view>
+5 -5
View File
@@ -1,5 +1,5 @@
<template> <template>
<crud-layout title="共富商城" :bg="filePath(SERVICE_IMAGE.blueBg)" ref="crudLayoutRef"> <crud-layout title="共富商城" :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))" ref="crudLayoutRef">
<z-paging ref="pageRef" height="100%" :fixed="false" v-model="pageHook.list" :default-page-size="10" <z-paging ref="pageRef" height="100%" :fixed="false" v-model="pageHook.list" :default-page-size="10"
safe-area-inset-bottom :loading-more-enabled="true" :show-scrollbar="false" @query="query" 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" @scroll="pageHook.scroll"> :auto-clean-list-when-reload="false" :auto-scroll-to-top-when-reload="false" @scroll="pageHook.scroll">
@@ -24,8 +24,8 @@
<view class="card p-20rpx mb-20rpx" v-for="(v, i) in pageHook.list" :key="i" @tap="toDetail(v)"> <view class="card p-20rpx mb-20rpx" v-for="(v, i) in pageHook.list" :key="i" @tap="toDetail(v)">
<view class="flex"> <view class="flex">
<image class="w-160rpx h-160rpx bg-#eeeeee rounded-12rpx" :src="filePath(v.images)" <image class="w-160rpx h-160rpx bg-#eeeeee rounded-12rpx" :src="$fileUrl(filePath(v.images))"
mode="aspectFill" /> mode="aspectFill" @error="$fileRetry(filePath(v.images))" />
<view class="ml-20rpx flex-1 overflow-hidden"> <view class="ml-20rpx flex-1 overflow-hidden">
<view class="sub-text-32 truncate">{{ v.name }}</view> <view class="sub-text-32 truncate">{{ v.name }}</view>
<view class="second-text-26 flex justify-between my-6rpx"> <view class="second-text-26 flex justify-between my-6rpx">
@@ -46,8 +46,8 @@
<view class="flex"> <view class="flex">
<view class="w-180rpx flex-col items-center mr-20rpx" v-for="(pV, pI) in v.featuredProducts" <view class="w-180rpx flex-col items-center mr-20rpx" v-for="(pV, pI) in v.featuredProducts"
:key="pI"> :key="pI">
<image class="size-180rpx rounded-12rpx bg-#eeeeee" :src="filePath(pV.coverImage)" <image class="size-180rpx rounded-12rpx bg-#eeeeee" :src="$fileUrl(filePath(pV.coverImage))"
mode="aspectFill" /> mode="aspectFill" @error="$fileRetry(filePath(pV.coverImage))" />
<view class="w-full mt-4rpx sub-text-24 truncate">{{ pV.title }}</view> <view class="w-full mt-4rpx sub-text-24 truncate">{{ pV.title }}</view>
<view class="w-full flex justify-between"> <view class="w-full flex justify-between">
<text class="text-26rpx color-red!"> <text class="text-26rpx color-red!">
+2 -2
View File
@@ -79,7 +79,7 @@ const toDetail = async (v: any) => {
</script> </script>
<template> <template>
<crud-layout title="共富商家" :bg="filePath(SERVICE_IMAGE.blueBg)" ref="crudLayoutRef"> <crud-layout title="共富商家" :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))" ref="crudLayoutRef">
<z-paging ref="pageRef" height="100%" :fixed="false" v-model="pageHook.list" :default-page-size="10" <z-paging ref="pageRef" height="100%" :fixed="false" v-model="pageHook.list" :default-page-size="10"
safe-area-inset-bottom :loading-more-enabled="true" :show-scrollbar="false" @query="query" 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" @scroll="pageHook.scroll"> :auto-clean-list-when-reload="false" :auto-scroll-to-top-when-reload="false" @scroll="pageHook.scroll">
@@ -103,7 +103,7 @@ const toDetail = async (v: any) => {
</template> </template>
<view class="flex-row card p-20rpx m-b-20rpx" v-for="(v, i) in pageHook.list" :key="i" @tap="toDetail(v)"> <view class="flex-row card p-20rpx m-b-20rpx" v-for="(v, i) in pageHook.list" :key="i" @tap="toDetail(v)">
<image class="w-180rpx h-180rpx rounded-12rpx my-auto" :src="filePath(v.logoPic)" mode="aspectFill" /> <image class="w-180rpx h-180rpx rounded-12rpx my-auto" :src="$fileUrl(filePath(v.logoPic))" mode="aspectFill" @error="$fileRetry(filePath(v.logoPic))" />
<view class="flex-1 flex-col m-l-20rpx"> <view class="flex-1 flex-col m-l-20rpx">
<view class="sub-text-34"> <view class="sub-text-34">
<text class="mr-16rpx">{{ v.name }}</text> <text class="mr-16rpx">{{ v.name }}</text>
+2 -2
View File
@@ -43,13 +43,13 @@ const toDetail = async (v: any) => {
</script> </script>
<template> <template>
<crud-layout title="自营商家" :bg="filePath(SERVICE_IMAGE.blueBg)" ref="crudLayoutRef"> <crud-layout title="自营商家" :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))" ref="crudLayoutRef">
<z-paging ref="pageRef" height="100%" :fixed="false" v-model="pageHook.list" :default-page-size="10" <z-paging ref="pageRef" height="100%" :fixed="false" v-model="pageHook.list" :default-page-size="10"
safe-area-inset-bottom :loading-more-enabled="true" :show-scrollbar="false" @query="query" 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" @scroll="pageHook.scroll"> :auto-clean-list-when-reload="false" :auto-scroll-to-top-when-reload="false" @scroll="pageHook.scroll">
<view class="h-20rpx" /> <view class="h-20rpx" />
<view class="flex-row card p-20rpx m-b-20rpx" v-for="(v, i) in pageHook.list" :key="i" @tap="toDetail(v)"> <view class="flex-row card p-20rpx m-b-20rpx" v-for="(v, i) in pageHook.list" :key="i" @tap="toDetail(v)">
<image class="w-180rpx h-180rpx rounded-12rpx" :src="filePath(v.logoPic)" mode="aspectFill" /> <image class="w-180rpx h-180rpx rounded-12rpx" :src="$fileUrl(filePath(v.logoPic))" mode="aspectFill" @error="$fileRetry(filePath(v.logoPic))" />
<view class="flex-1 flex-col m-l-20rpx"> <view class="flex-1 flex-col m-l-20rpx">
<view class="flex-row items-center justify-between"> <view class="flex-row items-center justify-between">
<view class="flex-row-center sub-text-34">{{ v.name }}</view> <view class="flex-row-center sub-text-34">{{ v.name }}</view>
+57 -4
View File
@@ -1,7 +1,7 @@
<template> <template>
<crud-layout mainIsScroll :bg="filePath(SERVICE_IMAGE.blueBg)"> <crud-layout mainIsScroll :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))">
<view class="w-full text-center mt-20"> <view class="w-full text-center mt-20">
<image :src="filePath(SERVICE_IMAGE.happyMall)" class="rounded-full w-1/3" mode="widthFix" /> <image :src="$fileUrl(filePath(SERVICE_IMAGE.happyMall))" class="rounded-full w-1/3" mode="widthFix" @error="$fileRetry(filePath(SERVICE_IMAGE.happyMall))" />
<view>美好商城</view> <view>美好商城</view>
<view class="h-100rpx" /> <view class="h-100rpx" />
<tm-button @click="show = true">继续跳转</tm-button> <tm-button @click="show = true">继续跳转</tm-button>
@@ -27,6 +27,58 @@ import { filePath } from "@@/utils/file"
const show = ref(false) const show = ref(false)
function normalizeMallUrl(rawUrl: string) {
const matched = rawUrl.match(/^(https:\/\/(?:[a-z0-9-]+\.)*nbcb\.com\.cn)(\/[^?#]*)?(\?[^#]*)?(#.*)?$/i)
if (!matched) throw new Error("商城登录地址不合法")
const [, origin, rawPathname = "/", search = "", hash = ""] = matched
const queryItems = search.slice(1).split("&").filter(Boolean)
const decodeQueryPart = (value: string) => {
try {
return decodeURIComponent(value)
} catch {
return value
}
}
const queryKeys = queryItems.map((item) => decodeQueryPart(item.split("=")[0] || ""))
const hasToken = queryItems.some((item) => {
const [key, ...valueParts] = item.split("=")
return decodeQueryPart(key || "").toLowerCase() === "token" && valueParts.join("=")
})
let pathname = rawPathname
// 宁波银行正常登录页是 /h5/thirdApp;部分环境会错误返回带 token 的根路径。
if (pathname === "/" && hasToken) {
pathname = "/h5/thirdApp"
}
console.log("[happyMall] 商城登录地址:", {
origin,
pathname,
queryKeys,
})
if (pathname === "/" || !hasToken) {
throw new Error("商城登录地址异常,请联系管理员")
}
// 宁波银行 H5 通过 platform 判断是否运行在微信小程序中。设置为 XR 后,
// H5 左上角返回按钮会调用 wx.miniProgram.navigateBack(),直接返回小程序上一页,
// 不会先消耗宁波银行 H5 自己的 history。覆盖旧值,避免后端返回其他平台参数。
const queryWithoutPlatform = queryItems.filter((item) => {
const key = decodeQueryPart(item.split("=")[0] || "").toLowerCase()
return key !== "platform"
})
queryWithoutPlatform.push("platform=XR")
const normalizedSearch = `?${queryWithoutPlatform.join("&")}`
console.log("[happyMall] 跳转平台:", {
platform: "XR",
queryKeys: queryWithoutPlatform.map((item) => decodeQueryPart(item.split("=")[0] || "")),
})
return `${origin}${pathname}${normalizedSearch}${hash}`
}
onLoad(async () => { onLoad(async () => {
const isLogin = await hooks.useCheckLogin() const isLogin = await hooks.useCheckLogin()
if (isLogin) { if (isLogin) {
@@ -42,10 +94,11 @@ const thirdAppJump = async () => {
if (res.code == 0) { if (res.code == 0) {
show.value = false show.value = false
if (!res.data) return uni.showToast({ title: "url错误", icon: "none" }) if (!res.data) return uni.showToast({ title: "url错误", icon: "none" })
router.jump("/pages/root/web-view", { url: encodeURIComponent(res.data) }, 2) const mallUrl = normalizeMallUrl(res.data)
router.jump("/pages/root/web-view", { url: encodeURIComponent(mallUrl) }, 2)
} }
} catch (error: any) { } catch (error: any) {
uni.showToast({ title: error, icon: "none" }) uni.showToast({ title: error?.message || String(error), icon: "none" })
} finally { } finally {
uni.hideLoading() uni.hideLoading()
} }
+1 -1
View File
@@ -45,7 +45,7 @@ const doTab = (e: any) => {
</script> </script>
<template> <template>
<crud-layout title="饭卡" :bg="filePath(SERVICE_IMAGE.blueBg)"> <crud-layout title="饭卡" :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))">
<z-paging ref="pageRef" height="100%" :fixed="false" v-model="pageHook.list" :default-page-size="10" <z-paging ref="pageRef" height="100%" :fixed="false" v-model="pageHook.list" :default-page-size="10"
safe-area-inset-bottom :loading-more-enabled="true" :show-scrollbar="false" @query="query" 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" @scroll="pageHook.scroll"> :auto-clean-list-when-reload="false" :auto-scroll-to-top-when-reload="false" @scroll="pageHook.scroll">
+2 -2
View File
@@ -141,7 +141,7 @@ const doTransferMoney = (v: any) => {
</script> </script>
<template> <template>
<crud-layout title="我的钱包" :bg="filePath(SERVICE_IMAGE.blueBg)"> <crud-layout title="我的钱包" :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))">
<z-paging ref="pageRef" height="100%" :fixed="false" v-model="pageHook.list" :default-page-size="5" <z-paging ref="pageRef" height="100%" :fixed="false" v-model="pageHook.list" :default-page-size="5"
safe-area-inset-bottom :loading-more-enabled="true" :show-scrollbar="false" @query="query" 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" @scroll="pageHook.scroll"> :auto-clean-list-when-reload="false" :auto-scroll-to-top-when-reload="false" @scroll="pageHook.scroll">
@@ -164,7 +164,7 @@ const doTransferMoney = (v: any) => {
</view> </view>
<view class="flex-row items-center justify-between m-t-10rpx"> <view class="flex-row items-center justify-between m-t-10rpx">
<view class="flex-row items-center"> <view class="flex-row items-center">
<image class="size-50rpx rounded-full" :src="filePath(v.walletTypeIcon)" mode="aspectFill" /> <image class="size-50rpx rounded-full" :src="$fileUrl(filePath(v.walletTypeIcon))" mode="aspectFill" @error="$fileRetry(filePath(v.walletTypeIcon))" />
<text class="m-l-25rpx sub-text-28"> <text class="m-l-25rpx sub-text-28">
{{ v.walletTypeName }} {{ v.walletTypeName }}
</text> </text>
+2 -2
View File
@@ -110,13 +110,13 @@ const toRechargeRecord = () => {
</script> </script>
<template> <template>
<crud-layout title="充值" :bg="filePath(SERVICE_IMAGE.blueBg)"> <crud-layout title="充值" :bg="$fileUrl(filePath(SERVICE_IMAGE.blueBg))">
<view class="h-30rpx" /> <view class="h-30rpx" />
<view @tap="toRechargeRecord" <view @tap="toRechargeRecord"
class="relative flex-row items-center justify-between h-120rpx p-x-30rpx bg-#222222 rounded-20rpx"> class="relative flex-row items-center justify-between h-120rpx p-x-30rpx bg-#222222 rounded-20rpx">
<view class="flex-row items-center"> <view class="flex-row items-center">
<view class="flex-row-center size-60rpx bg-white rounded-full"> <view class="flex-row-center size-60rpx bg-white rounded-full">
<image class="size-50rpx rounded-full" :src="state.detail.walletIcon" mode="aspectFill" /> <image class="size-50rpx rounded-full" :src="$fileUrl(state.detail.walletIcon)" mode="aspectFill" @error="$fileRetry(state.detail.walletIcon)" />
</view> </view>
<view class="flex-row-center"> <view class="flex-row-center">
<view class="m-l-25rpx main-text-32 color-#fbddb0!"> <view class="m-l-25rpx main-text-32 color-#fbddb0!">
+9
View File
@@ -19,4 +19,13 @@ export default {
method: "GET", method: "GET",
params, params,
}), }),
/**
* 用户领取核销券列表
*/
mchList: (data: any) =>
request({
url: "/wallet/act-coupon-record/app/mch-list",
method: "POST",
data,
}),
} }
+10
View File
@@ -1,6 +1,16 @@
import request from "@/sheep/utils/request" import request from "@/sheep/utils/request"
export default { export default {
// ========== 轮播图相关接口 ==========
/**
* 获取轮播图列表
*/
getSwiperList: () =>
request({
url: "/integrated/sysBanner/app/slides",
method: "GET",
}),
// 公告分页列表 // 公告分页列表
getNoticePage: (params: { current: number; size: number }) => getNoticePage: (params: { current: number; size: number }) =>
request({ request({
+22 -11
View File
@@ -1,6 +1,7 @@
import base64 from "base64-utf8" import base64 from "base64-utf8"
import request from "@/sheep/utils/request" import request from "@/sheep/utils/request"
import { encryption } from "../utils/encryption"
// 将 Basic Auth 字符串提取为常量 // 将 Basic Auth 字符串提取为常量
const BASIC_AUTH = "Basic " + base64.encode(import.meta.env.VITE_OAUTH2_APP_CLIENT) const BASIC_AUTH = "Basic " + base64.encode(import.meta.env.VITE_OAUTH2_APP_CLIENT)
@@ -48,21 +49,31 @@ export default {
// 账号密码登录 // 账号密码登录
login: (params: any) => { login: (params: any) => {
const { username, password, randomStr, code, grant_type, scope } = params const { username, password, randomStr, code, grant_type, scope } = params
const encKey = import.meta.env.VITE_PWD_ENC_KEY
// 未配置密钥时只能明文(需与 auth 客户端 encFlag 一致);或显式 VITE_OAUTH_PWD_PLAIN=true
const plainPwd = import.meta.env.VITE_OAUTH_PWD_PLAIN === "true" || !encKey
const pwdForWire = plainPwd ? password : encryption(password, encKey)
const query: Record<string, string> = {
username: username ?? "",
grant_type: grant_type ?? "",
scope: scope ?? "",
}
if (randomStr != null && String(randomStr) !== "") {
query.randomStr = String(randomStr)
}
if (code != null && String(code) !== "") {
query.code = String(code)
}
return request({ return request({
url: `/auth/oauth2/token`, url: `/auth/oauth2/token`,
method: "POST", method: "POST",
params: { params: query,
username, data: `password=${encodeURIComponent(pwdForWire)}`,
randomStr, // data: `password=`+password,
code,
grant_type,
scope,
},
data: { password },
header: { header: {
skipToken: true, skipToken: true,
Authorization: BASIC_AUTH, Authorization: BASIC_AUTH,
"content-type": "application/x-www-form-urlencoded", "Content-Type": "application/x-www-form-urlencoded",
}, },
}) })
}, },
@@ -112,7 +123,7 @@ export default {
}), }),
// 设备认证 // 设备认证
verifyDevice: (data: { mobilePhone: string; code: string, uniqueCode?: string, deviceJson?: string }) => verifyDevice: (data: { mobilePhone: string; code: string; deviceUniqueCode?: string }) =>
request({ request({
url: "/thirdparty/thirdPartyOAth2/mini-app/verify-device", url: "/thirdparty/thirdPartyOAth2/mini-app/verify-device",
method: "POST", method: "POST",
@@ -120,7 +131,7 @@ export default {
}), }),
// 判断该设备是否被验证 // 判断该设备是否被验证
isVerifyDevice: (data: { mobile: string; userId: string, uniqueCode?: string, deviceJson?: string }) => isVerifyDevice: (data: { mobile: string; userId: string; uniqueCode?: string; deviceUniqueCode?: string }) =>
request({ request({
url: "/thirdparty/thirdPartyOAth2/is-verify-device", url: "/thirdparty/thirdPartyOAth2/is-verify-device",
method: "POST", method: "POST",
@@ -46,6 +46,7 @@ function getNavTop() {
} }
function doToDashboard() { function doToDashboard() {
tabbarStore.setRootActiveIndex(0)
router.jump("/pages/root/dashboard", {}, 3) router.jump("/pages/root/dashboard", {}, 3)
} }
</script> </script>
@@ -46,6 +46,7 @@ function doBackClick() {
} }
function doToDashboard() { function doToDashboard() {
tabbarStore.setRootActiveIndex(0)
router.jump("/pages/root/dashboard", {}, 3) router.jump("/pages/root/dashboard", {}, 3)
} }
</script> </script>
@@ -11,17 +11,29 @@ const props = withDefaults(defineProps<Props>(), {
}) })
function getHeight() { function getHeight() {
const { top, bottom } = uni.getSystemInfoSync().safeAreaInsets! let _statusBarHeight = 0;
let _bottom = 0;
const { statusBarHeight, safeAreaInsets } = uni.getSystemInfoSync()
const { bottom = 0 } = safeAreaInsets ?? {}
if (statusBarHeight) {
_statusBarHeight = statusBarHeight
}
if (_statusBarHeight <= 20) {
_statusBarHeight = 20
}
if (bottom) {
_bottom = bottom
}
if (_bottom <= 8) {
_bottom = 8
}
if (props.type && props.type === "bottom") { if (props.type && props.type === "bottom") {
return bottom + "px" return _bottom + "px"
} }
if (props.type && props.type === "top") { if (props.type && props.type === "top") {
if (top < 22) { return _statusBarHeight + "px"
return "22px"
}
return top + "px"
} }
return 22 + "px" return 20 + "px"
} }
</script> </script>
@@ -5,6 +5,7 @@ import { router, hooks, constant } from "@@/index"
import { useTabbarStore } from "@@/stores/tabbar" import { useTabbarStore } from "@@/stores/tabbar"
import { jump } from "@/sheep/router"; import { jump } from "@/sheep/router";
import { userApi } from "@@/api/index" import { userApi } from "@@/api/index"
import { buildUserDeviceFingerprint } from "@@/utils/device-unique"
import VerifyDeviceImpModel from "../../verify-device-imp-model.vue" import VerifyDeviceImpModel from "../../verify-device-imp-model.vue"
@@ -92,6 +93,10 @@ function initBackStyle() {
function tabbarChange(index: number) { function tabbarChange(index: number) {
const target = tabbarStore.tabbarList[index] const target = tabbarStore.tabbarList[index]
if (!target) return if (!target) return
if (target.pagePath === "/pages/root/dashboard") {
// 从业务导航返回工作台时,让首页根 Tabbar 同步选中“首页”。
tabbarStore.setRootActiveIndex(0)
}
jump(target.pagePath, {}, 3) jump(target.pagePath, {}, 3)
} }
@@ -133,9 +138,11 @@ async function checkVerifyDevice() {
if (constant.DEVICE_VERIFY_WRITE_LIST.includes(userInfo.sysUser.phone)) { if (constant.DEVICE_VERIFY_WRITE_LIST.includes(userInfo.sysUser.phone)) {
return return
} }
const fingerprint = await buildUserDeviceFingerprint(true)
const _params = { const _params = {
mobile: userInfo.sysUser.phone, mobile: userInfo.sysUser.phone,
userId: userInfo.sysUser.userId, userId: userInfo.sysUser.userId,
deviceUniqueCode: fingerprint?.fingerprint,
} }
try { try {
const { data, code }: any = await userApi.isVerifyDevice(_params) const { data, code }: any = await userApi.isVerifyDevice(_params)
@@ -162,7 +169,7 @@ defineExpose({
<template> <template>
<view class="layout"> <view class="layout">
<image class="layout_bg" v-if="props.bg" :src="props.bg" mode="aspectFill" /> <image class="layout_bg" v-if="props.bg" :src="$fileUrl(props.bg)" mode="aspectFill" @error="$fileRetry(props.bg)" />
<view class="layout_header"> <view class="layout_header">
<base-safe-height :bgColor="props.navBgColor" v-if="state.visble.topSafe" type="top" /> <base-safe-height :bgColor="props.navBgColor" v-if="state.visble.topSafe" type="top" />
<base-nav v-if="state.visble.topNav" :titleColor="props.navBgTitleColor" :bgColor="props.navBgColor" <base-nav v-if="state.visble.topNav" :titleColor="props.navBgTitleColor" :bgColor="props.navBgColor"
@@ -182,12 +189,14 @@ defineExpose({
<slot name="main-bottom"></slot> <slot name="main-bottom"></slot>
</view> </view>
</view> </view>
<view class="layout_foot"> <view v-if="tabbarStore.isTabbar && tabbarStore.rootActiveIndex != 2" class="layout_foot">
<tm-tabbar v-if="tabbarStore.isTabbar && tabbarStore.rootActiveIndex != 2" class="layout_foot" <tm-tabbar class="layout_foot" :activeIndex="tabbarStore.activeIndex" :list="tabbarStore.pageTabbarList"
:activeIndex="tabbarStore.activeIndex" :list="tabbarStore.pageTabbarList" :outIndex="-1" fontSize="12px" :outIndex="-1" fontSize="12px" iconSize="24px" bgColor="#F6F5FA" :zIndex="99999" darkBgColor="#F6F5FA"
iconSize="24px" bgColor="#F6F5FA" :zIndex="99999" darkBgColor="#F6F5FA" borderColor="#DBDDDE" borderColor="#DBDDDE" color="#8E8E8E" selected-color="#0094ff" position="fixed"
color="#8E8E8E" selected-color="#0094ff" position="fixed" @change="tabbarChange" /> @change="tabbarChange" />
<tm-tabbar v-else class="layout_foot" :activeIndex="tabbarStore.rootActiveIndex" </view>
<view class="layout_foot" v-else-if="tabbarStore.isDashboardPage">
<tm-tabbar class="layout_foot" :activeIndex="tabbarStore.rootActiveIndex"
:list="tabbarStore.rootPageTabbarList" :outIndex="1" fontSize="12px" iconSize="24px" bgColor="#F6F5FA" :list="tabbarStore.rootPageTabbarList" :outIndex="1" fontSize="12px" iconSize="24px" bgColor="#F6F5FA"
:zIndex="99999" darkBgColor="#F6F5FA" borderColor="#DBDDDE" color="#8E8E8E" outBgColor="#0094ff" :zIndex="99999" darkBgColor="#F6F5FA" borderColor="#DBDDDE" color="#8E8E8E" outBgColor="#0094ff"
selected-color="#0094ff" position="fixed" @change="rootTabbarChange" /> selected-color="#0094ff" position="fixed" @change="rootTabbarChange" />
+1 -1
View File
@@ -3,7 +3,7 @@
<view v-for="item in nodeData" :key="item.id"> <view v-for="item in nodeData" :key="item.id">
<view class="tree-item" :style="item.id == selectedNodeId ? { borderColor: '#1879ff' } : { color: '#000' }" <view class="tree-item" :style="item.id == selectedNodeId ? { borderColor: '#1879ff' } : { color: '#000' }"
@click.stop="toCurrentNode(item)"> @click.stop="toCurrentNode(item)">
<image class="tree-item-icon" :src="filePath(item.picUrl)" /> <image class="tree-item-icon" :src="$fileUrl(filePath(item.picUrl))" @error="$fileRetry(filePath(item.picUrl))" />
<text>{{ item.name }}</text> <text>{{ item.name }}</text>
</view> </view>
+89 -28
View File
@@ -1,33 +1,35 @@
<template> <template>
<view class="bg-[#f5f5f5] px-8rpx py-16rpx rounded-4 fixed bottom-0 left-0 right-0 w-full"> <view class="keyboard-wrap">
<view class="flex justify-end"> <view class="keyboard-panel" :class="{ 'keyboard-panel--open': show }">
<image v-if="show" class="w-[50rpx] h-[50rpx] mx-16rpx mb-8rpx" src="@/static/page/fold.svg" <view class="keyboard-toggle" @tap.stop="toggleShow">
@click="show = !show" /> <image class="keyboard-toggle__icon"
<image v-if="!show" class="w-[50rpx] h-[50rpx] mx-16rpx mb-8rpx" src="@/static/page/unfold.svg" :src="$fileUrl(show ? '/static/page/fold.svg' : '/static/page/unfold.svg')" @error="$fileRetry(show ? '/static/page/fold.svg' : '/static/page/unfold.svg')" />
@click="show = !show" />
</view>
<view v-if="show" class="flex mb-20rpx" v-for="(row, rowIndex) in keyboardLayout" :key="rowIndex">
<view
class="flex-1 h-[90rpx] flex items-center justify-center bg-white text-2xl text-[#333] mx-[5px] rounded-lg cursor-pointer select-none active:bg-[#f0f0f0]"
v-for="(key, keyIndex) in row" :key="keyIndex" @click="handleKeyPress(key.toString())">
{{ key }}
</view> </view>
</view> <view v-show="show" class="keyboard-content">
<view v-if="show" class="flex mb-0rpx"> <view class="flex mb-20rpx" v-for="(row, rowIndex) in keyboardLayout" :key="rowIndex">
<view <view
class="flex-1 h-[90rpx] flex items-center justify-center bg-white text-2xl text-[#333] mx-[5px] rounded-lg cursor-pointer select-none active:bg-[#f0f0f0]" class="flex-1 h-[90rpx] flex items-center justify-center bg-white text-2xl text-[#333] mx-[5px] rounded-lg cursor-pointer select-none active:bg-[#f0f0f0]"
@click="handleKeyPress('.')"> v-for="(key, keyIndex) in row" :key="keyIndex" @tap="handleKeyPress(key.toString())">
. {{ key }}
</view> </view>
<view </view>
class="flex-auto h-[90rpx] flex items-center justify-center bg-white text-2xl text-[#333] mx-[5px] rounded-lg cursor-pointer select-none active:bg-[#f0f0f0]" <view class="flex mb-0rpx">
@click="handleKeyPress('0')"> <view
0 class="flex-1 h-[90rpx] flex items-center justify-center bg-white text-2xl text-[#333] mx-[5px] rounded-lg cursor-pointer select-none active:bg-[#f0f0f0]"
</view> @tap="handleKeyPress('.')">
<view .
class="flex-1 h-[90rpx] flex items-center justify-center bg-white mx-[5px] rounded-lg cursor-pointer select-none active:bg-[#f0f0f0]" </view>
@click="handleDelete"> <view
<image class="size-45rpx" src="@/static/page/keyboard-delete.svg" /> class="flex-auto h-[90rpx] flex items-center justify-center bg-white text-2xl text-[#333] mx-[5px] rounded-lg cursor-pointer select-none active:bg-[#f0f0f0]"
@tap="handleKeyPress('0')">
0
</view>
<view
class="flex-1 h-[90rpx] flex items-center justify-center bg-white mx-[5px] rounded-lg cursor-pointer select-none active:bg-[#f0f0f0]"
@tap="handleDelete">
<image class="size-45rpx" src="@/static/page/keyboard-delete.svg" />
</view>
</view>
</view> </view>
</view> </view>
</view> </view>
@@ -45,6 +47,10 @@ const keyboardLayout = ref([
]) ])
const show = ref(true) const show = ref(true)
const toggleShow = () => {
show.value = !show.value
}
// 按下键盘 // 按下键盘
const handleKeyPress = (key) => { const handleKeyPress = (key) => {
if (key === "." && amount.value.includes(".")) return if (key === "." && amount.value.includes(".")) return
@@ -74,3 +80,58 @@ const handleDelete = () => {
} }
} }
</script> </script>
<style scoped lang="scss">
.keyboard-wrap {
--keyboard-toggle-size: 82rpx;
--keyboard-expanded-height: 538rpx;
position: fixed;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
z-index: 1000001;
}
.keyboard-panel {
position: absolute;
right: 0;
bottom: calc(var(--keyboard-expanded-height) - var(--keyboard-toggle-size));
width: 82rpx;
padding: 12rpx 0 12rpx 12rpx;
background: transparent;
overflow: hidden;
pointer-events: auto;
transition: width 0.2s ease, padding 0.2s ease, background-color 0.2s ease, bottom 0.2s ease;
&--open {
bottom: 0;
width: 100%;
padding: 12rpx;
background: #f5f5f5;
border-radius: 24rpx 24rpx 0 0;
box-shadow: 0 0 24rpx rgba(0, 0, 0, 0.08);
}
}
.keyboard-toggle {
width: 82rpx;
height: 82rpx;
margin-left: auto;
display: flex;
align-items: center;
justify-content: center;
background: #f5f5f5;
border-radius: 24rpx 0 0 24rpx;
box-shadow: 0 0 24rpx rgba(0, 0, 0, 0.08);
}
.keyboard-toggle__icon {
width: 50rpx;
height: 50rpx;
}
.keyboard-content {
margin-top: 12rpx;
}
</style>
@@ -36,7 +36,7 @@
}, },
{ 'img-error': !item2[data.imageKey] }, { 'img-error': !item2[data.imageKey] },
]" ]"
:src="item2[data.imageKey]" :src="$fileUrl(item2[data.imageKey])"
mode="widthFix" mode="widthFix"
@load="imgLoad(item2, index + 1)" @load="imgLoad(item2, index + 1)"
@error="imgError(item2, index + 1)" @error="imgError(item2, index + 1)"
@@ -31,6 +31,7 @@ import { ref, reactive, watch, onUnmounted } from "vue"
import { constant } from "@@/index" import { constant } from "@@/index"
import { userApi } from "@@/api" import { userApi } from "@@/api"
import { getMiniUniqueCode, getDeviceKey } from "@@/utils/mini-tool" import { getMiniUniqueCode, getDeviceKey } from "@@/utils/mini-tool"
import { buildUserDeviceFingerprint } from "@@/utils/device-unique"
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@@ -109,9 +110,11 @@ const doSubmit = async () => {
if (!isPass) return if (!isPass) return
uni.showLoading({ title: "验证中...", mask: true }) uni.showLoading({ title: "验证中...", mask: true })
const fingerprint = await buildUserDeviceFingerprint(false)
const params = { const params = {
mobilePhone: form.mobilePhone, mobilePhone: form.mobilePhone,
code: form.code, code: form.code,
deviceUniqueCode: fingerprint?.fingerprint,
} }
const res: any = await userApi.verifyDevice(params) const res: any = await userApi.verifyDevice(params)
if (res.code != 0) { if (res.code != 0) {
+1
View File
@@ -76,6 +76,7 @@ const actUserReceiveEnum = {
"4": "已领取,核销码在我的礼券中", "4": "已领取,核销码在我的礼券中",
"5": "用户不能参加此活动", "5": "用户不能参加此活动",
"6": "其他错误", "6": "其他错误",
"7": "已领完",
"99": "用户可以参加活动", "99": "用户可以参加活动",
// 特殊 // 特殊
"-100": "仅限生日当月使用", "-100": "仅限生日当月使用",
+2 -1
View File
@@ -1,7 +1,8 @@
import { previewFiles } from '@/sheep/utils/file-access'
export const usePreviewImage = (str: string | undefined) => { export const usePreviewImage = (str: string | undefined) => {
if (str == undefined) return if (str == undefined) return
let urlList = str.split(",") let urlList = str.split(",")
uni.previewImage({ previewFiles({
urls: urlList, urls: urlList,
}) })
} }
+9 -2
View File
@@ -1,8 +1,15 @@
import { router } from ".." import { router } from ".."
import { getUrlParamsObj } from "../utils/app" import { getUrlParamsObj, isDevEnv } from "../utils/app"
const normalizeScanResult = (scanResult: string) => scanResult.replace("https://stgl.pj.gov.cn", "https://www.pjzhhq.cn")
export function useScanCode(actModalShow) { export function useScanCode(actModalShow) {
async function scanCodeAction(): Promise<string | undefined> { async function scanCodeAction(): Promise<string | undefined> {
if (isDevEnv()) {
const testQrCode = "https://stgl.pj.gov.cn/h5/#/pages/pay/scanPay?id=1931916121241079810"
uni.showModal({ content: "开发环境不是浙政钉和诗画环境,扫码仅供测试" })
return normalizeScanResult(testQrCode)
}
const scanResult = await new Promise<string>((resolve, reject) => { const scanResult = await new Promise<string>((resolve, reject) => {
uni.scanCode({ uni.scanCode({
success: (res) => { success: (res) => {
@@ -15,7 +22,7 @@ export function useScanCode(actModalShow) {
}, },
}) })
}) })
return scanResult return normalizeScanResult(scanResult || "")
} }
// 处理url扫码结果 // 处理url扫码结果
+7 -10
View File
@@ -1,11 +1,13 @@
import storageConstant from "../constant/storage-constant" import storageConstant from "../constant/storage-constant"
import { jump } from "../router/index" import { jump } from "../router/index"
import { isDevEnv } from "../utils/app"
export const useToken = () => { export const useToken = () => {
const tokenKey = storageConstant.TOKEN const tokenKey = storageConstant.TOKEN
const tokenStorage = uni.getStorageSync(tokenKey) const tokenStorage = uni.getStorageSync(tokenKey)
return tokenStorage?.access_token ? `Bearer ${tokenStorage.access_token}` : "" // 登录与刷新接口历史上存在两种字段格式,上传等原生请求也必须读取同一个有效 token。
const rawToken = tokenStorage?.accessToken ?? tokenStorage?.access_token
const tokenValue = typeof rawToken === "string" ? rawToken : rawToken?.tokenValue
return tokenValue ? `Bearer ${tokenValue}` : ""
} }
export const useDefaultHeader = () => { export const useDefaultHeader = () => {
@@ -18,17 +20,12 @@ export const useDefaultHeader = () => {
} }
export const useStaticCheckLogin = () => { export const useStaticCheckLogin = () => {
const tokenKey = storageConstant.TOKEN // 不能只判断缓存对象存在,刷新失败留下的空对象不代表已登录。
const tokenStorage = uni.getStorageSync(tokenKey) return Boolean(useToken())
if (tokenStorage) return true
return false
} }
export const useCheckLogin = async (): Promise<boolean> => { export const useCheckLogin = async (): Promise<boolean> => {
const tokenKey = storageConstant.TOKEN if (useToken()) return true
const tokenStorage = uni.getStorageSync(tokenKey)
if (tokenStorage) return true
uni.showModal({ uni.showModal({
title: "提示", title: "提示",
content: "您还未登录,请先登录", content: "您还未登录,请先登录",
+16
View File
@@ -1,5 +1,17 @@
import { throttle } from "radash" 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 }) { function paramsToQuery(params: { [key: string]: string }) {
let query = [] let query = []
for (let key in params) { for (let key in params) {
@@ -14,6 +26,10 @@ function _jump(path = "", params = {}, type = 1) {
if (!path) { if (!path) {
console.error("router::jump>>path 不能为空") console.error("router::jump>>path 不能为空")
} }
const targetPath = normalizePath(path)
if (targetPath && getCurrentPagePath() === targetPath) {
return
}
if (hasTabbar(path)) { if (hasTabbar(path)) {
uni.switchTab({ url: path }) uni.switchTab({ url: path })
return return
+68 -12
View File
@@ -118,16 +118,62 @@ const TABBAR_CONSTANT: Array<TabbarItem> = [
launchParam: ["integrated"], launchParam: ["integrated"],
auth: ["user"], auth: ["user"],
}, },
{
pagePath: "/pages/integrated/notice/index",
text: "通知公告",
iconPath: "/static/tabbar/notice.png",
selectedIconPath: "/static/tabbar/notice-select.png",
launchParam: ["notice"],
auth: ["user"],
},
{
pagePath: "/pages/integrated/feedback/index",
text: "意见反馈",
iconPath: "/static/tabbar/feedback.png",
selectedIconPath: "/static/tabbar/feedback-select.png",
launchParam: ["feedback"],
auth: ["user"],
},
{ {
pagePath: "/pages/tabbar/my", pagePath: "/pages/tabbar/my",
text: "我的", text: "我的",
iconPath: "/static/tabbar/my.png", iconPath: "/static/tabbar/my.png",
selectedIconPath: "/static/tabbar/my-select.png", selectedIconPath: "/static/tabbar/my-select.png",
launchParam: ["canteen", "selfMerchant", "onlineShop", "coopMerchant", "integrated", "prosperityShop", "prosperityShopMchList"], launchParam: [
"canteen",
"selfMerchant",
"onlineShop",
"coopMerchant",
"integrated",
"prosperityShop",
"prosperityShopMchList",
"notice",
"feedback",
],
auth: ["user", "merchant", "doorAdmin"], auth: ["user", "merchant", "doorAdmin"],
}, },
] ]
const WORKBENCH_TABBAR_ITEM: TabbarItem = {
pagePath: "/pages/root/dashboard",
text: "工作台",
iconPath: "/static/tabbar/home.png",
selectedIconPath: "/static/tabbar/home-select.png",
launchParam: [],
auth: [],
}
function getTabbarLaunchParam(url?: string) {
const launchParam = getLaunchParam(url)
if (launchParam) return launchParam
if (!url) return ""
// 常用功能也可能直接配置 Tabbar 页面路径,需要由页面反推出所属业务场景。
const pagePath = url.split("?")[0]
const tabbarItem = TABBAR_CONSTANT.find((item) => item.pagePath === pagePath)
return tabbarItem?.launchParam[0] || ""
}
const defaultRootTabbarList: Array<TabbarItem> = [ const defaultRootTabbarList: Array<TabbarItem> = [
{ {
pagePath: "/pages/root/dashboard", pagePath: "/pages/root/dashboard",
@@ -165,11 +211,18 @@ export const useTabbarStore = defineStore("tabbarStore", {
currentRoute: "/pages/root/dashboard", currentRoute: "/pages/root/dashboard",
}), }),
getters: { getters: {
isDashboardPage(state) {
const currentRoute = state.currentRoute.startsWith("/") ? state.currentRoute : `/${state.currentRoute}`
return currentRoute === "/pages/root/dashboard" || currentRoute === "/pages/tabbar/my"
},
isTabbar(state) { isTabbar(state) {
return state.tabbarList.some((item) => item.pagePath === `/${state.currentRoute}`) const currentRoute = state.currentRoute.startsWith("/") ? state.currentRoute : `/${state.currentRoute}`
// 工作台只作为业务 Tabbar 的返回入口,进入页面后仍展示首页自己的根 Tabbar。
return currentRoute !== WORKBENCH_TABBAR_ITEM.pagePath && state.tabbarList.some((item) => item.pagePath === currentRoute)
}, },
activeIndex(state) { activeIndex(state) {
return state.tabbarList.findIndex((item) => item.pagePath === `/${state.currentRoute}`) const currentRoute = state.currentRoute.startsWith("/") ? state.currentRoute : `/${state.currentRoute}`
return state.tabbarList.findIndex((item) => item.pagePath === currentRoute)
}, },
pageTabbarList(state) { pageTabbarList(state) {
return state.tabbarList.map((item) => { return state.tabbarList.map((item) => {
@@ -193,22 +246,25 @@ export const useTabbarStore = defineStore("tabbarStore", {
}, },
actions: { actions: {
async componetTabbarList(launchParam?: string) { async componetTabbarList(launchParam?: string) {
let _launchParam = "" const _launchParam = getTabbarLaunchParam(launchParam)
if (launchParam) { const hasMyTabbar = this.tabbarList.some((item) => item.pagePath === "/pages/tabbar/my")
_launchParam = getLaunchParam(launchParam) const hasWorkbenchTabbar = this.tabbarList.some((item) => item.pagePath === WORKBENCH_TABBAR_ITEM.pagePath)
} else { // 热更新或旧状态中可能已有相同场景但尚未加入工作台,此时仍需重新组装。
_launchParam = getLaunchParam() if (this.launchParam === _launchParam && (!hasMyTabbar || hasWorkbenchTabbar)) return
}
if (this.launchParam === _launchParam) return
const appInfo = await getAppInfo() const appInfo = await getAppInfo()
if (!_launchParam || !appInfo) return if (!_launchParam || !appInfo) return
const tabbarList: Array<TabbarItem> = [] const tabbarList: Array<TabbarItem> = []
TABBAR_CONSTANT.forEach((item, index) => { TABBAR_CONSTANT.forEach((item) => {
const visible = item.launchParam.includes(_launchParam) const visible = item.launchParam.includes(_launchParam)
if (visible) { if (visible) {
tabbarList.push(item) tabbarList.push(item)
} }
}) })
const myTabbarIndex = tabbarList.findIndex((item) => item.pagePath === "/pages/tabbar/my")
if (myTabbarIndex !== -1) {
// 仅对带“我的”的业务导航生效,并保证“工作台”紧邻其左侧。
tabbarList.splice(myTabbarIndex, 0, WORKBENCH_TABBAR_ITEM)
}
this.tabbarList = tabbarList this.tabbarList = tabbarList
this.launchParam = _launchParam this.launchParam = _launchParam
}, },
@@ -217,7 +273,7 @@ export const useTabbarStore = defineStore("tabbarStore", {
if (!firstTabbar) { if (!firstTabbar) {
return return
} }
jump(firstTabbar.pagePath, {}, 3) jump(firstTabbar.pagePath, {})
}, },
setCurrentRoute(route: string) { setCurrentRoute(route: string) {
if (route === this.currentRoute) { if (route === this.currentRoute) {
+38
View File
@@ -0,0 +1,38 @@
import CryptoJS from "crypto-js"
import { constant } from "@@/index"
const INSTALL_ID_KEY = "DEVICE_INSTALL_ID"
export async function buildUserDeviceFingerprint(force?: boolean) {
if (!force) {
const cacheResult = uni.getStorageSync(INSTALL_ID_KEY)
if (cacheResult) {
return cacheResult
}
}
const userInfo = uni.getStorageSync(constant.storageConstant.USERINFO)
const systemInfo = await uni.getSystemInfo()
const payload = {
userId: userInfo.sysUser.userId || "",
uniPlatform: systemInfo.uniPlatform || "",
hostName: systemInfo.hostName || "",
osName: systemInfo.osName || "",
osVersion: systemInfo.osVersion || "",
deviceModel: systemInfo.deviceModel || systemInfo.model || "",
deviceType: systemInfo.deviceType || "",
devicePixelRatio: String(systemInfo.devicePixelRatio || ""),
pixelRatio: String(systemInfo.pixelRatio || ""),
screenWidth: String(systemInfo.screenWidth || ""),
screenHeight: String(systemInfo.screenHeight || ""),
windowWidth: String(systemInfo.windowWidth || ""),
windowHeight: String(systemInfo.windowHeight || ""),
}
const raw = JSON.stringify(payload)
const fingerprint = CryptoJS.MD5(raw).toString()
const result = {
fingerprint,
payload,
}
uni.setStorageSync(INSTALL_ID_KEY, result)
return result
}
+15
View File
@@ -0,0 +1,15 @@
import * as CryptoJS from "crypto-js"
/**
*加密处理
*/
export function encryption(src: string, keyWord: string) {
const key = CryptoJS.enc.Utf8.parse(keyWord)
// 加密
var encrypted = CryptoJS.AES.encrypt(src, key, {
iv: key,
mode: CryptoJS.mode.CFB,
padding: CryptoJS.pad.NoPadding,
})
return encrypted.toString()
}
+163
View File
@@ -0,0 +1,163 @@
import * as CryptoJS from 'crypto-js'
export const FILE_ACCESS_FIELDS = ['appId', 'kid', 'ts', 'sign']
const FILE_PATHS = ['/sys-file/oss/file', '/admin/sys-file/oss/file', '/api/admin/sys-file/oss/file']
export interface FileAccessOptions {
appId: string; kid: string; key: string; baseUrl: () => string
allowedOrigins?: string[]; now?: () => number
}
const decode = (s: string) => decodeURIComponent(s.replace(/\+/g, ' '))
const encode = (s: string) => encodeURIComponent(s).replace(/[!'()*]/g, c => '%' + c.charCodeAt(0).toString(16).toUpperCase())
const origin = (s: string) => {
const match = /^(https?):\/\/([^/?#]+)/i.exec(s)
if (!match || match[2].includes('@')) return ''
return (match[1] + '://' + match[2]).toLowerCase().replace(match[1].toLowerCase() === 'https' ? /:443$/ : /:80$/, '')
}
export function fileAccessCanonical(file: string, app: string, kid: string, ts: number): string {
const name = CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(file)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
return ['v1', 'oss-file-read', app, kid, String(ts), name].join('\n')
}
export function fileAccessSignature(file: string, app: string, kid: string, ts: number, key: string): string {
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(key) || CryptoJS.enc.Base64.parse(key).sigBytes < 32) throw new Error('Invalid file access key')
return CryptoJS.HmacSHA256(fileAccessCanonical(file, app, kid, ts), CryptoJS.enc.Base64.parse(key)).toString(CryptoJS.enc.Hex)
}
export function fileUploadCanonical(target: string, app: string, kid: string, ts: number, nonce: string): string {
if (!['upload', 'inner-upload', 'qn-upload'].includes(target)) throw new Error('Invalid file upload target')
return ['v1', 'oss-file-upload', 'POST', target, app, kid, String(ts), nonce].join('\n')
}
export function fileUploadSignature(target: string, app: string, kid: string, ts: number, nonce: string, key: string): string {
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(key) || CryptoJS.enc.Base64.parse(key).sigBytes < 32) throw new Error('Invalid file upload key')
return CryptoJS.HmacSHA256(fileUploadCanonical(target, app, kid, ts, nonce), CryptoJS.enc.Base64.parse(key)).toString(CryptoJS.enc.Hex)
}
export function createFileAccess(options: FileAccessOptions) {
const now = options.now || Date.now
let offset = 0, ttl = 1800
const cache = new Map<string, { url: string; ts: number }>()
let uploadSequence = 0
const base = () => (options.baseUrl() || '').replace(/\/+$/, '')
const allowed = () => [origin(base()), ...(options.allowedOrigins || []).map(origin)].filter(Boolean)
function resolve(source?: string | null): string {
const raw = typeof source === 'string' ? source.trim() : ''
if (!raw || /^(?:data:|blob:|file:|wxfile:|content:|\/static\/|static\/|\/assets\/|\/src\/|@\/|#)/i.test(raw)) return raw
if (/^[a-z][a-z0-9+.-]*:/i.test(raw)) return raw
if (raw.startsWith('//')) return (base().startsWith('http:') ? 'http:' : 'https:') + raw
const root = base()
if (/^[^/?#:\\]+\.[a-z0-9]{1,10}$/i.test(raw)) return root + '/admin/sys-file/oss/file?fileName=' + encode(raw)
const path = raw.startsWith('/') ? raw : '/' + raw
if (/^(?:\/api)?\/admin\/sys-file\/oss\/(?:file|access-info)(?:[?#]|$)/.test(path)) return root + path.replace(/^\/api\//, '/')
const prefix = root.replace(/^https?:\/\/[^/?#]+/i, '')
if ((prefix && path.startsWith(prefix + '/')) || path.startsWith('/api/')) return (origin(root) || '') + path
if (path.startsWith('/sys-file/oss/')) return root + '/admin' + path
if (raw.startsWith('?')) return root + '/admin/sys-file/oss/file' + raw
return root + path
}
function parse(source?: string | null) {
const resolved = resolve(source)
const ownOrigin = origin(resolved)
if (ownOrigin && !allowed().includes(ownOrigin)) return null
if (/^[a-z][a-z0-9+.-]*:/i.test(resolved) && !ownOrigin) return null
const relative = ownOrigin ? resolved.replace(/^https?:\/\/[^/?#]+/i, '') : resolved
const path = relative.split(/[?#]/)[0]
const prefix = base().replace(/^https?:\/\/[^/?#]+/i, '')
if (!FILE_PATHS.includes(path) && path !== prefix + '/admin/sys-file/oss/file') return null
const hashAt = resolved.indexOf('#'), head = hashAt < 0 ? resolved : resolved.slice(0, hashAt)
const hash = hashAt < 0 ? '' : resolved.slice(hashAt)
const question = head.indexOf('?')
const endpoint = question < 0 ? head : head.slice(0, question)
const pairs = (question < 0 ? '' : head.slice(question + 1)).split('&').filter(Boolean).map(pair => {
const at = pair.indexOf('='); return [decode(at < 0 ? pair : pair.slice(0, at)), decode(at < 0 ? '' : pair.slice(at + 1))]
})
const names = pairs.filter(([key]) => key === 'fileName')
if (names.length !== 1 || !names[0][1] || /[\r\n]/.test(names[0][1])) throw new Error('Invalid file-center URL')
const extras = pairs.filter(([key]) => key !== 'fileName' && !FILE_ACCESS_FIELDS.includes(key))
const unsigned = endpoint + '?' + [...extras, ['fileName', names[0][1]]].map(([k, v]) => encode(k) + '=' + encode(v)).join('&') + hash
return { endpoint, file: names[0][1], extras, hash, unsigned }
}
function url(source?: string | null, force = false): string {
const parsed = parse(source)
if (!parsed) return resolve(source)
const seconds = Math.floor((now() + offset) / 1000), cached = cache.get(parsed.unsigned)
if (!force && cached && seconds >= cached.ts - 300 && seconds < cached.ts + ttl - Math.min(300, ttl / 6)) return cached.url
if (!options.appId || !options.kid) throw new Error('Missing file access configuration')
const sign = fileAccessSignature(parsed.file, options.appId, options.kid, seconds, options.key)
const pairs = [...parsed.extras, ['appId', options.appId], ['kid', options.kid], ['ts', String(seconds)], ['sign', sign], ['fileName', parsed.file]]
const result = parsed.endpoint + '?' + pairs.map(([k, v]) => encode(k) + '=' + encode(v)).join('&') + parsed.hash
if (cache.size >= 2048) cache.delete(cache.keys().next().value as string)
cache.set(parsed.unsigned, { url: result, ts: seconds })
return result
}
function strip(source?: string | null): string {
if (!source) return source || ''
const parsed = parse(source)
if (!parsed) return source
// Preserve the original origin/path form in persisted values.
const hashAt = source.indexOf('#'), hash = hashAt < 0 ? '' : source.slice(hashAt)
const head = hashAt < 0 ? source : source.slice(0, hashAt), at = head.indexOf('?')
if (at < 0) return source
const query = head.slice(at + 1).split('&').filter(p => !FILE_ACCESS_FIELDS.includes(decode(p.split('=')[0])))
return head.slice(0, at) + (query.length ? '?' + query.join('&') : '') + hash
}
function uploadTarget(source: string): string | null {
const resolved = resolve(source), host = origin(resolved)
if ((host && !allowed().includes(host)) || (/^[a-z][a-z0-9+.-]*:/i.test(resolved) && !host)) return null
const path = (host ? resolved.replace(/^https?:\/\/[^/?#]+/i, '') : resolved).split(/[?#]/)[0]
const prefix = base().replace(/^https?:\/\/[^/?#]+/i, '')
for (const [target, suffix] of [['upload', 'upload'], ['inner-upload', 'inner-upload'], ['qn-upload', 'qn/upload']]) {
if (['/sys-file/', '/admin/sys-file/', '/api/sys-file/', '/api/admin/sys-file/', prefix + '/admin/sys-file/', prefix + '/sys-file/']
.some(start => path === start + suffix)) return target
}
return null
}
function uploadHeaders(source: string, fixedNonce?: string): Record<string, string> {
const target = uploadTarget(source)
if (!target) return {}
const seconds = Math.floor((now() + offset) / 1000)
// The nonce is a unique request identifier, not a secret; HMAC provides authorization.
const nonce = fixedNonce || Math.floor(now()).toString(36) + '-' + (++uploadSequence).toString(36) + '-'
+ Array.from({ length: 4 }, () => Math.floor(Math.random() * 0x100000000).toString(16).padStart(8, '0')).join('')
return {
'X-File-App-Id': options.appId, 'X-File-Key-Id': options.kid, 'X-File-Timestamp': String(seconds), 'X-File-Nonce': nonce,
'X-File-Signature': fileUploadSignature(target, options.appId, options.kid, seconds, nonce, options.key),
}
}
function synchronize(serverTime: number, ttlSeconds = ttl, roundTripMs = 0) {
if (!Number.isFinite(serverTime) || serverTime <= 0) return
offset = serverTime + roundTripMs / 2 - now()
if (Number.isFinite(ttlSeconds) && ttlSeconds > 300 && ttlSeconds <= 86400) ttl = ttlSeconds
// url() checks cached timestamps against the corrected clock. Preserve usable
// signatures so routine calibration does not change every image's src.
}
function invalidate(source?: string | null) {
const parsed = parse(source)
if (parsed) cache.delete(parsed.unsigned)
}
return { uploadHeaders, uploadTarget, url, resolve, strip, protected: (s?: string | null) => !!parse(s), synchronize, invalidate, clear: () => cache.clear(), ttl: () => ttl }
}
const unescapeAttribute = (s: string) => s.replace(/&amp;/gi, '&').replace(/&quot;/gi, '"').replace(/&#(?:39|x27);/gi, "'").replace(/&#(x[0-9a-f]+|\d+);/gi, (_, n) => { const code = n[0].toLowerCase() === 'x' ? parseInt(n.slice(1), 16) : Number(n); return code <= 0x10ffff ? String.fromCodePoint(code) : ''; })
const escapeAttribute = (s: string) => s.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/'/g, '&#39;')
/** Rewrite only resource attributes; no DOM/window dependency (also used by mini programs). */
export function rewriteFileHtml(html: string, transform: (url: string) => string): string {
if (typeof html !== 'string' || !html) return html
return html.replace(/<[a-z][^>]*>/gi, tag => tag.replace(/\b(src|href|poster|srcset)\s*=\s*(["'])([\s\S]*?)\2/gi, (attribute, name, quote, raw) => {
const value = unescapeAttribute(raw)
const next = name.toLowerCase() === 'srcset' && !value.startsWith('data:')
? value.split(',').map(item => item.trim().replace(/^\S+/, transform)).join(', ')
: transform(value)
return next === value ? attribute : name + '=' + quote + escapeAttribute(next) + quote
}).replace(/url\(\s*(["']?)([^)'"\s]+)\1\s*\)/gi, (all, quote, value) => {
const next = transform(unescapeAttribute(value)); return next === value ? all : 'url(' + quote + escapeAttribute(next) + quote + ')'
}))
}
export function rewriteFileNodes(nodes: any, transform: (url: string) => string): any {
if (typeof nodes === 'string') return rewriteFileHtml(nodes, transform)
if (Array.isArray(nodes)) return nodes.map(node => rewriteFileNodes(node, transform))
if (!nodes || typeof nodes !== 'object') return nodes
const copy = { ...nodes }
if (copy.attrs) {
copy.attrs = { ...copy.attrs }
for (const key of ['src', 'href', 'poster']) if (typeof copy.attrs[key] === 'string') copy.attrs[key] = transform(copy.attrs[key])
}
if (copy.children) copy.children = rewriteFileNodes(copy.children, transform)
return copy
}
+159
View File
@@ -0,0 +1,159 @@
import { ref } from 'vue'
import { createFileAccess, rewriteFileHtml, rewriteFileNodes } from './file-access-core'
import { isDevEnv } from './app'
export const fileAccessRevision = ref(0)
const client = createFileAccess({
appId: import.meta.env.VITE_FILE_ACCESS_APP_ID,
kid: import.meta.env.VITE_FILE_ACCESS_KEY_ID,
key: import.meta.env.VITE_FILE_ACCESS_KEY,
baseUrl: () => isDevEnv() ? (import.meta.env.VITE_IMAGE_DEV_PATH || import.meta.env.VITE_APP_DEV_SERVICE) : import.meta.env.VITE_APP_PROD_IMAGE,
allowedOrigins: [import.meta.env.VITE_IMAGE_DEV_PATH, import.meta.env.VITE_APP_DEV_SERVICE, import.meta.env.VITE_APP_PROD_IMAGE, import.meta.env.VITE_APP_PROD_SERVICE, ...(import.meta.env.VITE_FILE_ACCESS_ALLOWED_ORIGINS || '').split(',')],
})
let timer: ReturnType<typeof setInterval> | undefined
let pending: Promise<void> | undefined
let lastSync = 0
const retried = new Map<string, number>()
const retrying = new Map<string, Promise<boolean>>()
export function fileUrl(source?: string | null): string {
void fileAccessRevision.value
try { return client.url(source) } catch { return '' }
}
export const accessFileUrl = (source?: string | null) => isProtectedFile(source) ? fileUrl(source) : (source || '')
export const fileUrls = (sources: any) => Array.isArray(sources) ? sources.map(accessFileUrl) : []
export const originalFileUrl = (source?: string | null) => client.strip(source)
export const resolveFileUrl = (source?: string | null) => client.resolve(source)
export const fileUploadHeaders = (source: string) => client.uploadHeaders(source)
export const fileHtml = (html: string) => rewriteFileHtml(html, accessFileUrl)
export const stripFileHtml = (html: string) => rewriteFileHtml(html, originalFileUrl)
export const fileNodes = (nodes: any) => rewriteFileNodes(nodes, accessFileUrl)
export const isProtectedFile = (source?: string | null) => { try { return client.protected(source) } catch { return false } }
export async function refreshFileAccess(force = false): Promise<void> {
if (pending) return pending
if (!force && Date.now() - lastSync < 60000) return
pending = (async () => {
const started = Date.now()
try {
const result: any = await readAccessInfo()
const info = result?.data || result
if (info?.serverTime) client.synchronize(Number(info.serverTime), Number(info.ttlSeconds), Date.now() - started)
lastSync = Date.now()
} catch { /* Retain the last clock offset and signed access; never fall back to a bare URL. */ }
finally { fileAccessRevision.value++; pending = undefined }
})()
return pending
}
export function startFileAccess() {
if (!timer) timer = setInterval(() => { fileAccessRevision.value++ }, 60000)
void refreshFileAccess()
}
export function stopFileAccess() { if (timer) clearInterval(timer); timer = undefined }
export async function retryFileAccess(source?: string | null): Promise<boolean> {
if (!isProtectedFile(source)) return false
const raw = resolveFileUrl(originalFileUrl(source)), previous = retried.get(raw) || 0
const active = retrying.get(raw)
if (active) return active
if (Date.now() - previous < 30 * 60 * 1000) return false
if (retried.size >= 2048) retried.clear()
retried.set(raw, Date.now())
const task = (async () => {
const previousUrl = fileUrl(source)
await refreshFileAccess(true)
client.invalidate(source)
if (fileUrl(source) === previousUrl) {
await new Promise(resolve => setTimeout(resolve, 1100))
client.invalidate(source)
}
fileAccessRevision.value++
return true
})()
retrying.set(raw, task)
try { return await task } finally { retrying.delete(raw) }
}
export function installFileAccess(app: any) {
if (typeof uni.addInterceptor === 'function') {
uni.addInterceptor('uploadFile', {
invoke(options: any) {
options.header = { ...(options.header || {}), ...fileUploadHeaders(options.url) }
},
})
}
Object.assign(app.config.globalProperties, {
$fileUrl: accessFileUrl, $fileUrls: fileUrls, $fileHtml: fileHtml, $fileNodes: fileNodes, $fileRetry: retryFileAccess,
})
if (typeof document !== 'undefined') {
document.addEventListener('click', event => {
const anchor = (event.target as Element | null)?.closest?.('a[href]')
const href = anchor?.getAttribute('href')
if (event.defaultPrevented || !href || !isProtectedFile(href)) return
event.preventDefault()
event.stopImmediatePropagation()
openFileLink(href)
}, true)
}
app.mixin({
onShow() { startFileAccess() },
})
uni.onNetworkStatusChange(({ isConnected }: any) => {
if (isConnected) { retried.clear(); void refreshFileAccess(true) }
})
}
function readAccessInfo(): Promise<any> {
return new Promise((resolve, reject) => uni.request({
url: client.resolve('/admin/sys-file/oss/access-info'), method: 'GET', timeout: 5000,
success: (response: any) => response.statusCode === 200 ? resolve(response.data) : reject(new Error('File clock unavailable')),
fail: reject,
}))
}
/** All preview entrypoints sign at the moment of opening, including already-open pages. */
export function previewFiles(options: any) {
const values = { ...options, urls: fileUrls(options.urls) }
if (typeof options.current === 'string') values.current = fileUrl(options.current)
return uni.previewImage(values)
}
/** One clock/signature retry. Only successful downloads are delivered to the caller. */
export function downloadFile(options: any) {
const run = (retry: boolean) => uni.downloadFile({
...options, url: fileUrl(options.url),
success: async (response: any) => {
if (response.statusCode === 200) { options.success?.(response); options.complete?.(response); return }
if (retry && response.statusCode === 403 && isProtectedFile(options.url)) {
await refreshFileAccess(true); client.invalidate(options.url); run(false); return
}
const error = { errMsg: '文件下载失败', statusCode: response.statusCode }
options.fail?.(error); options.complete?.(error)
if (!options.fail) uni.showToast({ title: error.errMsg, icon: 'none' })
},
complete: undefined,
fail: (error: any) => { options.fail?.(error); options.complete?.(error) },
})
return run(true)
}
/** Open owned rich-text file links without navigating the browser to the file endpoint. */
export function openFileLink(source: string) {
const match = /[?&]fileName=([^&#]*)/.exec(resolveFileUrl(source))
const filename = match ? decodeURIComponent(match[1].replace(/\+/g, ' ')) : '文件'
if (/\.(?:jpe?g|png|gif|webp|bmp|svg|ico)$/i.test(filename)) {
return previewFiles({ urls: [source] })
}
return downloadFile({
url: source,
success: (response: any) => {
if (typeof document !== 'undefined') {
const anchor = document.createElement('a')
anchor.href = response.tempFilePath
anchor.download = filename
document.body.appendChild(anchor)
anchor.click()
anchor.remove()
} else {
uni.openDocument({
filePath: response.tempFilePath, showMenu: true,
fail: () => uni.showToast({ title: '无法打开此文件', icon: 'none' }),
})
}
},
})
}
+10 -23
View File
@@ -1,24 +1,11 @@
import { isDevEnv } from "./app" export { fileUrl as filePath, originalFileUrl, stripFileHtml, downloadFile, previewFiles } from './file-access'
import { fileHtml } from './file-access'
const normalizeBase = (base: string | undefined) => { /** Preserve existing responsive article images while signing resource attributes. */
if (!base) return "" export const parseHtml = (html: string) => fileHtml(html || '').replace(/<img\b[^>]*>/gi, img => {
return base.endsWith("/") ? base.slice(0, -1) : base const required = 'max-width: 100%; height: auto; display: block;'
} const style = img.match(/\bstyle\s*=\s*(['"])([\s\S]*?)\1/i)
if (!style) return img.replace(/<img\b/i, `<img style="${required}"`)
const ensureLeadingSlash = (url: string) => { if (/(?:max-)?width\s*:/i.test(style[2])) return img
if (!url) return "" return img.replace(style[0], `style=${style[1]}${style[2].replace(/;?\s*$/, '; ')}${required}${style[1]}`)
return url.startsWith("/") ? url : `/${url}` })
}
export const filePath = (path?: string | null) => {
const rawPath = (path || "").trim()
if (!rawPath) return ""
if (/^https?:\/\//i.test(rawPath)) return rawPath
const devBase = normalizeBase(import.meta.env.VITE_IMAGE_DEV_PATH || import.meta.env.VITE_APP_DEV_SERVICE)
const prodBase = normalizeBase(import.meta.env.VITE_APP_PROD_IMAGE)
const base = isDevEnv() ? devBase : prodBase
if (!base) return rawPath
return `${base}${ensureLeadingSlash(rawPath)}`
}
+159 -56
View File
@@ -4,7 +4,7 @@ import { isEmpty } from "radash"
import * as router from "@@/router" import * as router from "@@/router"
import * as constant from "@@/constant" import * as constant from "@@/constant"
import { createCooldown, getBaseApiUrl } from "@@/utils/app" import { getBaseApiUrl } from "@@/utils/app"
const TOKEN_KEY = constant.storageConstant.TOKEN const TOKEN_KEY = constant.storageConstant.TOKEN
const USER_INFO_KEY = constant.storageConstant.USERINFO const USER_INFO_KEY = constant.storageConstant.USERINFO
@@ -12,14 +12,22 @@ const SKIP_AUTH_URLS = [
"/thirdparty/thirdPartyOAth2", "/thirdparty/thirdPartyOAth2",
"/sys/auth/refresh-token", "/sys/auth/refresh-token",
"/auth/oauth2/token", "/auth/oauth2/token",
"/third/third-mini-app-oath/auto-admin-login",
"/admin/user/forgot-password", "/admin/user/forgot-password",
"/admin/stats/access/track", "/admin/stats/access/track",
] ]
const SKIP_REFRESH_TOKEN = ["/sys/auth/refresh-token", "/auth/token/check_token"] const SKIP_REFRESH_TOKEN = ["/sys/auth/refresh-token", "/auth/token/check_token"]
const AUTH_EXPIRED_CODES = [401, 424]
const REFRESH_AHEAD_MS = 60 * 1000
let refreshTokenLock = true let refreshTokenPromise: Promise<GlobalSetting.TokenStorage | null> | null = null
const runLoginRedirect = createCooldown(1500) let loginExpiredHandled = false
const BASIC_AUTH = "Basic " + base64.encode(import.meta.env.VITE_OAUTH2_APP_CLIENT) 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({ const https = new Request({
baseURL: getBaseApiUrl(), baseURL: getBaseApiUrl(),
@@ -53,18 +61,31 @@ https.interceptors.request.use(async (config) => {
// 判断是否需要设置认证头 // 判断是否需要设置认证头
if (!SKIP_AUTH_URLS.some((url) => config.url?.startsWith(url))) { if (!SKIP_AUTH_URLS.some((url) => config.url?.startsWith(url))) {
if (!isEmpty(tokenStorage)) { if (!isEmpty(tokenStorage)) {
// token存在,检查是否过期 // 提前一分钟刷新,避免 token 在请求传输或服务端处理过程中到期。
if (isTokenExpires(tokenStorage) && !SKIP_REFRESH_TOKEN.some((url) => config.url?.startsWith(url)) && refreshTokenLock) { if (isTokenExpires(tokenStorage) && !SKIP_REFRESH_TOKEN.some((url) => config.url?.startsWith(url))) {
// 过期,刷新token // 所有并发请求都会等待同一个 refreshTokenPromise,不能让后续请求继续携带旧 token。
const newTokenStorage = await refreshToken() const newTokenStorage = await refreshToken()
if (newTokenStorage) { if (newTokenStorage) {
const tokenValue = getTokenValue(newTokenStorage) tokenValue = getTokenValue(newTokenStorage)
if (tokenValue) config_header.Authorization = tokenValue } else {
config.custom = { ...(config.custom || {}), authRefreshAttempted: true }
return Promise.reject({
statusCode: 424,
errMsg: "refresh token failed",
data: { msg: "登录已过期,请重新登录" },
config,
})
} }
} }
} }
config_header["Authorization"] = tokenValue if (tokenValue) {
config_header.Authorization = tokenValue
loginExpiredHandled = false
} else {
delete config_header.Authorization
}
} }
config.header = config_header
return config return config
}, Promise.reject) }, Promise.reject)
@@ -79,75 +100,157 @@ https.interceptors.response.use(
msg: null, msg: null,
}) })
} }
// 部分网关以 HTTP 200 + 业务码返回 token 过期,也要进入同一套刷新和重试流程。
if (
AUTH_EXPIRED_CODES.includes(Number(response.data?.code)) &&
!SKIP_AUTH_URLS.some((url) => response.config.url?.startsWith(url))
) {
return recoverExpiredRequest({
statusCode: Number(response.data.code),
data: response.data,
config: response.config,
})
}
return Promise.resolve(response.data) return Promise.resolve(response.data)
}, },
async (error) => { async (error) => {
uni.hideLoading() uni.hideLoading()
// 处理 424 错误(token过期) // HTTP 401/424 先刷新并重放原请求,只有刷新失败才要求重新登录。
if (error.statusCode === 424) { if (
// 判断是否在登录页面 AUTH_EXPIRED_CODES.includes(Number(error.statusCode)) &&
const currentPage = router.getCurrentPage() !SKIP_AUTH_URLS.some((url) => error.config?.url?.startsWith(url))
const currentRoute = currentPage?.route ) {
if (currentRoute === "pages/root/login" || currentRoute === "/pages/root/login") { return recoverExpiredRequest(error)
return
}
uni.removeStorageSync(TOKEN_KEY)
uni.removeStorageSync(USER_INFO_KEY)
runLoginRedirect(() => {
uni.showToast({ title: "登录已过期,请重新登录", icon: "none" })
uni.navigateTo({ url: "/pages/root/login" })
})
} }
// 显示错误信息 // 显示错误信息
else if (error?.data?.msg) { else if (error?.data?.msg) {
uni.showToast({ title: error.data.msg, icon: "none", mask: true }) uni.showToast({ title: error.data.msg, icon: "none", mask: true })
} }
return Promise.resolve(error.data) return Promise.resolve(buildErrorResponse(error))
}, },
) )
const unwrapTokenValue = (value: any): string | null => {
if (typeof value === "string" && value.trim()) return value.trim()
if (value && typeof value.tokenValue === "string" && value.tokenValue.trim()) return value.tokenValue.trim()
return null
}
// 从tokenStorage中读取tokenValue值 // 从tokenStorage中读取tokenValue值
const getTokenValue = (tokenStorage: GlobalSetting.TokenStorage): string | null => { const getTokenValue = (tokenStorage: GlobalSetting.TokenStorage): string | null => {
return tokenStorage?.accessToken ? `Bearer ${tokenStorage?.accessToken}` : null const value = unwrapTokenValue(tokenStorage?.accessToken) || unwrapTokenValue((tokenStorage as any)?.access_token)
return value ? `Bearer ${value}` : null
}
const getRefreshTokenValue = (tokenStorage: GlobalSetting.TokenStorage): string | null => {
return unwrapTokenValue(tokenStorage?.refreshToken) || unwrapTokenValue((tokenStorage as any)?.refresh_token)
}
const getExpiresAt = (tokenStorage: any, tokenKey: "accessToken" | "refreshToken", expiresKey: string): number => {
const raw = tokenStorage?.[expiresKey] || tokenStorage?.[tokenKey]?.expiresAt
if (!raw) return Number.NaN
return new Date(raw).getTime()
} }
// 检查tokenStorage中tokenValue是否过期 // 检查tokenStorage中tokenValue是否过期
const isTokenExpires = (tokenStorage: GlobalSetting.TokenStorage): boolean => { const isTokenExpires = (tokenStorage: GlobalSetting.TokenStorage): boolean => {
const { accessTokenExpiresIn } = tokenStorage const expiresAt = getExpiresAt(tokenStorage, "accessToken", "accessTokenExpiresIn")
const expiresDate = new Date(accessTokenExpiresIn).getTime() // 老版本缓存没有过期时间时交给服务端 401/424 兜底,避免把仍可用的 token 直接踢下线。
return new Date().getTime() >= expiresDate if (!Number.isFinite(expiresAt)) return false
return Date.now() + REFRESH_AHEAD_MS >= expiresAt
} }
async function refreshToken(): Promise<any> { const expiresInSecondsToIso = (seconds: any): string => {
refreshTokenLock = false const value = Number(seconds)
try { return Number.isFinite(value) && value > 0 ? new Date(Date.now() + value * 1000).toISOString() : ""
const tokenStorage = uni.getStorageSync(TOKEN_KEY) as GlobalSetting.TokenStorage }
const res: any = await https.middleware({
url: "/auth/oauth2/token", /** 将 OAuth 下划线格式和后台嵌套格式统一为项目使用的 TokenStorage。 */
header: { export const normalizeTokenStorage = (data: any, previous?: GlobalSetting.TokenStorage): GlobalSetting.TokenStorage | null => {
skipToken: "true", const accessToken = unwrapTokenValue(data?.accessToken) || unwrapTokenValue(data?.access_token)
Authorization: BASIC_AUTH, if (!accessToken) return null
"Content-Type": "application/x-www-form-urlencoded",
}, const refreshToken =
method: "POST", unwrapTokenValue(data?.refreshToken) || unwrapTokenValue(data?.refresh_token) || (previous ? getRefreshTokenValue(previous) : null) || ""
params: { refresh_token: tokenStorage.refreshToken, grant_type: "refresh_token", scope: "server" }, const accessTokenExpiresIn =
}) data?.accessTokenExpiresIn || data?.accessToken?.expiresAt || expiresInSecondsToIso(data?.expires_in)
const { data } = res || {} const refreshTokenExpiresIn =
if (data) { data?.refreshTokenExpiresIn ||
tokenStorage.accessToken = data?.accessToken?.tokenValue data?.refreshToken?.expiresAt ||
tokenStorage.refreshToken = data?.refreshToken?.tokenValue expiresInSecondsToIso(data?.refresh_expires_in) ||
tokenStorage.accessTokenExpiresIn = data?.accessToken?.expiresAt previous?.refreshTokenExpiresIn ||
tokenStorage.refreshTokenExpiresIn = data?.refreshToken?.expiresAt ""
uni.setStorageSync(TOKEN_KEY, tokenStorage)
return tokenStorage return { accessToken, refreshToken, accessTokenExpiresIn, refreshTokenExpiresIn }
}
async function refreshToken(): Promise<GlobalSetting.TokenStorage | null> {
if (refreshTokenPromise) return refreshTokenPromise
refreshTokenPromise = (async () => {
try {
const tokenStorage = uni.getStorageSync(TOKEN_KEY) as GlobalSetting.TokenStorage
const refreshTokenValue = getRefreshTokenValue(tokenStorage)
if (!refreshTokenValue) return null
const refreshExpiresAt = getExpiresAt(tokenStorage, "refreshToken", "refreshTokenExpiresIn")
if (Number.isFinite(refreshExpiresAt) && Date.now() >= refreshExpiresAt) return null
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: refreshTokenValue, grant_type: "refresh_token", scope: "server" },
})
const newTokenStorage = res?.code === 0 ? normalizeTokenStorage(res.data, tokenStorage) : null
if (!newTokenStorage) return null
uni.setStorageSync(TOKEN_KEY, newTokenStorage)
loginExpiredHandled = false
return newTokenStorage
} catch (error) {
return null
} finally {
refreshTokenPromise = null
}
})()
return refreshTokenPromise
}
const clearExpiredSession = () => {
// 多个并发请求同时失败时只提示和跳转一次。
if (loginExpiredHandled) return
loginExpiredHandled = true
uni.removeStorageSync(TOKEN_KEY)
uni.removeStorageSync(USER_INFO_KEY)
uni.showToast({ title: "登录已过期,请重新登录", icon: "none" })
router.jump("/pages/root/login")
}
const recoverExpiredRequest = async (error: any) => {
const config = error?.config || {}
const requestUrl = config.url || ""
const custom = config.custom || {}
const canRefresh = !SKIP_AUTH_URLS.some((url) => requestUrl.startsWith(url)) && !custom.authRefreshAttempted
if (canRefresh) {
const newTokenStorage = await refreshToken()
const authorization = newTokenStorage ? getTokenValue(newTokenStorage) : null
if (authorization) {
// 标记只重试一次,避免服务端持续返回 401/424 时形成请求死循环。
return https.middleware({
...config,
header: { ...(config.header || {}), Authorization: authorization },
custom: { ...custom, authRefreshAttempted: true },
})
} }
return null
} catch (error) {
return false
} finally {
refreshTokenLock = true
} }
clearExpiredSession()
return buildErrorResponse(error, "登录已过期,请重新登录")
} }
// 请求函数 // 请求函数
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Loaded 100 of 119 files, more files were not shown because too many files have changed in this diff. Show more