index.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677
  1. <script setup lang="tsx">
  2. import { computed, ref, useTemplateRef } from 'vue';
  3. import { NButton, NImage, NInputNumber, NSelect } from 'naive-ui';
  4. // import dayjs from 'dayjs';
  5. // import { fetchGetStoreList } from '@/service/api/xsb-manage/store-info';
  6. // import {
  7. // fetchGetAllChannelList
  8. // fetchImportGoods,
  9. // fetchSetUpChannels
  10. // } from '@/service/api/goods/store-goods';
  11. // import { fetchGetDictDataList } from '@/service/api/system-manage';
  12. import {
  13. // fetchGetAllChannelList,
  14. fetchChangeStatus,
  15. fetchFindThirdPartyBalance,
  16. fetchGetChannelList,
  17. fetchImportGoods,
  18. fetchProductList,
  19. fetchSetUpChannels,
  20. fetchUpdateName,
  21. fetchUpdateProductImg
  22. } from '@/service/api/goods-center/virtual-goods';
  23. import { areAllItemsAllFieldsFilled } from '@/utils/zt';
  24. import { commonExport } from '@/utils/common';
  25. import { useTable } from '@/components/zt/Table/hooks/useTable';
  26. import SvgIcon from '@/components/custom/svg-icon.vue';
  27. import { useModal } from '@/components/zt/Modal/hooks/useModal';
  28. import { useModalFrom } from '@/components/zt/ModalForm/hooks/useModalForm';
  29. type Price = { channelId: number | undefined; channelProdPrice: number; id?: number; channelName?: string };
  30. const importTemplateRef = useTemplateRef('importTemplateRef');
  31. const Balance = ref<number>(0);
  32. const options = ref<Api.goods.Channel[]>([]);
  33. // const TypeName = ['企业用户', 'B端用户', 'C端用户'];
  34. const statusList = ['上架', '下架'];
  35. const columns: NaiveUI.TableColumn<Api.goods.ShopSku>[] = [
  36. {
  37. type: 'selection',
  38. align: 'center',
  39. width: 48,
  40. fixed: 'left'
  41. },
  42. {
  43. key: 'productNumber',
  44. title: '商品ID',
  45. align: 'left',
  46. width: 200
  47. },
  48. {
  49. key: 'productImg',
  50. title: '商品图片',
  51. align: 'center',
  52. width: 120,
  53. render: (row: any) => {
  54. return <NImage src={row.productImg} width={60} height={60}></NImage>;
  55. }
  56. },
  57. {
  58. key: 'productName',
  59. title: '商品名称',
  60. align: 'center',
  61. width: 120,
  62. ellipsis: {
  63. tooltip: true
  64. }
  65. },
  66. {
  67. key: 'localProductName',
  68. title: '本地商品名称',
  69. align: 'center',
  70. width: 120,
  71. ellipsis: {
  72. tooltip: true
  73. }
  74. },
  75. {
  76. key: 'productType',
  77. title: '商品类型',
  78. align: 'center',
  79. width: 120,
  80. ellipsis: {
  81. tooltip: true
  82. }
  83. },
  84. {
  85. key: 'businessType',
  86. title: '业务类型',
  87. align: 'center',
  88. width: 120,
  89. ellipsis: {
  90. tooltip: true
  91. }
  92. },
  93. // {
  94. // key: 'shopName',
  95. // title: '商户',
  96. // align: 'center',
  97. // width: 120,
  98. // ellipsis: {
  99. // tooltip: true
  100. // },
  101. // render: (row: any) => {
  102. // return '-';
  103. // }
  104. // },
  105. {
  106. key: 'faceValue',
  107. title: '面值(元)',
  108. align: 'center',
  109. width: 120,
  110. ellipsis: {
  111. tooltip: true
  112. }
  113. },
  114. {
  115. key: 'purchasePrice',
  116. title: '采购价',
  117. align: 'center',
  118. width: 120,
  119. ellipsis: {
  120. tooltip: true
  121. }
  122. },
  123. {
  124. key: 'pmsVideoChannelPrices',
  125. title: '价格',
  126. align: 'center',
  127. width: 120,
  128. render: (row: any) => {
  129. return (
  130. <div>
  131. {row.pmsVideoChannelPrices.map((it: Api.government.ChannelVO) => {
  132. return (
  133. <div>
  134. {it.channelName}:¥{it.price}
  135. </div>
  136. );
  137. })}
  138. </div>
  139. );
  140. }
  141. },
  142. {
  143. key: 'inventory',
  144. title: '库存',
  145. align: 'center',
  146. width: 120,
  147. ellipsis: {
  148. tooltip: true
  149. }
  150. },
  151. {
  152. key: 'productStatus',
  153. title: '状态',
  154. align: 'center',
  155. width: 120,
  156. render: (row: any) => {
  157. return (
  158. <div class="flex items-center justify-center">
  159. <n-badge color={row.productStatus == 0 ? 'green' : 'red'} value={row.productStatus} dot />
  160. <span class="ml-2">{statusList[row.productStatus]}</span>
  161. </div>
  162. );
  163. }
  164. },
  165. {
  166. key: 'productStatusMsg',
  167. title: '变更原因',
  168. align: 'center',
  169. width: 120,
  170. ellipsis: {
  171. tooltip: true
  172. },
  173. render: (row: any) => {
  174. return row.productStatus == 1 ? row.productStatusMsg : '';
  175. }
  176. },
  177. {
  178. key: 'updateTime',
  179. title: '更新时间',
  180. align: 'center',
  181. width: 120,
  182. ellipsis: {
  183. tooltip: true
  184. }
  185. }
  186. ];
  187. const PriceColumns: NaiveUI.TableColumn<Price>[] = [
  188. {
  189. title: '销售渠道',
  190. key: 'channelId',
  191. align: 'center',
  192. width: 250,
  193. render: row => {
  194. return (
  195. <NSelect
  196. options={options.value}
  197. labelField="channelName"
  198. valueField="id"
  199. value={row.channelId}
  200. clearable
  201. onUpdate:value={value => {
  202. options.value.map(it => {
  203. if (it.id == row.channelId) {
  204. it.disabled = false;
  205. }
  206. return it;
  207. });
  208. row.channelId = value ? Number(value) : undefined;
  209. row.channelName = options.value.find(it => it.id == row.channelId)?.channelName;
  210. }}
  211. onUpdate:show={value => {
  212. console.log('show', value);
  213. options.value.map(it => {
  214. if (it.id == row.channelId) {
  215. if (value) {
  216. it.disabled = false;
  217. }
  218. if (!value) {
  219. it.disabled = true;
  220. }
  221. }
  222. return it;
  223. });
  224. }}
  225. ></NSelect>
  226. );
  227. }
  228. },
  229. {
  230. title: '售价(元)',
  231. key: 'channelProdPrice',
  232. align: 'center',
  233. width: 250,
  234. render: row => {
  235. return (
  236. <NInputNumber
  237. value={row.channelProdPrice}
  238. precision={2}
  239. onUpdate:value={value => {
  240. row.channelProdPrice = Number(value);
  241. }}
  242. min={0}
  243. />
  244. );
  245. }
  246. },
  247. {
  248. title: () => {
  249. return (
  250. <div onClick={() => handleAddPrice()} class={'w-full flex items-center justify-center'}>
  251. <SvgIcon
  252. icon={'proicons:add-square'}
  253. class={'cursor-pointer text-24px'}
  254. style={'color:var(--n-color)'}
  255. ></SvgIcon>
  256. </div>
  257. );
  258. },
  259. key: 'action',
  260. width: 80,
  261. align: 'center',
  262. render: (_row, index) => {
  263. return (
  264. <div onClick={() => handleDelPrice(index)} class={'w-full flex items-center justify-center'}>
  265. <SvgIcon
  266. icon={'proicons:subtract-square'}
  267. class={'cursor-pointer text-24px'}
  268. style={'color:#f5222d'}
  269. ></SvgIcon>
  270. </div>
  271. );
  272. }
  273. }
  274. ];
  275. const PriceData = ref<Price[]>([]);
  276. const selectData = ref<Api.goods.ShopSku>();
  277. const [registerTable, { refresh, getTableData, getSeachForm, setTableLoading }] = useTable({
  278. searchFormConfig: {
  279. schemas: [
  280. // {
  281. // label: '门店名称',
  282. // component: 'ApiSelect',
  283. // field: 'shopId',
  284. // componentProps: {
  285. // api: fetchGetStoreList,
  286. // resultFeild: 'data.list',
  287. // labelFeild: 'shopName',
  288. // valueFeild: 'shopId'
  289. // }
  290. // },
  291. {
  292. label: '关键词',
  293. component: 'NInput',
  294. field: 'keywords'
  295. },
  296. // {
  297. // label: '业务类型',
  298. // field: 'channelCode',
  299. // component: 'ApiSelect',
  300. // componentProps: {
  301. // api: fetchGetDictDataList,
  302. // labelFeild: 'name',
  303. // valueFeild: 'value',
  304. // resultFeild: 'data.list',
  305. // params: {
  306. // typeCode: 'sys_business_type'
  307. // }
  308. // }
  309. // },
  310. {
  311. label: '状态',
  312. field: 'productStatus',
  313. component: 'NSelect',
  314. componentProps: {
  315. options: [
  316. {
  317. label: '下架',
  318. value: 1
  319. },
  320. {
  321. label: '上架',
  322. value: 0
  323. }
  324. ]
  325. }
  326. },
  327. // {
  328. // label: '分类',
  329. // component: 'NCascader',
  330. // field: 'skuName',
  331. // componentProps: {}
  332. // },
  333. {
  334. label: '更新时间',
  335. component: 'NDatePicker',
  336. field: 'createTime',
  337. componentProps: {
  338. type: 'datetimerange',
  339. defaultTime: ['00:00:00', '23:59:59']
  340. }
  341. },
  342. {
  343. label: '价格范围',
  344. component: 'NInput',
  345. field: 'price',
  346. componentProps: {
  347. separator: '-',
  348. pair: true,
  349. placeholder: ['最低价', '最高价'],
  350. allowInput: (value: string) => !value || /^\d+$/.test(value)
  351. }
  352. }
  353. ],
  354. inline: false,
  355. size: 'small',
  356. labelPlacement: 'left',
  357. isFull: false
  358. },
  359. tableConfig: {
  360. keyField: 'skuId',
  361. title: '商品列表',
  362. showAddButton: false,
  363. scrollX: 1800,
  364. fieldMapToTime: [
  365. ['price', ['minPrice', 'maxPrice']],
  366. ['createTime', ['startTime', 'endTime']]
  367. ]
  368. }
  369. });
  370. const [
  371. registerModalPrice,
  372. { openModal: openPriceModal, setSubLoading: setSubModalLoding, closeModal: closePriceModal }
  373. ] = useModal({
  374. title: '设置渠道及价格',
  375. width: 800,
  376. height: 300
  377. });
  378. const [
  379. registerModalForm,
  380. {
  381. openModal: openModalForm,
  382. setFieldsValue: setModalFormValue,
  383. getFieldsValue: getModalFormValue,
  384. closeModal: closeModalForm,
  385. setSubLoading
  386. }
  387. ] = useModalFrom({
  388. modalConfig: {
  389. title: '封面',
  390. isShowHeaderText: true
  391. },
  392. formConfig: {
  393. schemas: [
  394. {
  395. field: 'productId',
  396. label: '分类ID',
  397. component: 'NInput',
  398. required: true
  399. },
  400. {
  401. field: 'productImg',
  402. component: 'zUpload',
  403. label: '广告位图片',
  404. componentProps: {
  405. max: 1
  406. },
  407. required: true
  408. }
  409. ],
  410. labelWidth: 120,
  411. layout: 'horizontal',
  412. gridProps: {
  413. cols: '1',
  414. itemResponsive: true
  415. }
  416. }
  417. });
  418. const [
  419. registerNameModalForm,
  420. {
  421. openModal: opeNamenModalForm,
  422. setFieldsValue: setNameModalFormValue,
  423. getFieldsValue: getNameModalFormValue,
  424. closeModal: closeNameModalForm,
  425. setSubLoading: setNameSubLoading
  426. }
  427. ] = useModalFrom({
  428. modalConfig: {
  429. title: '商品名称',
  430. isShowHeaderText: true,
  431. height: 300,
  432. width: 500
  433. },
  434. formConfig: {
  435. schemas: [
  436. {
  437. field: 'videoProductId',
  438. label: '分类ID',
  439. component: 'NInput',
  440. required: true,
  441. show: false
  442. },
  443. {
  444. field: 'videoProductName',
  445. label: '本地商品名称',
  446. component: 'NInput',
  447. required: true
  448. }
  449. ],
  450. labelWidth: 120,
  451. layout: 'horizontal',
  452. gridProps: {
  453. cols: '1',
  454. itemResponsive: true
  455. }
  456. }
  457. });
  458. // const isDisabledExport = computed(() => {
  459. // return !getTableCheckedRowKeys().length;
  460. // });
  461. const tableData = computed(() => {
  462. return getTableData();
  463. });
  464. async function handleSubmitImport(file: File) {
  465. const { error } = await fetchImportGoods({ file });
  466. if (!error) {
  467. importTemplateRef.value?.closeModal();
  468. }
  469. importTemplateRef.value?.setSubLoading(false);
  470. }
  471. function openImportModal() {
  472. importTemplateRef.value?.openModal();
  473. }
  474. function handleModalPrice(row: Api.goods.ShopSku) {
  475. selectData.value = row;
  476. if (row.pmsVideoChannelPrices) {
  477. PriceData.value = row.pmsVideoChannelPrices?.map((it: Api.government.ChannelVO) => {
  478. options.value.map(its => {
  479. if (its.id == it.channelId) {
  480. its.disabled = true;
  481. }
  482. return its;
  483. });
  484. return {
  485. channelName: it.channelName,
  486. channelId: Number(it.channelId),
  487. channelProdPrice: Number(it.price),
  488. id: it.id
  489. };
  490. });
  491. }
  492. openPriceModal();
  493. }
  494. function handleModalImg(row: Api.goods.ShopSku) {
  495. openModalForm(row);
  496. setModalFormValue({ productImg: row.productImg, productId: row.productId });
  497. }
  498. function handleModalName(row: Api.goods.ShopSku) {
  499. opeNamenModalForm(row);
  500. setNameModalFormValue({ videoProductId: row.id, videoProductName: row.localProductName });
  501. }
  502. function handleAddPrice() {
  503. // if (PriceData.value.length == 3) {
  504. // window.$message?.error('最多只能添加3条数据');
  505. // return;
  506. // }
  507. PriceData.value.push({
  508. channelName: '',
  509. channelId: undefined,
  510. channelProdPrice: 1
  511. // id: dayjs().valueOf()
  512. });
  513. console.log(PriceData.value);
  514. }
  515. function handleDelPrice(index: number) {
  516. // PriceData.value = PriceData.value.filter(item => item.id != id);
  517. PriceData.value.splice(index, 1);
  518. console.log('删除', index, PriceData.value);
  519. }
  520. async function handleSubmitPrice() {
  521. console.log(PriceData.value);
  522. if (!PriceData.value.length) {
  523. window.$message?.error('最少填写一条数据');
  524. return;
  525. }
  526. if (!areAllItemsAllFieldsFilled(PriceData.value)) {
  527. window.$message?.error('请填写完整数据');
  528. return;
  529. }
  530. setSubModalLoding(true);
  531. console.log(PriceData.value);
  532. const form = {
  533. // shopId: selectData.value?.shopId,
  534. videoProductId: selectData.value?.id,
  535. // purchasePrice: selectData.value?.channelProdList?.length ? selectData.value.channelProdList[0].purchasePrice : null,
  536. // deliveryPrice: selectData.value?.channelProdList?.length ? selectData.value.channelProdList[0].deliveryPrice : null,
  537. purchasePrice: selectData.value?.pmsVideoChannelPrices?.length
  538. ? selectData.value.pmsVideoChannelPrices[0].purchasePrice
  539. : null,
  540. deliveryPrice: selectData.value?.pmsVideoChannelPrices?.length
  541. ? selectData.value.pmsVideoChannelPrices[0].deliveryPrice
  542. : null,
  543. videoChannelPriceForms: PriceData.value.map(item => {
  544. return {
  545. // channelName: item.channelName,
  546. id: item?.id,
  547. channelId: item.channelId,
  548. price: item.channelProdPrice
  549. };
  550. })
  551. };
  552. const { error } = await fetchSetUpChannels(form);
  553. if (!error) {
  554. closePriceModal();
  555. refresh();
  556. PriceData.value = [];
  557. options.value.map(it => (it.disabled = false));
  558. } else {
  559. setSubModalLoding(false);
  560. }
  561. }
  562. async function getData() {
  563. const { data, error } = await fetchGetChannelList();
  564. if (!error) {
  565. options.value = data;
  566. }
  567. }
  568. getData();
  569. async function handleExport() {
  570. setTableLoading(true);
  571. try {
  572. await commonExport('/smqjh-pms/v1/videoProduct/export', getSeachForm(), '虚拟商品列表.xlsx');
  573. } finally {
  574. setTableLoading(false);
  575. }
  576. }
  577. function handleStatus(row: any) {
  578. window.$dialog?.info({
  579. title: '提示',
  580. content: `你确定要该操作吗?`,
  581. positiveText: '确定',
  582. negativeText: '取消',
  583. onPositiveClick: async () => {
  584. const { error } = await fetchChangeStatus({ id: row.id });
  585. if (!error) {
  586. refresh();
  587. }
  588. }
  589. });
  590. }
  591. async function handleSubmit() {
  592. const form = await getModalFormValue();
  593. // console.log(form, '表单', selectChekedKeys.value);
  594. await fetchUpdateProductImg(form);
  595. setSubLoading(false);
  596. closeModalForm();
  597. refresh();
  598. }
  599. async function handleSubmitName() {
  600. const form = await getNameModalFormValue();
  601. // console.log(form, '表单', selectChekedKeys.value);
  602. await fetchUpdateName(form);
  603. setNameSubLoading(false);
  604. closeNameModalForm();
  605. refresh();
  606. }
  607. async function handleFindThirdPartyBalance() {
  608. const { data, error } = await fetchFindThirdPartyBalance();
  609. if (!error) {
  610. Balance.value = data;
  611. }
  612. }
  613. handleFindThirdPartyBalance();
  614. </script>
  615. <template>
  616. <LayoutTable>
  617. <ZTable :columns="columns" :api="fetchProductList" @search="handleFindThirdPartyBalance" @register="registerTable">
  618. <template #op="{ row }">
  619. <NSpace align="center">
  620. <NButton size="small" ghost type="primary" @click="handleModalImg(row)">编辑图片</NButton>
  621. <NButton size="small" ghost type="primary" @click="handleModalName(row)">修改名称</NButton>
  622. <NButton size="small" ghost type="primary" @click="handleModalPrice(row)">设置渠道及价格</NButton>
  623. <NButton size="small" ghost type="primary" @click="handleStatus(row)">
  624. {{ row.productStatus == 1 ? '上架' : '下架' }}
  625. </NButton>
  626. </NSpace>
  627. </template>
  628. <template #prefix="{ loading }">
  629. <NSpace>
  630. <NButton type="primary" size="small" @click="handleFindThirdPartyBalance">
  631. 蓝色兄弟余额:
  632. <div class="text-red">{{ Balance }}元</div>
  633. </NButton>
  634. <NButton size="small" @click="openImportModal">导入商品销售渠道及价格</NButton>
  635. <NButton size="small" :disabled="tableData.length == 0" :loading="loading" @click="handleExport">
  636. 导出全部
  637. </NButton>
  638. <!-- <NButton size="small" :disabled="isDisabledExport">导出选中数据</NButton> -->
  639. <!-- <NButton size="small">修改记录</NButton> -->
  640. </NSpace>
  641. </template>
  642. </ZTable>
  643. <ZImportTemplate
  644. ref="importTemplateRef"
  645. url="/smqjh-pms/v1/videoProduct/exportTemplate"
  646. template-text="虚拟商品渠道及价格导入模版.xlsx"
  647. modal-text="导入虚拟商品销售渠道及价格"
  648. @submit="handleSubmitImport"
  649. ></ZImportTemplate>
  650. <BasicModal @register="registerModalPrice" @ok="handleSubmitPrice">
  651. <NDataTable :columns="PriceColumns" :data="PriceData" :row-key="row => row.channelId" />
  652. </BasicModal>
  653. <BasicModelForm @register-modal-form="registerModalForm" @submit-form="handleSubmit"></BasicModelForm>
  654. <BasicModelForm @register-modal-form="registerNameModalForm" @submit-form="handleSubmitName"></BasicModelForm>
  655. </LayoutTable>
  656. </template>
  657. <style scoped></style>