Skip to content

二、框架核心组件

1. z-block 数据区块组件

z-block 是一个“区块容器组件”,用于按需加载数据或挂载子页面组件,支持懒加载、延迟加载、轮询刷新。

1.1 组件定位

  • 数据块模式:传 url,组件进入视口后请求接口,把结果传给默认插槽渲染。

  • 页面块模式:传 href,按路由路径找到对应页面组件并嵌入渲染。

1.2 组件属性

属性类型默认值说明
urlStringundefined数据请求地址。存在时走接口加载模式。
hrefStringundefined路由路径。存在时走页面块模式(渲染路由组件)。
paramsObjectundefined请求参数或子页面参数。
pStringundefined预留字段(源码中未实际使用)。
laterNumber0延迟首次加载(毫秒)。
intervalNumber0轮询加载间隔(毫秒)。>0 时优先于 later。
resourcesString | Arrayundefined进入视口后先加载的资源(JS/CSS)。
lazyBooleanfalse/true是否等待数据加载完成后再渲染默认插槽。

1.3 事件

事件参数触发时机
finish(result, el)url 请求成功后触发。
fail(error)url 请求失败后触发。

1.4 插槽

插槽名作用域参数说明
defaultresult数据模式下渲染内容,参数是接口返回结果。

渲染规则:

  • href 模式:忽略默认插槽,直接渲染匹配到的路由组件。

  • url 模式:当 visible=true 且(lazy=false 或 loaded=true)时才渲染默认插槽。

  • 未满足条件时仅渲染空 <div> 占位。

1.5 使用示例

示例 A:标准数据块

xml
<z-block url="/api/employee/makeEmployeeBindLink" :params="params">
    <template #default="data">
        <z-copy label="复制链接" :value="data"
                style="width:100%;margin-bottom:6px;text-align: center; width: 100%" />
        <z-qr :value="data" height="160px" width="160px" />
        <div style="text-align: center; width: 100%;">
            二维码有效期十分钟
        </div>
    </template>
</z-block>

示例 B:页面块模式(按路由组件嵌入)

java
<z-block
  href="/orders/detail"
  :params="{ id: orderId }"
/>

示例 C:轮询刷新

java
<z-block
  url="/api/task/status"
  :params="{ taskId }"
  :interval="5000"
  @finish="onTick"
/>

1.6 注意事项

  • params 监听是浅监听,建议替换对象引用触发更新,不要只改内部字段。

  • interval 会持续轮询,请避免过短间隔导致请求压力。

  • resources 仅在首次进入视口时加载一次。

  • href 是按 route.path 精确匹配,路径不一致会找不到目标组件。

  • finish/fail 只在 url 请求模式有意义。

  • lazy=true 时加载前只会渲染空容器,建议外层给最小高度避免布局跳动。

  • 组件内部有节流(loadFc),高频参数变化不会每次都立刻发请求。

2. z-table 表格数据组件

z-table 是框架的核心数据表格组件,封装了:

  • 远程分页加载

  • 本地数据渲染

  • 条件检索

  • 列显示配置(筛选/顺序)

  • 行选择跨页记忆

  • 操作列渲染

  • 全屏、刷新、Tab 过滤

2.1 组件定位

  • 标准模式:按列配置自动渲染 el-table。

  • 自定义模式:提供 default 插槽后,按行渲染你自己的布局。

  • 数据来源支持:

    • url 远程接口

    • datasource 本地数据

    • 都不传时会走 mock 数据(基于配置生成)。

2.2 组件属性

属性类型默认值说明
urlString-远程接口地址。
columnsArray-列定义。
nameString-表格唯一标识(用于列配置缓存 key)。
entitysObject-字段实体配置,会与 configs 合并。
maxPageNumber0最大页保护,超过后回到第 1 页。
titleString-表头标题。
rowKeyString"id"行主键字段。
modelValueArray-选中行的双向绑定值。
conditionArray-检索条件定义。
slotsObject-代码传入的插槽映射(与模板插槽合并)。
tabsObject-头部 Tab 过滤配置。
gutterNumber-自定义模式下行间距。
heightNumber0表格高度(可透传到 el-table)。
datasourceObject | Array-本地数据。数组直接渲染;对象支持 { list, total }。
hideTitleBooleanfalse隐藏标题栏。
paramsObject-请求参数。
borderBooleantrue表格边框。
showIndexBooleanfalse显示序号列。
actionWidthNumber220操作列宽度。
sizeNumber20每页数量(分页 pageSize)。

2.3 事件

事件名说明
finish(result:查询结果,params:查询参数)表格接口数据返回事件

2.4 插槽

插槽名作用域参数说明
default启用“自定义模式”渲染整行内容。
action$scope(行作用域)标准模式下“操作列”内容。
header$-表头检索区的自定义扩展区域。
列名同名插槽scope(行作用域)覆盖某一列单元格渲染。示例:#status="{ row }"。

说明:

  • 列级插槽优先于默认列渲染。

  • slots prop 与模板插槽会合并。

2.5 公开能力(通过 ref 可调用)

常用方法:

  • reload():回到第一页并重新加载。

  • jump(page):跳转到指定页并加载。

  • on(keyOrObject, value?):设置检索条件并加载。

  • load():执行一次加载。

  • initData():按当前数据源初始化。

使用示例:

xml
<template>
    <z-table
        url="/do/select/customer"
        :columns="[
            { label: '名称', name: 'title' },
            { label: '积分', name: 'points' },
        ]"
        :size="2"
        ref="tableRef"
    >
        <template #action$="{ row }">
            <z-action
                label="查看"
                :data="row"
                href="/task/detail"
                @click="click"
            />
        </template>
    </z-table>
</template>

<script>

export default {
    name: "p-3kpgc2ln",
    setup() {
    },
    methods: {
        finish(result, el) {
            console.log(result);
            console.log(el);
        },
        click() {
            this.$refs.tableRef.on("title", "aa");
        },
    },
};
</script>

注意:声明ref后,直接通过this.$refs.调用,无需再引入ref

2.6 columns 字段规范

columns 支持两种写法:

  1. 字符串写法
java
columns: ["id", "status", "createdAt"]

会按 entitys 里同名字段自动补全配置。

  • 对象写法
yaml
columns: [
  { name: "id", label: "ID", width: 120 },
  { name: "status", label: "状态", code: "order_status" },
  { name: "enabled", label: "启用", type: "switch", url: "/api/user/switch" }
]

会与 entitys 同名配置合并(对象优先)。

列对象字段说明:

字段类型是否关键用途
nameString必填字段名(对应 row 上的键)。
labelString常用列标题。
widthNumber/String可选列宽。
typeString可选指定渲染类型。
codeString可选字典码,走 z-dict 显示。
dependString可选关联展示,走 z-text。
componentComponent可选自定义单元格组件(只读渲染)。
urlString特殊type="switch" 时用于行内开关提交。

渲染优先级

单元格渲染顺序是:

  1. 如果有同名插槽(#<name>)→ 用插槽

  2. 否则如果有 component → 渲染自定义组件

  3. 否则如果 name === "id" → 用 z-copy

  4. 否则如果 type === "switch" → 用 z-form-item(支持 url)

  5. 否则如果 type 属于下列类型 → 用只读 z-form-item

  6. 否则如果有 code → z-dict

  7. 否则如果有 depend → z-text

  8. 否则显示原始值

“走只读 z-form-item”的类型列表(源码常量):

  • date

  • daterange

  • user

  • image

  • address

  • autoid

  • money

  • tenantUser

注意:

  • name 必须和后端返回字段一致,否则该列拿不到值。

  • switch 行内修改依赖 url,不配时只读展示。

  • 字典列建议统一走 code,不要前后端重复维护文案。

  • 写了 #default 插槽就进入自定义模式,columns 的单元格渲染规则会被绕过。

2.7 使用示例

示例 A:标准远程表格

sql
<z-table
  url="/api/order/list"
  :columns="[
    { label: '订单号', name: 'orderNo' },
    { label: '状态', name: 'status', code: 'order_status' },
    { label: '创建时间', name: 'createdAt', type: 'date' }
  ]"
  :params="{ tenantId }"
  :size="20"
  :show-index="true"
  @finish="onLoaded"
