2025-12.8
view → 所有块级元素(div, section, article, header, footer...) text → 所有文本元素(span, p, h1-h6, label...) image → 所有图片 input → 表单输入 button → 按钮 scroll-view → 滚动区域
商品列表
// 定义组件
组件 {
// 响应式数据存储
数据 {
配置项 {
获取数据接口: 初始为空字符串 (后续动态赋值)
删除数据接口: 初始为空字符串 (后续动态赋值)
编辑页面路径: '/pages/goods/edit' (固定路径)
菜单ID: 'f7a907fb-b79e-4fee-a512-7482ff796236' (唯一标识)
}
列表数据: 空数组 (用于存储从API获取的商品列表)
}
// 计算属性区 (当前为空)
计算属性 {
}
// 下拉刷新事件处理器
下拉刷新事件 {
调用搜索框组件的搜索方法 (触发数据更新)
设置500毫秒延时
延时结束后停止下拉刷新动画
}
// 组件创建时初始化
创建生命周期 {
将获取数据接口指向全局API的商品查询方法
将删除数据接口指向全局API的商品删除方法
}
// 显示页面时触发
显示生命周期 {
- 调用搜索框组件的搜索方法 (确保显示最新数据)
}
// 方法区 (当前为空)
方法 {
}
}

商品编辑
定义组件 {
//属性
属性: {
title: 字符串 // 标题
},
//响应式数据存储
数据() {
return{
状态: {
操作类型: 'add' // 当前操作类型:add/update
},
表单配置选项: [
{
标题: '商品状态',
键: 'status',
值: 1,
类型: 开关,
必填: true
},
],
规格表头: [
{ 标题: '序号', 键: 'sort', 类型: 数字输入 }
],
附文件列表: [],
规格列表: [],
选择器页面参数: { a: 1 },
编辑参数: {},
API配置: {
编辑接口: '',
新增接口: '',
更新接口: '',
删除接口: ''
}
}
},
//组件创建时初始化
创建时() {
// 初始化API配置
设置API配置.编辑接口 = 商品API.编辑
设置API配置.新增接口 = 商品API.新增
设置API配置.更新接口 = 商品API.更新
设置API配置.删除接口 = 商品API.删除
},
//加载页面时触发
页面加载时(参数) {
如果 参数.id 存在 {
显示加载中('数据加载中...')
调用编辑方法(参数.id)
}
},
//方法
方法: {
删除商品() {
显示确认弹窗({
'删除提示',
'确定要删除当前数据吗?',
success: (e) => {
如果 用户确认 {
发送GET请求(删除接口, {
id: 编辑参数.id
}) .then(data => {
如果 成功 {
显示成功提示({
'删除成功!'
})
延迟1秒后 返回上一页
}
}).catch(e => {})
}
}
});
},
编辑商品(商品ID) {
设置操作类型为'update'
发送GET请求(编辑接口, {
id: 商品ID
}).then (data=> {
如果 数据存在 {
保存编辑参数 = 返回的数据
- 回填表单选项
遍历 表单选项 {
如果 数据中有对应字段 {
设置选项值 = 数据中的值
1.特殊处理供应商:供应商未选择时,不回显
如果 选项是供应商 {
检查供应商ID是否无效 {
清空供应商选项
}
}
}
}
- 回填附件文件
文件列表 = 处理文件数据(数据.files) {
映射每个文件为 {
原文件属性,
url: 优先使用原url,无则尝试从path/saveName构造,
状态: 'success'
}
}
- 回填规格列表
规格列表 = 处理规格数据(数据.specList) {
映射每个规格为 {
原规格属性,
images: 处理规格图片(规格.files), // 类似文件处理
qty: 转换为数字,
subAmount: 转换为数字
}
}
}
隐藏加载中
}
},
取消返回() {
返回上一页
},
保存商品() {
如果 表单验证通过 {
确定要使用的API = 编辑参数.id存在 ? 更新接口 : 新增接口
构建请求参数 = {}
如果 是更新 {
请求参数 = 复制编辑参数
}
- 收集表单数据
遍历 表单选项 {
如果 必填或有值 {
请求参数[字段键] = 选项值
1.特殊处理选择器类型(分类、单位、供应商)
如果 字段是选择器类型 {
从数组中取第一个值
}
}
}
- 处理规格数据
如果 规格列表存在且非空 {
请求参数.specList = 映射规格列表 {
转换每个规格为 {
除images外的规格属性,
files: 处理图片数据(规格.images), // 前端images转后端files
sort: 规格序号或索引+1
}
}
}
- 处理附件文件
如果 文件列表存在 {
请求参数.files = 映射文件列表 {
处理文件ID: 只保留有效的GUID格式ID
}
}
- 提交保存
显示加载中('保存中...')
发送POST请求(对应API, 请求参数) {
隐藏加载中
显示成功提示('保存成功!')
延迟1秒后返回上一页
}
}
},
表单选项变化回调(新选项) {
更新表单选项 = 新选项
},
添加规格行() {
新序号 = 当前规格列表长度 + 1
添加新规格到列表({
sort: 新序号,
name: '',
purchasePrice: 0,
tradePrice: 0,
vipPrice: 0,
remark: '',
images: [],
qty: 0,
subAmount: 0
})
},
规格变化回调(事件) {
如果 事件.索引存在 {
更新规格列表[索引] = 事件.数据项
}
},
删除规格({索引}) {
第1行:onSpecDelete({ index }) {
函数定义:名为 onSpecDelete,接收一个包含 index 属性的对象作为参数
第2行:if (!Array.isArray(this.specList)) return;
条件判断:如果 this.specList 不是数组类型
则执行:直接返回,结束函数执行
第3行:const row = this.specList[index];
变量声明:从 this.specList 数组中获取索引为 index 的元素
赋值给变量 row
第4行:const guidRe = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
变量声明:创建一个正则表达式对象 guidRe
模式说明:匹配GUID/UUID格式字符串
格式要求:8位十六进制-4位十六进制-4位十六进制-4位十六进制-12位十六进制
第5行:// 后端确定主键是 id,这里只用 row.id
注释:说明后端确认的主键字段名是 "id"
第6行:const serverIdCandidate = row?.id;
变量声明:获取 row 对象的 id 属性值
使用可选链操作符:如果 row 为 null 或 undefined,返回 undefined
赋值给变量 serverIdCandidate
第7行:const hasServerId = typeof serverIdCandidate === 'string' && guidRe.test(serverIdCandidate);
变量声明:判断是否有有效的服务器ID
条件1:serverIdCandidate 的数据类型是字符串
条件2:serverIdCandidate 符合 guidRe 正则表达式模式
两个条件都为真时:hasServerId = true
否则:hasServerId = false
第8行:空行(代码分隔)
第9行:// 把本地列表里这条删除,并把后续序号重排(保持 sort 从1开始递增)
注释:说明接下来定义的函数功能
第10行:const removeLocally = () => {
函数声明:定义一个名为 removeLocally 的箭头函数
该函数没有参数
第11行:this.specList.splice(index, 1);
数组操作:从 this.specList 数组中删除一个元素
删除位置:索引为 index 的位置
删除数量:1 个元素
第12行:this.specList = this.specList.map((s, i) => ({ ...s, sort: i + 1 }));
数组操作:重新赋值 this.specList
使用 map 方法:遍历数组中的每个元素
参数说明:s 表示当前元素,i 表示当前索引
操作:为每个元素创建一个新对象
展开原对象的所有属性:...s
添加或覆盖 sort 属性:sort: i + 1
i + 1 表示从1开始的连续序号
第13行:};
函数定义结束
第14行:空行(逻辑分隔)
第15行:if (hasServerId) {
条件判断:如果 hasServerId 为 true
则执行:第16-33行的代码块
第16行:// 后端删除:POST /App/Goods/DeleteSpec
注释:说明接下来执行后端删除操作
第17行:uni.showLoading({ title: '删除规格中...', mask: true });
调用函数:显示加载提示框
参数说明:
title: '删除规格中...' 提示文字
mask: true 显示遮罩层,阻止用户操作
第18行:空行
第19行:// 后端要求:URL 后面带上 id, /App/Goods/DeleteSpec?id=xxxx
注释:说明URL构建规则
第20行:// POST 方法,拼到 URL 上
注释:说明HTTP方法
第21行:const url = `{this.api.goods.deleteSpec}?id=${encodeURIComponent(serverIdCandidate)}`;
变量声明:构建请求URL
字符串模板:{this.api.goods.deleteSpec} 获取API基础地址
查询参数:?id=${encodeURIComponent(serverIdCandidate)}
encodeURIComponent:对 serverIdCandidate 进行URL编码
第22行:this.$http.Post(url, {})
调用函数:发送HTTP POST请求
参数1:url 请求地址
参数2:{} 空对象作为请求体
第23行:.then(() => {
Promise 成功回调:当HTTP请求成功时执行
开始定义回调函数
第24行:uni.hideLoading();
调用函数:隐藏加载提示框
第25行:uni.showToast({ title: '规格已删除', icon: 'success', duration: 800 });
调用函数:显示成功提示
参数说明:
title: '规格已删除' 提示文字
icon: 'success' 成功图标
duration: 800 显示800毫秒后自动消失
第26行:removeLocally();
调用函数:执行本地删除操作(第10-13行定义的函数)
第27行:})
Promise 成功回调函数结束
第28行:.catch(() => {
Promise 失败回调:当HTTP请求失败时执行
开始定义回调函数
第29行:uni.hideLoading();
调用函数:隐藏加载提示框
第30行:// 如果后端删除失败,不改动本地列表
注释:说明失败时的处理原则
第31行:uni.showToast({ title: '删除失败', icon: 'error', duration: 1000 });
调用函数:显示错误提示
参数说明:
title: '删除失败' 提示文字
icon: 'error' 错误图标
duration: 1000 显示1000毫秒后自动消失
第32行:});
Promise 失败回调函数结束
第33行:}
if 语句代码块结束
第34行:else {
else 分支:当 hasServerId 为 false 时执行
第35行:// 没有合法id(说明还没保存到服务器),直接本地删除即可
注释:说明else分支的逻辑
第36行:removeLocally();
调用函数:执行本地删除操作
第37行:}
else 分支代码块结束
第38行:}
函数 onSpecDelete 定义结束
}
}
}
列表子组件
template
第1行:<template>
开始模板部分
第2行: <div class="table-u" :style="{height: tableHeigth+'px'}">
根div元素,类名table-u,动态绑定样式高度为tableHeight像素
第3行: <u-list @scrolltolower="scrolltolower" style="width: 100%;overflow-y: auto;height: 100%;" :height="tableHeigth+'px'"
使用u-list组件,绑定滚动到底部事件,设置宽度100%,垂直滚动,高度100%,动态高度绑定
第4行: refresherEnabled :refresherTriggered="refresherTriggered" @onRefresh="onRefresh">
启用下拉刷新,绑定刷新触发状态和刷新事件
第5行: <u-list-item v-for="(item, index) in list" :key="index">
遍历list数组,为每个元素创建u-list-item,key使用索引index
第6行: <!-- 主战 -->
注释:主战模式(type=1)
第7行: <template v-if="type===1">
条件渲染:当type等于1时显示此模板
第8行: <div @click="listItemClick(item)">
点击事件,触发listItemClick方法,传入当前item
第9行: <div class="content active" :class="[status.multiple&&item.check?'check':'']">
div元素,类名content和active,动态类名:当status.multiple为真且item.check为真时添加check类
第10行: <!-- 内容1 -->
注释:内容1区域
第11行: <div class="content1 ">
类名为content1的div
第12行: <div class="df-jb-ac f30 emphasis-font" style="padding:0 30rpx;">
flex布局(两端对齐,垂直居中),字体大小30,强调字体,内边距0 30rpx
第13行: <div class="df-js-ac" v-for="(header,index) in firstHeaders">
遍历firstHeaders数组,每个元素创建div,flex布局(主轴起始对齐,垂直居中)
第14行: <checkbox v-if="status.multiple&&index===0" :checked="item.check" color="#0388FF" />
条件渲染复选框:当status.multiple为真且是第一个表头时显示,选中状态绑定item.check,颜色蓝色
第15行: <div>{{item[header.aName]||''}}</div>
显示item对象中header.aName属性的值,如果为空则显示空字符串
第16行: </div>
结束firstHeaders遍历
第17行: </div>
结束content1的flex容器
第18行: </div>
结束content1区域
第19行: <!-- 内容2 -->
注释:内容2区域
第20行: <div class="f28 content2">
字体大小28,类名content2
第21行: <div class="df-js-ac content2-item" v-for="(header,index) in headers" style="flex-wrap: wrap">
遍历headers数组,每个元素创建div,flex布局,允许换行
第22行: <!-- 名 -->
注释:字段名称
第23行: <div class="content-font">{{header.description}}:</div>
显示表头描述,添加冒号
第24行: <!-- 值 -->
注释:字段值
第25行: <div v-if="header.aName==='status'" class="content-font2" style="flex: 1;">{{item[header.aName]|Status}}</div>
条件渲染:当字段名为status时,显示格式化后的状态值(使用Status过滤器)
第26行: <div v-else class="content-font2" style="flex: 1;">{{item[header.aName]||''}}</div>
否则显示字段值,如果为空显示空字符串
第27行: </div>
结束headers遍历
第28行: </div>
结束content2区域
第29行: <!-- 内容3 -->
注释:内容3区域
第30行: <div class="content3 f30">
类名content3,字体大小30
第31行: <div class="df-fc-jc-ac content3-item " v-for="(header,index) in lastHeaders">
遍历lastHeaders数组,每个元素创建div,flex列布局,主轴和交叉轴居中
第32行: <!-- 值 -->
注释:字段值
第33行: <div v-if="header.aName==='status'" class="df-jc-ac primary-font">{{item[header.aName]|Status}}</div>
条件渲染:当字段名为status时,显示格式化后的状态值
第34行: <div v-else class="df-jc-ac primary-font">{{item[header.aName]||'无'}}</div>
否则显示字段值,如果为空显示"无"
第35行: <!-- 名 -->
注释:字段名称
第36行: <div class="df-jc-ac content-font">{{header.description}}</div>
显示表头描述
第37行: </div>
结束lastHeaders遍历
第38行: </div>
结束content3区域
第39行: </div>
结束最内层div
第40行: </div>
结束点击事件div
第41行: </template>
结束type=1模板
第42行: <!-- 简易横拉 -->
注释:简易横拉模式(type=2)
第43行: <template v-if="type===2">
条件渲染:当type等于2时显示此模板
第44行: <!-- <u-swipe-action> -->
注释:侧滑操作组件(被注释)
第45行: <u-swipe-action-item :ref="'actionItemRef'+index" :options="options" :disabled="type2Disabled" @click="(v)=>{swipeClick(v,item)}">
侧滑操作项,动态ref绑定,操作选项绑定options,禁用状态绑定type2Disabled,点击事件
第46行: <div class="cell f28 df-jb-ac active1" @click="listItemClick(item)">
div元素,类名cell、f28、df-jb-ac、active1,点击事件
第47行: <div>{{item.name}}</div>
第48行: <u-icon name="arrow-right" color="#6d6d6d" size="16" style="margin-right: 10rpx;"></u-icon>
右侧箭头图标,颜色灰色,大小16,右边距10rpx
第49行: </div>
结束cell div
第50行: </u-swipe-action-item>
结束侧滑操作项
第51行: <!-- </u-swipe-action> -->
注释:结束侧滑操作组件
第52行: </template>
结束type=2模板
第53行: <!-- 简易多选 -->
注释:简易多选模式(type=3)
第54行: <template v-if="type===3">
条件渲染:当type等于3时显示此模板
第55行:
空行
第56行: <!-- 自定义展示 -->
注释:自定义展示
第57行: <template v-if="initHeaders&&initHeaders.length">
条件渲染:当initHeaders存在且长度大于0时
第58行: <div @click="listItemClick(item)">
点击事件div
第59行: <div class="content active" :class="[item.check?'check':'']">
div元素,类名content和active,动态类名:item.check为真时添加check类
第60行: <div class="content1 ">
类名content1
第61行: <div class="df-jb-ac f30 emphasis-font" style="padding:0 30rpx;">
flex布局,字体大小30,强调字体,内边距
第62行: <div class="df-js-ac" v-for="(header,index) in firstHeaders">
遍历firstHeaders
第63行: <checkbox v-if="index===0" :checked="item.check" color="#0388FF" />
条件渲染复选框:第一个表头时显示
第64行: <div>{{item[header.aName]||''}}</div>
显示字段值
第65行: </div>
结束firstHeaders遍历
第66行: </div>
结束flex容器
第67行: </div>
结束content1
第68行: <div class="f28 content2">
字体大小28,类名content2
第69行: <div class="df-js-ac content2-item" v-for="(header,index2) in headers" style="flex-wrap: wrap">
遍历headers数组(使用index2避免冲突)
第70行: <div class="content-font">{{header.description}}:</div>
显示表头描述
第71行:
空行
第72行: <div v-if="header.inputType==='number'" class="content-font2" style="width: 120rpx;border-bottom:1rpx solid #e0e0e0;;">
条件渲染:当inputType为number时,显示数字输入框容器
第73行: <u--input v-model="item[header.aName]" slot="value" placeholder=" " border="none" maxlength="11" inputAlign="right"
输入框组件,双向绑定item[header.aName],无占位符,无边框,最大长度11,右对齐
第74行: type="number" style=""></u--input>
输入类型为数字
第75行: </div>
结束数字输入框容器
第76行: <div v-else class="content-font2" style="flex: 1;">{{item[header.aName]||''}}</div>
否则显示普通文本
第77行:
空行
第78行: </div>
结束headers遍历
第79行: </div>
结束content2
第80行: </div>
结束动态类名div
第81行: </div>
结束点击事件div
第82行: </template>
结束initHeaders条件渲染
第83行: <template v-else>
否则(initHeaders不存在或为空)
第84行: <div class="cell f28 df-jb-ac active1" :class="[checkbox&&item.check?'check':'']" @click="listItemClick(item)">
div元素,类名cell、f28、df-jb-ac、active1,动态类名:checkbox为真且item.check为真时添加check类,点击事件
第85行: <div>
内层div
第86行: <div>{{item.name||item.label}}</div>
显示item.name或item.label
第87行: <!-- zyy:如果有规格(specName),在商品的名称下方追加显示 -->
注释:规格信息显示
第88行: <div v-if="item.specName" style="color: #6d6d6d;margin-top: 6rpx;font-size: 24rpx;">规格:{{item.specName}}</div>
条件渲染:当有specName时显示规格信息
第89行: </div>
结束内层div
第90行:
空行
第91行: <div class="df-je-ac">
flex布局,主轴结束对齐,垂直居中
第92行: <!-- zyy:不同接口的"库存/未出库数量"字段名不一致。这里统一用方法读取:优先取 availableStock,其次 notOutQty/stockQuantity,最后用 qty。 -->
注释:库存显示逻辑说明
第93行: <div v-if="getStock(item) !== null" style="margin-right: 20rpx;color: #0388FF;">库存:{{getStock(item)}}</div>
条件渲染:当getStock返回值不为null时显示库存信息
第94行: <!-- <u-icon name="arrow-right" color="#6d6d6d" size="16" style="margin-right: 10rpx;"></u-icon> -->
注释:箭头图标(被注释)
第95行: <checkbox v-if="checkbox" :checked="item.check" color="#0388FF" />
条件渲染复选框:当checkbox为真时显示
第96行: </div>
结束flex容器
第97行:
空行
第98行: </div>
结束cell div
第99行: </template>
结束else模板
第100行:
空行
第101行: <template v-if="item.childrenOpen" v-for="item2 in item.children">
条件渲染+遍历:当item.childrenOpen为真时,遍历item.children
第102行: <div class="cell f28 df-jb-ac active1" :class="[checkbox&&item2.check?'check':'']" @click="listItemClick(item2)"
二级子项div,样式类似主项,左边距50rpx
第103行: style="margin-left: 50rpx;">
内联样式:左边距50rpx
第104行: <!-- zyy:子级同样支持显示规格 -->
注释:子级规格显示
第105行: <div>
内层div
第106行: <div>{{item2.name||item2.label}}</div>
显示名称
第107行: <div v-if="item2.specName" style="color: #6d6d6d;margin-top: 6rpx;font-size: 24rpx;">规格:{{item2.specName}}</div>
条件渲染规格信息
第108行: </div>
结束内层div
第109行:
空行
第110行: <checkbox v-if="checkbox" :checked="item2.check" color="#0388FF" />
条件渲染复选框
第111行: </div>
结束二级子项div
第112行:
空行
第113行: <template v-if="item2.childrenOpen" v-for="item3 in item2.children">
条件渲染+遍历:当item2.childrenOpen为真时,遍历item2.children
第114行: <template v-if="checkbox">
条件渲染:当checkbox为真时
第115行: <!-- @click="listItemClick(item3)" -->
注释:点击事件(被注释)
第116行: <div class="cell f28 df-jb-ac active1" :class="[checkbox&&item3.check?'check':'']" style="margin-left: 100rpx;">
三级子项div,左边距100rpx
第117行: <div>
内层div
第118行: <div>{{item3.name||item3.label}}</div>
显示名称
第119行: <div v-if="item3.specName" style="color: #6d6d6d;margin-top: 6rpx;font-size: 24rpx;">规格:{{item3.specName}}</div>
条件渲染规格信息
第120行: </div>
结束内层div
第121行: <!-- <checkbox v-if="checkbox" :checked="item3.check" color="#0388FF" /> -->
注释:复选框(被注释)
第122行: </div>
结束三级子项div
第123行: </template>
结束checkbox条件渲染
第124行: <template v-else>
否则
第125行: <div class="cell f28 df-jb-ac active1" :class="[checkbox&&item3.check?'check':'']" @click="listItemClick(item3)"
三级子项div,带点击事件
第126行: style="margin-left: 100rpx;">
左边距100rpx
第127行: <div>
内层div
第128行: <div>{{item3.name||item3.label}}</div>
显示名称
第129行: <div v-if="item3.specName" style="color: #6d6d6d;margin-top: 6rpx;font-size: 24rpx;">规格:{{item3.specName}}</div>
条件渲染规格信息
第130行: </div>
结束内层div
第131行: <!-- <checkbox v-if="checkbox" :checked="item3.check" color="#0388FF" /> -->
注释:复选框(被注释)
第132行: </div>
结束三级子项div
第133行:
空行
第134行: </template>
结束else模板
第135行: </template>
结束item2.childrenOpen遍历
第136行: </template>
结束item.childrenOpen遍历
第137行:
空行
第138行:
空行
第139行:
空行
第140行: </template>
结束type=3模板
第141行: <!-- 用户管理界面 -->
注释:用户管理模式(type=4)
第142行: <template v-if="type===4">
条件渲染:当type等于4时
第143行: <div class="cell f28 df-jb-ac active1" @click="listItemClick(item)" style="padding: 16rpx 30rpx;">
div元素,类名cell、f28、df-jb-ac、active1,点击事件,内边距16rpx 30rpx
第144行:
空行
第145行:
空行
第146行: <div class="df-jb-ac">
flex布局,两端对齐,垂直居中
第147行: <div style="margin-right: 20rpx;">
div元素,右边距20rpx
第148行:
空行
第149行: <template v-if="item.userFile&&item.userFile.length">
条件渲染:当userFile存在且长度大于0时
第150行: <u-avatar class="avatar-img" :src="item.userFile[0].path" size="100rpx"></u-avatar>
头像组件,使用userFile第一个元素的path作为图片源,大小100rpx
第151行: </template>
结束条件渲染
第152行: <u-avatar v-else class="avatar-img" :src="'https://dev.stall.lkxlkf.com/ca/icons/default/default-icon.png'"
否则显示默认头像,使用默认图片URL
第153行: size="100rpx"></u-avatar>
头像大小100rpx
第154行: <!-- <open-data type="userAvatarUrl" style="width: 100rpx;height: 100rpx;margin-right: 20rpx;border-radius: 5pt;" -->
注释:open-data组件(被注释)
第155行: default-avatar="https://dev.stall.lkxlkf.com/ca/icons/default/default-icon.png"></open-data> -->
注释结束
第156行: </div>
结束头像容器div
第157行: <div>
右侧信息div
第158行: <div>{{item.realName}}</div>
显示真实姓名
第159行: <div style="color: #e2a10e;">{{item.roleName}}</div>
显示角色名称,颜色黄色
第160行: </div>
结束右侧信息div
第161行: </div>
结束flex容器
第162行:
空行
第163行:
空行
第164行: <div class="df-je-ac">
flex布局,主轴结束对齐,垂直居中
第165行: <div style="color: #6d6d6d;">设置</div>
"设置"文字,颜色灰色
第166行: <u-icon name="arrow-right" color="#6d6d6d" size="16" style="margin-right: 10rpx;"></u-icon>
右侧箭头图标
第167行: </div>
结束flex容器
第168行: </div>
结束type=4主div
第169行: </template>
结束type=4模板
第170行: </u-list-item>
结束u-list-item
第171行:
空行
第172行: <u-loading-page v-if="list.length===0&&status.getLoading"></u-loading-page>
条件渲染加载页:当列表为空且正在加载时显示
第173行: <u-empty v-if="list.length===0&&!status.getLoading" mode="list" marginTop="40"> </u-empty>
条件渲染空状态:当列表为空且不在加载时显示
第174行: <div v-if="addPath" style="text-align: center;color:#0388FF;margin-bottom: 30px;" :style="{marginTop:list.length===0?'50px':'0px'}"
条件渲染新增按钮:当addPath存在时显示,文本居中,颜色蓝色,动态上边距
第175行: @click="toAddPath">前往新增</div>
点击事件触发toAddPath方法,显示文字"前往新增"
第176行:
空行
第177行: </u-list>
结束u-list组件
第178行: <div class="else-heigth"></div>
占位div,类名else-heigth
第179行:
空行
第180行:
空行
第181行: <image v-if="editPath" class="add-button active" src="../../static/images/add.png" @click="toEdit('')"></image>
条件渲染添加按钮图片:当editPath存在时显示,点击触发toEdit方法
第182行: <!-- <u-empty mode="list"></u-empty> -->
注释:空状态组件(被注释)
第183行: </div>
结束根div
第184行:
空行
第185行:</template>
结束模板部分
script
第一部分:Props 定义(第1-74行)
第1-2行:export default {
导出默认组件配置对象
第3-4行: props: {
开始定义props(组件接收的属性)
第5-8行: nameTemplate: { type: String, default: '' },
属性:nameTemplate,字符串类型,默认值空字符串
第9-12行: type: { type: Number, default: 1 },
属性:type,数字类型,默认值1
第13-16行: editPath: { type: String, default: '' },
属性:editPath,字符串类型,默认值空字符串
第17-24行: options: { type: Array, default: () => [{ text: '删除', style: { backgroundColor: '#f56c6c' } }] },
属性:options,数组类型,默认返回包含删除操作的数组
第25-28行: optionsDisabled: { type: Boolean, default: true },
属性:optionsDisabled,布尔类型,默认值true
第29-32行: api: { type: String, default: () => '' },
属性:api,字符串类型,默认值空字符串
第33-36行: method: { type: String, default: () => 'Post' },
属性:method,字符串类型,默认值'Post'
第37-40行: deleteApi: { type: String, default: () => '' },
属性:deleteApi,字符串类型,默认值空字符串
第41-44行: checkbox: { type: Boolean, default: false },
属性:checkbox,布尔类型,默认值false
第45-48行: checkbox1: { type: Boolean, default: false },
属性:checkbox1,布尔类型,默认值false
第49-52行: menuId: { type: String, default: () => '' },
属性:menuId,字符串类型,默认值空字符串
第53-57行: // menuIdEdit 专用于"跳转到编辑页"携带的菜单ID
注释:menuIdEdit的用途说明
第58-61行: menuIdEdit: { type: String, default: () => '' },
属性:menuIdEdit,字符串类型,默认值空字符串
第62-66行: // 是否仅在跳转编辑页时传递 id(不传 vouchecode/menuId)
注释:passOnlyId的用途说明
第67-70行: passOnlyId: { type: Boolean, default: false },
属性:passOnlyId,布尔类型,默认值false
第71-74行: type2Disabled: { type: Boolean, default: false },
属性:type2Disabled,布尔类型,默认值false
第75-78行: addPath: { type: String, default: () => '' },
属性:addPath,字符串类型,默认值空字符串
第79-82行: title: { type: String, default: () => '' },
属性:title,字符串类型,默认值空字符串
第83-86行: appendGetPms: { type: Object, default: () => {} },
属性:appendGetPms,对象类型,默认空对象
第87-90行: dontAdd: { type: Boolean, default: false },
属性:dontAdd,布尔类型,默认值false
第91-94行: // zyy:需要隐藏经理/店员/内勤/收货人/联系人等人员列。
注释:supplierExcludeDescriptions的用途说明
第95-98行: supplierExcludeDescriptions: { type: Array, default: () => [] },
属性:supplierExcludeDescriptions,数组类型,默认空数组
第99-102行: initHeaders: { type: Array, default: () => [] },
属性:initHeaders,数组类型,默认空数组
第103-106行: // 默认选中的ID列表。用于再次进入多选页时自动勾选之前的选择。
注释:defaultCheckedIds的用途说明
第107-110行: defaultCheckedIds: { type: Array, default: () => [] },
属性:defaultCheckedIds,数组类型,默认空数组
第111-114行: // 默认的抵扣金额映射,键是行的 id,值是该行的抵扣金额
注释:defaultDeductMap的用途说明
第115-118行: defaultDeductMap: { type: Object, default: () => ({}) },
属性:defaultDeductMap,对象类型,默认空对象
第119-122行: // 表格数据过滤方法
注释:tableDataFilter的用途说明
第123-126行: tableDataFilter: { type: Function, default: null },
属性:tableDataFilter,函数类型,默认null
第127-130行: //内容一过滤
注释:firstHeaderKeys的用途说明
第131-134行: firstHeaderKeys: { type: Array, default: () => [] },
属性:firstHeaderKeys,数组类型,默认空数组
第135-138行: //内容三过滤
注释:lastHeaderKeys的用途说明
第139-142行: lastHeaderKeys: { type: Array, default: () => [] },
属性:lastHeaderKeys,数组类型,默认空数组
第143行: },
结束props定义
第二部分:Data 定义(第144-187行)
第144-146行: data() {
开始定义组件数据
第147行: return {
返回数据对象
第148-149行: componentsName: 'tableU',
数据:componentsName,字符串'tableU'
第150-154行: status: { heigthInitializeLoading: false, multiple: false, getLoading: false },
数据:status对象,包含三个布尔值
第155行: list: [],
数据:list,空数组
第156行: headers_: [],
数据:headers_,空数组
第157行: firstHeaders: [],
数据:firstHeaders,空数组
第158行: headers: [],
数据:headers,空数组
第159行: lastHeaders: [],
数据:lastHeaders,空数组
第160-171行: urls: [ "https://uviewui.com/album/1.jpg", ... "https://uviewui.com/album/10.jpg", ],
数据:urls,包含10个图片URL的数组
第172行: tableHeigth: 660,
数据:tableHeigth,数字660
第173行: refresherTriggered: false,
数据:refresherTriggered,布尔值false
第174-177行: getPms: { pageNo: 1, pageSize: 20 },
数据:getPms对象,包含页码和每页大小
第178行: maxPageNo: 0,
数据:maxPageNo,数字0
第179行: lastGetPms: {},
数据:lastGetPms,空对象
第180行: checkbox1Item: {}
数据:checkbox1Item,空对象
第181行: }
结束数据返回
第182行: },
结束data函数
第三部分:生命周期和监听器(第183-195行)
第183-185行: created() {
创建完成生命周期钩子
第186行:
空行
第187行: },
结束created
第188-190行: mounted() {
挂载完成生命周期钩子
第191-193行: setTimeout(() => { this.heightInit(); }, 0)
延迟0毫秒后调用heightInit方法
第194行: },
结束mounted
第195-197行: watch: {
开始定义监听器
第198-200行: initHeaders() { this.initHeader(); }
监听initHeaders变化,调用initHeader方法
第201行: },
结束watch
第四部分:Methods 方法(第202-618行)
initHeader 方法(第203-218行)
第202行: methods: {
开始定义方法
第203-205行: initHeader() {
方法:initHeader
第206行: console.log('initHeader')
控制台输出
第207-217行: if (this.initHeaders) {
let hs = [...this.initHeaders];
this.headers_ = hs;
this.firstHeaders = hs.slice(0, 2);
if (this.isLastHeaders) {
this.headers = hs.slice(2, -3);
this.lastHeaders = hs.slice(-3);
} else {
this.headers = hs.slice(2);
}
this.setSearchBox();
}
如果initHeaders存在:复制数组,设置表头分段,调用setSearchBox
第218行: },
结束initHeader
getStock 方法(第219-229行)
第219-221行: getStock(item) {
方法:getStock,接收item参数
第222行: if (!item || typeof item !== 'object') return null;
如果item无效返回null
第223行: const v = (item.availableStock ?? item.notOutQty ?? item.stockQuantity ?? item.qty);
获取库存值,按优先级查找字段
第224行: if (v === undefined || v === null || v === '') return null;
如果值为空返回null
第225-228行: // 如果是字符串数字,转为数字显示;否则原样返回
const n = Number(v);
return Number.isNaN(n) ? v : n;
尝试转换为数字,失败则返回原值
第229行: },
结束getStock
toAddPath 方法(第230-237行)
第230-232行: toAddPath() {
方法:toAddPath
第233-236行: uni.navigateTo({ url: this.addPath, complete: (v) => {} });
跳转到addPath指定的页面
第237行: },
结束toAddPath
swipeClick 方法(第238-270行)
第238-240行: swipeClick(v, item) {
方法:swipeClick,接收v和item参数
第241-242行: if (this.options[v.index].text === '删除') {
如果点击的操作文本是"删除"
第243-244行: if (this.deleteApi) {
如果deleteApi存在
第245-268行: uni.showModal({
title: '删除提示',
content: '确定要删除当前数据吗?',
success: (e) => {
if (e.confirm) {
this.$http.Get(this.deleteApi, { id: item.id }).then(data => {
if (data) {
uni.showToast({ title: '删除成功!', icon: 'success', duration: 1000 })
setTimeout(() => { this.getList(); }, 1000)
}
}).catch(e => {})
}
}
});
显示确认对话框,确认后调用删除接口,成功则提示并刷新列表
第269行: }
结束if
第270行: },
结束swipeClick
getList 方法(第271-510行) - 太长,分段解释:
第271-284行:参数处理
第271-273行: getList(getPms = {}) {
方法:getList,接收getPms参数,默认空对象
第274-280行: if (Object.keys(getPms).length !== 0) {
this.lastGetPms = { ...getPms }
} else {
getPms = { ...this.lastGetPms }
}
如果有新参数则保存,否则使用上次参数
第281-283行: return new Promise((resolve, reject) => {
返回Promise对象
第285-302行:模拟数据模式
第285-287行: if (!this.api) {
如果api不存在
第288-295行: this.list = [];
for (let i = 0; i < 5; i++) {
this.list.push({
check: false,
url: this.urls[uni.$u.random(0, this.urls.length - 1)],
});
}
resolve();
return
创建模拟数据并返回
第296行: }
结束if
第297-302行:准备请求参数
第297-303行: let pms = { ...this.getPms, ...getPms, idMenuDTO: this.menuId, ...this.appendGetPms };
合并所有参数
第304-508行:完整数据处理流程(由于太长,只总结关键点)
发送请求(第304-306行)
数据规范化(第307-340行):处理不同格式的响应数据
表头处理(第341-409行):
过滤不需要的列(supplierExcludeDescriptions)
排序处理(firstHeaderKeys优先,lastHeaderKeys末尾)
默认排序逻辑(时间/日期/备注列处理)
分段表头(firstHeaders, headers, lastHeaders)
行数据处理(第410-490行):
递归关闭子级展开
名称兼容处理(goodsName, label)
名称模板渲染
子级展开状态初始化
ID字段统一映射
地址字段统一映射
时间字段清理
回显逻辑(defaultCheckedIds)
自定义数据过滤(tableDataFilter)
数据更新逻辑(第491-507行):
第一页直接覆盖
后续页智能合并(避免重复)
关闭侧滑操作
计算总页数
设置搜索框
更新状态
其他方法简要说明:
multipleChange 方法(第511-513行):切换多选模式
listItemClick 方法(第514-551行):处理列表项点击,支持单选、多选、跳转
toEdit 方法(第552-578行):跳转到编辑页面,参数处理
onRefresh 方法(第579-590行):下拉刷新处理
heightInit 方法(第591-628行):初始化表格高度,考虑屏幕尺寸和其他元素
setTotal 方法(第629-640行):通知合计组件更新总数
setSearchBox 方法(第641-652行):通知搜索组件更新排序选项
scrolltolower 方法(第653-658行):滚动到底部加载更多
第659-660行:
第659行: }
结束methods
第660行: }
结束组件导出
组件功能总结:
这是一个高度可配置的表格组件,主要功能包括:
四种显示模式:通过type切换
数据获取:支持API请求和模拟数据
智能表头:动态分段、排序、过滤
多级数据:支持三级嵌套结构
多选功能:单选、多选、树形选择
编辑跳转:灵活的参数传递
下拉刷新:支持刷新和加载更多
高度自适应:根据屏幕计算高度
组件联动:与搜索框、合计组件联动
数据回显:支持选中状态回显
组件代码量较大,但结构清晰,每个方法职责明确,是一个功能完备的业务表格组件。
