Kaynağa Gözat

Merge branch 'feature/wl-coupon'

wuwenyi 2 hafta önce
ebeveyn
işleme
d2cc43cdda

+ 8 - 2
src/components/Layout/index.vue

@@ -1,8 +1,10 @@
 <template>
   <el-container class="layout-container">
-    <Sidebar />
+    <!-- 被 SCRM 等宿主系统内嵌时隐藏左侧菜单(嵌入方自带导航,避免双层菜单) -->
+    <Sidebar v-if="!embedded" />
     <el-container direction="vertical">
-      <Navbar />
+      <!-- 内嵌时同时隐藏顶部导航栏(通知消息 / 个人中心 / 用户菜单),只保留内容区 -->
+      <Navbar v-if="!embedded" />
       <el-main class="main-content">
         <router-view />
       </el-main>
@@ -13,6 +15,10 @@
 <script setup lang="ts">
 import Sidebar from '@/components/Sidebar/index.vue'
 import Navbar from '@/components/Navbar/index.vue'
+import { isEmbedded } from '@/utils/auth'
+
+// 嵌入态判定:宿主通过 ?embed=1 显式标记(主),iframe 环境探测兜底
+const embedded = new URLSearchParams(window.location.search).get('embed') === '1' || isEmbedded()
 </script>
 
 <style scoped>

+ 6 - 1
src/main.ts

@@ -1,6 +1,7 @@
 import { createApp } from 'vue'
 import { createPinia } from 'pinia'
 import ElementPlus from 'element-plus'