/>

示例 B:行选择双向绑定

  • 绑定 v-model 后,表格会开启选择逻辑,并在勾选变化时回传数组。
java
<z-table
  v-model="selectedRows"
  row-key="id"
  url="/api/user/list"
  :columns="columns"
/>

示例 C:自定义操作列 + 列插槽

xml
<z-table url="/api/task/list" :columns="columns">
  <template #status="{ row }">
    <el-tag :type="row.status === 1 ? 'success' : 'info'">
      {{ row.statusText }}
    </el-tag>
  </template>

  <template #action$="{ row }">
    <z-action label="查看" :data="row" href="/task/detail" />
  </template>
</z-table>

示例 D:自定义模式(整行自由布局)

xml
<z-table :datasource="list" :gutter="16">
  <template #default="{ row, $index }">
    <my-card :index="$index" :data="row" />
  </template>
</z-table>

2.8 注意事项

  • params 是浅监听,建议替换对象引用触发更新,不要只改对象内部字段。

  • 当 default 插槽存在时会进入“自定义模式”,标准列渲染逻辑会被绕过。

  • 若后端返回空页且当前页 > 1,组件会自动回退一页再请求。

  • 列配置筛选会使用本地缓存(依赖 name/id 作为 key),name 建议稳定且唯一。

  • 多选结果会做“跨页合并记忆”,依赖 rowKey;请确保 rowKey 唯一且稳定。

  • maxPage 配置不当会导致频繁跳回第一页(仅在大页数轮询场景建议使用)。

  • 组件内部依赖 Element Plus(el-table、el-pagination、$loading)。

2.9 与 configs.jsx 配合使用 (由低代码生成,最常用)

configs.jsx:

sql
export default {
  url: "/do/select/customer",
  conditionLimit:null,
  selectable: false,
  showIndex: false,
  compact: 220,
  path: 'settings/customer',
  title: "客户管理",
  entitys: [{"name":"id","label":"编号","type":"search"},{"name":"creator","label":"创建人","type":"user"},{"name":"createGmt","label":"创建时间","type":"date"},{"name":"updateGmt","label":"更新时间","type":"date"},{"name":"title","label":"客户名称"},{"name":"level","label":"客户等级","code":"cfg_customerLevel"},{"name":"account","label":"所属账户","type":"search","depend":"account"},{"name":"beginDebt","label":"期初欠款","type":"money"},{"name":"receiveDebt","label":"应收欠款","type":"money"},{"name":"contract","label":"联系人"},{"name":"telephone","label":"联系电话"},{"name":"address","label":"客户地址","type":"address"},{"name":"detail","label":"详细地址"},{"name":"bank","label":"开户银行"},{"name":"bankNumber","label":"银行账号"},{"name":"tin","label":"税号"},{"name":"extra","label":"备注","type":"textarea"},{"name":"beginVerified","label":"期初收款","type":"money"},{"name":"stored","label":"储值金额","type":"money"},{"name":"points","label":"客户积分","type":"number"},{"name":"salesperson","label":"业务员","type":"search","depend":"salesperson"}],
  columns: ["id","title","level","beginDebt","receiveDebt","stored","points","contract","telephone","createGmt","salesperson"],
  condition: ["title","level","salesperson"],
  slots: {
      header$() {
      return (
        <>
        <z-action p='fii5objw' label='新增客户' mode='dialog' fields={["title","level","beginDebt","contract","telephone","address","detail","bank","bankNumber","tin","extra","salesperson"]} rules={{"title":{"message":"请输入正确内容","required":true}}} br='beforePut' type='primary' url='/api/common/putCustomer' />
        </>
      )
    },
    action$({ row }) {
      return (
        <>
          <z-action p='xx0y3a7q' label='编辑' mode='dialog' fields={["title","level","beginDebt","contract","telephone","address","detail","bank","bankNumber","tin","extra","salesperson"]} rules={{"title":{"message":"请输入正确内容","required":true}}} link data={row} url='/api/common/patchCustomer' />
<z-action p='lphjvw09' label='删除' mode='confirm' link data={row} url='/do/delete/customer' />
        </>
      )
    }
  }
}

customer.vue:

xml
<template>
    <z-table name="mg3h9mgo" :beforePut="beforePut"> </z-table>
</template>

<script>
import { provide } from "vue";
import configs from "./.lowcode/configs";

export default {
    name: "p-mg3h9mgo",
    setup() {
        $.extend(configs, {
            salesperson: { account: "account" },
            level: { default: "1" },
        });
        provide("configs", configs);
    },
    methods: {
        beforePut(formdata) {
            if (formdata.beginDebt) {
                formdata.receiveDebt = formdata.beginDebt;
            }
        },
    },
};
</script>

可以使用 $.extend(configs:Object, entitys:Object[])函数增加字段自定义属性

3. z-action 前后端交互组件

z-action 是框架里的统一“动作组件”,用来承载按钮点击后的行为:确认、弹窗表单、抽屉、气泡、事件派发、跳转等。

3.1 组件定位

一个 z-action 同时解决:

  • 按钮展示(图标/文本/样式)

  • 行操作数据注入(data)

  • 动作弹层(confirm/dialog/drawer/popover)

  • 提交逻辑(url + beforeSubmit)

  • 与 z-table 联动刷新/事件派发

3.2 mode 行为

支持的模式(源码实际行为):

  1. confirm

    • 默认确认模式(桌面端是 el-popconfirm)。

    • portable 场景下会变成底部抽屉确认。

  2. dialog

    • 打开 el-dialog。

    • 可渲染 fields 自动表单,或默认插槽内容。

  3. drawer

    • 打开 el-drawer。

    • 可渲染 fields 自动表单,或默认插槽内容。

    • 支持 direction(通过 $attrs 透传)。

  4. emit

    • 不弹层,直接向表格实例触发 cb 事件(如打印)。
  5. blank

  • 配合 href 时直接新窗口打开。

默认模式推导(未显式给 mode 时):

  • 有 fields / 默认插槽 / href -> dialog

  • 否则 -> confirm

  • portable 注入为 true 时,除了 confirm 外强制走 drawer

3.3 组件属性

属性名说明类型默认值枚举值
id事件总线订阅 key($.emit(id, data) 可唤起)。String--
label按钮显示名称String--
icon图标(字符串用 z-icon,函数当组件渲染)。String--
icon-size图标尺寸String--
type展现样式Enumprimarybubble/default/success/warning/danger
mode交互形式Enumdialogdrawer/confirm
url提交到服务端的接口地址String--
data初始化数据/from 接口的初始化参数String--
from数据初始化接口String--
href外链地址String--
titledialog/drawer/confirm 的标题String--
size按钮的大小Enumdefaultlarge/small -
rules表单的验证规则Array--
fields表单字段配置Array--
permission权限码String--
beforeSubmit提交前拦截函数Function--
beforeShowdialog/drawer 显示前钩子函数Function--
maphref 外链页面时,data 数据与子页面入参映射配置Object--
cb回调事件名(emit 模式或提交后触发表格事件)。String--
br从表格 $attrs[br] 取 beforeSubmit 方法名。String--
later绑定事件/路由恢复延迟毫秒。Number--
widthdialog/drawer 宽度String | Number--
refresh非表格场景提交后是否 location.reload()。Booleanfalse-
sd批量模式:使用表格已选行逐条提交。Booleanfalse-
fixed与路由 query 同步的 key(支持直达恢复弹层)。String--

3.4 事件

事件参数时机
finish(result, payload)提交成功后触发。
confirm(result)confirm 模式点击确认后触发。
input(visible)弹层开关变化时(兼容旧式 v-model)。
close(initData)关闭动作弹层时。
change(payload)提交前,数据处理完成后触发。

额外:mode='emit' 时会对表格实例触发 cb 事件(不是组件自身事件)。

3.5 插槽

  1. #label

    • 自定义触发器内容(替代默认按钮文案/图标)。
  2. default

    • 在 dialog/drawer 且未使用 fields 时,作为弹层主体内容。

    • 作用域参数:initData(当前动作数据)。

  3. action$

    • 表单操作按钮插槽,替代弹窗的提交按钮

3.6 公开方法(ref 可调用)

  • show(data?, callback?):打开动作并注入数据。

  • close():关闭。

  • post(formData, autoClose=true):执行提交逻辑。

  • click(e):触发 show(内部使用)。

3.7 使用示例

1. 行删除确认

java
<z-action
  label="删除"
  mode="confirm"
  :data="row"
  url="/api/user/delete"
/>

3.2 弹窗表单新增

java
<z-action
  label="新增"
  mode="dialog"
  :fields="[{ name:'title', label:'名称' }]"
  :rules="{ title:{ required:true, message:'必填' } }"
  url="/api/user/add"
/>

3.3 抽屉打开明细页

python
<z-action
  label="明细"
  mode="drawer"
  href="/sales/salesDetail"
  :data="row"
  :map="{ salesId: 'id' }"
/>

3.4 打印(事件派发)

java
<z-action
  label="打印"
  mode="emit"
  cb="print"
  :data="row"
  link
/>

3.5 批量提交(依赖表格勾选)

java
<z-action
  label="批量审核"
  :sd="true"
  mode="confirm"
  url="/api/order/review"
/>

3.8 注意事项

  1. mode='emit' 依赖表格上下文,通常放在 z-table 的 action$ 里用。

  2. sd=true 必须先选中行,否则会报“请先选择要删除的项”。

  3. fixed 会写入路由 query,用于恢复弹层状态。

  4. beforeSubmit 返回 false 会中断提交。

  5. fields 存在时默认由 z-form 承担提交;无 fields 时你可用默认插槽自定义。

  6. refresh 仅在无表格上下文时影响是否整页刷新。

4. z-form 表单组件

z-form 是 Zen 内置的表单容器组件,负责字段渲染、校验、提交、重置,并可切换为普通表单/搜索表单/分步表单。

可以使用 $.extend(configs:Object, entitys:Object[])函数增加字段自定义属性

4.1 组件属性

属性类型默认值说明
fieldsArray[]表单字段配置(通常来自 .lowcode/configs.jsx)
dataObject{}表单初始值
typeString''表单类型:normal/steped/search
spanNumber6默认栅格宽度(字段布局)
rulesObject{}校验规则(会按字段转换为数组规则)
urlString''提交接口地址;有值时会走内置 post 提交
readonlyBooleanfalse只读模式
beforeSubmitFunction-提交前钩子:(formData, closable) => any
closableBooleantrue提交后是否关闭(在弹层/动作场景下)
labelWidthString/Number'100px'标签宽度

4.2 事件

事件名参数触发时机
finishresult提交成功后触发(独立使用 z-form 时)

说明:如果 z-form 放在 z-action 内部,完成事件会优先发给外层动作组件($button.$emit('finish', data))。

4.3 插槽

插槽名作用域参数说明
$formData动态插槽,替换同名 field
defaultformData表单内容后追加自定义内容
action$formData自定义操作区(在提交按钮基础上增加其他按钮)

4.4 公开方法(通过 ref 调用)

  • validate():执行校验,返回校验结果

  • submit(e?):手动触发提交

  • reset():重置表单

  • close():关闭当前动作容器(若存在)

4.5 z-form-items 多个 field 组件

组件属性:

属性名说明类型
title分组名称String
fields选项配置Array
value初始化数据Object
rules验证规则Array

4.6 z-form-item 单个 field 组件

属性名说明类型默认值
label字段标题String-
name字段名称String-
type表单类型enum-
code字典 CodeString/Number-
rules验证规则Array-
tip验证规则String-
value表单初始化值Object继承父级 form 的 data 属性
default默认值Any-
visible字段是否显示Boolean | Function(formdata)-
readonly字段是否只读Boolean | Function(formdata)-
onChange值变化事件Function(value,formData,name)-

4.7 表单type

