Skip to content
ts
const api = $inject.api;
const isOverseas = api.formData()?.isOverseas
console.log("isOverseas",isOverseas)
api.setValue({
	regions:undefined
})
if(isOverseas=='1'){
  console.log("111")
  	api.mergeRules({
      customerLicense:{
        "$required":undefined
      },
      regions:{
      	props:{
        	regionMode: 'foreign'
        }
      }
    })
}else if(isOverseas=='0'){
  console.log("222")
	api.mergeRules({
		customerLicense:{
    	"$required":"纳税识别号/企业标识ID不能为空"
    },
    regions:{
      	props:{
        	regionMode: 'zh'
        }
      }
  })
}
ts

const api = $inject.api;
// 获取顶级表单的API对象
const topFormApi = api.top;
console.log(api,'api')
const childApi = api.children;
// 重新加载顶级表单的规则
if (topFormApi) {
  //跟进记录动态表单
  const tableInstance = topFormApi.el('ref_Fzfymkgjlu1zhhc');
  const tableApi = tableInstance.fapi;
  console.log("tableInstance",tableInstance)
  if(tableInstance){
    	const idddd = tableApi.formData() // 拿不到formData????
      console.log(idddd,'kkkk',tableApi,tableApi.getValue('productIds'))
      const followAddItemForms = topFormApi.formData()?.followAddItemForms ?? []
      console.log("followAddItemForms",followAddItemForms)
  const allIds = []
  if(Array.isArray(followAddItemForms)){
  	for(let i = 0;i<followAddItemForms.length;i++){
    	const productIds = followAddItemForms[i]?.productIds ?? []
      allIds.push(...productIds)
    }
  }
    console.log("allIds",allIds)
    //拿到具体的跟进产品的api
  	
  	console.log("tableApi",tableApi)
    tableApi.mergeRules({
    	productIds:{
      	props:{
        	filterIds: allIds
        }
      }
    })
    tableApi.setValue({
    	productIds:undefined
    })
  }
}
ts

const api = $inject.api;
// 获取顶级表单的API对象
const topFormApi = api.top;
console.log(api,'api')
const childApi = api.children;
// 重新加载顶级表单的规则
if (topFormApi) {
  //跟进记录动态表单
  const tableInstance = topFormApi.el('ref_Fzfymkgjlu1zhhc');
  const tableApi = tableInstance.fapi;
  console.log("tableInstance",tableInstance)
  if(tableInstance){
    nextTick(() => {
      const idddd = tableApi.formData() // 拿不到formData????
      console.log(idddd,'kkkk',tableApi,tableApi.getValue('productIds'))
      const followAddItemForms = topFormApi.formData()?.followAddItemForms ?? []
      console.log("followAddItemForms",followAddItemForms)
      const allIds = []
      if(Array.isArray(followAddItemForms)){
        for(let i = 0;i<followAddItemForms.length;i++){
          const productIds = followAddItemForms[i]?.productIds ?? []
          allIds.push(...productIds)
        }
      }
      console.log("allIds",allIds)
      //拿到具体的跟进产品的api

      console.log("tableApi",tableApi)
      tableApi.mergeRules({
        productIds:{
          props:{
            filterIds: allIds
          }
        }
      })
      tableApi.setValue({
        productIds:undefined
      })
    })
  }
}









<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
import VueDraggableResizable from '/@/components/VueDraggable/vue-draggable-resizable.vue'
import { buildUUID } from '/@/utils/uuid'

// ========================
// 网格常量
// ========================
const COLS = 16          // 总列数
const ROW_H = 90         // 每行高度 px
const GAP = 10           // 间距 px

// ========================
// 组件数据结构
// ========================
interface WidgetItem {
  id: string
  title: string
  col: number      // 起始列 (0-based)
  row: number      // 起始行 (0-based)
  colSpan: number  // 占几列
  rowSpan: number  // 占几行
}

// ========================
// 容器尺寸
// ========================
const containerRef = ref<HTMLElement | null>(null)
const containerWidth = ref(0)

/** 每列像素宽度(不含间距) */
const cellW = computed(() =>
  containerWidth.value > 0
    ? (containerWidth.value - GAP * (COLS + 1)) / COLS
    : 0
)

// 像素 ↔ 网格互转
const colToX  = (c: number) => GAP + c * (cellW.value + GAP)
const rowToY  = (r: number) => GAP + r * (ROW_H + GAP)
const xToCol  = (x: number) => Math.max(0, Math.min(COLS - 1, Math.round((x - GAP) / (cellW.value + GAP))))
const yToRow  = (y: number) => Math.max(0, Math.round((y - GAP) / (ROW_H + GAP)))
const wToSpan = (w: number) => Math.max(1, Math.round((w + GAP) / (cellW.value + GAP)))
const hToSpan = (h: number) => Math.max(1, Math.round((h + GAP) / (ROW_H + GAP)))

const itemPxW = (item: WidgetItem) => item.colSpan * (cellW.value + GAP) - GAP
const itemPxH = (item: WidgetItem) => item.rowSpan * (ROW_H + GAP) - GAP
const itemX   = (item: WidgetItem) => colToX(item.col)
const itemY   = (item: WidgetItem) => rowToY(item.row)

// ========================
// 布局数据
// ========================
const widgets = ref<WidgetItem[]>([
  { id: buildUUID(), title: '销售概览',     col: 0,  row: 0, colSpan: 8,  rowSpan: 1 },
  { id: buildUUID(), title: '订单统计',     col: 8,  row: 0, colSpan: 8,  rowSpan: 1 },
  { id: buildUUID(), title: '用户活跃趋势', col: 0,  row: 1, colSpan: 10, rowSpan: 2 },
  { id: buildUUID(), title: '待办事项',     col: 10, row: 1, colSpan: 6,  rowSpan: 2 },
])

// 用于强制 VDR 重建,解决 moveHorizontally snapToGrid 绝对坐标漂移问题
const layoutKey = ref(0)
const bumpKey = () => { layoutKey.value++ }

// ========================
// 布局算法
// ========================

/** 两组件是否在网格上重叠 */
function isOverlap(a: WidgetItem, b: WidgetItem): boolean {
  return (
    a.col < b.col + b.colSpan &&
    a.col + a.colSpan > b.col &&
    a.row < b.row + b.rowSpan &&
    a.row + a.rowSpan > b.row
  )
}

/**
 * 布局计算:
 * 1. 把 anchor(拖动项)锁在目标位置
 * 2. 把与 anchor 冲突的项向下推
 * 3. 重复碰撞检测直到稳定
 * 4. 对所有项做向上 compact
 */
function resolveLayout(list: WidgetItem[], anchorId?: string): WidgetItem[] {
  // 深拷贝,避免直接修改 reactive 对象
  let items: WidgetItem[] = list.map(i => ({ ...i }))

  // -- 步骤 1-3:先推走与 anchor 冲突的项(重复至稳定)--
  if (anchorId) {
    const MAX_ITER = 30
    let changed = true
    let iter = 0
    while (changed && iter < MAX_ITER) {
      changed = false
      iter++
      const anchor = items.find(i => i.id === anchorId)!
      for (const item of items) {
        if (item.id === anchorId) continue
        if (isOverlap(anchor, item)) {
          // 把被压项推到 anchor 下方
          item.row = anchor.row + anchor.rowSpan
          changed = true
        }
      }
      // 推走后还可能互相冲突,再排查一遍
      for (let i = 0; i < items.length; i++) {
        for (let j = i + 1; j < items.length; j++) {
          if (items[i].id === anchorId || items[j].id === anchorId) continue
          if (isOverlap(items[i], items[j])) {
            // 让行号大的那个再往下挪
            const lower = items[i].row >= items[j].row ? items[i] : items[j]
            const upper = lower === items[i] ? items[j] : items[i]
            lower.row = upper.row + upper.rowSpan
            changed = true
          }
        }
      }
    }
  }

  // -- 步骤 4:按行列排序后向上 compact --
  items.sort((a, b) => a.row !== b.row ? a.row - b.row : a.col - b.col)

  // 占格矩阵
  const occupied = new Map<string, string>()
  const mark = (item: WidgetItem) => {
    for (let r = item.row; r < item.row + item.rowSpan; r++) {
      for (let c = item.col; c < item.col + item.colSpan; c++) {
        occupied.set(`${r}_${c}`, item.id)
      }
    }
  }
  const conflict = (item: WidgetItem, row: number): boolean => {
    for (let r = row; r < row + item.rowSpan; r++) {
      for (let c = item.col; c < item.col + item.colSpan; c++) {
        if (c >= COLS) return true
        const k = occupied.get(`${r}_${c}`)
        if (k && k !== item.id) return true
      }
    }
    return false
  }

  const compacted: WidgetItem[] = []
  for (const item of items) {
    let row = 0
    while (conflict(item, row)) row++
    const final = { ...item, row }
    mark(final)
    compacted.push(final)
  }

  return compacted
}

function applyLayout(anchorId?: string) {
  widgets.value = resolveLayout(widgets.value, anchorId)
  nextTick(bumpKey)
}

// ========================
// 容器总高度
// ========================
const containerHeight = computed(() => {
  if (!widgets.value.length) return 300
  const maxRow = Math.max(...widgets.value.map(w => w.row + w.rowSpan))
  return maxRow * (ROW_H + GAP) + GAP
})

// ========================
// 拖拽 / Resize 事件
// ========================
// dragstop(left, top, customId)
function handleDragStop(left: number, top: number, id: string) {
  const item = widgets.value.find(w => w.id === id)
  if (!item) return
  item.col = Math.max(0, Math.min(COLS - item.colSpan, xToCol(left)))
  item.row = Math.max(0, yToRow(top))
  applyLayout(id)
}

// resizestop(customId, left, top, width, height)
function handleResizeStop(id: string, _l: number, _t: number, width: number, height: number) {
  const item = widgets.value.find(w => w.id === id)
  if (!item) return
  const newColSpan = Math.max(1, Math.min(COLS - item.col, wToSpan(width)))
  const newRowSpan = Math.max(1, hToSpan(height))
  item.colSpan = newColSpan
  item.rowSpan = newRowSpan
  applyLayout(id)
}

// ========================
// 激活状态
// ========================
const activeId = ref<string | undefined>()
function handleActivated(id: string) { activeId.value = id }

// ========================
// 添加 / 删除组件
// ========================
const widgetTemplates = [
  { title: '快捷入口',   colSpan: 4,  rowSpan: 1 },
  { title: '数据看板',   colSpan: 8,  rowSpan: 2 },
  { title: '消息通知',   colSpan: 4,  rowSpan: 1 },
  { title: '图表分析',   colSpan: 12, rowSpan: 2 },
]

function addWidget(tpl: typeof widgetTemplates[0]) {
  const maxRow = widgets.value.length
    ? Math.max(...widgets.value.map(w => w.row + w.rowSpan))
    : 0
  widgets.value.push({
    id: buildUUID(),
    title: tpl.title,
    col: 0,
    row: maxRow,
    colSpan: tpl.colSpan,
    rowSpan: tpl.rowSpan,
  })
  applyLayout()
}

function removeWidget(id: string) {
  widgets.value = widgets.value.filter(w => w.id !== id)
  applyLayout()
}

// ========================
// 响应式宽度监听
// ========================
let ro: ResizeObserver | null = null
onMounted(() => {
  nextTick(() => {
    if (containerRef.value) {
      containerWidth.value = containerRef.value.clientWidth
      ro = new ResizeObserver(entries => {
        containerWidth.value = entries[0].contentRect.width
        bumpKey()
      })
      ro.observe(containerRef.value)
    }
  })
})
onUnmounted(() => ro?.disconnect())
</script>

<template>
  <div class="workspace-page">
    <!-- 顶部工具栏 -->
    <div class="toolbar">
      <span class="toolbar-title">工作台设计</span>
      <div class="toolbar-actions">
        <span class="add-label">添加组件:</span>
        <a-button
          v-for="tpl in widgetTemplates"
          :key="tpl.title"
          size="small"
          class="add-btn"
          @click="addWidget(tpl)"
        >
          + {{ tpl.title }}
        </a-button>
      </div>
    </div>

    <!-- 网格工作区 -->
    <div class="workspace-scroll">
      <div
        ref="containerRef"
        class="workspace-container"
        :style="{ height: containerHeight + 'px' }"
      >
        <!-- 列参考线 -->
        <template v-if="cellW > 0">
          <div
            v-for="c in COLS"
            :key="'col-' + c"
            class="grid-col-guide"
            :style="{
              left: colToX(c - 1) + 'px',
              width: cellW + 'px',
              top: GAP + 'px',
              bottom: GAP + 'px',
            }"
          />
        </template>

        <!-- 组件列表:key 包含 layoutKey,触发 VDR 重建以精确定位 -->
        <template v-if="cellW > 0">
          <VueDraggableResizable
            v-for="item in widgets"
            :key="item.id + '_' + layoutKey"
            :customId="item.id"
            :w="itemPxW(item)"
            :h="itemPxH(item)"
            :x="itemX(item)"
            :y="itemY(item)"
            :min-width="cellW"
            :min-height="ROW_H"
            :parent="true"
            :handles="['mr', 'ml', 'br', 'bm', 'bl']"
            class-name-active="widget-active"
            class-name="widget-vdr"
            @activated="handleActivated(item.id)"
            @dragstop="handleDragStop"
            @resizestop="handleResizeStop"
          >
            <div
              class="widget-card"
              :class="{ 'is-active': activeId === item.id }"
            >
              <div class="widget-header">
                <span class="widget-title">{{ item.title }}</span>
                <span class="widget-meta">{{ item.colSpan }}列 × {{ item.rowSpan }}行</span>
                <a-button
                  size="small"
                  danger
                  class="widget-del"
                  @click.stop="removeWidget(item.id)"
                >删除</a-button>
              </div>
              <div class="widget-body">
                <div class="widget-placeholder">
                  <div class="placeholder-icon">📊</div>
                  <div>{{ item.title }}</div>
                </div>
              </div>
            </div>
          </VueDraggableResizable>
        </template>
      </div>
    </div>
  </div>
</template>

<style scoped>
.workspace-page {
  display: flex;
  flex-direction: column;
  height: 100%;
  background: #f0f2f5;
}

/* 工具栏 */
.toolbar {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 16px;
  background: #fff;
  border-bottom: 1px solid #e8e8e8;
  flex-shrink: 0;
  flex-wrap: wrap;
}
.toolbar-title {
  font-size: 16px;
  font-weight: 600;
  color: #1a1a2e;
}
.toolbar-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}
.add-label { font-size: 13px; color: #666; }
.add-btn   { border-radius: 6px; }

/* 工作区 */
.workspace-scroll {
  flex: 1;
  overflow-y: auto;
  overflow-x: hidden;
  padding: 16px;
}
.workspace-container {
  position: relative;
  width: 100%;
  background: #fff;
  border-radius: 8px;
  box-shadow: 0 1px 6px rgba(0, 0, 0, 0.08);
  overflow: hidden;
}

/* 列参考线 */
.grid-col-guide {
  position: absolute;
  background: rgba(24, 144, 255, 0.04);
  border: 1px dashed rgba(24, 144, 255, 0.15);
  border-radius: 4px;
  pointer-events: none;
  z-index: 0;
}

/* VDR 覆写 */
:deep(.widget-vdr) {
  position: absolute !important;
  border: none !important;
  background: transparent !important;
  z-index: 1;
}
:deep(.widget-active) {
  z-index: 10 !important;
}
:deep(.handle) {
  width: 8px;
  height: 8px;
  background: #1890ff;
  border-radius: 50%;
  border: 2px solid #fff;
  box-shadow: 0 0 0 1px #1890ff;
}

/* 组件卡片 */
.widget-card {
  width: 100%;
  height: 100%;
  background: #fff;
  border-radius: 8px;
  border: 2px solid transparent;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  transition: border-color 0.2s, box-shadow 0.2s;
  cursor: grab;
}
.widget-card:active { cursor: grabbing; }
.widget-card.is-active {
  border-color: #1890ff;
  box-shadow: 0 0 0 3px rgba(24, 144, 255, 0.15), 0 4px 16px rgba(0, 0, 0, 0.12);
}

.widget-header {
  display: flex;
  align-items: center;
  padding: 8px 12px;
  background: linear-gradient(135deg, #f8f9ff 0%, #f0f4ff 100%);
  border-bottom: 1px solid #eef0f8;
  gap: 8px;
  flex-shrink: 0;
}
.widget-title { font-size: 13px; font-weight: 600; color: #1a1a2e; flex: 1; }
.widget-meta  { font-size: 11px; color: #aaa; }
.widget-del   { flex-shrink: 0; }

.widget-body {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 12px;
  overflow: hidden;
}
.widget-placeholder {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  color: #bbb;
  font-size: 13px;
}
.placeholder-icon { font-size: 28px; line-height: 1; }
</style>

Lucking