Commit 2a453d67 by pangchong

feat(editor): 添加图纸页和插入模板功能及桥接接口支持

- 配置环境变量支持自定义定制代码和后端接口地址
- 优化 vite 配置,增加 api 代理和路径别名动态调整
- 新增桥接调用统一管理,封装模板列表、保存与图片上传接口
- 扩展 DocNodeRenderer,支持 SHEET、GDESC、TITLE 等节点的渲染
- 编辑器面板新增回到顶部悬浮按钮及滚动同步优化
- 工具栏新增插入图片和模板弹窗,实现图纸与模板可视化插入
- 新增 CreateGraphicModal,实现图片和图纸节点的插入及上传功能
- 新增 TemplateSelectModal,支持模板列表展示、分页、XML预览及效果预览
- 加强异步加载模板及错误处理,确保接口异常有友好提示
- 完善样式与动画,提升用户交互体验与视觉反馈
parent 80257d3c
VITE_PARTITION_NAME = 'from_project_001'
\ No newline at end of file
VITE_PARTITION_NAME = from_project_001
\ No newline at end of file
VITE_PARTITION_NAME = 'from_project_001'
VITE_CUSTOM_CODE = 'xm'
# 自定义定制代码目录 (例如 'xm')
VITE_CUSTOM_CODE = xm
# 分区名称标识
VITE_PARTITION_NAME = from_project_001
# 后端 API 目标服务基准地址
VITE_API_URL = https://amro-vue.anyremote.cn
# 接口前缀路径
VITE_API_PREFIX_URL = /api/v1
# 是否使用本地开发桥接服务 (true 使用本地接口直接请求,false 降级使用父项目传递的 promise)
VITE_IF_LOCAL_BRIDGE = true
\ No newline at end of file
......@@ -283,3 +283,13 @@ export const COMPOSITE_CONTAINER_MAP: Record<string, string[]> = {
### 20.3 Git 提交限制原则
- **禁止提交到本地 Git 仓库**:在开发过程中,所有代码的修改和新增操作,**严禁使用 git commit、git push 等任何 Git 提交或推送命令**。所有修改仅保留在本地工作区中。
---
## 21. 外部接口与桥接调用规范 (Bridge)
- **外部系统与后端接口的一元化管理 (强制)**:所有需要与外部宿主系统对接、或直接向后端发起网络请求的桥接方法(如获取模板列表 `getTemplateList`、保存模板 `saveTemplate`、上传图片资源 `uploadImage` 等),**必须**统一在 [bridge.ts](file:///e:/refactor-Editor/Ifar-Xml-Editor/src/utils/bridge.ts) 文件中进行定义与导出。
- **实体接口请求与异步处理**
-`bridge.ts` 中封装网络及上传接口时,应当与 `getTemplateList` 风格保持一致,采用标准的 `try...catch` 块捕获异常并上抛。
- 直接通过 `apiService` 向后端实体接口(例如 `/plugins/TD_JC_CEP_UPLOAD_IMG` 等)发起数据请求。对于上传等表单提交,**强制直接在 `apiService.postForm` 中传入含有 `file` 与参数的普通 JavaScript 对象**(底层服务已实现拦截自动转化为 `FormData` 包装),**严禁**在业务和桥接层手动进行 `new FormData()` 的实例化与 append 操作;同时,**禁止**在内部做任何针对外部宿主系统环境(例如 window、window.parent 等)的挂载检测与判断。
- **业务组件调用规范**:在任何 Vue 组件或 functionals 逻辑类中,**禁止**直接通过 `apiService` 或原生 ajax 方式编写特定的外部接口请求逻辑,**必须**`bridge.ts` 中导入对应的封装方法进行调用,实现视图层与接口传输层的彻底解耦。
......@@ -238,7 +238,7 @@ const getBaseURL = () => {
return '/translations'
}
// 生产/打包环境下,若配置了 VITE_APP_PROXY_URL,则将接口基准地址指向对应地址的 /translations 路径
const proxyUrl = import.meta.env.VITE_APP_PROXY_URL
const proxyUrl = import.meta.env.VITE_API_URL
if (proxyUrl) {
const cleanUrl = proxyUrl.trim().replace(/\/$/, '')
return cleanUrl.endsWith('/translations') ? cleanUrl : `${cleanUrl}/translations`
......@@ -267,3 +267,6 @@ export const serviceManage = createService(getManageBaseURL())
// 兼容老代码中直接引用 alovaInstance 的方式
export const alovaInstance = service.alova
// 用于请求 /api/v1 路径下的正式接口
export const apiService = createService(import.meta.env.VITE_API_PREFIX_URL)
import { execute } from '@/utils/bridgeHelper'
export interface TemplateItem {
id: number
name: string
creator: string
createTime: string
content: string
}
export interface TemplateResponse {
code: number | string
msg?: string
total: number
data: TemplateItem[]
}
const localBridge = {
getTemplateList: async (page: number, rows: number): Promise<TemplateResponse> => {
return {
code: 200,
msg: 'Success',
total: 0,
data: []
}
},
saveTemplate: async (name: string, content: string): Promise<any> => {
return null
},
uploadImage: async (file: File): Promise<any> => {
return null
}
}
/**
* 获取模板列表
*/
export const getTemplateList = execute('getTemplateList', localBridge.getTemplateList, (page, rows) => ({ page, rows }))
/**
* 保存模板
*/
export const saveTemplate = execute('saveTemplate', localBridge.saveTemplate, (name, content) => ({ name, content }))
/**
* 上传工卡图片资源
*/
export const uploadImage = execute('uploadImage', localBridge.uploadImage, (file) => ({ file }))
import { usePostMessage } from '@/hooks/usePostMessage'
/**
* 判断当前是否使用本地开发桥接服务
*/
export const isLocalBridge = (): boolean => {
return import.meta.env.VITE_IF_LOCAL_BRIDGE === 'true'
}
/**
* 跨端/跨窗口调用桥接方法的核心逻辑
* @param methodName 调用的方法名
* @param params 作为单对象传递给 postMessage 的参数
* @param args 直接调用时传入的参数列表
*/
export const callBridge = async <T = any>(methodName: string, params?: any, ...args: any[]): Promise<T> => {
// 1. 优先尝试直接从当前 window 上调用 (父项目可以直接在 iframe.contentWindow 上挂载这些方法)
const win = window as any
if (typeof win[methodName] === 'function') {
return win[methodName](...(args.length ? args : [params]))
}
if (win.bridge && typeof win.bridge[methodName] === 'function') {
return win.bridge[methodName](...(args.length ? args : [params]))
}
// 2. 尝试从父窗口同域调用 (如果同域,且父窗口挂载了对应方法)
try {
if (window.parent && window.parent !== window) {
const parentWin = window.parent as any
if (typeof parentWin[methodName] === 'function') {
return await parentWin[methodName](...(args.length ? args : [params]))
}
if (parentWin.bridge && typeof parentWin.bridge[methodName] === 'function') {
return await parentWin.bridge[methodName](...(args.length ? args : [params]))
}
}
} catch (e) {
// 跨域安全报错忽略
}
// 3. 降级使用 postMessage RPC 进行跨域通信
const postMessageHelper = usePostMessage()
return await postMessageHelper.call<T>(methodName, params)
}
/**
* 统一高阶执行函数,用于消除不同 bridge 文件中重复的分支、异常捕获与调用模版代码
* @param methodName 方法名
* @param localImpl 本地接口的实现函数
* @param paramsMapper 远程调用时的参数转换映射函数
*/
export const execute = <T extends (...args: any[]) => Promise<any>>(
methodName: string,
localImpl: T,
paramsMapper: (...args: Parameters<T>) => any
): T => {
return (async (...args: Parameters<T>) => {
if (isLocalBridge()) {
try {
return await localImpl(...args)
} catch (error) {
console.error(`本地执行 ${methodName} 失败:`, error)
throw error
}
} else {
const params = paramsMapper(...args)
return await callBridge(methodName, params, ...args)
}
}) as unknown as T
}
......@@ -95,7 +95,7 @@ export const getBaseOrigin = () => {
if (import.meta.env.DEV) {
return window.location.origin
}
const proxyUrl = import.meta.env.VITE_APP_PROXY_URL
const proxyUrl = import.meta.env.VITE_API_URL
if (proxyUrl) {
return proxyUrl.trim().replace(/\/$/, '')
}
......
......@@ -732,7 +732,8 @@
<!-- 18. CALS TABLE (表格) 可视化编辑器集成 -->
<template v-else-if="node.tagName === 'TABLE'">
<div class="my-4 border border-divider rounded-lg overflow-hidden bg-card p-3 shadow-sm">
<TableEditor v-if="isDiffMode" :node="node" />
<div v-else class="my-4 border border-divider rounded-lg overflow-hidden bg-card p-3 shadow-sm">
<div class="flex items-center space-x-2 mb-2 pb-2 border-b border-divider print-hide">
<n-icon color="var(--primary-color)"><grid-outline /></n-icon>
<span class="text-xs font-bold text-color2">表格编辑区域</span>
......@@ -747,25 +748,23 @@
<div class="w-full flex items-center justify-between pb-2 border-b border-divider mb-3 print-hide">
<span class="text-xs font-bold text-color2 flex items-center space-x-1">
<n-icon><image-outline /></n-icon>
<span>工卡附图 [GNBR: {{ node.children.find((c) => c.tagName === 'SHEET')?.attributes.GNBR || '无' }}]</span>
<span>工卡附图</span>
</span>
<CommonTag size="small" type="primary">GRAPHIC</CommonTag>
</div>
<!-- 拟物化卡片模拟设计图 -->
<div
class="w-full max-w-lg aspect-[16/10] rounded border border-divider bg-fill-3 flex flex-col items-center justify-center relative p-4 shadow-inner"
style="background-image: radial-gradient(circle, rgba(0, 0, 0, 0.03) 1px, transparent 1px); background-size: 16px 16px"
>
<div class="text-center space-y-2 select-none pointer-events-none">
<n-icon size="48" color="var(--text-color-3)"><image-outline /></n-icon>
<div class="text-xs text-color3">航空器结构部件装配原理示意图</div>
<div class="text-[10px] text-color3/60 font-mono">
GNBR Ref: {{ node.children.find((c) => c.tagName === 'SHEET')?.attributes.GNBR }}
</div>
</div>
<!-- 遍历并渲染所有的 SHEET 子节点(每个 SHEET 节点自己渲染图片或占位卡片) -->
<div class="w-full space-y-4">
<DocNodeRenderer
v-for="sheet in node.children.filter((c) => c.tagName === 'SHEET')"
:key="sheet.id"
:node="sheet"
:parent="node"
/>
</div>
<!-- 图片标题编辑 -->
<div class="w-full mt-2 text-center select-none flex items-center justify-center">
<div class="w-full mt-3 text-center select-none flex items-center justify-center">
<span class="text-xs text-color3 italic mr-1">图标题:</span>
<template v-for="tag in COMPOSITE_CONTAINER_MAP['GRAPHIC']" :key="tag">
<DocNodeRenderer
......@@ -779,6 +778,49 @@
</div>
</template>
<!-- 19.2 SHEET (独立图纸页) 可视化处理 -->
<template v-else-if="node.tagName === 'SHEET'">
<div class="my-4 border border-divider rounded-lg overflow-hidden bg-fill-2 p-4 flex flex-col items-center">
<div class="w-full flex items-center justify-between pb-2 border-b border-divider mb-3 print-hide">
<span class="text-xs font-bold text-color2 flex items-center space-x-1">
<n-icon><image-outline /></n-icon>
<span>图纸页 [GNBR: {{ node.attributes.GNBR || '无' }}]</span>
</span>
<CommonTag size="small" type="primary">SHEET</CommonTag>
</div>
<!-- 如果是真实图片地址,则展示真实图片 -->
<template v-if="getImgSrc(node.attributes.GNBR || '')">
<div class="w-full rounded border border-divider bg-fill-3 flex items-center justify-center p-2 shadow-inner">
<n-image :src="getImgSrc(node.attributes.GNBR || '')" class="w-full rounded" style="width: 100%" alt="图纸页" />
</div>
</template>
<!-- 否则显示拟物化卡片模拟设计图 -->
<template v-else>
<div
class="w-full max-w-lg aspect-[16/10] rounded border border-divider bg-fill-3 flex flex-col items-center justify-center relative p-4 shadow-inner"
style="background-image: radial-gradient(circle, rgba(0, 0, 0, 0.03) 1px, transparent 1px); background-size: 16px 16px"
>
<div class="text-center space-y-2 select-none pointer-events-none">
<n-icon size="48" color="var(--text-color-3)"><image-outline /></n-icon>
<div class="text-xs text-color3">航空器结构部件装配原理图纸页</div>
<div class="text-[10px] text-color3/60 font-mono">GNBR Ref: {{ node.attributes.GNBR }}</div>
</div>
</div>
</template>
<!-- 如果 SHEET 没有自己的 TITLE 子节点,则继承展示 GRAPHIC 的 TITLE -->
<div v-if="!node.children.some((c) => c.tagName === 'TITLE' || c.tagName === 'TITLEC') && getInheritedTitle()" class="text-sm font-bold text-center mt-3 text-color1">
{{ getInheritedTitle() }}
</div>
<!-- 如果有子节点(例如 TITLE 或者是 EFFECT),渲染子节点 -->
<div v-if="node.children && node.children.length > 0" class="w-full mt-3 space-y-1">
<DocNodeRenderer v-for="child in node.children" :key="child.id" :node="child" :parent="node" />
</div>
</div>
</template>
<!-- 19.5 TXTGRPHC (文本图形块) 处理 -->
<template v-else-if="node.tagName === 'TXTGRPHC'">
<div
......@@ -802,6 +844,43 @@
></div>
</template>
<!-- 19.7 GDESC (图形描述) 处理 -->
<template v-else-if="node.tagName === 'GDESC'">
<div
class="my-1.5 pl-[14pt] pr-[6pt] text-xs text-color3 leading-relaxed"
@click.stop="editorStore.setSelectedNodeId(node.id)"
>
<DocNodeRenderer v-for="child in node.children" :key="child.id" :node="child" :parent="node" />
</div>
</template>
<!-- 19.8 TITLE / TITLEC (标题) 处理 -->
<template v-else-if="node.tagName === 'TITLE' || node.tagName === 'TITLEC'">
<div
class="text-color1 font-bold my-2"
:class="[
parent && (parent.tagName === 'SHEET' || parent.tagName === 'GRAPHIC') ? 'text-sm text-center' : 'text-base'
]"
@click.stop="editorStore.setSelectedNodeId(node.id)"
>
<template v-if="node.mixedContent && node.mixedContent.length > 0">
<span v-for="(mixed, idx) in node.mixedContent" :key="idx">
<template v-if="mixed.type === 'text'">{{ mixed.text }}</template>
<template v-else-if="mixed.type === 'element'">
<DocNodeRenderer
:node="node.children.find((c) => c.id === mixed.nodeId)!"
:parent="node"
is-inline
/>
</template>
</span>
</template>
<template v-else>
{{ node.textContent }}
</template>
</div>
</template>
<!-- 20. RECORD-LINE (记录项) 处理 -->
<template v-else-if="node.tagName === 'RECORD-LINE'">
<div class="my-1 py-1">
......@@ -1130,6 +1209,44 @@ import { ImageOutline, GridOutline } from '@vicons/ionicons5'
import type { XmlNode } from '@/types/xmlNode'
import TableEditor from '../TableEditor/index.vue'
import { useDocNodeRenderer, getSplitListChildren, getCepTaskNumber, isAllEinDataSameEffect, getDiffWordClass } from './functionals'
const getImgSrc = (gnbr: string) => {
if (!gnbr) return ''
if (gnbr.startsWith('http://') || gnbr.startsWith('https://')) {
return gnbr
}
if (gnbr.startsWith('/')) {
// vite.config.ts 已将 /mnt 代理到 https://amro.anyremote.cn,直接使用相对路径
return gnbr
}
// 拼接工卡基础物理路径目录,让 vite proxy 代理到真实图片地址
const basePath = '/mnt/disk2/ftp/tdms/jobcard/cep/MU/MU-B-3333-20260701-001/A320/SMJC/AMEA-A282400-02-1/0/'
const lowerGnbr = gnbr.toLowerCase()
const hasExtension = lowerGnbr.endsWith('.png') ||
lowerGnbr.endsWith('.jpg') ||
lowerGnbr.endsWith('.jpeg') ||
lowerGnbr.endsWith('.gif') ||
lowerGnbr.endsWith('.svg')
return `${basePath}${gnbr}${hasExtension ? '' : '.png'}`
}
const getInheritedTitle = () => {
if (props.node.tagName !== 'SHEET') return ''
if (props.node.children.some((c) => c.tagName === 'TITLE' || c.tagName === 'TITLEC')) {
return ''
}
if (props.parent && props.parent.tagName === 'GRAPHIC') {
const parentTitleNode = props.parent.children.find((c) => c.tagName === 'TITLE' || c.tagName === 'TITLEC')
const parentTitle = parentTitleNode?.textContent || ''
const sheetNbr = props.node.attributes.SHEETNBR || ''
if (parentTitle) {
return sheetNbr ? `${parentTitle} - Sheet ${sheetNbr}` : parentTitle
}
}
const sheetNbr = props.node.attributes.SHEETNBR || ''
return sheetNbr ? `Figure - Sheet ${sheetNbr}` : ''
}
import {
ALERT_BLOCK_TAGS,
PARA_TAGS,
......
......@@ -17,7 +17,8 @@ export const getEstimatedHeight = (tagName: string): number => {
case 'TABLE':
return 400 // 表格通常较高
case 'GRAPHIC':
return 300 // 图片/图纸
case 'SHEET':
return 350 // 图片/图纸/图纸页通常较高
case 'WARNING':
return 180 // 警告块
case 'CAUTION':
......
......@@ -194,6 +194,16 @@ export function useEditorPanel() {
const h = entry.contentRect.height
if (h > 0 && heightsMap.value[id] !== h) {
heightsMap.value[id] = h
// 首次加载大图时,可能因图片加载撑开高度导致原先计算好的滚动定位偏差。
// 一旦检测到当前选中节点所在的块高度发生变化,我们触发一次精确定位重修,以使图片完美居中。
if (editorStore.selectedNodeId) {
const nearestBlockIdx = findNearestBlockIdx(editorStore.selectedNodeId)
if (nearestBlockIdx !== -1 && blocksList.value[nearestBlockIdx]?.id === id) {
nextTick(() => {
syncEditorScroll(editorStore.selectedNodeId, true)
})
}
}
}
}
}
......@@ -366,6 +376,7 @@ export function useEditorPanel() {
visibleBlocks,
handleScroll,
setBlockRef,
syncEditorScroll
syncEditorScroll,
scrollTop
}
}
......@@ -68,6 +68,17 @@
<!-- 编辑区非 Table 元素右键上下文菜单弹窗 -->
<EditAreaContextMenuModal v-model="contextMenuVisible" :node-ids="contextMenuNodeIds" />
<!-- 回到顶部悬浮球 -->
<Transition name="fade">
<div
v-if="scrollTop > 400"
class="absolute bottom-6 right-6 z-30 w-10 h-10 rounded-full bg-primary text-white shadow-lg flex items-center justify-center cursor-pointer hover:bg-primary-hover hover:shadow-xl active:scale-95 transition-all"
@click="scrollToTop"
>
<n-icon size="20"><ArrowUpOutline /></n-icon>
</div>
</Transition>
</template>
<template v-else>
<div class="flex-1 flex flex-col items-center justify-center text-color3">
......@@ -78,7 +89,7 @@
</template>
<script setup lang="ts">
import { SettingsOutline, SearchOutline } from '@vicons/ionicons5'
import { SettingsOutline, SearchOutline, ArrowUpOutline } from '@vicons/ionicons5'
import { useEditorStore } from '@/store/editor'
import DocNodeRenderer from '../DocNodeRenderer/index.vue'
import FindReplacePanel from './components/FindReplacePanel/index.vue'
......@@ -88,8 +99,19 @@ import { addNodeVisible, addNodeMode, addNodeTargetId, addNodeAllowedTags } from
const editorStore = useEditorStore()
const { selectedNode, nodePath, editorTitle, viewportRef, totalHeight, startOffset, visibleBlocks, handleScroll, setBlockRef, syncEditorScroll } =
useEditorPanel()
const {
selectedNode,
nodePath,
editorTitle,
viewportRef,
totalHeight,
startOffset,
visibleBlocks,
handleScroll,
setBlockRef,
syncEditorScroll,
scrollTop
} = useEditorPanel()
const findReplaceVisible = ref(false)
......@@ -97,6 +119,12 @@ const toggleFindReplace = () => {
findReplaceVisible.value = !findReplaceVisible.value
}
const scrollToTop = () => {
if (viewportRef.value) {
viewportRef.value.scrollTop = 0
}
}
// 查找快捷键监听 (Ctrl+F)
useKeyboardShortcuts({
onFind: () => {
......@@ -173,4 +201,15 @@ const handleContextMenu = (e: MouseEvent) => {
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
background: var(--primary-color, #18a058);
}
.fade-enter-active,
.fade-leave-active {
transition:
opacity 0.25s ease,
transform 0.25s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
transform: translateY(10px) scale(0.9);
}
</style>
export interface GraphicFormState {
hasGraphicEffect: boolean
hasGraphicTitle: boolean
hasSheetEffect: boolean
hasSheetTitle: boolean
}
export const DEFAULT_FORM_STATE: GraphicFormState = {
hasGraphicEffect: true,
hasGraphicTitle: true,
hasSheetEffect: true,
hasSheetTitle: true
}
import type { XmlNode } from '@/types/xmlNode'
import { openUploadModal } from '@/utils/render'
import { uploadImage } from '@/utils/bridge'
import { DEFAULT_FORM_STATE } from '../constants'
import type { GraphicFormState } from '../constants'
export const useCreateGraphicModal = (emit: (event: 'confirm', node: XmlNode) => void) => {
const show = ref(false)
const isSheetOnly = ref(false)
const uploadedImageName = ref('')
const form = reactive<GraphicFormState>({ ...DEFAULT_FORM_STATE })
const open = (sheetOnly = false) => {
isSheetOnly.value = sheetOnly
uploadedImageName.value = ''
Object.assign(form, DEFAULT_FORM_STATE)
show.value = true
}
const displayImageName = computed(() => {
if (!uploadedImageName.value) return ''
const parts = uploadedImageName.value.split('/')
return parts[parts.length - 1]
})
const handleUpload = () => {
openUploadModal({
title: '上传图片资源',
accept: 'image/*',
uploadFunc: (file: File) => {
return uploadImage(file)
},
onSuccess: (res, file) => {
// 4. 将接口返回的 data 字段路径,直接赋给 uploadedImageName
if (res && res.data) {
uploadedImageName.value = res.data
} else if (res && typeof res === 'string') {
uploadedImageName.value = res
} else if (file) {
uploadedImageName.value = file.name
}
}
})
}
const handleConfirm = () => {
if (!uploadedImageName.value) {
window.$message.error('请先上传图片资源!')
return
}
// 直接使用上传返回的完整 data 路径作为 GNBR 属性值
const gnbrValue = uploadedImageName.value
if (isSheetOnly.value) {
// 如果仅插入 SHEET 节点
const sheetNodeId = crypto.randomUUID()
const sheetNode: XmlNode = {
id: sheetNodeId,
tagName: 'SHEET',
attributes: {
GNBR: gnbrValue
},
children: [],
textContent: '',
mixedContent: [],
parentId: undefined as any
}
if (form.hasSheetEffect) {
sheetNode.children.push({
id: crypto.randomUUID(),
tagName: 'EFFECT',
attributes: {},
children: [],
textContent: '',
mixedContent: [],
parentId: sheetNodeId
})
}
if (form.hasSheetTitle) {
sheetNode.children.push({
id: crypto.randomUUID(),
tagName: 'TITLE',
attributes: {},
children: [],
textContent: '',
mixedContent: [],
parentId: sheetNodeId
})
}
emit('confirm', sheetNode)
} else {
// 插入完整 GRAPHIC 节点包装
const graphicNodeId = crypto.randomUUID()
const sheetNodeId = crypto.randomUUID()
const graphicNode: XmlNode = {
id: graphicNodeId,
tagName: 'GRAPHIC',
attributes: {},
children: [],
textContent: '',
mixedContent: [],
parentId: undefined as any
}
if (form.hasGraphicEffect) {
graphicNode.children.push({
id: crypto.randomUUID(),
tagName: 'EFFECT',
attributes: {},
children: [],
textContent: '',
mixedContent: [],
parentId: graphicNodeId
})
}
if (form.hasGraphicTitle) {
graphicNode.children.push({
id: crypto.randomUUID(),
tagName: 'TITLE',
attributes: {},
children: [],
textContent: '',
mixedContent: [],
parentId: graphicNodeId
})
}
const sheetNode: XmlNode = {
id: sheetNodeId,
tagName: 'SHEET',
attributes: {
GNBR: gnbrValue
},
children: [],
textContent: '',
mixedContent: [],
parentId: graphicNodeId
}
if (form.hasSheetEffect) {
sheetNode.children.push({
id: crypto.randomUUID(),
tagName: 'EFFECT',
attributes: {},
children: [],
textContent: '',
mixedContent: [],
parentId: sheetNodeId
})
}
if (form.hasSheetTitle) {
sheetNode.children.push({
id: crypto.randomUUID(),
tagName: 'TITLE',
attributes: {},
children: [],
textContent: '',
mixedContent: [],
parentId: sheetNodeId
})
}
graphicNode.children.push(sheetNode)
emit('confirm', graphicNode)
}
show.value = false
}
return {
show,
isSheetOnly,
uploadedImageName,
displayImageName,
form,
open,
handleUpload,
handleConfirm
}
}
<template>
<CommonModal v-model="show" :title="isSheetOnly ? '插入图纸页 (SHEET)' : '插入图片 (GRAPHIC)'" :width="400" @confirm="handleConfirm">
<div class="flex flex-col gap-4 py-2">
<div class="text-xs text-color3 mb-1">
请选择插入时需要携带的可选节点:
</div>
<!-- GRAPHIC 级别的可选节点 (仅在非 SHEET-only 模式下显示) -->
<div v-if="!isSheetOnly" class="p-3 bg-fill-2 border border-divider rounded-lg">
<div class="font-bold text-xs text-color2 mb-2">图片 (GRAPHIC) 可选节点</div>
<div class="flex flex-col gap-2">
<n-checkbox v-model:checked="form.hasGraphicEffect">
包含适用性说明 &lt;EFFECT&gt;
</n-checkbox>
<n-checkbox v-model:checked="form.hasGraphicTitle">
包含附图标题 &lt;TITLE&gt;
</n-checkbox>
</div>
</div>
<!-- SHEET 级别的可选节点 (始终显示) -->
<div class="p-3 bg-fill-2 border border-divider rounded-lg">
<div class="font-bold text-xs text-color2 mb-2">图纸 (SHEET) 可选节点</div>
<div class="flex flex-col gap-2">
<n-checkbox v-model:checked="form.hasSheetEffect">
包含适用性说明 &lt;EFFECT&gt;
</n-checkbox>
<n-checkbox v-model:checked="form.hasSheetTitle">
包含图纸标题 &lt;TITLE&gt;
</n-checkbox>
</div>
</div>
<!-- 图片资源上传区 (必填) -->
<div class="p-3 bg-fill-2 border border-divider rounded-lg">
<div class="font-bold text-xs text-color2 mb-2 flex items-center justify-between">
<span>图片资源 (必填)</span>
<span v-if="!uploadedImageName" class="text-danger text-[10px]">* 未上传</span>
<span v-else class="text-success text-[10px]">已上传</span>
</div>
<div class="flex items-center justify-between gap-3">
<div class="text-xs truncate max-w-[200px]" :class="uploadedImageName ? 'text-color1' : 'text-color3 italic'">
{{ displayImageName || '暂未上传图片资源' }}
</div>
<CommonButton size="small" type="primary" secondary @click="handleUpload">
{{ uploadedImageName ? '重新上传' : '上传图片' }}
</CommonButton>
</div>
</div>
</div>
</CommonModal>
</template>
<script setup lang="ts">
import type { XmlNode } from '@/types/xmlNode'
import { useCreateGraphicModal } from './functionals'
const emit = defineEmits<{
confirm: [node: XmlNode]
}>()
const { show, isSheetOnly, uploadedImageName, displayImageName, form, open, handleUpload, handleConfirm } = useCreateGraphicModal(emit)
defineExpose({ open })
</script>
export interface ViewEffectState {
visible: boolean
title: string
xmlTree: any
}
import { parseXmlToTree } from '@/utils/xmlParser'
import type { XmlNode } from '@/types/xmlNode'
export function useViewEffectModal() {
const visible = ref(false)
const title = ref('')
const xmlTree = ref<XmlNode | null>(null)
const open = (modalTitle: string, xmlContent: string) => {
title.value = modalTitle
try {
// 解析 XML 片段
xmlTree.value = parseXmlToTree(xmlContent)
visible.value = true
} catch (err: any) {
window.$message?.error(`解析 XML 效果失败: ${err.message || err}`)
}
}
return {
visible,
title,
xmlTree,
open
}
}
<template>
<CommonModal v-model="visible" :title="title" :width="900" :show-confirm="false" cancel-text="关闭">
<DocNodeRenderer v-if="xmlTree" :node="xmlTree" :parent="null" />
</CommonModal>
</template>
<script setup lang="ts">
import DocNodeRenderer from '../../../../../DocNodeRenderer/index.vue'
import { useViewEffectModal } from './functionals'
const { visible, title, xmlTree, open } = useViewEffectModal()
// 注入只读模式的对比状态,禁用一切编辑行为及组件内部编辑事件
provide('diffMode', true)
defineExpose({
open
})
</script>
<style scoped></style>
import type { TemplateItem } from '@/utils/bridge'
export type { TemplateItem }
import { useEditorStore } from '@/store/editor'
import { getTemplateList } from '@/utils/bridge'
import type { TemplateItem } from '../constants'
import { apiConfig } from '@/xm/config/api'
export function useTemplateSelectModal() {
const editorStore = useEditorStore()
const visible = ref(false)
const loading = ref(false)
const templates = ref<TemplateItem[]>([])
const selectedTemplate = ref<TemplateItem | null>(null)
const insertBelowSetting = ref(false)
// 分页
const page = ref(1)
const pageSize = ref(10)
const total = ref(0)
// 预览 XML 弹窗 Ref
const viewXmlModalRef = ref<any>(null)
// 预览效果弹窗 Ref
const viewEffectModalRef = ref<any>(null)
const fetchData = async () => {
loading.value = true
try {
const res = await getTemplateList(page.value, pageSize.value)
if (apiConfig.isSuccess(res)) {
templates.value = res.data
total.value = res.total
} else {
window.$message?.error(res.msg || '获取模板数据失败')
}
} catch (err: any) {
window.$message?.error(err.message || '网络请求错误')
} finally {
loading.value = false
}
}
const previewXml = (item: TemplateItem) => {
viewXmlModalRef.value?.open(`预览模板:${item.name}`, item.content)
}
const previewEffect = (item: TemplateItem) => {
viewEffectModalRef.value?.open(`预览模板效果:${item.name}`, item.content)
}
const open = (insertBelow: boolean) => {
insertBelowSetting.value = insertBelow
selectedTemplate.value = null
page.value = 1
fetchData()
visible.value = true
}
const handleInsert = () => {
if (!selectedTemplate.value) return
try {
const mode = insertBelowSetting.value ? 'below' : 'inside'
const count = editorStore.insertXmlFragment(selectedTemplate.value.content, mode)
window.$message?.success(`成功插入 ${count} 个节点`)
visible.value = false
} catch (error: any) {
window.$message?.error(`插入模板失败: ${error.message}`)
}
}
return {
visible,
loading,
templates,
selectedTemplate,
insertBelowSetting,
page,
pageSize,
total,
viewXmlModalRef,
viewEffectModalRef,
fetchData,
previewXml,
previewEffect,
handleInsert,
open
}
}
<template>
<CommonModal
v-model="visible"
title="插入 XML 模板"
:width="750"
:loading="loading"
confirm-text="确认插入"
:confirm-disabled="!selectedTemplate"
@confirm="handleInsert"
>
<div class="flex flex-col space-y-4">
<div class="text-xs text-color3 bg-primary/5 p-3 rounded-lg border border-primary/20">
提示:模板中包含预先设计好的复杂节点结构(如 CALS 表格、多列配置等)。请选择要插入的模板,并直接进行插入。
</div>
<!-- 模板列表 -->
<div class="template-list-container border border-divider rounded-lg overflow-hidden max-h-[380px] overflow-y-auto bg-card">
<template v-if="templates.length > 0">
<n-list hoverable clickable show-divider>
<n-list-item
v-for="item in templates"
:key="item.id"
:class="['transition-colors cursor-pointer', selectedTemplate?.id === item.id ? 'bg-primary/5 border-l-4 border-primary' : '']"
@click="selectedTemplate = item"
@dblclick="handleInsert"
>
<n-thing
:title="item.name"
:description="`创建人: ${item.creator} | 创建时间: ${item.createTime}`"
/>
<template #suffix>
<n-space>
<CommonButton size="tiny" secondary type="primary" @click.stop="previewXml(item)">
预览 XML
</CommonButton>
<CommonButton size="tiny" secondary type="info" @click.stop="previewEffect(item)">
预览效果
</CommonButton>
</n-space>
</template>
</n-list-item>
</n-list>
</template>
<template v-else>
<div class="py-12 flex flex-col items-center justify-center">
<n-empty description="暂无可用的 XML 模板数据" />
</div>
</template>
</div>
<!-- 分页 -->
<div class="flex justify-between items-center mt-2">
<!-- 左侧:已选中的模板名称提示 -->
<div class="text-xs text-color2">
<span v-if="selectedTemplate">
已选择:<strong class="text-primary">{{ selectedTemplate.name }}</strong>
<span class="text-color3 ml-3">({{ insertBelowSetting ? '插入下方' : '作为子节点' }})</span>
</span>
<span v-else class="text-color3">请从列表中选择模板</span>
</div>
<n-pagination
v-model:page="page"
v-model:page-size="pageSize"
:item-count="total"
@update:page="fetchData"
/>
</div>
</div>
</CommonModal>
<!-- XML 模板内容预览弹窗 -->
<ViewXmlModal ref="viewXmlModalRef" />
<!-- XML 模板效果预览弹窗 -->
<ViewEffectModal ref="viewEffectModalRef" />
</template>
<script setup lang="ts">
import ViewXmlModal from '../../../NodeTree/components/ViewXmlModal/index.vue'
import ViewEffectModal from './components/ViewEffectModal/index.vue'
import { useTemplateSelectModal } from './functionals'
const {
visible,
loading,
templates,
selectedTemplate,
page,
pageSize,
total,
insertBelowSetting,
viewXmlModalRef,
viewEffectModalRef,
fetchData,
previewXml,
previewEffect,
handleInsert,
open
} = useTemplateSelectModal()
defineExpose({
open
})
</script>
<style scoped>
.template-list-container {
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.05);
}
</style>
......@@ -6,9 +6,9 @@ import { ImageOutline, GridOutline, DocumentTextOutline, CreateOutline, RemoveOu
export const TOOLBAR_TITLE = 'XML 编辑工具栏'
export const GREEN_BUTTONS: any[] = [
// { label: '插入图片', tag: 'GRAPHIC', icon: ImageOutline },
{ label: '插入图片', tag: 'GRAPHIC', icon: ImageOutline },
{ label: '插入表格', tag: 'TABLE', icon: GridOutline },
// { label: '插入模板', tag: 'PRETOPIC', icon: DocumentTextOutline },
{ label: '插入模板', tag: 'TEMPLATE', icon: DocumentTextOutline },
{ label: '插入签字点', tag: 'SIGNOFF', icon: CreateOutline },
{ label: '插入下划线', tag: 'RECORD-LINE', icon: RemoveOutline }
]
......@@ -15,11 +15,18 @@ export function useEditorToolbar(emit: any) {
const isUploading = ref(false)
const createTableModalRef = ref<any>(null)
const createSignoffModalRef = ref<any>(null)
const templateSelectModalRef = ref<any>(null)
const createGraphicModalRef = ref<any>(null)
const canUndo = computed(() => editorStore.undoStack.length > 0)
const canRedo = computed(() => editorStore.redoStack.length > 0)
const handleInsert = (tag: string) => {
if (tag === 'TEMPLATE') {
templateSelectModalRef.value?.open(insertBelow.value)
return
}
const selected = editorStore.selectedNode
if (!selected) {
window.$message.warning('请先在树中选择一个目标节点!')
......@@ -40,18 +47,27 @@ export function useEditorToolbar(emit: any) {
parentTag = selected.tagName
}
const existingCount = parentNode.children.filter((c) => c.tagName === tag).length
if (!canAddChild(parentTag, tag, existingCount)) {
window.$message.warning(`DTD 校验失败: 节点 <${parentTag}> 无法接受子元素 <${tag}>`)
// 动态判定当前想插入的实际标签类型以适配 DTD 校验
let activeTag = tag
if (tag === 'GRAPHIC' && parentTag === 'GRAPHIC') {
activeTag = 'SHEET'
}
const existingCount = parentNode.children.filter((c) => c.tagName === activeTag).length
if (!canAddChild(parentTag, activeTag, existingCount)) {
window.$message.warning(`DTD 校验失败: 节点 <${parentTag}> 无法接受子元素 <${activeTag}>`)
return
}
if (tag === 'TABLE') {
if (activeTag === 'TABLE') {
createTableModalRef.value?.open()
} else if (tag === 'SIGNOFF') {
} else if (activeTag === 'SIGNOFF') {
createSignoffModalRef.value?.open()
} else if (tag === 'GRAPHIC') {
// 如果 activeTag 为 SHEET,说明在 GRAPHIC 内部或 SHEET 兄弟级插入单个 SHEET;否则插入完整 GRAPHIC
createGraphicModalRef.value?.open(activeTag === 'SHEET')
} else {
editorStore.insertNode(tag, insertBelow.value)
editorStore.insertNode(activeTag, insertBelow.value)
}
}
......@@ -70,6 +86,10 @@ export function useEditorToolbar(emit: any) {
editorStore.insertNode('SIGNOFF', insertBelow.value, signoffNode)
}
const handleCreateGraphicConfirm = (node: XmlNode) => {
editorStore.insertNode(node.tagName, insertBelow.value, node)
}
const handleCreateTableConfirm = (rows: number, cols: number, cellChildTags: string[]) => {
const tableNode = createTableStructure(rows, cols, cellChildTags)
editorStore.insertNode('TABLE', insertBelow.value, tableNode)
......@@ -151,10 +171,12 @@ export function useEditorToolbar(emit: any) {
handleCreateTableConfirm,
createSignoffModalRef,
handleCreateSignoffConfirm,
createGraphicModalRef,
handleCreateGraphicConfirm,
batchTranslateModalRef,
extractTranslateModalRef,
searchTranslateModalRef,
xmlSizeStr,
handleFileUpload: () => {} // 保留空函数以防组件 template 尚未完全更新时报错
templateSelectModalRef,
}
}
......@@ -227,6 +227,9 @@
<!-- 插入签字点弹窗 -->
<CreateSignoffModal ref="createSignoffModalRef" @confirm="handleCreateSignoffConfirm" />
<!-- 插入图片弹窗 -->
<CreateGraphicModal ref="createGraphicModalRef" @confirm="handleCreateGraphicConfirm" />
<!-- 批量翻译弹窗 -->
<BatchTranslateModal ref="batchTranslateModalRef" />
......@@ -238,6 +241,9 @@
<!-- 工卡对比弹窗 -->
<CompareModal ref="compareModalRef" />
<!-- 插入模板弹窗 -->
<TemplateSelectModal ref="templateSelectModalRef" />
</div>
</template>
......@@ -265,10 +271,12 @@ import SettingsDrawer from '@/layouts/components/SettingsDrawer.vue'
import InsertFragmentModal from './components/InsertFragmentModal/index.vue'
import CreateTableModal from './components/CreateTableModal/index.vue'
import CreateSignoffModal from './components/CreateSignoffModal/index.vue'
import CreateGraphicModal from './components/CreateGraphicModal/index.vue'
import BatchTranslateModal from './components/BatchTranslateModal/index.vue'
import ExtractTranslateModal from './components/ExtractTranslateModal/index.vue'
import SearchTranslateModal from './components/SearchTranslateModal/index.vue'
import CompareModal from './components/CompareModal/index.vue'
import TemplateSelectModal from './components/TemplateSelectModal/index.vue'
const emit = defineEmits(['save', 'validate', 'export', 'preview', 'download-html'])
......@@ -287,10 +295,13 @@ const {
handleCreateTableConfirm,
createSignoffModalRef,
handleCreateSignoffConfirm,
createGraphicModalRef,
handleCreateGraphicConfirm,
batchTranslateModalRef,
extractTranslateModalRef,
searchTranslateModalRef,
xmlSizeStr
xmlSizeStr,
templateSelectModalRef
} = useEditorToolbar(emit)
const insertFragmentModalRef = ref<any>(null)
......
......@@ -97,7 +97,7 @@ export function useAddNodeModal(formRef: Ref<FormInst | null>) {
name,
typeDefinition: def.typeDefinition,
enumValues: def.enumValues,
required: def.requirement === 'REQUIRED',
required: def.requirement === '#REQUIRED',
defaultValue: def.defaultValue
}))
})
......
......@@ -11,7 +11,13 @@
<n-divider class="!my-2">
<span class="text-xs text-color3">属性配置</span>
</n-divider>
<n-form-item v-for="attr in attributeDefs" :key="attr.name" :label="attr.name" :path="`attrs.${attr.name}`">
<n-form-item
v-for="attr in attributeDefs"
:key="attr.name"
:label="attr.name"
:path="`attrs.${attr.name}`"
:rule="attr.required ? { required: true, message: '此属性为必填项', trigger: ['input', 'change'] } : undefined"
>
<!-- 枚举类型:select -->
<CommonSelect
v-if="attr.enumValues && attr.enumValues.length > 0"
......
import { formatXmlText } from '@/utils/xmlParser'
import { viewXmlVisible, viewXmlTitle, viewXmlContent } from '../../../functionals'
export const useViewXmlModal = () => {
const formattedXmlContent = computed(() => {
return formatXmlText(viewXmlContent.value)
})
const handleCopyXml = async () => {
try {
await navigator.clipboard.writeText(viewXmlContent.value)
// 将所有换行和换行后的多余空格全部替换,确保复制到剪贴板的是一行文本
const singleLineXml = viewXmlContent.value.replace(/\r?\n\s*/g, '').trim()
await navigator.clipboard.writeText(singleLineXml)
window.$message?.success('XML 片段已成功复制到剪贴板')
} catch (err) {
window.$message?.error('复制失败,请手动选择复制')
......@@ -20,6 +27,7 @@ export const useViewXmlModal = () => {
viewXmlVisible,
viewXmlTitle,
viewXmlContent,
formattedXmlContent,
handleCopyXml,
open
}
......
......@@ -7,7 +7,7 @@
style="background-image: radial-gradient(circle, rgba(0, 0, 0, 0.02) 1px, transparent 1px); background-size: 16px 16px"
>
<div class="p-6 font-mono text-sm leading-relaxed overflow-x-auto max-h-[500px] scrollbar-thin select-all">
<pre class="text-color1"><code class="xml-content-pre">{{ viewXmlContent }}</code></pre>
<pre class="text-color1" @copy.prevent="handleCopyXml"><code class="xml-content-pre">{{ formattedXmlContent }}</code></pre>
</div>
</div>
</div>
......@@ -27,7 +27,7 @@
import { ClipboardOutline } from '@vicons/ionicons5'
import { useViewXmlModal } from './functionals'
const { viewXmlVisible, viewXmlTitle, viewXmlContent, handleCopyXml, open } = useViewXmlModal()
const { viewXmlVisible, viewXmlTitle, formattedXmlContent, handleCopyXml, open } = useViewXmlModal()
defineExpose({ open })
</script>
......
......@@ -281,6 +281,10 @@ export function useNodeTree(
} else {
subtitle = `${min || max} ${unit}`
}
} else if (node.tagName === 'SHEET') {
const sheetNbr = node.attributes.SHEETNBR || ''
const gnbr = node.attributes.GNBR || ''
subtitle = sheetNbr ? `Sheet ${sheetNbr} [GNBR: ${gnbr}]` : `GNBR: ${gnbr}`
} else if (node.attributes.ID) {
subtitle = node.attributes.ID
} else if (node.attributes.EFFRG) {
......
......@@ -192,11 +192,22 @@
<!-- 批量删除确认弹窗 -->
<BatchDeleteConfirmModal ref="batchDeleteConfirmModalRef" @confirm="clearBatchSelection" />
<!-- 回到顶部悬浮球 -->
<Transition name="fade">
<div
v-if="scrollTop > 300"
class="absolute bottom-4 right-4 z-20 w-8 h-8 rounded-full bg-primary text-white shadow-lg flex items-center justify-center cursor-pointer hover:bg-primary-hover active:scale-95 transition-all"
@click="scrollToTop"
>
<n-icon size="16"><ArrowUpOutline /></n-icon>
</div>
</Transition>
</div>
</template>
<script setup lang="ts">
import { SearchOutline, ListOutline, TrashOutline, CloseOutline, SyncOutline } from '@vicons/ionicons5'
import { SearchOutline, ListOutline, TrashOutline, CloseOutline, SyncOutline, ArrowUpOutline } from '@vicons/ionicons5'
import { useEditorStore } from '@/store/editor'
import {
useNodeTree,
......@@ -230,6 +241,7 @@ const isAnyModalVisible = computed(() => {
const {
pattern,
viewportRef,
scrollTop,
flatList,
totalHeight,
visibleVerticalLines,
......@@ -268,6 +280,12 @@ const {
}
)
const scrollToTop = () => {
if (viewportRef.value) {
viewportRef.value.scrollTop = 0
}
}
const batchDeleteConfirmModalRef = ref<any>(null)
</script>
......@@ -448,4 +466,13 @@ const batchDeleteConfirmModalRef = ref<any>(null)
transform: translateY(-3px);
}
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.25s ease, transform 0.25s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
transform: translateY(10px) scale(0.9);
}
</style>
<template>
<div class="flex-1 flex flex-col p-4 space-y-4 overflow-auto bg-transparent min-h-0" @click.stop @click="closeContextMenu">
<!-- 表格工具栏 -->
<div class="flex items-center pb-2 border-b border-divider space-x-2">
<div v-if="!isDiffMode" class="flex items-center pb-2 border-b border-divider space-x-2">
<CommonTag type="info" size="small">{{ node.tagName }}</CommonTag>
<span class="text-xs text-color3">{{ CELL_EDIT_TIP }}</span>
<span v-if="!isDiffMode" class="text-xs text-color3 select-none">· 右键单元格可快速操作</span>
......@@ -19,7 +19,7 @@
<colgroup>
<col class="w-12" />
<col v-for="(_, idx) in structure.cols" :key="idx" :style="{ width: colWidthStyles[idx] }" />
<col class="w-12" />
<col v-if="!isDiffMode" class="w-12" />
</colgroup>
<!-- THEAD 渲染 -->
......@@ -44,7 +44,7 @@
is-inline
/>
</th>
<th class="p-1 border border-divider bg-fill-4"></th>
<th v-if="!isDiffMode" class="p-1 border border-divider bg-fill-4"></th>
</tr>
<tr
......
export const apiConfig = {
// 校验接口响应是否成功
isSuccess: (res: any) => {
return res && res.code === 200
}
}
import { apiService } from '@/api'
import { execute } from '@/utils/bridgeHelper'
export interface TemplateItem {
id: number
name: string
creator: string
createTime: string
content: string
}
export interface TemplateResponse {
code: number | string
msg?: string
total: number
data: TemplateItem[]
}
// 本地接口的具体实现
const localBridge = {
getTemplateList: async (page: number, rows: number): Promise<TemplateResponse> => {
const res = await apiService.post<any>('/plugins/EM_JOBCARD_GX_TEMPLATE_SELECT_PAGE', {
page,
rows
})
if (res) {
const listData = Array.isArray(res.data) ? res.data : res.rows || []
const totalCount = res.total !== undefined ? res.total : listData.length
return {
code: 200,
msg: res.msg || 'Success',
total: totalCount,
data: listData
}
}
throw new Error('接口未返回有效的数据结构')
},
saveTemplate: async (name: string, content: string): Promise<any> => {
return await apiService.post<any>('/plugins/EM_JOBCARD_GX_TEMPLATE_SAVE', {
name,
content
})
},
uploadImage: async (file: File): Promise<any> => {
return await apiService.postForm<any>('/plugins/TD_JC_CEP_UPLOAD_IMG', {
file,
jcpkid: '13054174'
})
}
}
/**
* 获取模板列表
*/
export const getTemplateList = execute('getTemplateList', localBridge.getTemplateList, (page, rows) => ({ page, rows }))
/**
* 保存模板
*/
export const saveTemplate = execute('saveTemplate', localBridge.saveTemplate, (name, content) => ({ name, content }))
/**
* 上传工卡图片资源
*/
export const uploadImage = execute('uploadImage', localBridge.uploadImage, (file) => ({ file }))
......@@ -7,11 +7,15 @@ import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
// https://vite.dev/config/
export default defineConfig(({ mode }) => {
loadEnv(mode, process.cwd())
const env = loadEnv(mode, process.cwd())
const customCode = env.VITE_CUSTOM_CODE || ''
const apiUrl = env.VITE_API_URL
return {
resolve: {
alias: {
'@/utils/bridge': customCode
? path.resolve(__dirname, `./src/${customCode}/utils/bridge`)
: path.resolve(__dirname, './src/utils/bridge'),
'@': path.resolve(__dirname, './src')
}
},
......@@ -62,6 +66,19 @@ export default defineConfig(({ mode }) => {
changeOrigin: true,
secure: false,
rewrite: (path) => path.replace(/^\/translations/, '')
},
'/api': {
target: apiUrl,
changeOrigin: true,
secure: false,
headers: {
Cookie: '_udid=de1f23e4-c072-42b9-a3f8-5f91d859b434; i18next=zh; JSESSIONID=16EA8A4AC66DD35455A3DACE83FA56B6; _amro_sk=ac11efbe-0a7b-467d-b597-212d5b0069cc'
}
},
'/mnt': {
target: apiUrl,
changeOrigin: true,
secure: false
}
}
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment