cloud-web-corejs 1.1.0-dev.27 → 1.1.0-dev.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/package.json +4 -11
  2. package/src/components/table/index.js +3 -0
  3. package/src/components/xform/form-designer/setting-panel/property-editor/container-data-table/data-table-editor.vue +18 -1
  4. package/src/components/xform/form-designer/setting-panel/property-editor/container-list-h5/list-h5-editor.vue +33 -20
  5. package/src/components/xform/form-render/container-item/data-table-mixin.js +16 -8
  6. package/src/components/xform/form-render/container-item/list-h5-item.vue +92 -309
  7. package/src/components/xform/utils/util.js +20 -0
  8. package/src/views/demo/bizTab/bill/billConfig.js +41 -0
  9. package/src/views/demo/bizTab/bill/detail.vue +436 -0
  10. package/src/views/demo/bizTab/bill/list.vue +288 -0
  11. package/src/views/demo/bizTab/bill/productDialog.vue +176 -0
  12. package/src/views/demo/bizTab/simple/billApi.js +123 -0
  13. package/src/views/demo/bizTab/simple/detail.vue +407 -0
  14. package/src/views/demo/bizTab/simple/list.vue +217 -0
  15. package/src/views/demo/bizTab/standalone/index.vue +121 -0
  16. package/src/views/demo/bizTab/wf/billConfig.js +44 -0
  17. package/src/views/demo/bizTab/wf/detail.vue +661 -0
  18. package/src/views/demo/bizTab/wf/list.vue +242 -0
  19. package/src/app-bootstrap.js +0 -523
  20. package/src/boot-assets.legacy.js +0 -7
  21. package/src/boot-assets.next.js +0 -7
  22. package/src/core-resources/css/h5.scss +0 -548
  23. package/src/core-resources/css/iconfont.css +0 -1447
  24. package/src/core-resources/font/iconfont.ttf +0 -0
  25. package/src/core-resources/font/iconfont.woff +0 -0
  26. package/src/core-resources/font/iconfont.woff2 +0 -0
  27. package/src/core-resources/images/default-header.png +0 -0
  28. package/src/core-resources/images/ico-default_wenjian.svg +0 -1
  29. package/src/core-resources/images/ico-excel.svg +0 -1
  30. package/src/core-resources/images/ico-ftl.svg +0 -7
  31. package/src/core-resources/images/ico-html.svg +0 -1
  32. package/src/core-resources/images/ico-java.svg +0 -1
  33. package/src/core-resources/images/ico-music.svg +0 -1
  34. package/src/core-resources/images/ico-pdf.svg +0 -1
  35. package/src/core-resources/images/ico-photo.svg +0 -1
  36. package/src/core-resources/images/ico-ppt.svg +0 -1
  37. package/src/core-resources/images/ico-psd.svg +0 -1
  38. package/src/core-resources/images/ico-service.png +0 -0
  39. package/src/core-resources/images/ico-txt.svg +0 -1
  40. package/src/core-resources/images/ico-video.svg +0 -1
  41. package/src/core-resources/images/ico-vue.svg +0 -8
  42. package/src/core-resources/images/ico-wenjianjia.svg +0 -1
  43. package/src/core-resources/images/ico-wrod.svg +0 -1
  44. package/src/core-resources/images/ico-xml.svg +0 -1
  45. package/src/core-resources/images/ico-yasuobao.svg +0 -1
  46. package/src/core-resources/images/icon-error.png +0 -0
  47. package/src/core-resources/images/icon-tips.png +0 -0
  48. package/src/core-resources/images/nodata.png +0 -0
  49. package/src/core-resources/images/nodata2.png +0 -0
  50. package/src/core-resources/js/base/common.js +0 -110
  51. package/src/icons/index.js +0 -9
  52. package/src/icons/svg/404.svg +0 -1
  53. package/src/icons/svg/alert.svg +0 -1
  54. package/src/icons/svg/bug.svg +0 -1
  55. package/src/icons/svg/button.svg +0 -1
  56. package/src/icons/svg/card.svg +0 -1
  57. package/src/icons/svg/cascader-field.svg +0 -1
  58. package/src/icons/svg/census.svg +0 -1
  59. package/src/icons/svg/chart.svg +0 -1
  60. package/src/icons/svg/checkbox-field.svg +0 -1
  61. package/src/icons/svg/clipboard.svg +0 -1
  62. package/src/icons/svg/color-field.svg +0 -1
  63. package/src/icons/svg/component.svg +0 -1
  64. package/src/icons/svg/custom-component.svg +0 -1
  65. package/src/icons/svg/dashboard.svg +0 -1
  66. package/src/icons/svg/data-table.svg +0 -1
  67. package/src/icons/svg/date-field.svg +0 -1
  68. package/src/icons/svg/date-range-field.svg +0 -1
  69. package/src/icons/svg/descriptions.svg +0 -1
  70. package/src/icons/svg/divider.svg +0 -1
  71. package/src/icons/svg/document.svg +0 -1
  72. package/src/icons/svg/documentation.svg +0 -1
  73. package/src/icons/svg/drag.svg +0 -1
  74. package/src/icons/svg/echart-category.svg +0 -1
  75. package/src/icons/svg/echart-pie.svg +0 -1
  76. package/src/icons/svg/echart.svg +0 -1
  77. package/src/icons/svg/edit.svg +0 -1
  78. package/src/icons/svg/education.svg +0 -1
  79. package/src/icons/svg/email.svg +0 -1
  80. package/src/icons/svg/example.svg +0 -1
  81. package/src/icons/svg/excel.svg +0 -1
  82. package/src/icons/svg/exit-fullscreen.svg +0 -1
  83. package/src/icons/svg/eye-open.svg +0 -1
  84. package/src/icons/svg/eye.svg +0 -1
  85. package/src/icons/svg/file-upload-field.svg +0 -1
  86. package/src/icons/svg/form.svg +0 -1
  87. package/src/icons/svg/fullscreen.svg +0 -1
  88. package/src/icons/svg/github.svg +0 -1
  89. package/src/icons/svg/grid.svg +0 -1
  90. package/src/icons/svg/guide.svg +0 -1
  91. package/src/icons/svg/html-text.svg +0 -1
  92. package/src/icons/svg/ico-more.svg +0 -1
  93. package/src/icons/svg/ico-wenjianjia.svg +0 -1
  94. package/src/icons/svg/icon.svg +0 -1
  95. package/src/icons/svg/input-batch.svg +0 -1
  96. package/src/icons/svg/international.svg +0 -1
  97. package/src/icons/svg/language.svg +0 -1
  98. package/src/icons/svg/link.svg +0 -1
  99. package/src/icons/svg/list-export.svg +0 -1
  100. package/src/icons/svg/list.svg +0 -1
  101. package/src/icons/svg/lock.svg +0 -1
  102. package/src/icons/svg/message.svg +0 -1
  103. package/src/icons/svg/money.svg +0 -1
  104. package/src/icons/svg/nested.svg +0 -1
  105. package/src/icons/svg/news-list.svg +0 -1
  106. package/src/icons/svg/node-tree.svg +0 -1
  107. package/src/icons/svg/number-field.svg +0 -1
  108. package/src/icons/svg/password.svg +0 -1
  109. package/src/icons/svg/pdf.svg +0 -1
  110. package/src/icons/svg/people.svg +0 -1
  111. package/src/icons/svg/peoples.svg +0 -1
  112. package/src/icons/svg/picture-upload-field.svg +0 -1
  113. package/src/icons/svg/qq.svg +0 -1
  114. package/src/icons/svg/query.svg +0 -1
  115. package/src/icons/svg/radio-field.svg +0 -1
  116. package/src/icons/svg/rate-field.svg +0 -1
  117. package/src/icons/svg/redo.svg +0 -1
  118. package/src/icons/svg/rich-editor-field.svg +0 -1
  119. package/src/icons/svg/search.svg +0 -1
  120. package/src/icons/svg/searchbox.svg +0 -1
  121. package/src/icons/svg/section.svg +0 -1
  122. package/src/icons/svg/select-field.svg +0 -1
  123. package/src/icons/svg/shopping.svg +0 -1
  124. package/src/icons/svg/size.svg +0 -1
  125. package/src/icons/svg/skill.svg +0 -1
  126. package/src/icons/svg/slider-field.svg +0 -1
  127. package/src/icons/svg/slot-component.svg +0 -1
  128. package/src/icons/svg/slot-field.svg +0 -1
  129. package/src/icons/svg/star.svg +0 -1
  130. package/src/icons/svg/static-text.svg +0 -1
  131. package/src/icons/svg/status.svg +0 -1
  132. package/src/icons/svg/sub-form.svg +0 -1
  133. package/src/icons/svg/switch-field.svg +0 -1
  134. package/src/icons/svg/tab.svg +0 -1
  135. package/src/icons/svg/table.svg +0 -1
  136. package/src/icons/svg/text-field.svg +0 -1
  137. package/src/icons/svg/text.svg +0 -1
  138. package/src/icons/svg/textarea-field.svg +0 -1
  139. package/src/icons/svg/theme.svg +0 -1
  140. package/src/icons/svg/time-field.svg +0 -1
  141. package/src/icons/svg/time-range-field.svg +0 -1
  142. package/src/icons/svg/tips-text.svg +0 -1
  143. package/src/icons/svg/title-head.svg +0 -1
  144. package/src/icons/svg/tree-pane.svg +0 -1
  145. package/src/icons/svg/tree-table.svg +0 -1
  146. package/src/icons/svg/tree.svg +0 -1
  147. package/src/icons/svg/undo.svg +0 -1
  148. package/src/icons/svg/uploadbox.svg +0 -1
  149. package/src/icons/svg/user.svg +0 -1
  150. package/src/icons/svg/vf-box.svg +0 -1
  151. package/src/icons/svg/vf-dialog.svg +0 -1
  152. package/src/icons/svg/vf-drawer.svg +0 -1
  153. package/src/icons/svg/vue-sfc.svg +0 -1
  154. package/src/icons/svg/wechat.svg +0 -1
  155. package/src/icons/svg/zip.svg +0 -1
  156. package/src/index.next.js +0 -5
  157. package/src/styles/btn.scss +0 -104
  158. package/src/styles/element-ui.scss +0 -155
  159. package/src/styles/element-variables.scss +0 -31
  160. package/src/styles/global.scss +0 -115
  161. package/src/styles/index.scss +0 -200
  162. package/src/styles/menu-icons.scss +0 -75
  163. package/src/styles/mixin.scss +0 -72
  164. package/src/styles/nprogress.scss +0 -14
  165. package/src/styles/sidebar.scss +0 -281
  166. package/src/styles/split-handle.scss +0 -52
  167. package/src/styles/theme-variables.scss +0 -7
  168. package/src/styles/transition.scss +0 -48
  169. package/src/styles/utilities.scss +0 -137
  170. package/src/styles/variables.scss +0 -44
