|
|
@@ -289,7 +289,7 @@
|
|
|
>
|
|
|
<!-- 图片图层 -->
|
|
|
<template v-if="layer.type !== 'text'">
|
|
|
- <img :src="layer.url" :alt="layer.name" draggable="false" />
|
|
|
+ <img :src="getImageLoadUrl(layer.url)" :alt="layer.name" draggable="false" />
|
|
|
</template>
|
|
|
|
|
|
<!-- 文字图层 -->
|
|
|
@@ -366,7 +366,7 @@
|
|
|
<div class="layer-card-thumb text-thumb">T</div>
|
|
|
</template>
|
|
|
<template v-else>
|
|
|
- <img v-if="layer.url" :src="layer.url" class="layer-card-thumb" alt="" />
|
|
|
+ <img v-if="layer.url" :src="getImageLoadUrl(layer.url)" class="layer-card-thumb" alt="" />
|
|
|
<div v-else class="layer-card-thumb img-placeholder"><el-icon><Picture /></el-icon></div>
|
|
|
</template>
|
|
|
<div class="layer-card-name-wrap" draggable="false" @dragstart.stop>
|
|
|
@@ -553,7 +553,7 @@ import { ElMessage } from 'element-plus'
|
|
|
import { Rank, ArrowUp, ArrowDown, Delete, View, Hide, Plus, Picture, Upload, Search, ArrowLeft, EditPen, Document, Box, MagicStick, InfoFilled, Lock, Unlock, List, Setting } from '@element-plus/icons-vue'
|
|
|
import { emitter } from '@/utils/bus.js'
|
|
|
import { useUserStore } from '@/pinia/modules/user'
|
|
|
-import { Material_List, Template_Material_Add, Template_Material_Update, Template_Material_Relation } from '@/api/mes/job'
|
|
|
+import { Material_List, Template_Material_Add, Template_Material_Update, Template_Material_Relation, GetHttpUrl } from '@/api/mes/job'
|
|
|
|
|
|
const props = defineProps({
|
|
|
/** 列表页传入的模版对象(编辑/做同款时必传) */
|
|
|
@@ -695,12 +695,82 @@ const canMoveDown = computed(() => {
|
|
|
|
|
|
let layerIdCounter = 0
|
|
|
|
|
|
-// 将 material_url 转为前端可请求的完整地址(相对路径加根路径,避免图片 404)
|
|
|
+// 获取服务器地址(用于拼接素材/图层图片 URL,显示用)
|
|
|
+const full_url = ref('')
|
|
|
+const fetchServerUrl = async () => {
|
|
|
+ try {
|
|
|
+ const res = await GetHttpUrl()
|
|
|
+ if (res.code === 0 && res.data && res.data.full_url) {
|
|
|
+ full_url.value = res.data.full_url || ''
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取服务器地址失败:', error)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 显示用:上传/素材的完整 base,必须用 9093 端口,不要用 9090(CLI 端口)
|
|
|
+function getDisplayBaseUrl() {
|
|
|
+ const base = import.meta.env.VITE_BASE_PATH || ''
|
|
|
+ const port = import.meta.env.VITE_UPLOADS_PORT || '9093'
|
|
|
+ const uploadsBase = base && port ? `${base.replace(/:(\d+)?$/, '')}:${port}` : ''
|
|
|
+ const fromApi = (full_url.value || '').replace(/\/$/, '')
|
|
|
+ if (fromApi && !fromApi.match(/:9090(?:$|\/)/)) return fromApi
|
|
|
+ return uploadsBase || ''
|
|
|
+}
|
|
|
+
|
|
|
+// 将 material_url 转为前端可请求的完整地址(用于显示;后端返回 9090 时替换为 9093)
|
|
|
function resolveMaterialUrl(path) {
|
|
|
if (!path || typeof path !== 'string') return ''
|
|
|
const p = path.trim()
|
|
|
- if (p.startsWith('http://') || p.startsWith('https://') || p.startsWith('data:')) return p
|
|
|
- return p.startsWith('/') ? p : '/' + p
|
|
|
+ if (!p) return ''
|
|
|
+ if (p.startsWith('data:')) return p
|
|
|
+ if (p.startsWith('http://') || p.startsWith('https://')) {
|
|
|
+ try {
|
|
|
+ const u = new URL(p)
|
|
|
+ if (u.port === '9090') return `${u.protocol}//${u.hostname}:9093${u.pathname}${u.search}`
|
|
|
+ } catch { /* ignore */ }
|
|
|
+ return p
|
|
|
+ }
|
|
|
+ const baseUrl = getDisplayBaseUrl()
|
|
|
+ const cleanPath = p.replace(/^public\//, '').replace(/^\//, '')
|
|
|
+ if (!baseUrl) return '/' + cleanPath
|
|
|
+ return `${baseUrl}/${cleanPath}`
|
|
|
+}
|
|
|
+
|
|
|
+// 图片加载用 URL:开发环境用 path 走 Vite 代理(同源),生产用 9093 完整 URL;画布与 generateCanvasPreview 共用
|
|
|
+function getImageLoadUrl(url) {
|
|
|
+ if (!url || typeof url !== 'string') return ''
|
|
|
+ const u = url.trim()
|
|
|
+ if (!u || u.startsWith('data:')) return u
|
|
|
+ let path = u
|
|
|
+ if (u.startsWith('http://')) {
|
|
|
+ try {
|
|
|
+ path = new URL(u).pathname
|
|
|
+ } catch {
|
|
|
+ return u
|
|
|
+ }
|
|
|
+ }
|
|
|
+ path = path.startsWith('/') ? path : '/' + path.replace(/^\/+/, '')
|
|
|
+ if (import.meta.env.DEV) return path
|
|
|
+ const base = getDisplayBaseUrl()
|
|
|
+ return base ? `${base.replace(/\/$/, '')}${path}` : path
|
|
|
+}
|
|
|
+
|
|
|
+// 保存/修改时:把 layer.url 转为仅路径(不要 http 前缀),data: 和已是路径的保持不变
|
|
|
+function toStoragePath(url) {
|
|
|
+ if (!url || typeof url !== 'string') return ''
|
|
|
+ const u = url.trim()
|
|
|
+ if (!u) return ''
|
|
|
+ if (u.startsWith('data:')) return u
|
|
|
+ if (u.startsWith('http://')) {
|
|
|
+ try {
|
|
|
+ const parsed = new URL(u)
|
|
|
+ return parsed.pathname || ('/' + u.replace(/^\/+/, ''))
|
|
|
+ } catch {
|
|
|
+ return u
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return u.startsWith('/') ? u : '/' + u
|
|
|
}
|
|
|
|
|
|
// 清空画布,进入新建流程(本组件内部或由 props.mode='create' 触发)
|
|
|
@@ -1042,7 +1112,7 @@ const fetchMaterials = async () => {
|
|
|
materialsLoading.value = true
|
|
|
console.log('开始获取素材库数据')
|
|
|
const response = await Material_List({
|
|
|
- search: (materialSearch.value || '').trim() || undefined
|
|
|
+ search: (materialSearch.value || '').trim()
|
|
|
})
|
|
|
console.log('素材库数据:', response)
|
|
|
if (response.code === 0) {
|
|
|
@@ -1075,22 +1145,47 @@ const generateCanvasPreview = async () => {
|
|
|
ctx.fillStyle = '#ffffff'
|
|
|
ctx.fillRect(0, 0, exportCanvas.width, exportCanvas.height)
|
|
|
|
|
|
- // 预加载图片图层
|
|
|
+ // 预加载图片图层:与画布共用 getImageLoadUrl,确保 URL 解析一致
|
|
|
const imageLayers = layers.value.filter(l => (l.type || 'image') !== 'text' && l.url)
|
|
|
- await Promise.all(
|
|
|
- imageLayers.map(layer => {
|
|
|
- return new Promise(resolve => {
|
|
|
- const img = new Image()
|
|
|
- img.crossOrigin = 'anonymous'
|
|
|
- img.onload = () => {
|
|
|
- layer._previewImg = img
|
|
|
- resolve()
|
|
|
- }
|
|
|
- img.onerror = () => resolve()
|
|
|
- img.src = layer.url
|
|
|
+ for (const layer of imageLayers) {
|
|
|
+ const url = layer.url
|
|
|
+ if (url.startsWith('data:')) {
|
|
|
+ const img = new Image()
|
|
|
+ await new Promise((r) => {
|
|
|
+ img.onload = () => r()
|
|
|
+ img.onerror = () => r()
|
|
|
+ img.src = url
|
|
|
})
|
|
|
- })
|
|
|
- )
|
|
|
+ layer._previewImg = img.complete && img.naturalWidth ? img : null
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ const fetchUrl = getImageLoadUrl(url)
|
|
|
+ let img = null
|
|
|
+ try {
|
|
|
+ const res = await fetch(fetchUrl)
|
|
|
+ if (res.ok) {
|
|
|
+ const blob = await res.blob()
|
|
|
+ const objUrl = URL.createObjectURL(blob)
|
|
|
+ img = new Image()
|
|
|
+ await new Promise((r, reject) => {
|
|
|
+ img.onload = () => r()
|
|
|
+ img.onerror = () => reject()
|
|
|
+ img.src = objUrl
|
|
|
+ })
|
|
|
+ URL.revokeObjectURL(objUrl)
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ img = new Image()
|
|
|
+ img.crossOrigin = 'anonymous'
|
|
|
+ await new Promise((r) => {
|
|
|
+ img.onload = () => r()
|
|
|
+ img.onerror = () => r()
|
|
|
+ img.src = fetchUrl
|
|
|
+ })
|
|
|
+ if (!img.complete || !img.naturalWidth) img = null
|
|
|
+ }
|
|
|
+ layer._previewImg = img
|
|
|
+ }
|
|
|
|
|
|
// 按当前顺序绘制所有可见图层
|
|
|
for (const layer of layers.value) {
|
|
|
@@ -1147,7 +1242,13 @@ const generateCanvasPreview = async () => {
|
|
|
delete layer._previewImg
|
|
|
}
|
|
|
|
|
|
- return exportCanvas.toDataURL('image/png')
|
|
|
+ try {
|
|
|
+ return exportCanvas.toDataURL('image/png')
|
|
|
+ } catch (e) {
|
|
|
+ // 跨域图片可能导致 canvas 污染,toDataURL 抛错
|
|
|
+ console.warn('画布预览图生成失败(可能因跨域图片):', e)
|
|
|
+ return null
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
let materialSearchTimer = null
|
|
|
@@ -1250,6 +1351,7 @@ const handleGlobalClick = (e) => {
|
|
|
watch(templateName, (v) => setTemplateNameElContent(v))
|
|
|
|
|
|
onMounted(() => {
|
|
|
+ fetchServerUrl()
|
|
|
fetchMaterials()
|
|
|
loadByMode()
|
|
|
nextTick(() => setTemplateNameElContent(templateName.value))
|
|
|
@@ -1281,16 +1383,16 @@ const saveTemplate = async () => {
|
|
|
|
|
|
const templateData = {
|
|
|
template_name: templateName.value || '未命名模版',
|
|
|
- sys_id:userStore.userInfo.nickName,
|
|
|
+ sys_id: userStore.userInfo.nickName,
|
|
|
canvasWidth: canvasWidth.value,
|
|
|
canvasHeight: canvasHeight.value,
|
|
|
canvasRatio: canvasRatio.value,
|
|
|
- previewImage, // 画布整体预览图(base64)
|
|
|
+ previewImage,
|
|
|
layers: layers.value.map(layer => ({
|
|
|
id: layer.id,
|
|
|
name: layer.name,
|
|
|
type: layer.type || 'image',
|
|
|
- url: layer.url,
|
|
|
+ url: toStoragePath(layer.url),
|
|
|
text: layer.text,
|
|
|
x: layer.x,
|
|
|
y: layer.y,
|