type 名说明其它属性
input单行文本微信端 scannable 属性支持扫码输入
inputTag标签输入框-
password密码-
textarea多行文本-
date时间组件参考element的DateTimePicker 日期时间选择器
daterange时间范围参考element的DateTimePicker 日期时间选择器
number数字-
tel电话号码输入框-
attach附件-
checkbox多选-
radiobox单选-
color颜色-
image图片上传native:是否原生上传,支持微信上传接口自动识别
map键值对象-
money金额-
moneyrange金额区间自动映射成 ${name}Start 与 ${name}End 两个字段
table表格输入fields:字段声明,noAction:无操作按钮,simple:boolean 简易模式,url:数据源
select下拉框Any
search搜索下拉框depend:指定依赖表,tenant 指定租户字段名,以 title 字段为关键字
switch开关Any
monaco代码编辑器Any
jsonJSON 编辑器Any
$应用全局组件Any

1. search 属性

属性名说明类型默认值枚举值
depend依赖表名String--
tenant租户字段名String--
account账户字段名String--
app指定应用String--

4.2 switch属性

属性名说明类型默认值枚举值
depend依赖表名String--
tenant租户字段名String--
account账户字段名String--
app指定应用String--

4.3 money属性

属性名说明类型默认值枚举值
allowMinus是否允许负数Booleanfalse-
unit币值单位String-

4.4 daterange属性

属性名说明类型默认值枚举值
endKey结束时间字段名String#{name}End-

4.8 使用示例

vue/src/pages/product/productEditor/.lowcode/configs.jsx:

sql
export default {
  url: "",
  conditionLimit:null,
  selectable: false,
  showIndex: false,
  compact: 220,
  path: 'product/productEditor',
  title: "编辑商品",
  entitys: [{"name":"id","label":"编号","type":"search"},{"name":"creator","label":"创建人","type":"user"},{"name":"createGmt","label":"创建时间","type":"date"},{"name":"updateGmt","label":"更新时间","type":"date"},{"name":"title","label":"商品名称"},{"name":"category","label":"商品分类","type":"category"},{"name":"brand","label":"商品品牌","type":"search","depend":"brand"},{"name":"images","label":"轮播图","type":"inputTag"},{"name":"mainImage","label":"主图","type":"image"},{"name":"description","label":"商品描述","type":"tiptap"},{"name":"status","label":"商品状态","code":"productStatus"},{"name":"skuConfig","label":"规格名","type":"inputTag"},{"name":"skuValue","label":"规格值","type":"inputTag"},{"name":"supplier","label":"供应商","type":"search","depend":"supplier"},{"name":"barcode","label":"条码"},{"name":"number","label":"商品编号"},{"name":"unit","label":"单位","type":"search","depend":"unit"},{"name":"location","label":"货位"},{"name":"stockWarning","label":"库存预警","type":"number"},{"name":"account","label":"所属账户","type":"search","depend":"account"},{"name":"price","label":"价格","type":"money"},{"name":"expriation","label":"保质期天数","type":"number"},{"name":"costPrice","label":"进货价","type":"money"},{"name":"wholesalePrice","label":"批发价","type":"money"},{"name":"isSku","label":"是否多规格","type":"switch"}],
  columns: [],
  condition: [],
  slots: {
    
  }
}

vue/src/pages/product/productEditor/productEditor.vue

javascript
<template>
    <el-tabs v-model="current" v-if="loaded">
        <el-tab-pane label="基本信息" name="basic">
            <z-form
                :url="basicUrl"
                :closable="false"
                :data="productData"
                :fields="basicFields"
                @finish="addFinish"
                :rules="rules"
            />
        </el-tab-pane>
        <el-tab-pane
            :disabled="isNew"
            label="商品规格"
            lazy
            name="models"
            v-if="productData.isSku"
        >
            <z-form
                url="/api/product/patchSku"
                :closable="false"
                :data="productData"
                lazy
                :fields="attrFields"
            />
        </el-tab-pane>
        <el-tab-pane :disabled="isNew" label="商品描述" lazy name="describe">
            <z-form
                url="/do/patch/product"
                :closable="false"
                :data="productData"
                :fields="describeFields"
            >
                <template #description="formData">
                    <z-tiptap
                        :modelValue="formData.description"
                        style="margin-bottom: 20px; height: 600px"
                        @update:modelValue="update"
                    />
                </template>
            </z-form>
        </el-tab-pane>
    </el-tabs>