@@ -0,0 +1,407 @@
1
+ <template>
2
+ <div class="detail-wrap">
3
+ <!--
4
+ 结构约定(即便本 demo 不接流程也照着写,将来加流程不用改结构):
5
+ wfUtil 把「流程信息」面板挂到 .el-form .d-cont、侧边导航挂到 .el-form .slide-wrap,
6
+ 所以 el-form 必须包住 baseTabs。
7
+ :disabled="billDisabled" 一绑,表单里的字段就整片自动禁用(本 demo 无流程,恒为 false)。
8
+ -->
9
+ <el-form ref="editForm" :model="billModel" :disabled="billDisabled">
10
+ <!-- 这层空 el-form 用来隔断 disabled 注入,头部按钮不受单据只读态影响 -->
11
+ <el-form>
12
+ <div class="d-header clearfix">
13
+ <div class="fl">
14
+ <i class="el-icon-info" />
15
+ {{ isAdd ? $t1("新增单据") : $t1("单据") + " " + (billModel.sn || "") }}
16
+ </div>
17
+ <!-- 上一单/下一单、重置、保存:显隐与禁用都由组件统一处理 -->
18
+ <biz-bill-actions :detail="_self">
19
+ <!-- 业务自己的按钮往默认插槽放 -->
20
+ <el-button
21
+ v-if="!isAdd"
22
+ type="primary"
23
+ plain
24
+ class="button-sty"
25
+ icon="el-icon-document-copy"
26
+ :disabled="billSaving"
27
+ @click="copyToNew"
28
+ >{{ $t1("复制新增") }}</el-button
29
+ >
30
+ </biz-bill-actions>
31
+ </div>
32
+ </el-form>
33
+
34
+ <baseTabs>
35
+ <baseTabPane :label="$t1('基本信息')">
36
+ <template #default>
37
+ <table class="table-detail">
38
+ <tbody>
39
+ <tr>
40
+ <th>{{ $t1("单号") }}</th>
41
+ <td>{{ billModel.sn || $t1("保存后自动生成") }}</td>
42
+ <th>{{ $t1("状态") }}</th>
43
+ <td>
44
+ <div :class="statusInfo.cls">{{ statusInfo.label }}</div>
45
+ </td>
46
+ </tr>
47
+ <tr>
48
+ <th><em class="f-red">*</em>{{ $t1("名称") }}</th>
49
+ <td>
50
+ <el-form-item
51
+ prop="name"
52
+ :rules="[
53
+ {
54
+ required: true,
55
+ message: $t1('名称不能为空'),
56
+ trigger: ['blur', 'change'],
57
+ },
58
+ ]"
59
+ >
60
+ <el-input v-model="billModel.name" clearable />
61
+ </el-form-item>
62
+ </td>
63
+ <th>{{ $t1("负责人") }}</th>
64
+ <td>
65
+ <el-form-item prop="owner">
66
+ <el-input v-model="billModel.owner" clearable />
67
+ </el-form-item>
68
+ </td>
69
+ </tr>
70
+ <tr>
71
+ <th>{{ $t1("金额") }}</th>
72
+ <td>
73
+ <el-form-item prop="amount">
74
+ <base-input-number v-model="billModel.amount" />
75
+ </el-form-item>
76
+ </td>
77
+ <th>{{ $t1("备注") }}</th>
78
+ <td>
79
+ <el-form-item prop="remark">
80
+ <el-input v-model="billModel.remark" clearable />
81
+ </el-form-item>
82
+ </td>
83
+ </tr>
84
+ <tr>
85
+ <!-- 表头多选必填:el-form-item 上写 required 校不出来(空数组会被 async-validator
86
+ 放行),靠 validateBillData() 里的 checkBillRequired + type:"multi" -->
87
+ <th><em class="f-red">*</em>{{ $t1("适用区域") }}</th>
88
+ <td>
89
+ <el-form-item prop="areas">
90
+ <el-select v-model="billModel.areas" multiple clearable>
91
+ <el-option
92
+ v-for="item in AREAS"
93
+ :key="item"
94
+ :label="item"
95
+ :value="item"
96
+ />
97
+ </el-select>
98
+ </el-form-item>
99
+ </td>
100
+ <!-- 表头附件必填:同理,靠 type:"attachment" -->
101
+ <th><em class="f-red">*</em>{{ $t1("附件") }}</th>
102
+ <td colspan="3">
103
+ <el-form-item prop="attachments">
104
+ <baseUpload
105
+ accept="file"
106
+ multi="true"
107
+ :file.sync="billModel.attachments"
108
+ ></baseUpload>
109
+ </el-form-item>
110
+ </td>
111
+ </tr>
112
+ </tbody>
113
+ </table>
114
+ </template>
115
+ </baseTabPane>
116
+
117
+ <baseTabPane :label="$t1('明细')">
118
+ <template #default>
119
+ <div class="clearfix screen-btns">
120
+ <div class="fl">
121
+ <vxe-button
122
+ status="primary"
123
+ type="text"
124
+ plain
125
+ class="button-sty"
126
+ icon="el-icon-plus"
127
+ @click="addItem"
128
+ >{{ $t1("增行") }}</vxe-button
129
+ >
130
+ <vxe-button
131
+ status="danger"
132
+ type="text"
133
+ plain
134
+ class="button-sty"
135
+ icon="el-icon-delete"
136
+ @click="removeItems"
137
+ >{{ $t1("删行") }}</vxe-button
138
+ >
139
+ </div>
140
+ </div>
141
+ <vxe-table ref="itemTable" :data="billModel.items" border height="260" size="small">
142
+ <vxe-column type="checkbox" width="48" />
143
+ <!--
144
+ 单元格里的必填要两样东西才看得见:
145
+ ① 表头星号自己加(element 的 * 是 el-form-item 按 label 渲染的,表格里没有 label);
146
+ ② 单元格套 el-form-item + 指向 model 的真实 prop 路径(items.0.qty),
147
+ element 才会加 is-error 让输入框标红 —— 裸着写 el-input 永远不会红。
148
+ 错误文案在窄单元格里放不下,所以 show-message 关掉,具体哪行哪个字段
149
+ 由 validateBillData() 的提示说明;可靠性也在那边(屏外行没有 form-item)。
150
+ -->
151
+ <vxe-column field="itemCode" :title="$t1('物料编码')" width="140">
152
+ <template #header>
153
+ <span><em class="f-red">*</em>{{ $t1("物料编码") }}</span>
154
+ </template>
155
+ <template #default="{ row, rowIndex }">
156
+ <el-form-item
157
+ :prop="`items.${rowIndex}.itemCode`"
158
+ :rules="[{ required: true, trigger: ['blur', 'change'] }]"
159
+ :show-message="false"
160
+ >
161
+ <el-input v-model="row.itemCode" size="small" />
162
+ </el-form-item>
163
+ </template>
164
+ </vxe-column>
165
+ <vxe-column field="itemName" :title="$t1('物料名称')" min-width="160">
166
+ <template #default="{ row }">
167
+ <el-input v-model="row.itemName" size="small" />
168
+ </template>
169
+ </vxe-column>
170
+ <vxe-column field="qty" :title="$t1('数量')" width="120">
171
+ <template #header>
172
+ <span><em class="f-red">*</em>{{ $t1("数量") }}</span>
173
+ </template>
174
+ <template #default="{ row, rowIndex }">
175
+ <el-form-item
176
+ :prop="`items.${rowIndex}.qty`"
177
+ :rules="[{ required: true, trigger: ['blur', 'change'] }]"
178
+ :show-message="false"
179
+ >
180
+ <base-input-number v-model="row.qty" />
181
+ </el-form-item>
182
+ </template>
183
+ </vxe-column>
184
+ <vxe-column field="price" :title="$t1('单价')" width="120">
185
+ <template #default="{ row }">
186
+ <base-input-number v-model="row.price" />
187
+ </template>
188
+ </vxe-column>
189
+ <!-- 行内多选 / 行内附件的必填也一样:行内 form-item 只覆盖已渲染的行,
190
+ 靠 validateBillData() 里的 checkRowsRequired 按 fullData 兜 -->
191
+ <vxe-column field="areas" :title="$t1('适用区域')" width="200">
192
+ <template #header>
193
+ <span><em class="f-red">*</em>{{ $t1("适用区域") }}</span>
194
+ </template>
195
+ <template #default="{ row }">
196
+ <!-- 多选的必填不写 form-item 的 rules:空数组会被 async-validator 放行,
197
+ 写了也不会红也不会拦,一律交给 validateBillData() 的 type:"multi" -->
198
+ <el-select v-model="row.areas" multiple size="small" clearable>
199
+ <el-option
200
+ v-for="item in AREAS"
201
+ :key="item"
202
+ :label="item"
203
+ :value="item"
204
+ />
205
+ </el-select>
206
+ </template>
207
+ </vxe-column>
208
+ <vxe-column field="files" :title="$t1('附件')" min-width="220">
209
+ <template #header>
210
+ <span><em class="f-red">*</em>{{ $t1("附件") }}</span>
211
+ </template>
212
+ <template #default="{ row }">
213
+ <baseUpload accept="file" multi="true" :file.sync="row.files"></baseUpload>
214
+ </template>
215
+ </vxe-column>
216
+ </vxe-table>
217
+ </template>
218
+ </baseTabPane>
219
+ </baseTabs>
220
+ </el-form>
221
+ </div>
222
+ </template>
223
+
224
+ <script>
225
+ /**
226
+ * demo A 详情:不走 $http,所以整个覆盖了 loadBill()。
227
+ *
228
+ * 混入 bizBillDetailMixin 后本文件只关心业务:
229
+ * billOptions / loadBill(或 buildLoadRequest) / applyBillData / saveBill
230
+ * 页签升编辑态、标题同步、翻单、关页签拦截都由 mixin + 宿主完成。
231
+ */
232
+ import { bizBillDetailMixin } from "@base/components/bizTab";
233
+ import BizBillActions from "@base/components/bizTab/BizBillActions.vue";
234
+ import { getOne, save, STATUS_MAP } from "./billApi";
235
+
236
+ // 多选选项(demo 用)
237
+ const AREAS = ["华东", "华南", "华北", "西南"];
238
+
239
+ export default {
240
+ name: "demoBizTabSimpleDetail",
241
+ components: { BizBillActions },
242
+ mixins: [bizBillDetailMixin],
243
+ data() {
244
+ return {
245
+ AREAS,
246
+ billModel: this.getDefaultModel(),
247
+ // 脏数据判定用的初始快照
248
+ billSnapshot: null,
249
+ };
250
+ },
251
+ computed: {
252
+ billOptions() {
253
+ return {
254
+ titleField: "sn",
255
+ wfEnabled: false, // 本 demo 不接流程,流程相关按钮全部不出
256
+ };
257
+ },
258
+ statusInfo() {
259
+ return STATUS_MAP[this.billModel.status] || { label: "-", cls: "" };
260
+ },
261
+ },
262
+ mounted() {
263
+ this.loadBill();
264
+ },
265
+ methods: {
266
+ getDefaultModel() {
267
+ return {
268
+ id: null,
269
+ sn: null,
270
+ name: null,
271
+ status: 0,
272
+ owner: null,
273
+ amount: null,
274
+ remark: null,
275
+ areas: [], // 表头多选
276
+ attachments: [], // 表头附件
277
+ items: [],
278
+ };
279
+ },
280
+
281
+ /**
282
+ * 数据级校验:附件、多选、子表至少一行、行内附件/多选 —— 这几类
283
+ * el-form-item 的 required 都兜不住(空数组被 async-validator 放行;
284
+ * 子表超 20 行开虚拟滚动,屏外行的 form-item 压根不存在)。
285
+ */
286
+ validateBillData() {
287
+ return (
288
+ this.checkBillRequired([
289
+ { field: "areas", label: this.$t1("适用区域"), type: "multi" },
290
+ { field: "attachments", label: this.$t1("附件"), type: "attachment" },
291
+ ])
292
+ || this.checkRowsRequired({
293
+ label: this.$t1("明细"),
294
+ rows: this.$refs.itemTable
295
+ ? this.$refs.itemTable.getTableData().fullData
296
+ : this.billModel.items,
297
+ minRows: 1,
298
+ fields: [
299
+ { field: "itemCode", label: this.$t1("物料编码") },
300
+ { field: "qty", label: this.$t1("数量") },
301
+ { field: "areas", label: this.$t1("适用区域"), type: "multi" },
302
+ { field: "files", label: this.$t1("附件"), type: "attachment" },
303
+ ],
304
+ })
305
+ );
306
+ },
307
+
308
+ // ===== 取数:不走 $http 就整个覆盖 loadBill(默认实现是拿 buildLoadRequest 去 _getBillInfo) =====
309
+ loadBill() {
310
+ if (this.isAdd) {
311
+ this.applyBillData(null);
312
+ return Promise.resolve();
313
+ }
314
+ return getOne(this.dataId).then((objx) => this.applyBillData(objx));
315
+ },
316
+ applyBillData(objx) {
317
+ this.billModel = objx
318
+ ? Object.assign(this.getDefaultModel(), objx)
319
+ : this.getDefaultModel();
320
+ if (!Array.isArray(this.billModel.items)) this.billModel.items = [];
321
+ this.billModel.items.forEach((row) => {
322
+ if (!Array.isArray(row.areas)) row.areas = [];
323
+ if (!Array.isArray(row.files)) row.files = [];
324
+ });
325
+ this.takeSnapshot();
326
+ },
327
+
328
+ // ===== 保存:返回单号(新增返回新单号),取消/失败返回 null =====
329
+ saveBill(options) {
330
+ let opts = options || {};
331
+ let done = () =>
332
+ save(this.billModel).then((res) => {
333
+ // 后台回填的单号/状态等要写回模型,页签标题就是从 titleField 取的
334
+ this.billModel = Object.assign({}, this.billModel, res);
335
+ this.takeSnapshot();
336
+ if (opts.successMsg !== false) {
337
+ this.$message({
338
+ message: this.$t1("保存成功"),
339
+ type: "success",
340
+ duration: 500,
341
+ });
342
+ }
343
+ return res.id;
344
+ });
345
+ if (opts.confirm === false) return done();
346
+ return this.$baseConfirm(this.$t1("您确定要保存吗?")).then(done, () => null);
347
+ },
348
+
349
+ // ===== 关页签拦截:实现了它,宿主关页签时就问它脏没脏 =====
350
+ isBillDirty() {
351
+ return JSON.stringify(this.billModel) !== this.billSnapshot;
352
+ },
353
+ takeSnapshot() {
354
+ this.billSnapshot = JSON.stringify(this.billModel);
355
+ },
356
+
357
+ // ===== 业务按钮 =====
358
+ copyToNew() {
359
+ let host = this.bizTabHost;
360
+ if (!host) return;
361
+ let tabName = host.openAdd();
362
+ if (!tabName) return;
363
+ // 等新页签的详情挂载完,把当前单据内容灌进去(去掉主键与单号)
364
+ this.$nextTick(() => {
365
+ setTimeout(() => {
366
+ let tab = host.getTab(tabName);
367
+ let detail = host.getDetail(tab);
368
+ if (!detail) return;
369
+ let copy = JSON.parse(JSON.stringify(this.billModel));
370
+ copy.id = null;
371
+ copy.sn = null;
372
+ copy.status = 0;
373
+ copy.createDate = null;
374
+ (copy.items || []).forEach((item) => {
375
+ item.id = null;
376
+ });
377
+ detail.applyBillData(copy);
378
+ detail.billModel.id = null;
379
+ detail.billSnapshot = null; // 复制来的内容算「未保存」,关页签时要拦一下
380
+ }, 300);
381
+ });
382
+ },
383
+ addItem() {
384
+ this.billModel.items.push({
385
+ id: null,
386
+ itemCode: null,
387
+ itemName: null,
388
+ qty: null,
389
+ price: null,
390
+ areas: [],
391
+ files: [],
392
+ });
393
+ },
394
+ removeItems() {
395
+ let table = this.$refs.itemTable;
396
+ let records = table ? table.getCheckboxRecords() : [];
397
+ if (!records.length) {
398
+ this.$message.warning(this.$t1("请先勾选要删除的明细"));
399
+ return;
400
+ }
401
+ this.billModel.items = this.billModel.items.filter(
402
+ (row) => records.indexOf(row) < 0
403
+ );
404
+ },
405
+ },
406
+ };
407
+ </script>
@@ -0,0 +1,217 @@
1
+ <template>
2
+ <biz-tab-list-page
3
+ ref="bizTab"
4
+ v-model="activeTab"
5
+ :listLabel="$t1('列表')"
6
+ titleField="sn"
7
+ :addTitle="$t1('新增')"
8
+ :maxTabs="6"
9
+ @refresh-list="loadList"
10
+ @tab-removed="onTabRemoved"
11
+ >
12
+ <!-- ===== 列表页签 ===== -->
13
+ <template #list>
14
+ <div class="clearfix screen-btns">
15
+ <div class="fl">
16
+ <vxe-button
17
+ status="primary"
18
+ type="text"
19
+ plain
20
+ class="button-sty"
21
+ icon="el-icon-plus"
22
+ @click="openAdd"
23
+ >{{ $t1("新增") }}</vxe-button
24
+ >
25
+ <vxe-button
26
+ type="text"
27
+ status="primary"
28
+ plain
29
+ class="button-sty"
30
+ icon="el-icon-question"
31
+ @click="showTip = true"
32
+ >{{ $t1("这个 demo 在演示什么") }}</vxe-button
33
+ >
34
+ </div>
35
+ <div class="fr">
36
+ <vxe-button
37
+ icon="el-icon-brush"
38
+ class="button-sty"
39
+ type="text"
40
+ status="primary"
41
+ plain
42
+ @click="resetEvent"
43
+ >{{ $t1("重置") }}</vxe-button
44
+ >
45
+ <vxe-button
46
+ status="warning"
47
+ icon="el-icon-search"
48
+ class="button-sty"
49
+ @click="loadList"
50
+ >{{ $t1("搜索") }}</vxe-button
51
+ >
52
+ </div>
53
+ </div>
54
+ <vxe-form class="screen-box" title-width="80px" title-align="right" :data="formData">
55
+ <vxe-form-item :title="$t1('单号') + ':'" field="sn">
56
+ <template v-slot>
57
+ <el-input v-model="formData.sn" size="small" clearable />
58
+ </template>
59
+ </vxe-form-item>
60
+ <vxe-form-item :title="$t1('名称') + ':'" field="name">
61
+ <template v-slot>
62
+ <el-input v-model="formData.name" size="small" clearable />
63
+ </template>
64
+ </vxe-form-item>
65
+ <vxe-form-item :title="$t1('状态') + ':'" field="status">
66
+ <template v-slot>
67
+ <el-select v-model="formData.status" size="small" clearable>
68
+ <el-option
69
+ v-for="(item, key) in STATUS_MAP"
70
+ :key="key"
71
+ :label="item.label"
72
+ :value="key"
73
+ />
74
+ </el-select>
75
+ </template>
76
+ </vxe-form-item>
77
+ </vxe-form>
78
+ <vxe-table
79
+ ref="table"
80
+ :data="rows"
81
+ :loading="listLoading"
82
+ border
83
+ height="auto"
84
+ size="small"
85
+ >
86
+ <vxe-column field="sn" :title="$t1('单号')" width="140">
87
+ <template #default="{ row, rowIndex }">
88
+ <a href="javascript:void(0);" class="a-link" @click="openDetail(row, rowIndex)">
89
+ {{ row.sn }}
90
+ </a>
91
+ </template>
92
+ </vxe-column>
93
+ <vxe-column field="name" :title="$t1('名称')" min-width="180" />
94
+ <vxe-column field="status" :title="$t1('状态')" width="100">
95
+ <template #default="{ row }">
96
+ <div :class="getStatus(row).cls">{{ getStatus(row).label }}</div>
97
+ </template>
98
+ </vxe-column>
99
+ <vxe-column field="owner" :title="$t1('负责人')" width="100" />
100
+ <vxe-column field="amount" :title="$t1('金额')" width="100" align="right" />
101
+ <vxe-column field="remark" :title="$t1('备注')" min-width="160" />
102
+ <vxe-column field="createDate" :title="$t1('创建时间')" width="160" />
103
+ <vxe-column :title="''" width="47" fixed="right">
104
+ <template #default="{ row, rowIndex }">
105
+ <a href="javascript:void(0);" class="a-link" @click="openDetail(row, rowIndex)">
106
+ <el-tooltip
107
+ :enterable="false"
108
+ effect="dark"
109
+ :content="$t1('查看')"
110
+ placement="top"
111
+ popper-class="tooltip-skin"
112
+ ><i class="el-icon-view"
113
+ /></el-tooltip>
114
+ </a>
115
+ </template>
116
+ </vxe-column>
117
+ </vxe-table>
118
+ </template>
119
+
120
+ <!-- ===== 详情页签(新增/查看共用同一个组件) ===== -->
121
+ <template #detail="{ tab }">
122
+ <bill-detail :tab="tab" />
123
+ </template>
124
+
125
+ <!-- ===== 页签之外:各类弹框挂这里(默认插槽) ===== -->
126
+ <el-dialog
127
+ :title="$t1('这个 demo 在演示什么')"
128
+ :visible.sync="showTip"
129
+ width="520px"
130
+ custom-class="dialog-style"
131
+ append-to-body
132
+ >
133
+ <ul class="demo-tip">
134
+ <li>{{ $t1("新增、查看详情都开独立页签;同一单据不重复开,新增可连开多个") }}</li>
135
+ <li>{{ $t1("新增页保存后就地升为编辑态(页签标题换成单号),不重新取数、不闪") }}</li>
136
+ <li>{{ $t1("详情里改了内容没保存,关页签会拦一下(isBillDirty 钩子)") }}</li>
137
+ <li>{{ $t1("详情头部「上一单/下一单」按列表当时的顺序翻单") }}</li>
138
+ <li>{{ $t1("关掉页签或手动切回列表,列表都会自动刷新") }}</li>
139
+ <li>{{ $t1("本 demo 不接流程、不打后台,数据在 billApi.js 里") }}</li>
140
+ </ul>
141
+ </el-dialog>
142
+ </biz-tab-list-page>
143
+ </template>
144
+
145
+ <script>
146
+ /**
147
+ * demo A:手写接口版(前端 mock,无流程)。
148
+ *
149
+ * 不依赖任何后台前置条件,clone 下来就能跑,可以当手写单据页面的模板抄。
150
+ * 数据源集中在 ./billApi.js,换真实接口只改那一个文件。
151
+ * 要看「保存并提交流程 / 通过自动保存 / 提交后关页签」,看 demo B:../bill/list.vue
152
+ *
153
+ * 菜单/路由:@base/views/demo/bizTab/simple/list(本地已在 router/modules/customer.js 注册)
154
+ */
155
+ import BizTabListPage from "@base/components/bizTab/BizTabListPage.vue";
156
+ import billDetail from "./detail.vue";
157
+ import { getPage, STATUS_MAP } from "./billApi";
158
+
159
+ export default {
160
+ name: "demo_bizTab_simple:list",
161
+ components: { BizTabListPage, billDetail },
162
+ data() {
163
+ return {
164
+ STATUS_MAP,
165
+ activeTab: "second",
166
+ formData: {},
167
+ rows: [],
168
+ listLoading: false,
169
+ showTip: false,
170
+ };
171
+ },
172
+ mounted() {
173
+ this.loadList();
174
+ },
175
+ methods: {
176
+ loadList() {
177
+ this.listLoading = true;
178
+ return getPage(this.formData)
179
+ .then((rows) => {
180
+ this.rows = rows || [];
181
+ })
182
+ .finally(() => {
183
+ this.listLoading = false;
184
+ });
185
+ },
186
+ resetEvent() {
187
+ this.formData = {};
188
+ this.loadList();
189
+ },
190
+ getStatus(row) {
191
+ return STATUS_MAP[row.status] || { label: "", cls: "" };
192
+ },
193
+ // 新增页签也传快照:保存成功后新单会被插到序列首位,翻单按钮随即可用
194
+ openAdd() {
195
+ this.$refs.bizTab.openAdd(null, { navRows: this.rows });
196
+ },
197
+ // 把当前列表顺序和行下标一起传进去,详情头部就有「上一单/下一单」
198
+ openDetail(row, rowIndex) {
199
+ this.$refs.bizTab.openDetail(row, null, {
200
+ navRows: this.rows,
201
+ navIndex: rowIndex,
202
+ });
203
+ },
204
+ onTabRemoved(tabName) {
205
+ // 列表刷新由组件在「回到列表页签」时统一收口,这里只是演示事件可用
206
+ console.log("[demo] 页签已关闭:", tabName);
207
+ },
208
+ },
209
+ };
210
+ </script>
211
+
212
+ <style scoped>
213
+ .demo-tip {
214
+ padding-left: 18px;
215
+ line-height: 26px;
216
+ }
217
+ </style>