+import zhCn from 'element-plus/es/locale/lang/zh-cn'
 import 'element-plus/dist/index.css'
 import {
   ArrowDown,
@@ -26,8 +27,12 @@ import {
 } from '@element-plus/icons-vue'
 import App from './App.vue'
 import router from './router'
+import { initEmbed } from './utils/embed'
 import './style.css'
 
+// 宿主免登:必须在路由初始导航(app.use(router) 触发的首个守卫)之前完成 token 落库
+initEmbed()
+
 const app = createApp(App)
 
 // 按需注册图标
@@ -59,6 +64,6 @@ for (const [key, component] of Object.entries(icons)) {
 
 app.use(createPinia())
 app.use(router)
-app.use(ElementPlus)
+app.use(ElementPlus, { locale: zhCn })
 
 app.mount('#app')

+ 3 - 1
src/types/flow.ts

@@ -95,8 +95,10 @@ export interface FlowSubNodeConfig {
 }
 
 export interface FlowNodeProperties {
-  assigneeType?: 'ROLE'
+  assigneeType?: 'ROLE' | 'SELF' | 'EXTERNAL'
   assigneeValue?: string
+  /** 外部指定审批人时,与 SCRM 审批人配置匹配的节点编码 */
+  nodeKey?: string
   approveMode?: 'or' | 'and'
   timeoutHours?: number
   timeoutAction?: 'remind' | 'pass' | 'reject'

+ 25 - 0
src/utils/auth.ts

@@ -4,11 +4,32 @@ const TokenKey = 'Admin-Token'
 
 const isSecure = window.location.protocol === 'https:'
 
+/**
+ * 是否运行在 iframe 内(被 SCRM 等宿主系统内嵌)
+ * 跨域访问 window.top 会抛异常,同样说明处于 iframe 中
+ */
+export function isEmbedded(): boolean {
+  try {
+    return window.self !== window.top
+  } catch {
+    return true
+  }
+}
+
 export function getToken(): string | undefined {
+  // iframe 内 token 走 sessionStorage:跨站 iframe 不会携带 SameSite=Strict 的 Cookie,
+  // 独立访问时行为保持原样(Cookie)
+  if (isEmbedded()) {
+    return sessionStorage.getItem(TokenKey) || undefined
+  }
   return Cookies.get(TokenKey)
 }
 
 export function setToken(token: string): void {
+  if (isEmbedded()) {
+    sessionStorage.setItem(TokenKey, token)
+    return
+  }
   Cookies.set(TokenKey, token, {
     secure: isSecure,
     sameSite: 'Strict',
@@ -17,5 +38,9 @@ export function setToken(token: string): void {
 }
 
 export function removeToken(): void {
+  if (isEmbedded()) {
+    sessionStorage.removeItem(TokenKey)
+    return
+  }
   Cookies.remove(TokenKey)
 }

+ 29 - 0
src/utils/embed.ts

@@ -0,0 +1,29 @@
+import { getToken, setToken } from '@/utils/auth'
+
+/**
+ * 宿主免登适配(yiwise 一智同款:token 随 URL 带进来,落地页即目标页)
+ *
+ * 宿主(SCRM「审批流配置」页)把服务账号 JWT 直接拼进 iframe 地址:
+ *   {qqflow}/flow/designer#embedToken=xxx
+ * 本模块在应用挂载前执行:
+ *  1. 从 URL 片段(hash)取 embedToken → 写入登录态(iframe 内为 sessionStorage,见 utils/auth.ts);
+ *  2. history.replaceState 清掉地址片段(token 不留在地址栏/浏览器历史);
+ *  3. 随后路由守卫直接放行到 /flow/designer,无登录页中转。
+ *
+ * 相对 yiwise 把 access_token 放 query 的做法,这里放 hash 片段:
+ * hash 不会随请求发给网关/静态服务器,不会进访问日志。
+ */
+const EMBED_TOKEN_KEY = 'embedToken'
+
+export function initEmbed(): void {
+  const hash = window.location.hash
+  if (!hash || hash.indexOf(EMBED_TOKEN_KEY) < 0) return
+  const token = new URLSearchParams(hash.replace(/^#/, '')).get(EMBED_TOKEN_KEY)
+  if (!token) return
+  if (getToken() !== token) {
+    setToken(token)
+  }
+  // 清理地址栏片段,保留 path/query
+  window.history.replaceState({}, '', window.location.pathname + window.location.search)
+  console.info('[embed] 已按 URL 参数完成免登,直达目标页')
+}

+ 12 - 1
src/views/flow/designer/DesignerCanvas.vue

@@ -41,6 +41,7 @@
 </template>
 
 <script setup lang="ts">
+import { computed } from 'vue'
 import type { ComponentPublicInstance } from 'vue'
 import { VideoPlay, User, Message, Operation, CircleCheck } from '@element-plus/icons-vue'
 import type { Role } from '@/types/system'
@@ -54,6 +55,8 @@ const props = defineProps<{
   roleList: Role[]
   selectedSubNodeIndex: number | null
   drawerVisible: boolean
+  /** 被 SCRM 内嵌时隐藏抄送节点入口,已有抄送节点仍按流程模型展示 */
+  embedded: boolean
 }>()
 
 const emit = defineEmits<{
@@ -67,7 +70,7 @@ const emit = defineEmits<{
   removeSubNode: [index: number]
 }>()
 
-const nodeTypes = [
+const allNodeTypes = [
   { type: 'start-node', label: '开始', icon: VideoPlay, color: '#67C23A' },
   { type: 'approval-node', label: '审批节点', icon: User, color: '#409EFF' },
   { type: 'cc-node', label: '抄送节点', icon: Message, color: '#E6A23C' },
@@ -75,6 +78,11 @@ const nodeTypes = [
   { type: 'end-node', label: '结束', icon: CircleCheck, color: '#F56C6C' }
 ]
 
+// 内嵌到 SCRM 时不允许再从左侧面板新增抄送节点;独立使用 qqflow 时保持完整能力
+const nodeTypes = computed(() => props.embedded
+  ? allNodeTypes.filter((node) => node.type !== 'cc-node')
+  : allNodeTypes)
+
 function handleDragStart(e: DragEvent, node: any) {
   const serializable = { type: node.type, label: node.label }
   e.dataTransfer?.setData('application/json', JSON.stringify(serializable))
@@ -90,6 +98,8 @@ function handleDragStart(e: DragEvent, node: any) {
 }
 .node-panel {
   width: 200px;
+  flex-shrink: 0;
+  box-sizing: border-box;
   border-right: 1px solid #dcdfe6;
   padding: 16px;
   background: #fafafa;
@@ -131,6 +141,7 @@ function handleDragStart(e: DragEvent, node: any) {
 }
 .canvas-area {
   flex: 1;
+  min-width: 0;
   position: relative;
   background: #f5f7fa;
   overflow: hidden;

+ 20 - 1
src/views/flow/designer/DesignerPropertyPanel.vue

@@ -15,10 +15,11 @@
               v-model="nodeProps.assigneeType"
               placeholder="请选择审批对象类型"
               style="width: 100%"
-              @change="nodeProps.assigneeValue = ''"
+              @change="onAssigneeTypeChange"
             >
               <el-option label="审批角色" value="ROLE" />
               <el-option label="发起人自己" value="SELF" />
+              <el-option label="外部指定(SCRM)" value="EXTERNAL" />
             </el-select>
           </el-form-item>
           <el-form-item v-if="nodeProps.assigneeType === 'ROLE'" label="审批角色">
@@ -37,6 +38,9 @@
               />
             </el-select>
           </el-form-item>
+          <el-form-item v-if="nodeProps.assigneeType === 'EXTERNAL'" label="节点编码">
+            <el-input v-model="nodeProps.nodeKey" placeholder="如 STORE_MANAGER" />
+          </el-form-item>
           <el-form-item label="审批方式">
             <el-select v-model="nodeProps.approveMode" placeholder="请选择" style="width: 100%">
               <el-option label="或签(一人通过即可)" value="or" />
@@ -356,6 +360,13 @@ function getSubNodeAssigneeDisplay(sub: any): string {
   return getRoleName(sub.assigneeValue) || '未设置审批角色'
 }
 
+function onAssigneeTypeChange() {
+  // 切换审批对象类型时清空另一种类型的配置,避免残留
+  nodeProps.value.assigneeValue = ''
+  nodeProps.value.nodeKey = ''
+  saveCurrentNodeProps?.()
+}
+
 function onSubNodesEnabledChange(enabled: boolean | string | number) {
   if (enabled && !Array.isArray(nodeProps.value.subNodes)) {
     nodeProps.value.subNodes = []
@@ -456,8 +467,16 @@ function handleDragEnd() {
 </script>
 
 <style scoped>
+.assignee-tip {
+  margin-top: 4px;
+  font-size: 12px;
+  color: #909399;
+  line-height: 1.4;
+}
 .property-panel {
   width: 300px;
+  flex-shrink: 0;
+  box-sizing: border-box;
   border-left: 1px solid #dcdfe6;
   padding: 16px;
   background: #fafafa;

+ 60 - 42
src/views/flow/designer/index.vue

@@ -1,6 +1,6 @@
 <template>
-  <div class="designer-container">
-    <!-- 流程信息栏(新增/编辑模式显示) -->
+  <div class="designer-container" :class="{ 'is-embedded': embedded }">
+    <!-- 流程信息栏(新增/编辑模式显示):放在正常流里,内嵌时不再用绝对定位压住画布工具栏 -->
     <div v-if="flowInfoVisible" class="flow-info-bar">
       <el-tag v-if="mode === 'create'" type="primary">新增流程</el-tag>
       <el-tag v-else-if="mode === 'edit'" type="warning">编辑流程</el-tag>
@@ -9,41 +9,44 @@
       <span v-if="flowCategory" class="flow-info-item"><strong>分类:</strong>{{ flowCategory }}</span>
     </div>
 
-    <DesignerCanvas
-      :set-container="setContainer"
-      :lf="lfRef"
-      :selected-node="selectedNode"
-      :node-props="nodeProps"
-      :role-list="roleList"
-      :selected-sub-node-index="selectedSubNodeIndex"
-      :drawer-visible="drawerVisible"
-      @drop="handleDrop"
-      @save="handleSave"
-      @clear="handleClear"
-      @back="goBack"
-      @update:drawer-visible="drawerVisible = $event"
-      @select-sub-node="selectedSubNodeIndex = $event; drawerVisible = true"
-      @add-sub-node="addSubNodeFromCanvas"
-      @remove-sub-node="removeSubNodeFromCanvas"
-    />
+    <div class="designer-body">
+      <DesignerCanvas
+        :set-container="setContainer"
+        :lf="lfRef"
+        :selected-node="selectedNode"
+        :node-props="nodeProps"
+        :role-list="roleList"
+        :selected-sub-node-index="selectedSubNodeIndex"
+        :drawer-visible="drawerVisible"
+        :embedded="embedded"
+        @drop="handleDrop"
+        @save="handleSave"
+        @clear="handleClear"
+        @back="goBack"
+        @update:drawer-visible="drawerVisible = $event"
+        @select-sub-node="selectedSubNodeIndex = $event; drawerVisible = true"
+        @add-sub-node="addSubNodeFromCanvas"
+        @remove-sub-node="removeSubNodeFromCanvas"
+      />
 
-    <DesignerPropertyPanel
-      :selected-node="selectedNode"
-      :node-props="nodeProps"
-      :outgoing-edges="outgoingEdges"
-      :role-list="roleList"
-      :get-edge-target-name="getEdgeTargetName"
-      :update-node-name="updateNodeName"
-      :on-default-change="onDefaultChange"
-      :on-branch-blur="onBranchBlur"
-      :generate-sub-node-id="generateSubNodeId"
-      :save-current-node-props="saveCurrentNodeProps"
-      :selected-sub-node-index="selectedSubNodeIndex"
-      :drawer-visible="drawerVisible"
-      @update:selected-sub-node-index="selectedSubNodeIndex = $event"
-      @update:drawer-visible="drawerVisible = $event"
-      @delete-node="deleteSelectedNode"
-    />
+      <DesignerPropertyPanel
+        :selected-node="selectedNode"
+        :node-props="nodeProps"
+        :outgoing-edges="outgoingEdges"
+        :role-list="roleList"
+        :get-edge-target-name="getEdgeTargetName"
+        :update-node-name="updateNodeName"
+        :on-default-change="onDefaultChange"
+        :on-branch-blur="onBranchBlur"
+        :generate-sub-node-id="generateSubNodeId"
+        :save-current-node-props="saveCurrentNodeProps"
+        :selected-sub-node-index="selectedSubNodeIndex"
+        :drawer-visible="drawerVisible"
+        @update:selected-sub-node-index="selectedSubNodeIndex = $event"
+        @update:drawer-visible="drawerVisible = $event"
+        @delete-node="deleteSelectedNode"
+      />
+    </div>
 
     <DesignerSaveDialog
       :visible="saveDialogVisible"
@@ -70,6 +73,7 @@
 <script setup lang="ts">
 import { ref, watch } from 'vue'
 import type { ComponentPublicInstance } from 'vue'
+import { isEmbedded } from '@/utils/auth'
 import { useFlowDesigner } from './useFlowDesigner'
 import DesignerCanvas from './DesignerCanvas.vue'
 import DesignerPropertyPanel from './DesignerPropertyPanel.vue'
@@ -123,6 +127,8 @@ const {
 
 const selectedSubNodeIndex = ref<number | null>(null)
 const drawerVisible = ref(false)
+// 被 SCRM 等宿主 iframe 内嵌时,顶部导航和左侧菜单都被隐藏,高度计算要去掉 Navbar
+const embedded = new URLSearchParams(window.location.search).get('embed') === '1' || isEmbedded()
 
 function addSubNodeFromCanvas() {
   if (!Array.isArray(nodeProps.subNodes)) {
@@ -170,25 +176,37 @@ watch(
 <style scoped>
 .designer-container {
   display: flex;
+  flex-direction: column;
+  /* Navbar 60px + el-main 上下 padding 40px */
   height: calc(100vh - 100px);
+  min-height: 0;
+  overflow: hidden;
   background: #fff;
+  box-sizing: border-box;
+}
+/* 内嵌时 Navbar 被隐藏,只剩 el-main 的上下 padding */
+.designer-container.is-embedded {
+  height: calc(100vh - 40px);
 }
 .flow-info-bar {
-  position: absolute;
-  top: 60px;
-  left: 220px;
-  right: 0;
-  height: 40px;
+  flex: 0 0 40px;
   background: #f0f9ff;
   border-bottom: 1px solid #d9ecff;
   display: flex;
   align-items: center;
   gap: 16px;
   padding: 0 16px;
-  z-index: 100;
+  box-sizing: border-box;
 }
 .flow-info-item {
   font-size: 13px;
   color: #303133;
 }
+.designer-body {
+  flex: 1;
+  min-height: 0;
+  min-width: 0;
+  display: flex;
+  overflow: hidden;
+}
 </style>

+ 36 - 11
src/views/flow/designer/useFlowDesigner.ts

@@ -39,6 +39,7 @@ export function useFlowDesigner(lfContainer: Ref<HTMLDivElement | undefined>) {
   const nodeProps = reactive<Record<string, any>>({
     assigneeType: 'ROLE',
     assigneeValue: '',
+    nodeKey: '',
     approveMode: 'or',
     timeoutHours: undefined as number | undefined,
     timeoutAction: 'remind',
@@ -131,6 +132,7 @@ export function useFlowDesigner(lfContainer: Ref<HTMLDivElement | undefined>) {
       return {
         assigneeType: 'ROLE',
         assigneeValue: '',
+        nodeKey: '',
         approveMode: 'or',
         timeoutHours: undefined,
         timeoutAction: 'remind',
@@ -152,6 +154,7 @@ export function useFlowDesigner(lfContainer: Ref<HTMLDivElement | undefined>) {
       if (type === 'approval-node') {
         props.assigneeType = nodeProps.assigneeType ?? 'ROLE'
         props.assigneeValue = nodeProps.assigneeValue ?? ''
+        props.nodeKey = nodeProps.nodeKey ?? ''
         props.approveMode = nodeProps.approveMode ?? 'or'
         props.timeoutHours = nodeProps.timeoutHours
         props.timeoutAction = nodeProps.timeoutAction ?? 'remind'
@@ -198,7 +201,7 @@ export function useFlowDesigner(lfContainer: Ref<HTMLDivElement | undefined>) {
     Object.assign(nodeProps, getDefaultProps(data.type))
     // 兼容旧格式:approver + approveType -> 新格式
     if (data.type === 'approval-node') {
-      const supportedTypes = ['ROLE', 'SELF']
+      const supportedTypes = ['ROLE', 'SELF', 'EXTERNAL']
       let assigneeType = cloned.assigneeType ?? 'ROLE'
       let assigneeValue = cloned.assigneeValue ?? ''
       const approveMode = cloned.approveMode ?? cloned.approveType ?? 'or'
@@ -206,17 +209,18 @@ export function useFlowDesigner(lfContainer: Ref<HTMLDivElement | undefined>) {
         assigneeType = 'ROLE'
         assigneeValue = cloned.approver
       }
-      // 当前设计器只支持 ROLE / SELF,其它旧类型降级为 ROLE 并清空值
+      // 当前设计器支持 ROLE / SELF / EXTERNAL,其它旧类型降级为 ROLE 并清空值
       if (!supportedTypes.includes(assigneeType)) {
         assigneeType = 'ROLE'
         assigneeValue = ''
       }
-      // SELF 不需要审批角色值
-      if (assigneeType === 'SELF') {
+      // SELF / EXTERNAL 不需要审批角色值
+      if (assigneeType === 'SELF' || assigneeType === 'EXTERNAL') {
         assigneeValue = ''
       }
       nodeProps.assigneeType = assigneeType
       nodeProps.assigneeValue = assigneeValue
+      nodeProps.nodeKey = cloned.nodeKey ?? ''
       nodeProps.approveMode = approveMode
       nodeProps.timeoutHours = cloned.timeoutHours ?? undefined
       nodeProps.timeoutAction = cloned.timeoutAction ?? 'remind'
@@ -655,6 +659,13 @@ export function useFlowDesigner(lfContainer: Ref<HTMLDivElement | undefined>) {
     optionDialogVisible.value = false
   }
 
+  function getNodeDisplayName(node: any): string {
+    // LogicFlow 的 node.text 可能是 { value: '节点名' },直接拼接会变成 [object Object]
+    const text = node?.text
+    const value = text && typeof text === 'object' ? text.value : text
+    return String(value || node?.name || '未命名节点')
+  }
+
   function validateFlow(graphData: any): string | null {
     const nodes = graphData.nodes || []
     const edges = graphData.edges || []
@@ -665,25 +676,39 @@ export function useFlowDesigner(lfContainer: Ref<HTMLDivElement | undefined>) {
     if (endNodes.length === 0) return '流程图缺少结束节点'
     if (startNodes.length > 1) return '流程图只能有一个开始节点'
     // 检查审批节点是否配置了审批对象
+    const externalNodeKeys = new Set<string>()
     for (const node of nodes) {
       if (node.type === 'approval-node' || node.type === 'approval') {
         const props = node.properties || {}
         const assigneeType = props.assigneeType ?? 'ROLE'
         const assigneeValue = props.assigneeValue || props.approver
         if (assigneeType === 'ROLE' && !assigneeValue && props.subNodesEnabled !== true) {
-          return `审批节点 "${node.text || node.name}" 未选择审批角色`
+          return `审批节点 ${getNodeDisplayName(node)} 未选择审批角色`
+        }
+        if (assigneeType === 'EXTERNAL') {
+          if (props.subNodesEnabled === true) {
+            return `审批节点 ${getNodeDisplayName(node)} 使用外部指定审批人时不支持子节点`
+          }
+          const nodeKey = String(props.nodeKey || '').trim()
+          if (!nodeKey) {
+            return `审批节点 ${getNodeDisplayName(node)} 未填写节点编码(nodeKey)`
+          }
+          if (externalNodeKeys.has(nodeKey)) {
+            return `节点编码 "${nodeKey}" 重复,请保证每个外部指定节点编码唯一`
+          }
+          externalNodeKeys.add(nodeKey)
         }
         if (props.subNodesEnabled === true) {
           const subNodes = props.subNodes || []
           if (subNodes.length === 0) {
-            return `审批节点 "${node.text || node.name}" 已启用子节点,请至少配置一个子节点`
+            return `审批节点 ${getNodeDisplayName(node)} 已启用子节点,请至少配置一个子节点`
           }
           for (let i = 0; i < subNodes.length; i++) {
             const sub = subNodes[i]
             const subAssigneeType = sub.assigneeType ?? 'ROLE'
             const subAssigneeValue = sub.assigneeValue
             if (subAssigneeType === 'ROLE' && !subAssigneeValue) {
-              return `审批节点 "${node.text || node.name}" 的子节点 ${i + 1} 未选择审批角色`
+              return `审批节点 ${getNodeDisplayName(node)} 的子节点 ${i + 1} 未选择审批角色`
             }
           }
         }
@@ -703,7 +728,7 @@ export function useFlowDesigner(lfContainer: Ref<HTMLDivElement | undefined>) {
         node.type !== 'end-node' &&
         node.type !== 'end'
       ) {
-        return `节点 "${node.text || node.name}" 是孤立节点,请删除或连接`
+        return `节点 ${getNodeDisplayName(node)} 是孤立节点,请删除或连接`
       }
     }
     // 检查条件分支:有且只能有一个默认分支,非默认分支必须填写条件
@@ -721,17 +746,17 @@ export function useFlowDesigner(lfContainer: Ref<HTMLDivElement | undefined>) {
             return props.isDefault || (!props.condition && !e.condition)
           })
           if (defaultEdges.length === 0) {
-            return `条件节点 "${node.text || node.name}" 需要至少一条默认分支`
+            return `条件节点 ${getNodeDisplayName(node)} 需要至少一条默认分支`
           }
           if (defaultEdges.length > 1) {
-            return `条件节点 "${node.text || node.name}" 只能有一条默认分支`
+            return `条件节点 ${getNodeDisplayName(node)} 只能有一条默认分支`
           }
           const invalid = outEdges.some((e: any) => {
             const props = e.properties || {}
             return !props.isDefault && !props.condition && !e.condition
           })
           if (invalid) {
-            return `条件节点 "${node.text || node.name}" 的非默认分支必须填写条件表达式`
+            return `条件节点 ${getNodeDisplayName(node)} 的非默认分支必须填写条件表达式`
           }
         }
       }

+ 6 - 3
vite.config.ts

@@ -2,6 +2,9 @@ import { defineConfig } from 'vite'
 import vue from '@vitejs/plugin-vue'
 import { resolve } from 'path'
 
+// 后端代理目标:默认 localhost:8080,可用 VITE_PROXY_TARGET 覆盖(如本地 8080 被其他服务占用)
+const backendTarget = process.env.VITE_PROXY_TARGET || 'http://localhost:8080'
+
 export default defineConfig(({ mode }) => ({
   base: '/',
   plugins: [vue()],
@@ -28,16 +31,16 @@ export default defineConfig(({ mode }) => ({
     port: 3000,
     proxy: {
       '/api': {
-        target: 'http://localhost:8080',
+        target: backendTarget,
         changeOrigin: true,
         rewrite: (path) => path.replace(/^\/api/, '')
       },
       '/uploads': {
-        target: 'http://localhost:8080',
+        target: backendTarget,
         changeOrigin: true
       },
       '/analysis': {
-        target: 'http://localhost:8080',
+        target: backendTarget,
         changeOrigin: true
       }
     }