</template>

<script>
import { markRaw } from "vue";
import configs from "./.lowcode/configs";
import SkuConfig from "./blocks/SkuConfig.vue";
import SkuValue from "./blocks/SkuValue.vue";
export default {
    name: "p-jd4k5c6p",
    inject: ["account"],
    provide: { configs },
    props: {
        params: null,
    },
    computed: {
        basicUrl() {
            return this.isNew
                ? "/api/product/putProduct"
                : "/api/product/patchProduct";
        },
    },
    data() {
        return {
            current: "basic",
            basicFields: [
                "title",
                "barcode",
                "number",
                "category",
                { name: "brand", account: "account" },
                { name: "unit", account: "account" },
                {
                    name: "images",
                    type: "image",
                    limit: 5,
                    onChange: this.imageChange,
                    tip: "800px * 800px",
                },
                {
                    name: "mainImage",
                    type: "image",
                    multiple: false,
                    tip: "白底,800px * 800px",
                },
                {
                    label: "供应商",
                    name: "supplier",
                    type: "search",
                    depend: "supplier",
                    account: "account",
                },
                "location",
                "stockWarning",
                { name: "expriation", label: "保质期天数", type: "number" },
                {
                    name: "expirWarning",
                    label: "保质期预警天数",
                    type: "number",
                },
                { name: "isSku", default: 1, visible: () => this.isNew },
            ],
            attrFields: [
                { name: "skuConfig", component: markRaw(SkuConfig) },
                { name: "skuValue", component: markRaw(SkuValue) },
            ],
            describeFields: [
                {
                    name: "description",
                    tip: "宽900px,高度不限",
                },
            ],
            loaded: false,
            isNew: true,
            productData: { account: this.account },
            rules: {
                title: { required: true, message: "商品名称不能为空" },
            },
        };
    },
    async created() {
        const { id } = this.params;
        if (id) {
            await this.loadDetail(id);
        }
        const config = await $.get({
            url: "/api/setting/systemConfig",
            data: { type: "pref" },
        });
        if (config != null && config.stockWarning != null)
            this.productData.stockWarning = config.stockWarning;
        this.loaded = true;
    },
    methods: {
        update(value) {
            this.productData.description = value;
        },
        imageChange(val, formData) {
            console.log(formData);
            if (val.length > 0) {
                formData.mainImage = val[0].url;
            }
        },
        async loadDetail(id) {
            this.productData = await $.get({
                url: "/do/get/product",
                data: { id },
            });
            if (this.productData.status === 3) {
                this.productData.status = 5;
            }
            this.isNew = false;
        },
        async addFinish(result) {
            await this.loadDetail(result.id);
            this.isNew = false;
        },
    },
};
</script>

vue/src/pages/product/productEditor/blocks/SkuConfig.vue

javascript
<template>
    <z-action label="添加规格" :fields="fields" :beforeSubmit="addProp" mode="popover" />
    <div class="bg" v-if="props.length > 0">
        <el-form-item v-for="(item, idx) in props" :key="item.pid" :value="item" :index="idx">
            <template #label>
                {{ item.title }}
                <z-action icon="x" type="text" mode="confirm" title="确定删除该规格吗?" :beforeSubmit="() => removeProp(idx)" />
                <z-icon v-if="idx > 0" value="arrowUp" class="remove hover" @click="() => move(idx, props)" />
            </template>
            <z-action v-if="item.isPic" type="default" size="small" label="规格属性" icon="plus" :fields="tagFieldsPic"
                :beforeSubmit="(data) => addPropValue(data, item)" mode="popover" width="260px" />
            <z-action v-else type="default" size="small" label="规格属性" icon="plus" :fields="tagFields"
                :beforeSubmit="(data) => addPropValue(data, item)" mode="popover" width="260px" />

            <SkuTag v-for="(tag, idx2) in item.list" @remove="() => remove(item, idx2)" :isPic="!!item.isPic"
                :value="tag" :key="tag.vid" :idx="idx2" @move="() => move(idx2, item.list)" />
        </el-form-item>
    </div>
