index.vue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767
  1. <template>
  2. <div class="box">
  3. <div class="tree">
  4. <treeList title="产品分类" submitType="1" :data="treeListData" v-model="sourceList.pagination.productClassifyId" @change="treeChange"
  5. @changeTreeList="getTreeList">
  6. </treeList>
  7. </div>
  8. <div class="content">
  9. <byTable :source="sourceList.data" :pagination="sourceList.pagination" :config="config" :loading="loading" highlight-current-row
  10. :selectConfig="selectConfig" :action-list="[
  11. {
  12. text: 'Excel导入',
  13. action: () => openExcel(),
  14. disabled: false,
  15. },
  16. {
  17. text: '添加',
  18. action: () => openModal('add'),
  19. disabled: false,
  20. },]" @get-list="getList">
  21. <template #name="{ item }">
  22. <div>
  23. <span class="el-click">{{ item.name }}</span>
  24. </div>
  25. </template>
  26. <template #pic="{ item }">
  27. <div v-if="item.fileList.length > 0">
  28. <img :src="item.fileList[0].fileUrl" class="pic" @click="handleClickFile(item.fileList[0])" />
  29. </div>
  30. <div v-else></div>
  31. </template>
  32. <template #size="{ item }">
  33. <div v-if="item['length'] && item.width && item.height">
  34. <span>{{ item['length'] }}cm</span>*
  35. <span>{{ item.width }}cm</span>*
  36. <span>{{ item.height }}cm</span>
  37. </div>
  38. <div v-else></div>
  39. </template>
  40. <template #price="{ item }">
  41. <div v-if="item.price">
  42. <span>{{ item.currency }} {{ moneyFormat(item.price ,2)}}</span>
  43. </div>
  44. <div v-else></div>
  45. </template>
  46. </byTable>
  47. </div>
  48. <el-dialog :title="modalType == 'add' ? '添加产品' : '编辑产品'" v-model="dialogVisible" width="80%" destroy-on-close>
  49. <div class="public_height_dialog">
  50. <byForm :formConfig="formConfig" :formOption="formOption" v-model="formData.data" :rules="rules" ref="formDom" v-loading="submitLoading">
  51. <template #nameEnglish>
  52. <div style="width: 100%">
  53. <el-input v-model="formData.data.nameEnglish" placeholder="请输入" onkeyup="value=value.replace(/[^\x00-\xff]/g, '')"></el-input>
  54. </div>
  55. </template>
  56. <template #productionFile>
  57. <div style="width: 100%">
  58. <span style="color:#409eff;cursor:pointer" @click="handleClickUpload">点击上传</span>
  59. </div>
  60. </template>
  61. <template #productionFileOne>
  62. <div style="width: 100%">
  63. <span style="color:#409eff;cursor:pointer" @click="handleClickUpload">点击上传</span>
  64. </div>
  65. </template>
  66. <template #size>
  67. <div style="width: 100%">
  68. <el-row>
  69. <el-col :span="8">
  70. <el-form-item prop="length" label-width="0px" class="margin-b-0 wid100">
  71. <el-input-number v-model="formData.data['length']" placeholder="长 (cm)" style="width: 100%" :precision="2" :controls="false"
  72. :min="0" onmousewheel="return false;" />
  73. </el-form-item>
  74. </el-col>
  75. <el-col :span="8">
  76. <el-form-item prop="width" label-width="0px" class="margin-b-0 wid100">
  77. <el-input-number v-model="formData.data.width" placeholder="宽 (cm)" style="width: 100%" :precision="2" :controls="false" :min="0"
  78. onmousewheel="return false;" />
  79. </el-form-item>
  80. </el-col>
  81. <el-col :span="8">
  82. <el-form-item prop="height" label-width="0px" class="margin-b-0 wid100">
  83. <el-input-number v-model="formData.data.height" placeholder="高 (cm)" style="width: 100%" :precision="2" :controls="false" :min="0"
  84. onmousewheel="return false;" />
  85. </el-form-item>
  86. </el-col>
  87. </el-row>
  88. </div>
  89. </template>
  90. </byForm>
  91. </div>
  92. <template #footer>
  93. <el-button @click="dialogVisible = false" size="defualt" v-debounce>取 消</el-button>
  94. <el-button type="primary" @click="submitForm()" size="defualt" v-debounce>确 定</el-button>
  95. </template>
  96. </el-dialog>
  97. <el-dialog :title="'BOM 配置'" v-model="bomDialog" width="60%" destroy-on-close>
  98. <!-- <div class="public_height_dialog"> -->
  99. <byForm :formConfig="bomFormConfig" :formOption="bomFormOption" v-model="formData.bomData" :rules="bomRules" ref="bomFormDom"
  100. v-loading="submitLoading">
  101. <template #accessories>
  102. <div style="width: 100%">
  103. <el-button type="primary" @click="openSelectMaterial = true" plain>选择</el-button>
  104. <el-table :data="formData.bomData.data" style="width: 100%; margin-top: 16px">
  105. <el-table-column prop="productCnName" label="物料名称" min-width="130" />
  106. <el-table-column prop="productCnName" label="物料编码" width="130" />
  107. <el-table-column label="数量" width="130">
  108. <template #default="{ row, $index }">
  109. <div style="width: 100%">
  110. <el-form-item :prop="'contractProductList.' + $index + '.quantity'" :rules="rules.quantity" :inline-message="true"
  111. class="margin-b-0 wid100">
  112. <el-input-number onmousewheel="return false;" v-model="row.quantity" placeholder="请输入" style="width: 100%" :precision="0"
  113. :controls="false" :min="1" />
  114. </el-form-item>
  115. </div>
  116. </template>
  117. </el-table-column>
  118. <el-table-column label="操作" width="60" align="center" fixed="right">
  119. <template #default="{ $index }">
  120. <el-button type="primary" link @click="handleRemove($index)">删除</el-button>
  121. </template>
  122. </el-table-column>
  123. </el-table>
  124. </div>
  125. </template>
  126. </byForm>
  127. <!-- </div> -->
  128. <template #footer>
  129. <el-button @click="bomDialog = false" size="defualt" v-debounce>取 消</el-button>
  130. <el-button type="primary" @click="submitBomForm()" size="defualt" v-debounce>确 定</el-button>
  131. </template>
  132. </el-dialog>
  133. <el-dialog :title="'物料选择'" v-model="openSelectMaterial" width="80%" destroy-on-close>
  134. <SelectMaterial @selectMaterial="selectMaterial"></SelectMaterial>
  135. <template #footer>
  136. <el-button @click="bomDialog = false" size="defualt" v-debounce>取 消</el-button>
  137. </template>
  138. </el-dialog>
  139. <el-dialog title="导入产品" v-model="openExcelDialog" width="400">
  140. <div v-loading="excelLoading">
  141. <el-upload :action="actionUrl + '/productInfo/excelImportByEhsd'" :headers="headers" :on-success="handleSuccess" :on-progress="handleProgress"
  142. :show-file-list="false" :on-error="handleError" accept=".xlsx">
  143. <el-button type="primary">点击导入</el-button>
  144. </el-upload>
  145. </div>
  146. <template #footer>
  147. <el-button @click="openExcelDialog = false" size="large">取 消</el-button>
  148. </template>
  149. </el-dialog>
  150. </div>
  151. </template>
  152. <script setup>
  153. import byTable from "@/components/byTable/index";
  154. import byForm from "@/components/byForm/index";
  155. import treeList from "@/components/product/treeList";
  156. import SelectMaterial from "@/components/product/SelectMaterial.vue";
  157. import { getToken } from "@/utils/auth";
  158. const { proxy } = getCurrentInstance();
  159. const actionUrl = import.meta.env.VITE_APP_BASE_API;
  160. const loading = ref(false);
  161. const submitLoading = ref(false);
  162. const treeData = ref([]);
  163. const treeListData = ref([]);
  164. const currencyData = computed(
  165. () => proxy.useUserStore().allDict["account_currency"]
  166. );
  167. const headers = ref({ Authorization: "Bearer " + getToken() });
  168. const sourceList = ref({
  169. data: [],
  170. pagination: {
  171. total: 3,
  172. pageNum: 1,
  173. pageSize: 10,
  174. type: "",
  175. productClassifyId: "",
  176. keyword: "",
  177. definition: "1",
  178. },
  179. });
  180. const dialogVisible = ref(false);
  181. const openExcelDialog = ref(false);
  182. const excelLoading = ref(false);
  183. const modalType = ref("add");
  184. const rules = ref({
  185. productClassifyId: [
  186. { required: true, message: "请选择产品分类", trigger: "change" },
  187. ],
  188. name: [{ required: true, message: "请输入产品名称", trigger: "blur" }],
  189. customCode: [{ required: true, message: "请输入产品编号", trigger: "blur" }],
  190. length: [{ required: true, message: "请输入长 (cm)", trigger: "blur" }],
  191. width: [{ required: true, message: "请输入宽 (cm)", trigger: "blur" }],
  192. height: [{ required: true, message: "请输入高 (cm)", trigger: "blur" }],
  193. });
  194. const props = defineProps({
  195. selectStatus: Boolean,
  196. });
  197. const selectConfig = computed(() => []);
  198. const config = computed(() => {
  199. return [
  200. {
  201. attrs: {
  202. label: "图片",
  203. slot: "pic",
  204. align: "center",
  205. width: 80,
  206. },
  207. },
  208. {
  209. attrs: {
  210. label: "产品分类",
  211. prop: "classifyName",
  212. "min-width": 150,
  213. },
  214. },
  215. {
  216. attrs: {
  217. label: "产品编码",
  218. prop: "customCode",
  219. width: 120,
  220. },
  221. },
  222. {
  223. attrs: {
  224. label: "产品名称",
  225. slot: "name",
  226. "min-width": 150,
  227. },
  228. },
  229. {
  230. attrs: {
  231. label: "产品英文名",
  232. prop: "nameEnglish",
  233. "min-width": 120,
  234. },
  235. },
  236. {
  237. attrs: {
  238. label: "产品规格",
  239. prop: "spec",
  240. width: 120,
  241. },
  242. },
  243. {
  244. attrs: {
  245. label: "尺寸",
  246. slot: "size",
  247. width: 150,
  248. },
  249. },
  250. {
  251. attrs: {
  252. label: "净重",
  253. prop: "netWeight",
  254. width: 100,
  255. },
  256. render(val) {
  257. if (val) {
  258. return val + " kg";
  259. }
  260. },
  261. },
  262. {
  263. attrs: {
  264. label: "销售价",
  265. slot: "price",
  266. width: 100,
  267. },
  268. },
  269. {
  270. attrs: {
  271. label: "海关编码",
  272. prop: "hsCode",
  273. width: 100,
  274. },
  275. },
  276. {
  277. attrs: {
  278. label: "原材料",
  279. prop: "hsCode",
  280. width: 100,
  281. },
  282. },
  283. {
  284. attrs: {
  285. label: "原材料编码",
  286. prop: "hsCode",
  287. width: 100,
  288. },
  289. },
  290. {
  291. attrs: {
  292. label: "操作",
  293. width: "160",
  294. align: "center",
  295. fixed: "right",
  296. },
  297. renderHTML(row) {
  298. return [
  299. // {
  300. // attrs: {
  301. // label: "选择",
  302. // type: "primary",
  303. // text: true,
  304. // },
  305. // el: "button",
  306. // click() {
  307. // clickSelect(row);
  308. // },
  309. // }
  310. {
  311. attrs: {
  312. label: "BOM",
  313. type: "primary",
  314. text: true,
  315. },
  316. el: "button",
  317. click() {
  318. bomSetting(row);
  319. },
  320. },
  321. {
  322. attrs: {
  323. label: "修改",
  324. type: "primary",
  325. text: true,
  326. },
  327. el: "button",
  328. click() {
  329. getDtl(row);
  330. },
  331. },
  332. {
  333. attrs: {
  334. label: "删除",
  335. type: "danger",
  336. text: true,
  337. },
  338. el: "button",
  339. click() {
  340. proxy
  341. .msgConfirm()
  342. .then((res) => {
  343. proxy
  344. .post("/productInfo/delete", {
  345. id: row.id,
  346. })
  347. .then((res) => {
  348. proxy.msgTip("删除成功", 1);
  349. getList();
  350. });
  351. })
  352. .catch((err) => {});
  353. },
  354. },
  355. ];
  356. },
  357. },
  358. ];
  359. });
  360. const formData = reactive({
  361. data: {},
  362. bomData: {},
  363. });
  364. const formOption = reactive({
  365. disabled: false,
  366. inline: true,
  367. labelWidth: 100,
  368. itemWidth: 100,
  369. });
  370. const formDom = ref(null);
  371. const formConfig = computed(() => {
  372. return [
  373. {
  374. type: "title1",
  375. title: "基本信息",
  376. },
  377. {
  378. type: "treeSelect",
  379. prop: "productClassifyId",
  380. label: "产品分类",
  381. data: treeData.value,
  382. itemWidth: 100,
  383. disabled: false,
  384. },
  385. {
  386. type: "input",
  387. prop: "name",
  388. label: "产品名称",
  389. itemWidth: 50,
  390. disabled: false,
  391. },
  392. {
  393. type: "slot",
  394. slotName: "nameEnglish",
  395. label: "英文名",
  396. itemWidth: 50,
  397. },
  398. {
  399. type: "input",
  400. prop: "customCode",
  401. label: "产品编号",
  402. itemWidth: 50,
  403. disabled: false,
  404. },
  405. {
  406. type: "uploadImg",
  407. // listType: "picture-card",
  408. // limit: 1,
  409. // accept: ".gif, .jpeg, .jpg, .png",
  410. prop: "fileList",
  411. imgProp: "imageUrl",
  412. label: "产品缩略图",
  413. },
  414. {
  415. type: "slot",
  416. slotName: "productionFileOne",
  417. label: "产品原图",
  418. itemWidth: 100,
  419. },
  420. {
  421. type: "slot",
  422. slotName: "productionFile",
  423. label: "生产文件",
  424. itemWidth: 100,
  425. },
  426. {
  427. type: "title1",
  428. title: "属性信息",
  429. },
  430. {
  431. type: "selectInput",
  432. prop: "price",
  433. selectProp: "currency",
  434. label: "销售价",
  435. itemWidth: 50,
  436. disabledSelect: true,
  437. data: currencyData.value,
  438. },
  439. {
  440. type: "select",
  441. prop: "technologyId",
  442. label: "生产工艺",
  443. itemWidth: 50,
  444. data: [],
  445. filterable: true,
  446. disabled: false,
  447. },
  448. {
  449. type: "input",
  450. prop: "spec",
  451. label: "规格型号",
  452. itemWidth: 50,
  453. disabled: false,
  454. },
  455. {
  456. type: "slot",
  457. slotName: "size",
  458. prop: "size",
  459. label: "尺寸",
  460. itemWidth: 50,
  461. disabled: false,
  462. },
  463. // {
  464. // type: "select",
  465. // prop: "innerPackMethod",
  466. // label: "内包装方式",
  467. // required: true,
  468. // itemWidth: 50,
  469. // multiple: true,
  470. // data: innerMethon.value,
  471. // filterable: true,
  472. // placeholder: "内包装方式",
  473. // style: {
  474. // width: "100%",
  475. // },
  476. // disabled: false,
  477. // },
  478. // {
  479. // type: "select",
  480. // prop: "outerPackMethod",
  481. // label: "外包装方式",
  482. // required: true,
  483. // itemWidth: 50,
  484. // multiple: true,
  485. // data: outsideMethon.value,
  486. // filterable: true,
  487. // placeholder: "外包装方式",
  488. // style: {
  489. // width: "100%",
  490. // },
  491. // disabled: false,
  492. // },
  493. {
  494. type: "number",
  495. prop: "netWeight",
  496. label: "净重(kg)",
  497. precision: 2,
  498. min: 0,
  499. controls: false,
  500. itemWidth: 50,
  501. },
  502. {
  503. type: "input",
  504. prop: "hsCode",
  505. label: "海关编码",
  506. itemWidth: 50,
  507. disabled: false,
  508. },
  509. {
  510. type: "input",
  511. itemType: "textarea",
  512. prop: "remark",
  513. label: "备注",
  514. itemWidth: 100,
  515. },
  516. ];
  517. });
  518. const getList = (req) => {
  519. sourceList.value.pagination = { ...sourceList.value.pagination, ...req };
  520. loading.value = true;
  521. proxy.post("/productInfo/page", sourceList.value.pagination).then(
  522. (res) => {
  523. sourceList.value.data = res.rows.map((x) => ({
  524. ...x,
  525. fileList: [],
  526. }));
  527. sourceList.value.pagination.total = res.total;
  528. setTimeout(() => {
  529. loading.value = false;
  530. }, 200);
  531. let productIdList = res.rows.map((x) => x.id);
  532. // 请求文件数据并回显
  533. if (productIdList.length > 0) {
  534. proxy
  535. .post("/fileInfo/getList", {
  536. businessIdList: productIdList,
  537. })
  538. .then((fileObj) => {
  539. for (let i = 0; i < sourceList.value.data.length; i++) {
  540. let e = sourceList.value.data[i];
  541. for (let key in fileObj) {
  542. if (e.id == key) {
  543. e.fileList = fileObj[key];
  544. }
  545. }
  546. }
  547. });
  548. }
  549. },
  550. (err) => {
  551. loading.value = false;
  552. }
  553. );
  554. };
  555. const treeChange = (e) => {
  556. if (e.id != undefined) {
  557. sourceList.value.pagination.productClassifyId = e.id;
  558. getList({ productClassifyId: e.id });
  559. }
  560. };
  561. const openModal = () => {
  562. dialogVisible.value = true;
  563. modalType.value = "add";
  564. formData.data = {
  565. definition: "1",
  566. fileList: [],
  567. currency: "",
  568. };
  569. if (currencyData.value && currencyData.value.length > 0) {
  570. formData.data.currency = currencyData.value[0].dictKey;
  571. }
  572. };
  573. const openExcel = () => {
  574. openExcelDialog.value = true;
  575. };
  576. const submitForm = () => {
  577. formDom.value.handleSubmit((valid) => {
  578. if (!formData.data.fileList.length > 0) {
  579. return proxy.msgTip("请上传图片", 2);
  580. }
  581. // formData.data.fileList = formData.data.fileList.map((x) => ({
  582. // id: x.id,
  583. // fileName: x.fileName,
  584. // fileUrl: x.fileUrl,
  585. // }));
  586. submitLoading.value = true;
  587. proxy.post("/productInfo/" + modalType.value, formData.data).then(
  588. (res) => {
  589. proxy.msgTip("操作成功", 1);
  590. dialogVisible.value = false;
  591. submitLoading.value = false;
  592. getList();
  593. },
  594. (err) => {
  595. submitLoading.value = false;
  596. }
  597. );
  598. });
  599. };
  600. const getTreeList = () => {
  601. proxy
  602. .post("/productClassify/tree", { parentId: "", name: "", definition: "1" })
  603. .then((message) => {
  604. treeListData.value = [
  605. {
  606. id: "",
  607. label: "全部",
  608. parentId: "",
  609. children: message,
  610. },
  611. ];
  612. treeData.value = message;
  613. });
  614. };
  615. const getDtl = (row) => {
  616. modalType.value = "edit";
  617. proxy.post("/productInfo/detail", { id: row.id }).then((res) => {
  618. formData.data = res;
  619. formData.data.fileList = row.fileList.map((x) => ({
  620. ...x,
  621. url: x.fileUrl,
  622. name: x.fileName,
  623. }));
  624. if (formData.data.fileList.length > 0) {
  625. formData.data.imageUrl = formData.data.fileList[0].fileUrl;
  626. }
  627. dialogVisible.value = true;
  628. });
  629. };
  630. const bomDialog = ref(false);
  631. const bomFormDom = ref(null);
  632. const bomFormOption = reactive({
  633. disabled: false,
  634. inline: true,
  635. labelWidth: 100,
  636. itemWidth: 100,
  637. });
  638. const bomFormConfig = computed(() => {
  639. return [
  640. {
  641. type: "select",
  642. prop: "technologyId",
  643. label: "原材料",
  644. itemWidth: 100,
  645. data: [],
  646. filterable: true,
  647. disabled: false,
  648. },
  649. {
  650. type: "slot",
  651. slotName: "accessories",
  652. label: "包材辅料",
  653. itemWidth: 100,
  654. },
  655. ];
  656. });
  657. const bomRules = ref({
  658. technologyId: [
  659. { required: true, message: "请选择原材料", trigger: "change" },
  660. ],
  661. });
  662. const bomSetting = () => {
  663. formData.bomData = {
  664. data: [],
  665. };
  666. bomDialog.value = true;
  667. };
  668. const submitBomForm = () => {
  669. bomFormDom.value.handleSubmit((valid) => {
  670. if (!formData.bomData.data.length > 0) {
  671. return proxy.msgTip("请选择包材辅料", 2);
  672. }
  673. submitLoading.value = true;
  674. proxy.post("/productInfo/", formData.bomData).then(
  675. (res) => {
  676. proxy.msgTip("操作成功", 1);
  677. bomDialog.value = false;
  678. submitLoading.value = false;
  679. },
  680. (err) => {
  681. submitLoading.value = false;
  682. }
  683. );
  684. });
  685. };
  686. const openSelectMaterial = ref(false);
  687. const selectMaterial = (row) => {
  688. formData.bomData.data.push({
  689. ...row,
  690. });
  691. proxy.msgTip("选择成功");
  692. };
  693. const handleRemove = (index) => {
  694. formData.bomData.data.splice(index, 1);
  695. };
  696. const handleClickFile = (file) => {
  697. window.open(file.fileUrl, "_blank");
  698. };
  699. const handleProgress = () => {
  700. excelLoading.value = true;
  701. };
  702. const handleError = (err) => {
  703. proxy.msgTip(`${err},请重试`, 2);
  704. openExcelDialog.value = false;
  705. excelLoading.value = false;
  706. };
  707. const handleSuccess = (res) => {
  708. if (res.code != 200) {
  709. return proxy.msgTip(`${err},请重试`, 2);
  710. } else {
  711. proxy.msgTip(`导入成功`, 1);
  712. openExcelDialog.value = false;
  713. excelLoading.value = false;
  714. getList();
  715. }
  716. };
  717. const clickSelect = (item) => {
  718. proxy.$emit("selectProduct", item);
  719. };
  720. const handleClickUpload = () => {
  721. let a = document.createElement("a");
  722. a.href = "printer://" + "ftp://192.168.1.13/123/";
  723. a.style.display = "none";
  724. document.body.appendChild(a);
  725. a.click();
  726. document.body.removeChild(a);
  727. };
  728. getTreeList();
  729. getList();
  730. </script>
  731. <style lang="scss" scoped>
  732. .box {
  733. padding: 20px;
  734. display: flex;
  735. justify-content: space-between;
  736. .tree {
  737. width: 300px;
  738. }
  739. .content {
  740. width: calc(100% - 320px);
  741. }
  742. }
  743. .pic {
  744. object-fit: contain;
  745. width: 50px;
  746. height: 50px;
  747. cursor: pointer;
  748. vertical-align: middle;
  749. }
  750. </style>