index.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664
  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: 'updateTime',
  167. title: '更新时间',
  168. align: 'center',
  169. width: 120,
  170. ellipsis: {
  171. tooltip: true
  172. }
  173. }
  174. ];
  175. const PriceColumns: NaiveUI.TableColumn<Price>[] = [
  176. {
  177. title: '销售渠道',
  178. key: 'channelId',
  179. align: 'center',
  180. width: 250,
  181. render: row => {
  182. return (
  183. <NSelect
  184. options={options.value}
  185. labelField="channelName"
  186. valueField="id"
  187. value={row.channelId}
  188. clearable
  189. onUpdate:value={value => {
  190. options.value.map(it => {
  191. if (it.id == row.channelId) {
  192. it.disabled = false;
  193. }
  194. return it;
  195. });
  196. row.channelId = value ? Number(value) : undefined;
  197. row.channelName = options.value.find(it => it.id == row.channelId)?.channelName;
  198. }}
  199. onUpdate:show={value => {
  200. console.log('show', value);
  201. options.value.map(it => {
  202. if (it.id == row.channelId) {
  203. if (value) {
  204. it.disabled = false;
  205. }
  206. if (!value) {
  207. it.disabled = true;
  208. }
  209. }
  210. return it;
  211. });
  212. }}
  213. ></NSelect>
  214. );
  215. }
  216. },
  217. {
  218. title: '售价(元)',
  219. key: 'channelProdPrice',
  220. align: 'center',
  221. width: 250,
  222. render: row => {
  223. return (
  224. <NInputNumber
  225. value={row.channelProdPrice}
  226. precision={2}
  227. onUpdate:value={value => {
  228. row.channelProdPrice = Number(value);
  229. }}
  230. min={0}
  231. />
  232. );
  233. }
  234. },
  235. {
  236. title: () => {
  237. return (
  238. <div onClick={() => handleAddPrice()} class={'w-full flex items-center justify-center'}>
  239. <SvgIcon
  240. icon={'proicons:add-square'}
  241. class={'cursor-pointer text-24px'}
  242. style={'color:var(--n-color)'}
  243. ></SvgIcon>
  244. </div>
  245. );
  246. },
  247. key: 'action',
  248. width: 80,
  249. align: 'center',
  250. render: (_row, index) => {
  251. return (
  252. <div onClick={() => handleDelPrice(index)} class={'w-full flex items-center justify-center'}>
  253. <SvgIcon
  254. icon={'proicons:subtract-square'}
  255. class={'cursor-pointer text-24px'}
  256. style={'color:#f5222d'}
  257. ></SvgIcon>
  258. </div>
  259. );
  260. }
  261. }
  262. ];
  263. const PriceData = ref<Price[]>([]);
  264. const selectData = ref<Api.goods.ShopSku>();
  265. const [registerTable, { refresh, getTableData, getSeachForm, setTableLoading }] = useTable({
  266. searchFormConfig: {
  267. schemas: [
  268. // {
  269. // label: '门店名称',
  270. // component: 'ApiSelect',
  271. // field: 'shopId',
  272. // componentProps: {
  273. // api: fetchGetStoreList,
  274. // resultFeild: 'data.list',
  275. // labelFeild: 'shopName',
  276. // valueFeild: 'shopId'
  277. // }
  278. // },
  279. {
  280. label: '关键词',
  281. component: 'NInput',
  282. field: 'keywords'
  283. },
  284. // {
  285. // label: '业务类型',
  286. // field: 'channelCode',
  287. // component: 'ApiSelect',
  288. // componentProps: {
  289. // api: fetchGetDictDataList,
  290. // labelFeild: 'name',
  291. // valueFeild: 'value',
  292. // resultFeild: 'data.list',
  293. // params: {
  294. // typeCode: 'sys_business_type'
  295. // }
  296. // }
  297. // },
  298. {
  299. label: '状态',
  300. field: 'productStatus',
  301. component: 'NSelect',
  302. componentProps: {
  303. options: [
  304. {
  305. label: '下架',
  306. value: 1
  307. },
  308. {
  309. label: '上架',
  310. value: 0
  311. }
  312. ]
  313. }
  314. },
  315. // {
  316. // label: '分类',
  317. // component: 'NCascader',
  318. // field: 'skuName',
  319. // componentProps: {}
  320. // },
  321. {
  322. label: '更新时间',
  323. component: 'NDatePicker',
  324. field: 'createTime',
  325. componentProps: {
  326. type: 'datetimerange',
  327. defaultTime: ['00:00:00', '23:59:59']
  328. }
  329. },
  330. {
  331. label: '价格范围',
  332. component: 'NInput',
  333. field: 'price',
  334. componentProps: {
  335. separator: '-',
  336. pair: true,
  337. placeholder: ['最低价', '最高价'],
  338. allowInput: (value: string) => !value || /^\d+$/.test(value)
  339. }
  340. }
  341. ],
  342. inline: false,
  343. size: 'small',
  344. labelPlacement: 'left',
  345. isFull: false
  346. },
  347. tableConfig: {
  348. keyField: 'skuId',
  349. title: '商品列表',
  350. showAddButton: false,
  351. scrollX: 1800,
  352. fieldMapToTime: [
  353. ['price', ['minPrice', 'maxPrice']],
  354. ['createTime', ['startTime', 'endTime']]
  355. ]
  356. }
  357. });
  358. const [
  359. registerModalPrice,
  360. { openModal: openPriceModal, setSubLoading: setSubModalLoding, closeModal: closePriceModal }
  361. ] = useModal({
  362. title: '设置渠道及价格',
  363. width: 800,
  364. height: 300
  365. });
  366. const [
  367. registerModalForm,
  368. {
  369. openModal: openModalForm,
  370. setFieldsValue: setModalFormValue,
  371. getFieldsValue: getModalFormValue,
  372. closeModal: closeModalForm,
  373. setSubLoading
  374. }
  375. ] = useModalFrom({
  376. modalConfig: {
  377. title: '封面',
  378. isShowHeaderText: true
  379. },
  380. formConfig: {
  381. schemas: [
  382. {
  383. field: 'productId',
  384. label: '分类ID',
  385. component: 'NInput',
  386. required: true
  387. },
  388. {
  389. field: 'productImg',
  390. component: 'zUpload',
  391. label: '广告位图片',
  392. componentProps: {
  393. max: 1
  394. },
  395. required: true
  396. }
  397. ],
  398. labelWidth: 120,
  399. layout: 'horizontal',
  400. gridProps: {
  401. cols: '1',
  402. itemResponsive: true
  403. }
  404. }
  405. });
  406. const [
  407. registerNameModalForm,
  408. {
  409. openModal: opeNamenModalForm,
  410. setFieldsValue: setNameModalFormValue,
  411. getFieldsValue: getNameModalFormValue,
  412. closeModal: closeNameModalForm,
  413. setSubLoading: setNameSubLoading
  414. }
  415. ] = useModalFrom({
  416. modalConfig: {
  417. title: '商品名称',
  418. isShowHeaderText: true,
  419. height: 300,
  420. width: 500
  421. },
  422. formConfig: {
  423. schemas: [
  424. {
  425. field: 'videoProductId',
  426. label: '分类ID',
  427. component: 'NInput',
  428. required: true,
  429. show: false
  430. },
  431. {
  432. field: 'videoProductName',
  433. label: '本地商品名称',
  434. component: 'NInput',
  435. required: true
  436. }
  437. ],
  438. labelWidth: 120,
  439. layout: 'horizontal',
  440. gridProps: {
  441. cols: '1',
  442. itemResponsive: true
  443. }
  444. }
  445. });
  446. // const isDisabledExport = computed(() => {
  447. // return !getTableCheckedRowKeys().length;
  448. // });
  449. const tableData = computed(() => {
  450. return getTableData();
  451. });
  452. async function handleSubmitImport(file: File) {
  453. const { error } = await fetchImportGoods({ file });
  454. if (!error) {
  455. importTemplateRef.value?.closeModal();
  456. }
  457. importTemplateRef.value?.setSubLoading(false);
  458. }
  459. function openImportModal() {
  460. importTemplateRef.value?.openModal();
  461. }
  462. function handleModalPrice(row: Api.goods.ShopSku) {
  463. selectData.value = row;
  464. if (row.pmsVideoChannelPrices) {
  465. PriceData.value = row.pmsVideoChannelPrices?.map((it: Api.government.ChannelVO) => {
  466. options.value.map(its => {
  467. if (its.id == it.channelId) {
  468. its.disabled = true;
  469. }
  470. return its;
  471. });
  472. return {
  473. channelName: it.channelName,
  474. channelId: Number(it.channelId),
  475. channelProdPrice: Number(it.price),
  476. id: it.id
  477. };
  478. });
  479. }
  480. openPriceModal();
  481. }
  482. function handleModalImg(row: Api.goods.ShopSku) {
  483. openModalForm(row);
  484. setModalFormValue({ productImg: row.productImg, productId: row.productId });
  485. }
  486. function handleModalName(row: Api.goods.ShopSku) {
  487. opeNamenModalForm(row);
  488. setNameModalFormValue({ videoProductId: row.id, videoProductName: row.localProductName });
  489. }
  490. function handleAddPrice() {
  491. // if (PriceData.value.length == 3) {
  492. // window.$message?.error('最多只能添加3条数据');
  493. // return;
  494. // }
  495. PriceData.value.push({
  496. channelName: '',
  497. channelId: undefined,
  498. channelProdPrice: 1
  499. // id: dayjs().valueOf()
  500. });
  501. console.log(PriceData.value);
  502. }
  503. function handleDelPrice(index: number) {
  504. // PriceData.value = PriceData.value.filter(item => item.id != id);
  505. PriceData.value.splice(index, 1);
  506. console.log('删除', index, PriceData.value);
  507. }
  508. async function handleSubmitPrice() {
  509. console.log(PriceData.value);
  510. if (!PriceData.value.length) {
  511. window.$message?.error('最少填写一条数据');
  512. return;
  513. }
  514. if (!areAllItemsAllFieldsFilled(PriceData.value)) {
  515. window.$message?.error('请填写完整数据');
  516. return;
  517. }
  518. setSubModalLoding(true);
  519. console.log(PriceData.value);
  520. const form = {
  521. // shopId: selectData.value?.shopId,
  522. videoProductId: selectData.value?.id,
  523. // purchasePrice: selectData.value?.channelProdList?.length ? selectData.value.channelProdList[0].purchasePrice : null,
  524. // deliveryPrice: selectData.value?.channelProdList?.length ? selectData.value.channelProdList[0].deliveryPrice : null,
  525. purchasePrice: selectData.value?.pmsVideoChannelPrices?.length
  526. ? selectData.value.pmsVideoChannelPrices[0].purchasePrice
  527. : null,
  528. deliveryPrice: selectData.value?.pmsVideoChannelPrices?.length
  529. ? selectData.value.pmsVideoChannelPrices[0].deliveryPrice
  530. : null,
  531. videoChannelPriceForms: PriceData.value.map(item => {
  532. return {
  533. // channelName: item.channelName,
  534. id: item?.id,
  535. channelId: item.channelId,
  536. price: item.channelProdPrice
  537. };
  538. })
  539. };
  540. const { error } = await fetchSetUpChannels(form);
  541. if (!error) {
  542. closePriceModal();
  543. refresh();
  544. PriceData.value = [];
  545. options.value.map(it => (it.disabled = false));
  546. } else {
  547. setSubModalLoding(false);
  548. }
  549. }
  550. async function getData() {
  551. const { data, error } = await fetchGetChannelList();
  552. if (!error) {
  553. options.value = data;
  554. }
  555. }
  556. getData();
  557. async function handleExport() {
  558. setTableLoading(true);
  559. try {
  560. await commonExport('/smqjh-pms/v1/videoProduct/export', getSeachForm(), '虚拟商品列表.xlsx');
  561. } finally {
  562. setTableLoading(false);
  563. }
  564. }
  565. function handleStatus(row: any) {
  566. window.$dialog?.info({
  567. title: '提示',
  568. content: `你确定要该操作吗?`,
  569. positiveText: '确定',
  570. negativeText: '取消',
  571. onPositiveClick: async () => {
  572. const { error } = await fetchChangeStatus({ id: row.id });
  573. if (!error) {
  574. refresh();
  575. }
  576. }
  577. });
  578. }
  579. async function handleSubmit() {
  580. const form = await getModalFormValue();
  581. // console.log(form, '表单', selectChekedKeys.value);
  582. await fetchUpdateProductImg(form);
  583. setSubLoading(false);
  584. closeModalForm();
  585. refresh();
  586. }
  587. async function handleSubmitName() {
  588. const form = await getNameModalFormValue();
  589. // console.log(form, '表单', selectChekedKeys.value);
  590. await fetchUpdateName(form);
  591. setNameSubLoading(false);
  592. closeNameModalForm();
  593. refresh();
  594. }
  595. async function handleFindThirdPartyBalance() {
  596. const { data, error } = await fetchFindThirdPartyBalance();
  597. if (!error) {
  598. Balance.value = data;
  599. }
  600. }
  601. handleFindThirdPartyBalance();
  602. </script>
  603. <template>
  604. <LayoutTable>
  605. <ZTable :columns="columns" :api="fetchProductList" @search="handleFindThirdPartyBalance" @register="registerTable">
  606. <template #op="{ row }">
  607. <NSpace align="center">
  608. <NButton size="small" ghost type="primary" @click="handleModalImg(row)">编辑图片</NButton>
  609. <NButton size="small" ghost type="primary" @click="handleModalName(row)">修改名称</NButton>
  610. <NButton size="small" ghost type="primary" @click="handleModalPrice(row)">设置渠道及价格</NButton>
  611. <NButton size="small" ghost type="primary" @click="handleStatus(row)">
  612. {{ row.productStatus == 1 ? '上架' : '下架' }}
  613. </NButton>
  614. </NSpace>
  615. </template>
  616. <template #prefix="{ loading }">
  617. <NSpace>
  618. <NButton type="primary" size="small" @click="handleFindThirdPartyBalance">
  619. 蓝色兄弟余额:
  620. <div class="text-red">{{ Balance }}元</div>
  621. </NButton>
  622. <NButton size="small" @click="openImportModal">导入商品销售渠道及价格</NButton>
  623. <NButton size="small" :disabled="tableData.length == 0" :loading="loading" @click="handleExport">
  624. 导出全部
  625. </NButton>
  626. <!-- <NButton size="small" :disabled="isDisabledExport">导出选中数据</NButton> -->
  627. <!-- <NButton size="small">修改记录</NButton> -->
  628. </NSpace>
  629. </template>
  630. </ZTable>
  631. <ZImportTemplate
  632. ref="importTemplateRef"
  633. url="/smqjh-pms/v1/videoProduct/exportTemplate"
  634. template-text="虚拟商品渠道及价格导入模版.xlsx"
  635. modal-text="导入虚拟商品销售渠道及价格"
  636. @submit="handleSubmitImport"
  637. ></ZImportTemplate>
  638. <BasicModal @register="registerModalPrice" @ok="handleSubmitPrice">
  639. <NDataTable :columns="PriceColumns" :data="PriceData" :row-key="row => row.channelId" />
  640. </BasicModal>
  641. <BasicModelForm @register-modal-form="registerModalForm" @submit-form="handleSubmit"></BasicModelForm>
  642. <BasicModelForm @register-modal-form="registerNameModalForm" @submit-form="handleSubmitName"></BasicModelForm>
  643. </LayoutTable>
  644. </template>
  645. <style scoped></style>