fix:图片访问
fix:权限修改
This commit is contained in:
1 parent
569884e804
commit
0fafe8762f
78 files changed
+759
-236
No files matched your search
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { startFileAccess, stopFileAccess } from "./sheep/utils/file-access"
|
||||
import { getLaunchParam, getUrlQueryParam } from "@/sheep/utils/app"
|
||||
import { getAppInfo } from "@@/utils/app"
|
||||
import { APPENV, INTEGRATED_LAUNCH_PARAMS } from "./sheep/constant"
|
||||
@@ -174,6 +175,7 @@ const setTabbar = (launchParam?: string, auth?: string) => {
|
||||
|
||||
export default {
|
||||
async onLaunch() {
|
||||
startFileAccess()
|
||||
try {
|
||||
const lp = getUrlQueryParam("launchParam")
|
||||
if (lp) {
|
||||
@@ -189,6 +191,7 @@ export default {
|
||||
}
|
||||
},
|
||||
async onShow() {
|
||||
startFileAccess()
|
||||
try {
|
||||
const { onZzdAppShowTrack } = await import("@/sheep/utils/zzd-aplus-track")
|
||||
await onZzdAppShowTrack()
|
||||
|
||||
+23
-21
@@ -1,13 +1,14 @@
|
||||
import { createSSRApp } from "vue"
|
||||
import "uno.css"
|
||||
import pinia from "../src/sheep/stores/index"
|
||||
|
||||
import App from "./App.vue"
|
||||
import tmUi from "./uni_modules/tm-ui"
|
||||
|
||||
// #ifdef H5
|
||||
import VConsole from "vconsole"
|
||||
import { installZzdNavigateTrack } from "@/sheep/utils/zzd-aplus-track"
|
||||
import { installFileAccess } from './sheep/utils/file-access'
|
||||
import { createSSRApp } from "vue"
|
||||
import "uno.css"
|
||||
import pinia from "../src/sheep/stores/index"
|
||||
|
||||
import App from "./App.vue"
|
||||
import tmUi from "./uni_modules/tm-ui"
|
||||
|
||||
// #ifdef H5
|
||||
import VConsole from "vconsole"
|
||||
import { installZzdNavigateTrack } from "@/sheep/utils/zzd-aplus-track"
|
||||
if (import.meta.env.VITE_VCONSOLE === "true") {
|
||||
new VConsole()
|
||||
}
|
||||
@@ -17,14 +18,15 @@ if (routeCompat) {
|
||||
}
|
||||
installZzdNavigateTrack()
|
||||
// #endif
|
||||
|
||||
export function createApp() {
|
||||
const app = createSSRApp(App)
|
||||
|
||||
app.use(pinia)
|
||||
app.use(tmUi)
|
||||
|
||||
return {
|
||||
app,
|
||||
}
|
||||
}
|
||||
|
||||
export function createApp() {
|
||||
const app = createSSRApp(App)
|
||||
installFileAccess(app)
|
||||
|
||||
app.use(pinia)
|
||||
app.use(tmUi)
|
||||
|
||||
return {
|
||||
app,
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,7 @@ const doSubmit = () => {
|
||||
:autoplay="true"
|
||||
>
|
||||
<swiper-item v-for="(v, i) in state.detail?.imagesArray" :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>
|
||||
|
||||
@@ -64,7 +64,7 @@ const doSubmit = () => {
|
||||
<view class="h-15rpx" />
|
||||
<view class="flex-col justify-between card p-y-20rpx p-x-30rpx">
|
||||
<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>
|
||||
<template #main-bottom>
|
||||
|
||||
@@ -18,7 +18,7 @@ const state = reactive<any>({
|
||||
// 重新设置图片路径
|
||||
const boxesList = computed(() => {
|
||||
return pageHook.list.map((item: any) => {
|
||||
return { ...item, images: filePath(item.images) }
|
||||
return { ...item, images: item.images }
|
||||
})
|
||||
})
|
||||
|
||||
@@ -89,7 +89,7 @@ onShow(() => {
|
||||
<view class="boxes-grid">
|
||||
<view v-for="item in boxesList" :key="item.id" class="box-card">
|
||||
<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">
|
||||
<text class="placeholder-icon">🍴</text>
|
||||
<text class="placeholder-text">餐桌</text>
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
<view class="bg-transparent rounded-lg overflow-hidden">
|
||||
<image
|
||||
class="w-180rpx h-180rpx rounded-md object-cover"
|
||||
:src="item.spuDetail.pic"
|
||||
:src="$fileUrl(item.spuDetail.pic)"
|
||||
@click.stop="hooks.usePreviewImage(item.spuDetail.pic)"
|
||||
/>
|
||||
@error="$fileRetry(item.spuDetail.pic)" />
|
||||
<view class="text-xs">{{ item.spuDetail.name }}</view>
|
||||
<view class="text-red-500 text-sm">¥{{ displayPrice(item.spuDetail.price) }}</view>
|
||||
</view>
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
<view class="absolute top-[-10rpx] right-[-10rpx] z-200" @click="doUnPutaway(v)">
|
||||
<image class="w-36rpx h-36rpx" src="@/static/page/关闭.svg" />
|
||||
</view>
|
||||
<image class="w-full h-150 rounded-2" :src="v.spuDetail.pic" @click.stop="hooks.usePreviewImage(v.spuDetail.pic)" />
|
||||
<image class="w-full h-150 rounded-2" :src="$fileUrl(v.spuDetail.pic)" @click.stop="hooks.usePreviewImage(v.spuDetail.pic)" @error="$fileRetry(v.spuDetail.pic)" />
|
||||
<view class="sub-text-22">
|
||||
{{ v.spuDetail.name?.length > 4 ? v.spuDetail.name.substring(0, 4) + "..." : v.spuDetail.name }}
|
||||
</view>
|
||||
@@ -105,7 +105,7 @@
|
||||
<tm-radio class="w-full h-full px-2" :value="item.id">
|
||||
<template v-slot:label="checked">
|
||||
<view class="flex items-center">
|
||||
<image class="w-130rpx h-130rpx" :src="item.pic" />
|
||||
<image class="w-130rpx h-130rpx" :src="$fileUrl(item.pic)" @error="$fileRetry(item.pic)" />
|
||||
<view class="ml-2 flex-1 flex-col">
|
||||
<tm-text font-size="30">{{ item.name }}</tm-text>
|
||||
<view class="flex justify-between my-1">
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
<tm-sheet v-for="(item, index) in state.cabinetPutawayList" :key="index" :margin="[0, 0, 0, 20]" :padding="15">
|
||||
<view class="flex items-center">
|
||||
<image class="w-180rpx h-180rpx" :src="item.spuDetail?.pic" />
|
||||
<image class="w-180rpx h-180rpx" :src="$fileUrl(item.spuDetail?.pic)" @error="$fileRetry(item.spuDetail?.pic)" />
|
||||
<view class="ml-2 flex-1 flex-col">
|
||||
<tm-text font-size="30">{{ item.spuDetail?.name }}</tm-text>
|
||||
<view class="flex justify-between mt-1">
|
||||
|
||||
@@ -50,8 +50,8 @@ const remove = () => {
|
||||
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 })"
|
||||
>
|
||||
<image v-if="props.mode == 'default'" class="size-180rpx rounded-12rpx" :src="props.pic" mode="aspectFill" />
|
||||
<image v-if="props.mode == 'cart'" class="size-120rpx 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="$fileUrl(props.pic)" mode="aspectFill" @error="$fileRetry(props.pic)" />
|
||||
<view class="flex-1 m-l-20rpx">
|
||||
<view class="sub-text-28 show-line">{{ props.name }}</view>
|
||||
<view class="m-t-10rpx flex-row items-center">
|
||||
|
||||
@@ -237,7 +237,7 @@ const doFood = () => {
|
||||
<swiper class="w-100vw h-30vh" circular :indicator-dots="true" indicator-active-color="#0779FD"
|
||||
indicator-color="#ffffff" :autoplay="true">
|
||||
<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>
|
||||
|
||||
@@ -246,8 +246,8 @@ const doFood = () => {
|
||||
<!-- start 简介 start -->
|
||||
<view class="h-15rpx" />
|
||||
<view class="flex-row card p-20rpx">
|
||||
<image class="w-180rpx h-180rpx rounded-12rpx" :src="filePath(state.canteenDetail.logoPic)"
|
||||
mode="aspectFill" />
|
||||
<image class="w-180rpx h-180rpx rounded-12rpx" :src="$fileUrl(filePath(state.canteenDetail.logoPic))"
|
||||
mode="aspectFill" @error="$fileRetry(filePath(state.canteenDetail.logoPic))" />
|
||||
<view class="flex-1 flex-col m-l-20rpx">
|
||||
<view class="flex-row items-center justify-between">
|
||||
<view class="flex-row-center sub-text-34">
|
||||
|
||||
@@ -51,7 +51,7 @@ const query = (page: number, pagesize: number) => {
|
||||
// 重新设置图片路径
|
||||
const foodList = computed(() => {
|
||||
return pageHook.list.map((item: any) => {
|
||||
return { ...item, picUrl: filePath(item.picUrl) }
|
||||
return { ...item, picUrl: item.picUrl }
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ const getDetail = async () => {
|
||||
<template>
|
||||
<crud-layout title="菜品详情" tabBarType="just-flot-back" bg="/static/page/blue-bg.png">
|
||||
<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="flex-col justify-between card p-y-20rpx p-x-30rpx">
|
||||
<view class="flex-row items-center justify-between">
|
||||
@@ -54,7 +54,7 @@ const getDetail = async () => {
|
||||
</view>
|
||||
<view v-if="state.detail.description" class="m-t-15rpx flex-col justify-between card p-y-20rpx p-x-30rpx">
|
||||
<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>
|
||||
|
||||
@@ -40,7 +40,7 @@ const getDetail = async () => {
|
||||
if (!isEmpty(data)) {
|
||||
const img = filePath(data.faceImg)
|
||||
applyForm.faceImg = [img]
|
||||
uploadFileUrl.value = img
|
||||
uploadFileUrl.value = data.faceImg
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -66,10 +66,10 @@ const doDel = (id: string) => {
|
||||
<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-2"
|
||||
:src="filePath(v.faceImg)"
|
||||
:src="$fileUrl(filePath(v.faceImg))"
|
||||
mode="aspectFill"
|
||||
@click.stop="hooks.usePreviewImage(filePath(v.faceImg))"
|
||||
/>
|
||||
@error="$fileRetry(filePath(v.faceImg))" />
|
||||
<view class="sub-text-30 mb-2">{{ v.username }}·{{ v.deptName }}</view>
|
||||
<view v-if="false" class="flex w-full items-center justify-end gap-2 second-text-26">
|
||||
<text class="botder_btn" @click="doEdit(v.id)">修改</text>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<image
|
||||
v-for="(img, idx) in imageList"
|
||||
:key="idx"
|
||||
:src="img.startsWith('http') || img.startsWith('https') ? img : filePath(img)"
|
||||
:src="$fileUrl(img.startsWith('http') || img.startsWith('https') ? img : filePath(img))"
|
||||
class="feedback-img"
|
||||
mode="aspectFill"
|
||||
@error="handleImageError(img)"
|
||||
|
||||
@@ -13,13 +13,13 @@
|
||||
</view>
|
||||
|
||||
<view v-if="imageList.length" class="image-list">
|
||||
<image v-for="(src, index) in imageList" :key="index" class="detail-image" :src="src" mode="aspectFill"
|
||||
@tap="previewImage(index)" />
|
||||
<image v-for="(src, index) in imageList" :key="index" class="detail-image" :src="$fileUrl(src)" mode="aspectFill"
|
||||
@tap="previewImage(index)" @error="$fileRetry(src)" />
|
||||
</view>
|
||||
|
||||
<view class="detail-section">
|
||||
<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>
|
||||
|
||||
@@ -39,11 +39,13 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { previewFiles } from '@/sheep/utils/file-access'
|
||||
|
||||
import { computed, ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import dayjs from 'dayjs'
|
||||
import lostAndFoundApi from '@/sheep/api/lostAndFound'
|
||||
import { filePath } from '@/sheep/utils/file'
|
||||
import { filePath, parseHtml } from '@/sheep/utils/file'
|
||||
|
||||
interface LostAndFoundDetail {
|
||||
id: string
|
||||
@@ -100,7 +102,7 @@ const imageList = computed(() => {
|
||||
const descriptionNodes = computed(() => {
|
||||
const desc = detail.value?.description
|
||||
if (!desc) return ''
|
||||
return desc?.replaceAll(`<img src="/api`, `<img src="https://stgl.pj.gov.cn/api`)
|
||||
return parseHtml(desc || '')
|
||||
})
|
||||
|
||||
const contactInfo = computed(() => {
|
||||
@@ -117,7 +119,7 @@ const formatDate = (date?: string) => {
|
||||
|
||||
const previewImage = (index: number) => {
|
||||
if (!imageList.value.length) return
|
||||
uni.previewImage({
|
||||
previewFiles({
|
||||
current: index,
|
||||
urls: imageList.value,
|
||||
})
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
@tap="viewDetail(item)"
|
||||
>
|
||||
<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">
|
||||
<text>地点:{{ item.location }}</text>
|
||||
<text>时间:{{ formatDate(item.dateFound) }}</text>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
<!-- 会议室信息 -->
|
||||
<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-title">{{ room.name }} <text class="room-capacity">{{ room.capacity }}人</text></view>
|
||||
<view class="room-address"><text>📍</text> {{ room.address }}</view>
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
</text>
|
||||
</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 class="timeline-bar">
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
</view>
|
||||
|
||||
<view class="detail-content">
|
||||
<rich-text :nodes="nodes || ''"></rich-text>
|
||||
<rich-text :nodes="$fileNodes(nodes || '')"></rich-text>
|
||||
</view>
|
||||
|
||||
<view v-if="detail.attachments && detail.attachments.length > 0" class="attachments">
|
||||
@@ -30,6 +30,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { parseHtml } from '@/sheep/utils/file'
|
||||
import { downloadFile as downloadProtectedFile } from '@/sheep/utils/file-access'
|
||||
|
||||
import { computed, ref } from "vue"
|
||||
import { onLoad } from "@dcloudio/uni-app"
|
||||
import { actCancel, actComApi, noticeApi } from "@/sheep/api"
|
||||
@@ -60,36 +63,11 @@ const getActDetail = async () => {
|
||||
const goGetCoupon = () => {
|
||||
uni.navigateTo({ url: `/pages/merchant/merchant-activity?actId=${actDetail.value.actId}&actType=${actDetail.value.actType}` })
|
||||
}
|
||||
const normalizeRichText = (html: string) => {
|
||||
const origin = "https://stgl.pj.gov.cn"
|
||||
return html.replace(/<img\b[^>]*>/gi, (imgTag) => {
|
||||
let next = imgTag
|
||||
|
||||
next = next.replace(/\bsrc\s*=\s*(['"])(.*?)\1/i, (_m, quote: string, src: string) => {
|
||||
let resolved = src
|
||||
if (resolved?.startsWith("/api")) resolved = `${origin}${resolved}`
|
||||
if (resolved?.startsWith("//")) resolved = `https:${resolved}`
|
||||
return `src=${quote}${resolved}${quote}`
|
||||
})
|
||||
|
||||
const styleMatch = next.match(/\bstyle\s*=\s*(['"])([\s\S]*?)\1/i)
|
||||
const required = "max-width: 100%; height: auto; display: block;"
|
||||
if (styleMatch) {
|
||||
const quote = styleMatch[1]
|
||||
const styleValue = styleMatch[2] ?? ""
|
||||
if (styleValue.includes("max-width") || styleValue.includes("width")) return next
|
||||
const merged = styleValue.trim().length ? `${styleValue.replace(/;?\s*$/, "; ")}${required}` : required
|
||||
return next.replace(/\bstyle\s*=\s*(['"])([\s\S]*?)\1/i, `style=${quote}${merged}${quote}`)
|
||||
}
|
||||
|
||||
return next.replace(/<img\b/i, `<img style="${required}"`)
|
||||
})
|
||||
}
|
||||
|
||||
const nodes = computed(() => {
|
||||
const html = detail.value.description
|
||||
if (!html) return ""
|
||||
return normalizeRichText(html)
|
||||
return parseHtml(html)
|
||||
})
|
||||
|
||||
function getTypeLabel(type: string) {
|
||||
@@ -126,7 +104,7 @@ function formatFileSize(size: number) {
|
||||
}
|
||||
|
||||
function downloadFile(file: any) {
|
||||
uni.downloadFile({
|
||||
downloadProtectedFile({
|
||||
url: file.url,
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
|
||||
@@ -31,10 +31,10 @@
|
||||
<image
|
||||
v-for="(img, idx) in imageList"
|
||||
:key="idx"
|
||||
:src="filePath(img)"
|
||||
:src="$fileUrl(filePath(img))"
|
||||
class="repair-img"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
@error="$fileRetry(filePath(img))" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -58,11 +58,11 @@
|
||||
<image
|
||||
v-for="(img, idx) in handleImageList"
|
||||
:key="idx"
|
||||
:src="filePath(img)"
|
||||
:src="$fileUrl(filePath(img))"
|
||||
class="handle-img"
|
||||
mode="aspectFill"
|
||||
@click="previewHandleImage(idx)"
|
||||
/>
|
||||
@error="$fileRetry(filePath(img))" />
|
||||
</view>
|
||||
<view class="reply-footer">
|
||||
<text class="handle-by" v-if="detail.handleBy">处理人:{{ detail.handleBy }}</text>
|
||||
@@ -73,6 +73,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { previewFiles } from '@/sheep/utils/file-access'
|
||||
|
||||
import { ref, computed } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import maintenanceApi from '@/sheep/api/maintenance'
|
||||
@@ -190,7 +192,7 @@ const hasHandleInfo = computed(() => {
|
||||
function previewHandleImage(index: number) {
|
||||
if (handleImageList.value.length === 0) return
|
||||
const urls = handleImageList.value.map(img => filePath(img))
|
||||
uni.previewImage({
|
||||
previewFiles({
|
||||
current: index,
|
||||
urls: urls
|
||||
})
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<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 || ''" />
|
||||
<rich-text :nodes="$fileNodes(richTextNodes || '')" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -16,7 +16,7 @@
|
||||
import { computed, ref } from "vue"
|
||||
import { onLoad } from "@dcloudio/uni-app"
|
||||
import sysBannerApi from "@/sheep/api/sysBanner"
|
||||
import { filePath } from "@/sheep/utils/file"
|
||||
import { filePath, parseHtml } from "@/sheep/utils/file"
|
||||
|
||||
const detail = ref<{ title?: string; richTextContent?: string }>({})
|
||||
const loading = ref(false)
|
||||
@@ -31,7 +31,7 @@ const toAbsoluteUrl = (url?: string | null) => {
|
||||
const richTextNodes = computed(() => {
|
||||
const html = detail.value.richTextContent
|
||||
if (!html) return ""
|
||||
return html?.replaceAll(`<img src="/api`, `<img style="width: 100%; height: auto;" src="https://stgl.pj.gov.cn/api`)
|
||||
return parseHtml(html || '')
|
||||
})
|
||||
|
||||
onLoad(async (options: any) => {
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
<text v-if="selectedUser?.id === employee.id" class="check-icon">✓</text>
|
||||
</view>
|
||||
<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>
|
||||
</view>
|
||||
<view class="user-info">
|
||||
@@ -239,7 +239,7 @@
|
||||
<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 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>
|
||||
</view>
|
||||
<text class="user-name">{{ user.name || '未知用户' }}</text>
|
||||
|
||||
@@ -112,14 +112,14 @@ const toDetail = async (v: any) => {
|
||||
<swiper class="w-full h-full" circular :indicator-dots="true" indicator-active-color="#0779FD"
|
||||
indicator-color="rgba(255,255,255,0.8)" :autoplay="true" interval="4000">
|
||||
<swiper-item v-for="(p, i) in swiperPics" :key="i">
|
||||
<image class="w-full h-full" :src="filePath(p)" mode="aspectFill" />
|
||||
<image class="w-full h-full" :src="$fileUrl(filePath(p))" mode="aspectFill" @error="$fileRetry(filePath(p))" />
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
</view>
|
||||
|
||||
<view class="card p-24rpx flex">
|
||||
<image class="w-160rpx h-160rpx rounded-16rpx bg-#F2F6FA my-auto flex-none" :src="filePath(state.detail.logoPic)"
|
||||
mode="aspectFill" />
|
||||
<image class="w-160rpx h-160rpx rounded-16rpx bg-#F2F6FA my-auto flex-none" :src="$fileUrl(filePath(state.detail.logoPic))"
|
||||
mode="aspectFill" @error="$fileRetry(filePath(state.detail.logoPic))" />
|
||||
<view class="flex-1 flex-col m-l-20rpx min-w-0">
|
||||
<view class="flex items-center">
|
||||
<text class="sub-text-30 font-bold flex-1 min-w-0 show-line">{{ state.detail.name }}</text>
|
||||
@@ -153,8 +153,8 @@ const toDetail = async (v: any) => {
|
||||
|
||||
<view v-if="state.parent" class="card p-20rpx m-t-20rpx flex items-center" hover-class="card-hover"
|
||||
@tap="toParentBrand">
|
||||
<image class="w-88rpx h-88rpx rounded-12rpx bg-#F2F6FA flex-none" :src="filePath(state.parent.logoPic)"
|
||||
mode="aspectFill" />
|
||||
<image class="w-88rpx h-88rpx rounded-12rpx bg-#F2F6FA flex-none" :src="$fileUrl(filePath(state.parent.logoPic))"
|
||||
mode="aspectFill" @error="$fileRetry(filePath(state.parent.logoPic))" />
|
||||
<view class="flex-1 min-w-0 m-l-16rpx">
|
||||
<view class="second-text-22">所属品牌</view>
|
||||
<view class="sub-text-28 font-medium m-t-2rpx show-line">{{ state.parent.name }}</view>
|
||||
@@ -169,8 +169,8 @@ const toDetail = async (v: any) => {
|
||||
</view>
|
||||
<view v-for="(v, i) in pageHook.list" :key="v.id ?? i" class="card p-20rpx m-b-20rpx flex"
|
||||
hover-class="card-hover" @tap="toDetail(v)">
|
||||
<image class="w-140rpx h-140rpx rounded-12rpx bg-#F2F6FA my-auto flex-none" :src="filePath(v.logoPic)"
|
||||
mode="aspectFill" />
|
||||
<image class="w-140rpx h-140rpx rounded-12rpx bg-#F2F6FA my-auto flex-none" :src="$fileUrl(filePath(v.logoPic))"
|
||||
mode="aspectFill" @error="$fileRetry(filePath(v.logoPic))" />
|
||||
<view class="flex-1 flex-col m-l-16rpx min-w-0">
|
||||
<view class="flex items-center">
|
||||
<text class="sub-text-28 font-bold flex-1 min-w-0 show-line">{{ v.name }}</text>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
:autoplay="true"
|
||||
>
|
||||
<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>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<view class="h-15rpx" />
|
||||
<!-- start 简介 start -->
|
||||
<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)" mode="aspectFill" />
|
||||
<image class="w-180rpx h-180rpx rounded-12rpx my-auto" :src="$fileUrl(filePath(state.detail.logoPic))" mode="aspectFill" @error="$fileRetry(filePath(state.detail.logoPic))" />
|
||||
<view class="flex-1 flex-col m-l-20rpx">
|
||||
<view class="sub-text-34">{{ state.detail.name }}</view>
|
||||
<view class="m-t-10rpx">
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
</template>
|
||||
</view>
|
||||
<view class="detail-content">
|
||||
<rich-text :nodes="nodes || ''"></rich-text>
|
||||
<rich-text :nodes="$fileNodes(nodes || '')"></rich-text>
|
||||
</view>
|
||||
<template #main-bottom>
|
||||
<view class="text-center">
|
||||
@@ -94,6 +94,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { parseHtml } from '@/sheep/utils/file'
|
||||
import { reactive, ref, nextTick, computed } from "vue"
|
||||
import { onLoad } from "@dcloudio/uni-app"
|
||||
|
||||
@@ -116,7 +117,7 @@ const state = reactive<any>({
|
||||
})
|
||||
|
||||
const nodes = computed(() => {
|
||||
return state.actDetail.description?.replaceAll(`<img src="/api`, `<img src="https://stgl.pj.gov.cn/api`)
|
||||
return parseHtml(state.actDetail.description || '')
|
||||
})
|
||||
|
||||
onLoad((option: any) => {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<crud-layout title="购物车" mainIsScroll bg="/static/page/blue-bg.png">
|
||||
<view class="h-20rpx"></view>
|
||||
<view v-if="cartStore.totalCount > 0" class="flex-row card px-15rpx py-25rpx mb-20rpx" v-for="(v, i) in cartStore.cart" :key="i">
|
||||
<image class="size-160rpx rounded-12rpx mr-20rpx bg-#eeeeee" :src="filePath(v.productPic)" mode="aspectFill" />
|
||||
<image class="size-160rpx rounded-12rpx mr-20rpx bg-#eeeeee" :src="$fileUrl(filePath(v.productPic))" mode="aspectFill" @error="$fileRetry(filePath(v.productPic))" />
|
||||
<view class="flex-1 overflow-hidden">
|
||||
<view class="sub-text-30 truncate">{{ v.productName }}</view>
|
||||
<view class="second-text-26 show-line">单位 {{ v.unit }}</view>
|
||||
|
||||
@@ -16,10 +16,10 @@
|
||||
<view class="flex items-center">
|
||||
<image
|
||||
class="size-160rpx bg-#eeeeee rounded-1 mr-2"
|
||||
:src="filePath(state.goodsDetail.picUrl)"
|
||||
:src="$fileUrl(filePath(state.goodsDetail.picUrl))"
|
||||
mode="aspectFill"
|
||||
@tap.stop="state.goodsDetail.picUrl && usePreviewImage(filePath(state.goodsDetail.picUrl))"
|
||||
/>
|
||||
@error="$fileRetry(filePath(state.goodsDetail.picUrl))" />
|
||||
<view class="flex-1 overflow-hidden">
|
||||
<view class="sub-text-32 truncate">{{ state.goodsDetail.productName }}</view>
|
||||
<view v-if="state.selectedSku.promotionPrice" class="second-text-32 c-red!">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
class="flex-row items-start card p-x-15rpx p-y-25rpx m-b-20rpx"
|
||||
@tap="props.productId && props.mode == 'default' && router.jump('/pages/online-shop/goods-detail', { id: props.productId })"
|
||||
>
|
||||
<image class="size-160rpx rounded-12rpx" :src="props.picUrl" mode="aspectFill" />
|
||||
<image class="size-160rpx rounded-12rpx" :src="$fileUrl(props.picUrl)" mode="aspectFill" @error="$fileRetry(props.picUrl)" />
|
||||
<view class="flex-1 m-l-20rpx">
|
||||
<view class="sub-text-28 show-line">{{ props.productName }}</view>
|
||||
<view v-if="mode === 'default'" class="flex-row items-center">
|
||||
|
||||
@@ -58,7 +58,7 @@ const goCart = async () => {
|
||||
:autoplay="true"
|
||||
>
|
||||
<swiper-item v-for="(v, i) in state.detail?.picUrl?.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>
|
||||
|
||||
@@ -103,7 +103,7 @@ const goCart = async () => {
|
||||
|
||||
<view class="flex-col justify-between card py-20rpx px-30rpx">
|
||||
<view class="prefix_title mb-15rpx">详情描述</view>
|
||||
<view class="second-text-28 px-3" v-html="state.detail?.description"></view>
|
||||
<view class="second-text-28 px-3" v-html="$fileHtml(state.detail?.description)"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
@@ -123,8 +123,8 @@ const doAgreeRefund = async () => {
|
||||
<view class="flex-row-center m-t-30rpx"
|
||||
v-if="state.detail.orderStatus === '5' && ['2', '7'].includes(state.detail.orderType)">
|
||||
<view class="flex-row-center bg-white w-550rpx rounded-16rpx">
|
||||
<image class="w-500rpx" :src="filePath(state.detail.reserveCode)" mode="widthFix"
|
||||
@click.stop="hooks.usePreviewImage(filePath(state.detail.reserveCode))" />
|
||||
<image class="w-500rpx" :src="$fileUrl(filePath(state.detail.reserveCode))" mode="widthFix"
|
||||
@click.stop="hooks.usePreviewImage(filePath(state.detail.reserveCode))" @error="$fileRetry(filePath(state.detail.reserveCode))" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="h-30rpx" />
|
||||
@@ -167,7 +167,7 @@ const doAgreeRefund = async () => {
|
||||
<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-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="sub-text-28">{{ v.productName }}</view>
|
||||
<view class="m-t-5rpx color-red">¥ {{ hooks.useMoney(v.productPrice) }}</view>
|
||||
@@ -225,7 +225,7 @@ const doAgreeRefund = async () => {
|
||||
<view>补充图片:</view>
|
||||
<view class="flex items-center gap-10rpx">
|
||||
<image v-for="(v, i) in applyPicUrlList" :key="i" class="size-100rpx rounded-8rpx"
|
||||
:src="filePath(v)" mode="scaleToFill" @tap="hooks.usePreviewImage(filePath(v))" />
|
||||
:src="$fileUrl(filePath(v))" mode="scaleToFill" @tap="hooks.usePreviewImage(filePath(v))" @error="$fileRetry(filePath(v))" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="flex-row items-center">
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
:class="{ 'border-4rpx border-solid border-blue!': selectWallet.id == v.id }"
|
||||
@click="selectWallet = v">
|
||||
<view class="flex items-center">
|
||||
<image class="w-50 h-50 mr-8rpx" :src="filePath(v.walletTypeIcon)" />
|
||||
<image class="w-50 h-50 mr-8rpx" :src="$fileUrl(filePath(v.walletTypeIcon))" @error="$fileRetry(filePath(v.walletTypeIcon))" />
|
||||
<view class="text-32">{{ v.walletTypeName }}</view>
|
||||
</view>
|
||||
<view class="text-28 flex items-center">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<view class="h-20rpx"></view>
|
||||
<view class="p-4 card flex items-center justify-between">
|
||||
<view class="flex items-center">
|
||||
<image class="w-64 h-64 mr-2" :src="filePath(payStore.sourceWalletData.walletTypeIcon)" />
|
||||
<image class="w-64 h-64 mr-2" :src="$fileUrl(filePath(payStore.sourceWalletData.walletTypeIcon))" @error="$fileRetry(filePath(payStore.sourceWalletData.walletTypeIcon))" />
|
||||
<view>{{ payStore.sourceWalletData.walletTypeName }}</view>
|
||||
</view>
|
||||
<view class="flex items-center c-#0094ff! font-bold">
|
||||
@@ -51,7 +51,7 @@
|
||||
:class="{ 'border-4 border-solid border-blue!': targetWalletType.id == v.walletTypeId }"
|
||||
@click="selectTargetWalletType(v)">
|
||||
<view class="flex items-center">
|
||||
<image class="w-70 h-70 mr-2" :src="filePath(v.walletTypeIcon)" />
|
||||
<image class="w-70 h-70 mr-2" :src="$fileUrl(filePath(v.walletTypeIcon))" @error="$fileRetry(filePath(v.walletTypeIcon))" />
|
||||
<view class="flex flex-col">
|
||||
<view class="main-text-32">{{ v.walletTypeName }}</view>
|
||||
<view class="text-gray-500 text-sm">{{ v.cardNumber }}</view>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<crud-layout mainIsScroll bg="/static/page/blue-bg.png">
|
||||
<view class="h-40rpx"></view>
|
||||
<view class="card flex justify-between items-center mb-2 mx-20rpx p-4" v-for="item in categoryList" :key="item.id">
|
||||
<image :src="filePath(item.picUrl)" class="size-100rpx mr-4" />
|
||||
<image :src="$fileUrl(filePath(item.picUrl))" class="size-100rpx mr-4" @error="$fileRetry(filePath(item.picUrl))" />
|
||||
<view class="flex-1 flex-col">
|
||||
<view class="flex justify-between items-center">
|
||||
<text class="sub-text-30">{{ item.name }}</text>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<tm-drawer z-index="800" v-model:show="modelValue" showClose title="购物车" size="80%" bgColor="#ECF4F9" @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">
|
||||
<image class="size-160rpx rounded-12rpx mr-20rpx bg-#eeeeee" :src="filePath(v.productPic)" mode="aspectFill" />
|
||||
<image class="size-160rpx rounded-12rpx mr-20rpx bg-#eeeeee" :src="$fileUrl(filePath(v.productPic))" mode="aspectFill" @error="$fileRetry(filePath(v.productPic))" />
|
||||
<view class="flex-1 overflow-hidden">
|
||||
<view class="sub-text-30 truncate">{{ v.productName }}</view>
|
||||
<view class="second-text-26 show-line">单位 {{ v.unit }}</view>
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
<view class="flex items-center">
|
||||
<image
|
||||
class="size-160rpx bg-#eeeeee rounded-1 mr-2"
|
||||
:src="filePath(goodsInfo.coverImage)"
|
||||
:src="$fileUrl(filePath(goodsInfo.coverImage))"
|
||||
mode="aspectFill"
|
||||
@tap.stop="goodsInfo.coverImage && usePreviewImage(filePath(goodsInfo.coverImage))"
|
||||
/>
|
||||
@error="$fileRetry(filePath(goodsInfo.coverImage))" />
|
||||
<view class="flex-1 overflow-hidden">
|
||||
<view class="sub-text-32 truncate">{{ goodsInfo.title }}</view>
|
||||
<view class="second-text-32 c-red!">
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
:autoplay="true"
|
||||
>
|
||||
<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>
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
|
||||
<view class="mt-20rpx card p-20rpx mx-20rpx">
|
||||
<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 class="h-30rpx" />
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
import { onLoad } from "@dcloudio/uni-app"
|
||||
import { computed, reactive } from "vue"
|
||||
import { prosperityShopApi } from "@/sheep/api"
|
||||
import { filePath } from "@/sheep/utils/file"
|
||||
import { filePath, parseHtml } from "@/sheep/utils/file"
|
||||
import { displayPrice } from "@/sheep/utils/formatMoney"
|
||||
import ProductTotal from "./components/product-total.vue"
|
||||
import ProductSku from "./components/product-sku.vue"
|
||||
@@ -98,7 +98,7 @@ const state = reactive<any>({
|
||||
})
|
||||
const goodsDetail = computed(() => state.goodsDetail)
|
||||
const description = computed(() => {
|
||||
return goodsDetail.value.description?.replaceAll(`<img`, `<img style="width: 100%;"`)
|
||||
return parseHtml(goodsDetail.value.description || '')
|
||||
})
|
||||
|
||||
onLoad(async (option: any) => {
|
||||
|
||||
@@ -52,11 +52,11 @@
|
||||
<view v-for="v in pageHook.list" :key="v.id" class="card mb-2 px-4 py-2">
|
||||
<view class="flex">
|
||||
<image
|
||||
:src="filePath(v.coverImage)"
|
||||
:src="$fileUrl(filePath(v.coverImage))"
|
||||
class="size-180rpx rounded-12rpx bg-#eeeeee"
|
||||
mode="aspectFill"
|
||||
@click.stop="hooks.usePreviewImage(filePath(v.coverImage))"
|
||||
/>
|
||||
@error="$fileRetry(filePath(v.coverImage))" />
|
||||
<view class="flex-1 ml-2 overflow-hidden">
|
||||
<view class="truncate sub-text-32">{{ v.title }}</view>
|
||||
<view class="sub-text-24 my-6rpx">
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
</template>
|
||||
|
||||
<view v-for="v in pageHook.list" :key="v.id" class="card flex mx-20rpx mb-2 p-2" @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-2 overflow-hidden">
|
||||
<view class="truncate sub-text-30">{{ v.title }}</view>
|
||||
<view>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<view class="prefix_title text-36rpx mb-2">自提点</view>
|
||||
<view class="flex-row items-center justify-between" @click="onPickupPoint">
|
||||
<view v-if="curPickup.id" class="sub-text-30 mb-2 flex items-center">
|
||||
<image class="size-80rpx rounded-1 mr-2" :src="filePath(curPickup.logo)" />
|
||||
<image class="size-80rpx rounded-1 mr-2" :src="$fileUrl(filePath(curPickup.logo))" @error="$fileRetry(filePath(curPickup.logo))" />
|
||||
<view>
|
||||
<view>{{ curPickup.name }}</view>
|
||||
<view class="sub-text-26">{{ curPickup.phone }}</view>
|
||||
@@ -29,7 +29,7 @@
|
||||
<view class="card p-20rpx mb-20rpx">
|
||||
<view class="prefix_title text-36rpx mb-2">我的购买</view>
|
||||
<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)" mode="aspectFill" />
|
||||
<image class="size-140rpx rounded-12rpx mr-20rpx bg-#eeeeee" :src="$fileUrl(filePath(v.productPic))" mode="aspectFill" @error="$fileRetry(filePath(v.productPic))" />
|
||||
<view class="flex-1 overflow-hidden">
|
||||
<view class="sub-text-30 truncate">{{ v.productName }}</view>
|
||||
<view class="flex justify-between items-center">
|
||||
@@ -76,7 +76,7 @@
|
||||
<tm-radio v-for="(v, i) in pageHook.list" :key="i" class="w-full h-full px-2" :value="v.id">
|
||||
<template v-slot:label="checked">
|
||||
<view class="flex items-center mb-1">
|
||||
<image class="size-140rpx rounded-12rpx mr-2" :src="filePath(v.logo)" mode="aspectFill" />
|
||||
<image class="size-140rpx rounded-12rpx mr-2" :src="$fileUrl(filePath(v.logo))" mode="aspectFill" @error="$fileRetry(filePath(v.logo))" />
|
||||
<view class="flex-1 overflow-hidden">
|
||||
<view class="sub-text-30 truncate">{{ v.name }}</view>
|
||||
<view class="sub-text-26">{{ v.phone }}</view>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<crud-layout mainIsScroll bg="/static/page/blue-bg.png" @scroll="onScroll">
|
||||
<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>
|
||||
<view
|
||||
@@ -43,7 +43,7 @@
|
||||
style="height: calc(100vh - 420rpx)"
|
||||
>
|
||||
<view v-for="v in state.goodsList" :key="v.id" class="card flex mx-15rpx mb-2 p-1" @click="doGoodsDetail(v.id)">
|
||||
<image :src="filePath(v.coverImage)" class="size-170rpx rounded-12rpx bg-#eeeeee" mode="aspectFill" />
|
||||
<image :src="$fileUrl(filePath(v.coverImage))" class="size-170rpx rounded-12rpx bg-#eeeeee" mode="aspectFill" @error="$fileRetry(filePath(v.coverImage))" />
|
||||
<view class="flex-1 ml-2 overflow-hidden">
|
||||
<view class="truncate sub-text-30">{{ v.title }}</view>
|
||||
<view>
|
||||
|
||||
@@ -125,7 +125,7 @@ const doSubmit = async () => {
|
||||
<view class="flex-row items-start justify-between" v-for="(v, i) in state.shopCarList" :key="i"
|
||||
@tap.stop="router.jump('/pages/canteen/product-detail', { id: v.productId })">
|
||||
<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="sub-text-28">{{ v.productName }}</view>
|
||||
<view class="m-t-5rpx">¥ {{ hooks.useMoney(v.productPrice) }}</view>
|
||||
|
||||
@@ -54,7 +54,7 @@ const toDetail = async (v: any) => {
|
||||
>
|
||||
<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)">
|
||||
<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-row items-center justify-between">
|
||||
<view class="flex-row-center sub-text-34">{{ v.name }}</view>
|
||||
|
||||
@@ -136,8 +136,8 @@ const toBranchDetail = async (b: any) => {
|
||||
<view class="card m-b-20rpx overflow-hidden" hover-class="card-hover" v-for="(v, i) in pageHook.list"
|
||||
:key="v.id ?? i" @tap="toDetail(v)">
|
||||
<view class="flex p-24rpx">
|
||||
<image class="w-160rpx h-160rpx rounded-16rpx bg-#F2F6FA my-auto flex-none" :src="filePath(v.logoPic)"
|
||||
mode="aspectFill" />
|
||||
<image class="w-160rpx h-160rpx rounded-16rpx bg-#F2F6FA my-auto flex-none" :src="$fileUrl(filePath(v.logoPic))"
|
||||
mode="aspectFill" @error="$fileRetry(filePath(v.logoPic))" />
|
||||
<view class="flex-1 flex-col m-l-20rpx min-w-0">
|
||||
<view class="flex items-center">
|
||||
<text class="sub-text-30 font-bold flex-1 min-w-0 show-line">{{ v.name }}</text>
|
||||
@@ -188,7 +188,7 @@ const toBranchDetail = async (b: any) => {
|
||||
<view v-for="(b, bi) in v._children || []" :key="b.id ?? bi" class="branch-item"
|
||||
hover-class="branch-item-hover" @tap.stop="toBranchDetail(b)">
|
||||
<image v-if="b.logoPic" class="w-56rpx h-56rpx rounded-10rpx bg-#F2F6FA flex-none"
|
||||
:src="filePath(b.logoPic)" mode="aspectFill" />
|
||||
:src="$fileUrl(filePath(b.logoPic))" mode="aspectFill" @error="$fileRetry(filePath(b.logoPic))" />
|
||||
<view v-else class="branch-logo-fallback flex-row-center">
|
||||
<tm-icon name="store-2-line" color="#9BBDF6" :size="28" />
|
||||
</view>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<view class="bg-white rounded-16rpx p-2 mb-2" v-for="(v, i) in pageHook.list" :key="i">
|
||||
<view class="flex justify-between items-center text-sm text-gray-800 mb-1">
|
||||
<view class="flex items-center">
|
||||
<image class="w-50rpx h-50rpx rounded-8rpx" :src="filePath(v.logoPic)" mode="aspectFill" />
|
||||
<image class="w-50rpx h-50rpx rounded-8rpx" :src="$fileUrl(filePath(v.logoPic))" mode="aspectFill" @error="$fileRetry(filePath(v.logoPic))" />
|
||||
<view class="ml-2">{{ v.merchantName }}</view>
|
||||
</view>
|
||||
<view>{{ v.day }}</view>
|
||||
|
||||
@@ -259,7 +259,7 @@ const doModuleJump = (item: any) => {
|
||||
:key="index">
|
||||
<view class="w-half text-center rounded-1 active:bg-#e8e8e8 duration-200"
|
||||
@tap="doModuleJump(item)">
|
||||
<image class="w-80 h-80 px-2 pt-2" :src="item.iconPath" />
|
||||
<image class="w-80 h-80 px-2 pt-2" :src="$fileUrl(item.iconPath)" @error="$fileRetry(item.iconPath)" />
|
||||
<tm-text font-size="32">{{ item.text }}</tm-text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div class="service-grid">
|
||||
<div v-for="item in services" :key="item.name" class="service-item" @click="goPage(item)">
|
||||
<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">{{ todoCount > 99 ? "99+" :
|
||||
todoCount }}</view>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<view class="flex items-center py-3 px-5">
|
||||
<view class="flex items-center" v-if="state.userInfo?.sysUser?.userId" @tap="doUserArea">
|
||||
<image v-if="state.userInfo?.sysUser?.avatar" 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"
|
||||
mode="aspectFill" />
|
||||
<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"
|
||||
@tap="onTabMenu(v)">
|
||||
<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>
|
||||
</view>
|
||||
<view class="iconfont icon-youjiantou" />
|
||||
|
||||
@@ -72,8 +72,8 @@
|
||||
:class="[state.curMerchantRadioId == item.id ? 'bg-#e8e8e8' : '']" :value="item.id">
|
||||
<template v-slot:label="checked">
|
||||
<view class="flex items-center">
|
||||
<image class="size-80rpx rounded-2 mr-2" :src="filePath(item.logoPic)"
|
||||
mode="aspectFill" />
|
||||
<image class="size-80rpx rounded-2 mr-2" :src="$fileUrl(filePath(item.logoPic))"
|
||||
mode="aspectFill" @error="$fileRetry(filePath(item.logoPic))" />
|
||||
<tm-text font-size="30">{{ item.name }}</tm-text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -91,7 +91,7 @@ const doRefund = (v: any) => {
|
||||
|
||||
<view class="flex-col justify-center card p-3 m-b-20rpx" v-for="(v, i) in pageHook.list" :key="i">
|
||||
<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-row items-center justify-between">
|
||||
<view class="sub-text-30">{{ v.merchantName }}</view>
|
||||
@@ -138,9 +138,9 @@ const doRefund = (v: any) => {
|
||||
<view class="flex items-center w-6/10">
|
||||
<view class="size-100rpx">
|
||||
<image v-if="v.payType == '9'" class="size-100rpx rounded-8rpx"
|
||||
:src="pV.productPic" mode="aspectFill" />
|
||||
<image v-else class="size-100rpx rounded-8rpx" :src="filePath(pV.productPic)"
|
||||
mode="aspectFill" />
|
||||
:src="$fileUrl(pV.productPic)" mode="aspectFill" @error="$fileRetry(pV.productPic)" />
|
||||
<image v-else class="size-100rpx rounded-8rpx" :src="$fileUrl(filePath(pV.productPic))"
|
||||
mode="aspectFill" @error="$fileRetry(filePath(pV.productPic))" />
|
||||
</view>
|
||||
<view class="ml-2 overflow-hidden">
|
||||
<view class="sub-text-26 truncate">{{ pV.productName }}</view>
|
||||
|
||||
@@ -36,8 +36,8 @@
|
||||
|
||||
<view class="card p-20rpx mb-20rpx" v-for="(v, i) in pageHook.list" :key="i" @tap="toDetail(v)">
|
||||
<view class="flex">
|
||||
<image class="w-160rpx h-160rpx bg-#eeeeee rounded-12rpx" :src="filePath(v.images)"
|
||||
mode="aspectFill" />
|
||||
<image class="w-160rpx h-160rpx bg-#eeeeee rounded-12rpx" :src="$fileUrl(filePath(v.images))"
|
||||
mode="aspectFill" @error="$fileRetry(filePath(v.images))" />
|
||||
<view class="ml-20rpx flex-1 overflow-hidden">
|
||||
<view class="sub-text-32 truncate">{{ v.name }}</view>
|
||||
<view class="second-text-26 flex justify-between my-6rpx">
|
||||
@@ -57,8 +57,8 @@
|
||||
<view class="flex mt-2">
|
||||
<view class="w-180rpx flex-col items-center mr-20rpx" v-for="(pV, pI) in v.featuredProducts"
|
||||
:key="pI">
|
||||
<image class="size-180rpx rounded-12rpx bg-#eeeeee" :src="filePath(pV.coverImage)"
|
||||
mode="aspectFill" />
|
||||
<image class="size-180rpx rounded-12rpx bg-#eeeeee" :src="$fileUrl(filePath(pV.coverImage))"
|
||||
mode="aspectFill" @error="$fileRetry(filePath(pV.coverImage))" />
|
||||
<view class="w-full mt-4rpx sub-text-24 truncate">{{ pV.title }}</view>
|
||||
<view class="w-full flex justify-between">
|
||||
<text class="text-26 color-red!">
|
||||
|
||||
@@ -94,7 +94,7 @@ const toDetail = async (v: any) => {
|
||||
</template>
|
||||
|
||||
<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="sub-text-34">
|
||||
<text class="mr-2">{{ v.name }}</text>
|
||||
|
||||
@@ -41,7 +41,7 @@ const toDetail = async (v: any) => {
|
||||
:auto-clean-list-when-reload="false" :auto-scroll-to-top-when-reload="false" @scroll="pageHook.scroll">
|
||||
<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)">
|
||||
<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-row items-center justify-between">
|
||||
<view class="flex-row-center sub-text-34">{{ v.name }}</view>
|
||||
|
||||
@@ -62,7 +62,7 @@ const toDetail = async (v: any) => {
|
||||
</view>
|
||||
</view>
|
||||
<view class="flex-col card p-20rpx m-b-20rpx" v-for="(v, i) in pageHook.list" :key="i" @tap="toDetail(v)">
|
||||
<image class="w-100% h-300rpx rounded-12rpx" :src="filePath(v.logoPic)" mode="aspectFill" />
|
||||
<image class="w-100% h-300rpx 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-row items-center justify-between mt-20rpx">
|
||||
<view class="flex-row-center sub-text-34">{{ v.name }}</view>
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<view class="flex-col justify-center card p-3 m-b-20rpx border-2rpx border-solid border-color-#eeeeee"
|
||||
v-for="(v, i) in pageHook.list" :key="i">
|
||||
<view class="flex-row">
|
||||
<image class="w-160rpx h-212rpx rounded-8rpx" :src="filePath(v.logoPic)" mode="aspectFill" />
|
||||
<image class="w-160rpx h-212rpx rounded-8rpx" :src="$fileUrl(filePath(v.logoPic))" mode="aspectFill" @error="$fileRetry(filePath(v.logoPic))" />
|
||||
<view class="flex-1 m-l-20rpx">
|
||||
<view class="flex-row items-center justify-between">
|
||||
<view class="flex-row items-center">
|
||||
@@ -149,8 +149,8 @@
|
||||
|
||||
<view class="flex-col justify-center card p-x-15rpx p-y-25rpx m-b-10rpx">
|
||||
<view class="flex-row items-center">
|
||||
<image class="w-100rpx h-100rpx rounded-8rpx" :src="filePath(state.curOrder.logoPic)"
|
||||
mode="aspectFill" />
|
||||
<image class="w-100rpx h-100rpx rounded-8rpx" :src="$fileUrl(filePath(state.curOrder.logoPic))"
|
||||
mode="aspectFill" @error="$fileRetry(filePath(state.curOrder.logoPic))" />
|
||||
<view class="flex-1 flex-col m-l-20rpx">
|
||||
<view class="flex-row items-center justify-between">
|
||||
<view class="flex-row items-center">
|
||||
|
||||
@@ -162,7 +162,7 @@ const doTransferMoney = (v: any) => {
|
||||
</view>
|
||||
<view class="flex-row items-center justify-between m-t-10rpx">
|
||||
<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">
|
||||
{{ v.walletTypeName }}
|
||||
</text>
|
||||
|
||||
@@ -97,7 +97,7 @@ const doPay = async () => {
|
||||
<view class="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-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 class="flex-row-center">
|
||||
<view class="m-l-25rpx main-text-32 color-#fbddb0!">
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
<view v-if="state.waterData.merchantId" class="bg-white rounded-16rpx p-3 mb-2 shadow-md">
|
||||
<view class="flex justify-between items-center text-sm text-gray-800 mb-1">
|
||||
<view class="flex items-center">
|
||||
<image class="w-50rpx h-50rpx rounded-8rpx" :src="filePath(state.waterData.logoPic)"
|
||||
mode="aspectFill" />
|
||||
<image class="w-50rpx h-50rpx rounded-8rpx" :src="$fileUrl(filePath(state.waterData.logoPic))"
|
||||
mode="aspectFill" @error="$fileRetry(filePath(state.waterData.logoPic))" />
|
||||
<view class="ml-2">{{ state.waterData.merchantName }}</view>
|
||||
</view>
|
||||
<view>{{ state.waterData.day }}</view>
|
||||
|
||||
@@ -79,7 +79,7 @@ function initBackStyle() {
|
||||
|
||||
<template>
|
||||
<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">
|
||||
<base-safe-height :bgColor="props.navBgColor" v-if="state.visble.topSafe" type="top" />
|
||||
<base-nav
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<view v-for="item in nodeData" :key="item.id">
|
||||
<view class="tree-item" :style="item.id == selectedNodeId ? { borderColor: '#1879ff' } : { color: '#000' }"
|
||||
@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>
|
||||
</view>
|
||||
|
||||
|
||||
@@ -167,7 +167,7 @@ const doCancel = (v: any) => {
|
||||
<!-- start 商户信息 start -->
|
||||
<view v-if="props.visibleMch" class="flex-row">
|
||||
<image class="size-90rpx rounded-8rpx bg-[#999] shadow-main overflow-hidden"
|
||||
:src="filePath(props.cardetail.logoPic) || '/static/logo/zax.png'" mode="aspectFill" />
|
||||
:src="$fileUrl(filePath(props.cardetail.logoPic) || '/static/logo/zax.png')" mode="aspectFill" @error="$fileRetry(filePath(props.cardetail.logoPic) || '/static/logo/zax.png')" />
|
||||
<view class="flex-1 ml-20rpx">
|
||||
<view class="flex-row items-center justify-between">
|
||||
<view v-if="props.visibleMchOrderStatus" class="sub-text-26">
|
||||
@@ -201,8 +201,8 @@ const doCancel = (v: any) => {
|
||||
<view class="min-w-0 flex-1">
|
||||
<view v-if="isGoodsCollapsed(props.cardetail, 0)" class="flex items-center">
|
||||
<image class="h-90rpx w-120rpx flex-shrink-0 rounded-8rpx bg-[#999] shadow-main overflow-hidden"
|
||||
:src="filePath(props.cardetail.carList?.[0]?.productPic) || '/static/logo/zax.png'"
|
||||
mode="aspectFill" />
|
||||
:src="$fileUrl(filePath(props.cardetail.carList?.[0]?.productPic) || '/static/logo/zax.png')"
|
||||
mode="aspectFill" @error="$fileRetry(filePath(props.cardetail.carList?.[0]?.productPic) || '/static/logo/zax.png')" />
|
||||
<view class="ml-16rpx min-w-0 flex-1">
|
||||
<view class="second-text-22 truncate">
|
||||
{{ props.cardetail.carList?.[0]?.productName || "暂无商品" }}
|
||||
@@ -220,7 +220,7 @@ const doCancel = (v: any) => {
|
||||
<view v-for="item in props.cardetail.carList || []" :key="item.id"
|
||||
class="mr-20rpx w-120rpx flex-shrink-0">
|
||||
<image class="h-90rpx w-120rpx rounded-8rpx bg-[#999] shadow-main overflow-hidden"
|
||||
:src="filePath(item.productPic) || '/static/logo/zax.png'" mode="aspectFill" />
|
||||
:src="$fileUrl(filePath(item.productPic) || '/static/logo/zax.png')" mode="aspectFill" @error="$fileRetry(filePath(item.productPic) || '/static/logo/zax.png')" />
|
||||
<view class="second-text-22 mt-8rpx truncate">{{ item.productName }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
},
|
||||
{ 'img-error': !item2[data.imageKey] },
|
||||
]"
|
||||
:src="item2[data.imageKey]"
|
||||
:src="$fileUrl(item2[data.imageKey])"
|
||||
mode="widthFix"
|
||||
@load="imgLoad(item2, index + 1)"
|
||||
@error="imgError(item2, index + 1)"
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
<view v-if="showPreview" class="preview-container">
|
||||
<view class="preview-title">预览效果</view>
|
||||
<view class="preview-content">
|
||||
<rich-text :nodes="previewNodes"></rich-text>
|
||||
<rich-text :nodes="$fileNodes(previewNodes)"></rich-text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
{{ placeholder }}
|
||||
</view>
|
||||
<view v-else class="content-preview">
|
||||
<rich-text :nodes="previewNodes" class="preview-text"></rich-text>
|
||||
<rich-text :nodes="$fileNodes(previewNodes)" class="preview-text"></rich-text>
|
||||
</view>
|
||||
<view class="input-actions">
|
||||
<tm-icon name="edit" size="16" color="#999"></tm-icon>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { previewFiles } from '@/sheep/utils/file-access'
|
||||
export const usePreviewImage = (str: string | undefined) => {
|
||||
if (str == undefined) return
|
||||
let urlList = str.split(",")
|
||||
uni.previewImage({
|
||||
previewFiles({
|
||||
urls: urlList,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
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 createFileAccess(options: FileAccessOptions) {
|
||||
const now = options.now || Date.now
|
||||
let offset = 0, ttl = 1800
|
||||
const cache = new Map<string, { url: string; ts: number }>()
|
||||
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 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
|
||||
cache.clear()
|
||||
}
|
||||
return { url, resolve, strip, protected: (s?: string | null) => !!parse(s), synchronize, clear: () => cache.clear(), ttl: () => ttl }
|
||||
}
|
||||
|
||||
const unescapeAttribute = (s: string) => s.replace(/&/gi, '&').replace(/"/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, '&').replace(/"/g, '"').replace(/'/g, ''')
|
||||
/** 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
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
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>()
|
||||
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 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) {
|
||||
if (!isProtectedFile(source)) return false
|
||||
const raw = originalFileUrl(source), previous = retried.get(raw) || 0
|
||||
if (Date.now() - previous < 30 * 60 * 1000) return false
|
||||
if (retried.size >= 2048) retried.clear()
|
||||
retried.set(raw, Date.now())
|
||||
const previousUrl = fileUrl(source)
|
||||
await refreshFileAccess(true)
|
||||
client.clear()
|
||||
if (fileUrl(source) === previousUrl) {
|
||||
await new Promise(resolve => setTimeout(resolve, 1100))
|
||||
client.clear()
|
||||
}
|
||||
fileAccessRevision.value++
|
||||
return true
|
||||
}
|
||||
export function installFileAccess(app: any) {
|
||||
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.clear(); 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
-24
@@ -1,25 +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) => {
|
||||
if (!base) return ""
|
||||
return base.endsWith("/") ? base.slice(0, -1) : base
|
||||
}
|
||||
|
||||
const ensureLeadingSlash = (url: string) => {
|
||||
if (!url) return ""
|
||||
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)}`
|
||||
}
|
||||
/** Preserve existing responsive article images while signing resource attributes. */
|
||||
export const parseHtml = (html: string) => fileHtml(html || '').replace(/<img\b[^>]*>/gi, img => {
|
||||
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}"`)
|
||||
if (/(?:max-)?width\s*:/i.test(style[2])) return img
|
||||
return img.replace(style[0], `style=${style[1]}${style[2].replace(/;?\s*$/, '; ')}${required}${style[1]}`)
|
||||
})
|
||||
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
import 'vue'
|
||||
declare module '@vue/runtime-core' {
|
||||
interface ComponentCustomProperties {
|
||||
$fileUrl: (source?: string | null) => string
|
||||
$fileUrls: (sources: any) => string[]
|
||||
$fileHtml: (html: string) => string
|
||||
$fileNodes: (nodes: any) => any
|
||||
$fileRetry: (source?: string | null) => Promise<boolean>
|
||||
}
|
||||
}
|
||||
export {}
|
||||
@@ -4,37 +4,37 @@
|
||||
<!-- 图片 -->
|
||||
<!-- 占位图 -->
|
||||
<image v-if="n.name==='img'&&!n.t&&((opts[1]&&!ctrl[i])||ctrl[i]<0)&&typeof (ctrl[i]<0?opts[2]:opts[1]) == 'string'" class="_img" :style="n.attrs.style"
|
||||
:src="ctrl[i]<0?opts[2]:opts[1]" mode="widthFix" />
|
||||
:src="$fileUrl(ctrl[i]<0?opts[2]:opts[1])" mode="widthFix" @error="$fileRetry(ctrl[i]<0?opts[2]:opts[1])" />
|
||||
<!-- 显示图片 -->
|
||||
<!-- #ifdef H5 || (APP-PLUS && VUE2) -->
|
||||
<img v-if="n.name==='img'&&typeof (n.attrs.src||(ctrl.load?n.attrs['data-src']:'')) == 'string'" :id="n.attrs.id" :class="'_img '+n.attrs.class"
|
||||
:style="(ctrl[i]===-1?'display:none;':'')+n.attrs.style"
|
||||
:src="n.attrs.src||(ctrl.load?n.attrs['data-src']:'')" :data-i="i" @load="imgLoad" @error="mediaError"
|
||||
:src="$fileUrl(n.attrs.src||(ctrl.load?n.attrs['data-src']:''))" :data-i="i" @load="imgLoad" @error="mediaError"
|
||||
@tap.stop="imgTap(n.attrs.src||(ctrl.load?n.attrs['data-src']:''))" @longpress="imgLongTap" />
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef H5 || (APP-PLUS && VUE2) -->
|
||||
<!-- 表格中的图片,使用 rich-text 防止大小不正确 -->
|
||||
<rich-text v-if="n.name==='img'&&n.t" :style="'display:'+n.t"
|
||||
:nodes="[{attrs:{style:n.attrs.style||'',src:n.attrs.src},name:'img'}]" :data-i="i"
|
||||
:nodes="$fileNodes([{attrs:{style:n.attrs.style||'',src:n.attrs.src},name:'img'}])" :data-i="i"
|
||||
@tap.stop="imgTap(n.attrs.sr)" />
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef H5 || APP-PLUS || MP-KUAISHOU -->
|
||||
<image v-else-if="n.name==='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class"
|
||||
:style="(ctrl[i]===-1?'display:none;':'')+'width:'+(ctrl[i]||1)+'px;height:1px;'+n.attrs.style"
|
||||
:src="n.attrs.src" :mode="!n.h?'widthFix':(!n.w?'heightFix':(n.m||'scaleToFill'))" :lazy-load="opts[0]"
|
||||
:src="$fileUrl(n.attrs.src)" :mode="!n.h?'widthFix':(!n.w?'heightFix':(n.m||'scaleToFill'))" :lazy-load="opts[0]"
|
||||
:webp="n.webp" :show-menu-by-longpress="opts[3]&&!n.attrs.ignore"
|
||||
:image-menu-prevent="!opts[3]||n.attrs.ignore" :data-i="i" @load="imgLoad" @error="mediaError"
|
||||
@tap.stop="imgTap(n.attrs.src)" @longpress="imgLongTap" />
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef MP-KUAISHOU -->
|
||||
<image v-else-if="n.name==='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class"
|
||||
:style="(ctrl[i]===-1?'display:none;':'')+n.attrs.style" :src="n.attrs.src" :lazy-load="opts[0]"
|
||||
:style="(ctrl[i]===-1?'display:none;':'')+n.attrs.style" :src="$fileUrl(n.attrs.src)" :lazy-load="opts[0]"
|
||||
:data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap(n.attrs.src)"></image>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef APP-PLUS && VUE3 -->
|
||||
<image v-else-if="n.name==='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class"
|
||||
:style="(ctrl[i]===-1?'display:none;':'')+'width:'+(ctrl[i]||1)+'px;'+n.attrs.style"
|
||||
:src="n.attrs.src||(ctrl.load?n.attrs['data-src']:'')"
|
||||
:src="$fileUrl(n.attrs.src||(ctrl.load?n.attrs['data-src']:''))"
|
||||
:mode="!n.h?'widthFix':(!n.w?'heightFix':(n.m||''))" :data-i="i" @load="imgLoad" @error="mediaError"
|
||||
@tap.stop="imgTap(n.attrs.src||(ctrl.load?n.attrs['data-src']:''))" @longpress="imgLongTap" />
|
||||
<!-- #endif -->
|
||||
@@ -54,24 +54,24 @@
|
||||
<!-- 视频 -->
|
||||
<!-- #ifdef APP-PLUS -->
|
||||
<view v-else-if="n.html" :id="n.attrs.id" :class="'_video '+n.attrs.class" :style="n.attrs.style"
|
||||
v-html="n.html" @vplay.stop="play" />
|
||||
v-html="$fileHtml(n.html)" @vplay.stop="play" />
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef APP-PLUS -->
|
||||
<video v-else-if="n.name==='video'" :id="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style"
|
||||
:autoplay="n.attrs.autoplay" :controls="n.attrs.controls" :loop="n.attrs.loop" :muted="n.attrs.muted"
|
||||
:object-fit="n.attrs['object-fit']" :poster="n.attrs.poster" :src="n.src[ctrl[i]||0]" :data-i="i"
|
||||
:object-fit="n.attrs['object-fit']" :poster="$fileUrl(n.attrs.poster)" :src="$fileUrl(n.src[ctrl[i]||0])" :data-i="i"
|
||||
@play="play" @error="mediaError" />
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef H5 || APP-PLUS -->
|
||||
<iframe v-else-if="n.name==='iframe'" :style="n.attrs.style" :allowfullscreen="n.attrs.allowfullscreen"
|
||||
:frameborder="n.attrs.frameborder" :src="n.attrs.src" />
|
||||
<embed v-else-if="n.name==='embed'" :style="n.attrs.style" :src="n.attrs.src" />
|
||||
:frameborder="n.attrs.frameborder" :src="$fileUrl(n.attrs.src)" />
|
||||
<embed v-else-if="n.name==='embed'" :style="n.attrs.style" :src="$fileUrl(n.attrs.src)" />
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef MP-TOUTIAO || ((H5 || APP-PLUS) && VUE3) -->
|
||||
<!-- 音频 -->
|
||||
<audio v-else-if="n.name==='audio'" :id="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style"
|
||||
:author="n.attrs.author" :controls="n.attrs.controls" :loop="n.attrs.loop" :name="n.attrs.name"
|
||||
:poster="n.attrs.poster" :src="n.src[ctrl[i]||0]" :data-i="i" @play="play" @error="mediaError" />
|
||||
:poster="$fileUrl(n.attrs.poster)" :src="$fileUrl(n.src[ctrl[i]||0])" :data-i="i" @play="play" @error="mediaError" />
|
||||
<!-- #endif -->
|
||||
<view v-else-if="(n.name==='table'&&n.c)||n.name==='li'" :id="n.attrs.id"
|
||||
:class="'_'+n.name+' '+n.attrs.class" :style="n.attrs.style">
|
||||
@@ -97,11 +97,11 @@
|
||||
<!-- 富文本 -->
|
||||
<!-- #ifdef H5 || ((MP-WEIXIN || MP-QQ || APP-PLUS || MP-360) && VUE2) -->
|
||||
<rich-text v-else-if="!n.c&&!handler.isInline(n.name, n.attrs.style)" :id="n.attrs.id" :style="n.f"
|
||||
:user-select="opts[4]" :nodes="[n]" />
|
||||
:user-select="opts[4]" :nodes="$fileNodes([n])" />
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef H5 || ((MP-WEIXIN || MP-QQ || APP-PLUS || MP-360) && VUE2) -->
|
||||
<rich-text v-else-if="!n.c" :id="n.attrs.id" :style="'display:inline;'+n.f" :preview="false"
|
||||
:selectable="opts[4]" :user-select="opts[4]" :nodes="[n]" />
|
||||
:selectable="opts[4]" :user-select="opts[4]" :nodes="$fileNodes([n])" />
|
||||
<!-- #endif -->
|
||||
<!-- 继续递归 -->
|
||||
<view v-else-if="n.c===2" :id="n.attrs.id" :class="'_block _'+n.name+' '+n.attrs.class"
|
||||
@@ -142,6 +142,8 @@
|
||||
}
|
||||
</script>
|
||||
<script>
|
||||
import { previewFiles, downloadFile, accessFileUrl, retryFileAccess, isProtectedFile, openFileLink } from '@/sheep/utils/file-access'
|
||||
|
||||
import node from './mp-node.vue'
|
||||
export default {
|
||||
name: 'node',
|
||||
@@ -187,7 +189,7 @@
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
watch:{
|
||||
imgs:{
|
||||
handler(val){
|
||||
@@ -266,11 +268,11 @@
|
||||
* @param {Event} e
|
||||
*/
|
||||
imgTap(e) {
|
||||
|
||||
|
||||
this.$emit('imgtap', e)
|
||||
// 自动预览图片
|
||||
if (this.opts[5]) {
|
||||
uni.previewImage({
|
||||
previewFiles({
|
||||
// #ifdef MP-WEIXIN
|
||||
showmenu: this.opts[3],
|
||||
// #endif
|
||||
@@ -283,10 +285,10 @@
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
onimgTop(src){
|
||||
this.$emit('imgtap', src)
|
||||
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -310,7 +312,7 @@
|
||||
})
|
||||
}
|
||||
if (this.root.imgList[attrs.i].startsWith('http')) {
|
||||
uni.downloadFile({
|
||||
downloadFile({
|
||||
url: this.root.imgList[attrs.i],
|
||||
success: res => save(res.tempFilePath)
|
||||
})
|
||||
@@ -366,10 +368,14 @@
|
||||
linkTap(e) {
|
||||
const node = e.currentTarget ? this.childs[e.currentTarget.dataset.i] : {}
|
||||
const attrs = node.attrs || e
|
||||
const href = attrs.href
|
||||
const href = accessFileUrl(attrs.href)
|
||||
this.root.$emit('linktap', Object.assign({
|
||||
innerText: this.root.getText(node.children || []) // 链接内的文本内容
|
||||
}, attrs))
|
||||
if (isProtectedFile(attrs.href)) {
|
||||
openFileLink(attrs.href)
|
||||
return
|
||||
}
|
||||
if (href) {
|
||||
if (href[0] === '#') {
|
||||
// 跳转锚点
|
||||
@@ -412,9 +418,11 @@
|
||||
* @description 错误事件
|
||||
* @param {Event} e
|
||||
*/
|
||||
mediaError(e) {
|
||||
async mediaError(e) {
|
||||
const i = e.currentTarget.dataset.i
|
||||
const node = this.childs[i]
|
||||
const source = node.name === 'img' ? node.attrs.src : node.src?.[this.ctrl[i] || 0]
|
||||
if (await retryFileAccess(source)) return
|
||||
// 加载其他源
|
||||
if (node.name === 'video' || node.name === 'audio') {
|
||||
let index = (this.ctrl[i] || 0) + 1
|
||||
|
||||
@@ -17,14 +17,14 @@
|
||||
<image
|
||||
v-if="isImg"
|
||||
@tap="onclick"
|
||||
:src="iconName"
|
||||
:src="$fileUrl(iconName)"
|
||||
:class="['tmIcon', isSpin?'tmIconSpin':'',_class]"
|
||||
:style="[
|
||||
{ width: iconSize, height: iconSize },
|
||||
rotateDeg ? { transform: `rotate(${rotateDeg})` } : {},
|
||||
_style
|
||||
]"
|
||||
></image>
|
||||
@error="$fileRetry(iconName)"></image>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
|
||||
@@ -12,12 +12,14 @@
|
||||
<image v-if="!isError" class="tmImageImg" :lazy-load="lazy" :draggable="draggable"
|
||||
:showMenuByLongpress="showMenuByLongpress"
|
||||
:class="[isLoading ? 'tmImageImgAbs' : '', fadeShow ? 'tmImageFadeShow' : '']" :mode="_model"
|
||||
:style="[_styleMap]" :src="_src" @error="imgError">
|
||||
:style="[_styleMap]" :src="$fileUrl(_src)" @error="imgError">
|
||||
</image>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { previewFiles } from '@/sheep/utils/file-access'
|
||||
|
||||
import { ref, computed, onMounted, onBeforeUnmount, watch, type PropType, getCurrentInstance, onUpdated, nextTick } from 'vue';
|
||||
import { arrayNumberValid, arrayNumberValidByStyleMP, covetUniNumber, arrayNumberValidByStyleBorderColor, linearValid, getUnit, getUid } from "../../libs/tool";
|
||||
import { getDefaultColor, getDefaultColorObj, getOutlineColorObj, getTextColorObj, getThinColorObj } from "../../libs/colors";
|
||||
@@ -37,7 +39,7 @@ import { onPageScroll } from '@dcloudio/uni-app';
|
||||
defineOptions({ name: 'TmBadge' });
|
||||
const { config } = useTmConfig()
|
||||
const props = defineProps({
|
||||
/**
|
||||
/**
|
||||
* 宽度,默认100%
|
||||
* 18rpx,18px,15%支持这三种单位,如果只写"18"就表示18rpx
|
||||
*/
|
||||
@@ -45,7 +47,7 @@ const props = defineProps({
|
||||
type: [String, Number],
|
||||
default: "100%"
|
||||
},
|
||||
/**
|
||||
/**
|
||||
* 高度,auto,%,rpx,px,string number
|
||||
* 18rpx,18px,15%支持这三种单位,如果只写"18"就表示18rpx
|
||||
*/
|
||||
@@ -58,7 +60,7 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
/**
|
||||
* 模式
|
||||
* @link https://uniapp.dcloud.net.cn/uni-app-x/component/image.html#mode-values
|
||||
*/
|
||||
@@ -260,7 +262,7 @@ const _styleMap = computed(() => {
|
||||
const prevImage = () => {
|
||||
emit('click');
|
||||
if (props.preview) {
|
||||
uni.previewImage({
|
||||
previewFiles({
|
||||
current: props.src,
|
||||
urls: [props.src]
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
<view class="tmUploadPhotoItem " v-if="_selectedCount&&props.addPos=='before'" @click="choose"
|
||||
:style="{ width: `calc(${_width} - 10rpx)`, height: _height }">
|
||||
<!--
|
||||
<!--
|
||||
@slot 上传按钮插槽
|
||||
-->
|
||||
<slot>
|
||||
@@ -15,8 +15,8 @@
|
||||
|
||||
<view class="tmUploadPhotoItem" :style="{ width: `calc(${_width} - 10rpx)`, height: _height,borderRadius:_round }"
|
||||
v-for="(item, index) in list" :key="index">
|
||||
<image @click="imagePreve(index)" :src="item.path" :mode="props.mode"
|
||||
style="width: 100%;height: 100%;border-radius: 10rpx;"></image>
|
||||
<image @click="imagePreve(index)" :src="$fileUrl(item.path)" :mode="props.mode"
|
||||
style="width: 100%;height: 100%;border-radius: 10rpx;" @error="$fileRetry(item.path)"></image>
|
||||
<view class="tmUploadLabel" :style="{ color: STATUS_COLOR.get(item.status) || 'white' }">{{ item.statusText
|
||||
}}
|
||||
</view>
|
||||
@@ -27,7 +27,7 @@
|
||||
</view>
|
||||
<view class="tmUploadPhotoItem " v-if="_selectedCount&&props.addPos=='after'" @click="choose"
|
||||
:style="{ width: `calc(${_width} - 10rpx)`, height: _height }">
|
||||
<!--
|
||||
<!--
|
||||
@slot 上传按钮插槽
|
||||
-->
|
||||
<slot>
|
||||
@@ -40,6 +40,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { previewFiles } from '@/sheep/utils/file-access'
|
||||
|
||||
import { type PropType, computed, onMounted, ref, shallowRef, watch } from 'vue'
|
||||
import { covetUniNumber } from '../../libs/tool';
|
||||
import { TMUPLOAD_PHOTO_STATUS } from '../../interface';
|
||||
@@ -83,17 +85,17 @@ const STATUS_COLOR = new Map<TMUPLOAD_PHOTO_STATUS, string>([
|
||||
const emit = defineEmits([
|
||||
/**
|
||||
* 每次全部上传完时触发
|
||||
* @param {TMUPLOAD_PHOTO_INFO[]} 列表数据
|
||||
* @param {TMUPLOAD_PHOTO_INFO[]} 列表数据
|
||||
*/
|
||||
'complete',
|
||||
/**
|
||||
* 变化时触发
|
||||
* @param {TMUPLOAD_PHOTO_INFO[]} 列表数据
|
||||
* @param {TMUPLOAD_PHOTO_INFO[]} 列表数据
|
||||
*/
|
||||
'change',
|
||||
/**
|
||||
* 图片被删除时触发
|
||||
* @param {TMUPLOAD_PHOTO_INFO[]} 列表数据
|
||||
* @param {TMUPLOAD_PHOTO_INFO[]} 列表数据
|
||||
*/
|
||||
'delete',
|
||||
/**
|
||||
@@ -539,7 +541,7 @@ const remove = async (index: number, item: TM.TMUPLOAD_PHOTO_INFO) => {
|
||||
emit('delete',item)
|
||||
}
|
||||
const imagePreve = (index: number) => {
|
||||
uni.previewImage({
|
||||
previewFiles({
|
||||
current: index,
|
||||
urls: list.value.map(item => item.path)
|
||||
})
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
<view class="tmUploadPhotoItem " v-if="_selectedCount && props.addPos == 'before'" @click="choose"
|
||||
:style="{ width: `calc(${_width} - 10rpx)`, height: _height }">
|
||||
<!--
|
||||
<!--
|
||||
@slot 上传按钮插槽
|
||||
-->
|
||||
<slot>
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
<view class="tmUploadPhotoItem" :style="{ width: `calc(${_width} - 10rpx)`, height: _height,borderRadius:_round }"
|
||||
v-for="(item, index) in list" :key="index">
|
||||
<video :autoplay="false" :controls="false" @click="imagePreve(index)" :src="item.path" :mode="props.mode"
|
||||
<video :autoplay="false" :controls="false" @click="imagePreve(index)" :src="$fileUrl(item.path)" :mode="props.mode"
|
||||
style="width: 100%;height: 100%;border-radius: 10rpx;"></video>
|
||||
<view class="tmUploadLabel" :style="{ color: STATUS_COLOR.get(item.status) || 'white' }">{{ item.statusText
|
||||
}}
|
||||
@@ -27,7 +27,7 @@
|
||||
</view>
|
||||
<view class="tmUploadPhotoItem " v-if="_selectedCount && props.addPos == 'after'" @click="choose"
|
||||
:style="{ width: `calc(${_width} - 10rpx)`, height: _height }">
|
||||
<!--
|
||||
<!--
|
||||
@slot 上传按钮插槽
|
||||
-->
|
||||
<slot>
|
||||
@@ -84,17 +84,17 @@ const STATUS_COLOR = new Map<TMUPLOAD_PHOTO_STATUS, string>([
|
||||
const emit = defineEmits([
|
||||
/**
|
||||
* 每次全部上传完时触发
|
||||
* @param {TMUPLOAD_PHOTO_INFO[]} 列表数据
|
||||
* @param {TMUPLOAD_PHOTO_INFO[]} 列表数据
|
||||
*/
|
||||
'complete',
|
||||
/**
|
||||
* 变化时触发
|
||||
* @param {TMUPLOAD_PHOTO_INFO[]} 列表数据
|
||||
* @param {TMUPLOAD_PHOTO_INFO[]} 列表数据
|
||||
*/
|
||||
'change',
|
||||
/**
|
||||
* 图片被删除时触发
|
||||
* @param {TMUPLOAD_PHOTO_INFO[]} 列表数据
|
||||
* @param {TMUPLOAD_PHOTO_INFO[]} 列表数据
|
||||
*/
|
||||
'delete',
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user