</template>
<script>
import SkuTag from './SkuTag.vue';
export default {
    inheritAttrs: false,
    components: { SkuTag },
    props: {
        modelValue: Array
    },
    data() {
        return {
            fields: [
                { label: '规格名', name: 'title' },
                { label: '是否图片', name: 'isPic', type: 'switch' }
            ],
            tagFields: [
                { label: '规格属性', name: 'title' },
            ],
            tagFieldsPic: [
                { label: '规格属性', name: 'title' },
                { label: '图片', name: 'picUrl', type: 'image' },
            ],
            selected: null,
            visible: false,
            props: [],
        };
    },
    async created() {
        if (this.modelValue) {
            this.props = this.modelValue
        }
    },
    methods: {
        remove(item, idx) {
            item.list.splice(idx, 1);
            this.update()
        },
        move(idx, parents) {
            const item = parents.splice(idx, 1)
            parents.splice(idx - 1, 0, item[0]);
        },
        removeProp(idx) {
            if (this.props[idx].list.length > 0) {
                return $.error('请先删除规格属性')
            }
            this.props.splice(idx, 1)
            this.update()
        },
        addProp(data) {
            data.pid = $.uid(8)
            data.list = []
            this.props.push(data)
        },
        addPropValue(data, parent) {
            parent.list.push({ ...data, vid: $.uid(8) })
            this.update()
            return true
        },
        update() {
            this.$emit("update:modelValue", this.props)
            $.emit('skuConfigUpdate', this.props)
        }
    }
};
</script>
<style lang="scss" scoped>
.bg {
    background: var(--a-fill-color);
    margin-top: 8px;
    padding: 8px;
    border-radius: 4px;
    width: 95%;

    table {
        width: 100%;
    }
}

.propValueName {
    display: inline-block;
    white-space: normal;
    width: 140px
}

:deep(.a-form-item__label) {
    padding-right: 24px;
    position: relative;

    .remove {
        display: none;
        position: absolute;
        top: 6px;
        right: 10px;
    }

    &:hover .remove {
        display: block;
    }
}

:deep(.el-radio__input) {
    vertical-align: top;
}
</style>

vue/src/pages/product/productEditor/blocks/SkuValue.vue

javascript
<template>
    <div class="bg">
        <z-form-item :value="formData" :noAction="true" type="table" name="skuValue" :fields="fields" />
    </div>
</template>
<script>
import SkuFunc from './skuFunc';
export default {
    props: {
        formData: Object
    },
    data() {
        return {
            fields: null,
            fixedColumns: [
                { name: 'outId', label: '外部ID', width: '180px' },
                { name: 'weight', label: '净重(克)', type: 'number', width: '140px' },
                { name: 'status', label: '是否启用', type: 'switch', width: '100px' }
            ]
        };
    },
    created() {
        $.on('skuConfigUpdate', this.updateSku)
        const skuConfig = this.formData['skuConfig']
        if (skuConfig) {
            this.updateSku(skuConfig)
        } else {
            this.fields = [...this.fixedColumns]
        }
    },
    methods: {
        updateSku(configs) {
            const skuData = SkuFunc.createSku(configs)
            const skuValue = skuData.map((sku) => {
                const titles = []
                configs.forEach(prop => {
                    titles.push(sku['p_' + prop.pid])
                });
                sku.title = titles.join(",")
                // 更存量数据合并
                const item = SkuFunc.skuData(sku, this.formData.skuValue || [], configs)
                // 同步更新SKU标题
                if (item) {
                    item.title = sku.title
                    return item
                }
                return sku
            })
            this.formData.skuValue = skuValue
            const columns = configs.map((item) => {
                return { name: "p_" + item.pid, label: item.title, type: 'text' }
            })
            this.fields = [...columns, ...this.fixedColumns]
        },
        //新增外部编码字段
        blur(outId) {
            if (/[^\w\.\/]/gi.test(outId)) {
                return $.error("外部编码不支持输入中文和空格");
            }
        }
    }
};
</script>
<style lang="scss" scoped>
.bg {
    background: var(--a-fill-color);
    padding: 8px;
    border-radius: 4px;
    width: 95%;
}
</style>