index.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  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 { fetchGetAllStoreList } from '@/service/api/goods/desk-category';
  6. import {
  7. fetchGetAllChannelList,
  8. fetchGetGoodsList,
  9. fetchImportGoods,
  10. fetchSetUpChannels
  11. } from '@/service/api/goods/store-goods';
  12. import { areAllItemsAllFieldsFilled } from '@/utils/zt';
  13. import { useTable } from '@/components/zt/Table/hooks/useTable';
  14. import SvgIcon from '@/components/custom/svg-icon.vue';
  15. import { useModal } from '@/components/zt/Modal/hooks/useModal';
  16. type Price = { channelId: number | undefined; channelProdPrice: number; id: number };
  17. const importTemplateRef = useTemplateRef('importTemplateRef');
  18. const options = ref<Api.goods.Channel[]>([]);
  19. const TypeName = ['企业用户', 'B端用户', 'C端用户'];
  20. const columns: NaiveUI.TableColumn<Api.goods.ShopSku>[] = [
  21. {
  22. type: 'selection',
  23. align: 'center',
  24. width: 48,
  25. fixed: 'left'
  26. },
  27. {
  28. key: 'skuName',
  29. title: '商品信息',
  30. align: 'left',
  31. width: 400,
  32. fixed: 'left',
  33. render: row => {
  34. if (!row.sku) {
  35. return '---';
  36. }
  37. const img = row.sku.pic?.split(',');
  38. return (
  39. <div class={'flex items-center'}>
  40. {img && img.length > 0 && <NImage src={img[0]} class="h-[80px] min-w-80px w-[80px]" lazy />}
  41. {!img && (
  42. <SvgIcon icon={'proicons:alert-triangle'} class={'cursor-pointer text-80px'} style={'color:#ccc'}></SvgIcon>
  43. )}
  44. <div class={'ml-[10px]'}>
  45. <div class={'text-[16px] font-semibold'}>{row.sku.skuName || '--'}</div>
  46. <div class={'text-gray'}>海博商品ID: {row.sku.hbSkuId || '--'} </div>
  47. <div class={'text-gray'}>商品编码:{row.sku.partyCode || '--'} </div>
  48. <div class={'text-gray'}>SPUID: {row.sku.hbSpuId || '--'} </div>
  49. <div class={'text-gray'}>外部商品编码:{row.sku.skuCode || '--'} </div>
  50. <div class={'text-gray'}>条形码:{row.sku.modelId || '--'} </div>
  51. <div class={'text-gray'}>
  52. 重量/规格/单位:{row.sku.weight || '--'} / {row.sku.spec || '--'} / {row.sku.weightUnit || '--'}
  53. </div>
  54. </div>
  55. </div>
  56. );
  57. }
  58. },
  59. {
  60. key: 'shopName',
  61. title: '门店名称',
  62. align: 'center',
  63. width: 120,
  64. ellipsis: {
  65. tooltip: true
  66. }
  67. },
  68. {
  69. key: 'shopSkuStocks',
  70. title: '线上可售库存',
  71. align: 'center',
  72. width: 120
  73. },
  74. {
  75. key: 'purchasePrice',
  76. title: '进货价(元)',
  77. align: 'center',
  78. width: 120,
  79. render: row => {
  80. if (!row.channelProdList?.length) return '--';
  81. if (row.channelProdList) return <div>{row.channelProdList[0].purchasePrice || '--'}</div>;
  82. return '--';
  83. }
  84. },
  85. {
  86. key: 'deliveryPrice',
  87. title: '出货价/控制价(元)',
  88. align: 'center',
  89. width: 150,
  90. render: row => {
  91. if (!row.channelProdList?.length) return '--';
  92. if (row.channelProdList) return <div>{row.channelProdList[0].deliveryPrice || '--'}</div>;
  93. return '--';
  94. }
  95. },
  96. {
  97. key: 'name',
  98. title: '销售渠道及价格',
  99. align: 'center',
  100. width: 250,
  101. render: row => {
  102. return row.channelProdList?.map(it => {
  103. return (
  104. <div>
  105. {TypeName[Number(it.channelId) - 1]} ({it.channelName || '--'}) : {it.channelProdPrice || '--'}
  106. </div>
  107. );
  108. });
  109. }
  110. }
  111. ];
  112. const PriceColumns: NaiveUI.TableColumn<Price>[] = [
  113. {
  114. title: '销售渠道',
  115. key: 'channelId',
  116. align: 'center',
  117. width: 250,
  118. render: row => {
  119. return (
  120. <NSelect
  121. options={options.value}
  122. labelField="channelName"
  123. valueField="id"
  124. value={row.channelId}
  125. clearable
  126. onUpdate:value={value => {
  127. options.value.map(it => {
  128. if (it.id == row.channelId) {
  129. it.disabled = false;
  130. }
  131. return it;
  132. });
  133. row.channelId = value ? Number(value) : undefined;
  134. }}
  135. onUpdate:show={value => {
  136. options.value.map(it => {
  137. if (it.id == row.channelId) {
  138. if (value) {
  139. it.disabled = false;
  140. }
  141. if (!value) {
  142. it.disabled = true;
  143. }
  144. }
  145. return it;
  146. });
  147. }}
  148. ></NSelect>
  149. );
  150. }
  151. },
  152. {
  153. title: '售价(元)',
  154. key: 'channelProdPrice',
  155. align: 'center',
  156. width: 250,
  157. render: row => {
  158. return (
  159. <NInputNumber
  160. value={row.channelProdPrice}
  161. precision={2}
  162. onUpdate:value={value => {
  163. row.channelProdPrice = Number(value);
  164. }}
  165. min={0}
  166. />
  167. );
  168. }
  169. },
  170. {
  171. title: () => {
  172. return (
  173. <div onClick={() => handleAddPrice()} class={'w-full flex items-center justify-center'}>
  174. <SvgIcon
  175. icon={'proicons:add-square'}
  176. class={'cursor-pointer text-24px'}
  177. style={'color:var(--n-color)'}
  178. ></SvgIcon>
  179. </div>
  180. );
  181. },
  182. key: 'action',
  183. width: 80,
  184. align: 'center',
  185. render: row => {
  186. return (
  187. <div onClick={() => handleDelPrice(row.id)} class={'w-full flex items-center justify-center'}>
  188. <SvgIcon
  189. icon={'proicons:subtract-square'}
  190. class={'cursor-pointer text-24px'}
  191. style={'color:#f5222d'}
  192. ></SvgIcon>
  193. </div>
  194. );
  195. }
  196. }
  197. ];
  198. const PriceData = ref<Price[]>([]);
  199. const selectData = ref<Api.goods.ShopSku>();
  200. const [registerTable, { getTableCheckedRowKeys, refresh, getTableData, getSeachForm }] = useTable({
  201. searchFormConfig: {
  202. schemas: [
  203. {
  204. label: '门店名称',
  205. component: 'ApiSelect',
  206. field: 'shopId',
  207. componentProps: {
  208. api: fetchGetAllStoreList,
  209. labelFeild: 'shopName',
  210. valueFeild: 'shopId'
  211. }
  212. },
  213. {
  214. label: '商品名称',
  215. component: 'NInput',
  216. field: 'skuName'
  217. }
  218. ],
  219. inline: false,
  220. size: 'small',
  221. labelPlacement: 'left',
  222. isFull: false
  223. },
  224. tableConfig: {
  225. keyField: 'id',
  226. title: '商品列表',
  227. showAddButton: false,
  228. scrollX: 1200
  229. }
  230. });
  231. const [
  232. registerModalPrice,
  233. { openModal: openPriceModal, setSubLoading: setSubModalLoding, closeModal: closePriceModal }
  234. ] = useModal({
  235. title: '设置渠道及价格',
  236. width: 800,
  237. height: 300
  238. });
  239. const isDisabledExport = computed(() => {
  240. return !getTableCheckedRowKeys().length;
  241. });
  242. const tableData = computed(() => {
  243. return getTableData();
  244. });
  245. async function handleSubmitImport(file: File) {
  246. const { error } = await fetchImportGoods({ file });
  247. if (!error) {
  248. importTemplateRef.value?.closeModal();
  249. }
  250. importTemplateRef.value?.setSubLoading(false);
  251. }
  252. function openImportModal() {
  253. importTemplateRef.value?.openModal();
  254. }
  255. function handleModalPrice(row: Api.goods.ShopSku) {
  256. selectData.value = row;
  257. if (row.channelProdList) {
  258. PriceData.value = row.channelProdList?.map(it => {
  259. options.value.map(its => {
  260. if (its.id == it.channelId) {
  261. its.disabled = true;
  262. }
  263. return its;
  264. });
  265. return {
  266. channelId: Number(it.channelId),
  267. channelProdPrice: Number(it.channelProdPrice),
  268. id: Number(it.id)
  269. };
  270. });
  271. }
  272. openPriceModal();
  273. }
  274. function handleAddPrice() {
  275. if (PriceData.value.length == 3) {
  276. window.$message?.error('最多只能添加3条数据');
  277. return;
  278. }
  279. PriceData.value.push({
  280. channelId: undefined,
  281. channelProdPrice: 1,
  282. id: dayjs().valueOf()
  283. });
  284. }
  285. function handleDelPrice(id: number) {
  286. PriceData.value = PriceData.value.filter(item => item.id != id);
  287. }
  288. async function handleSubmitPrice() {
  289. if (!PriceData.value.length) {
  290. window.$message?.error('最少填写一条数据');
  291. return;
  292. }
  293. if (!areAllItemsAllFieldsFilled(PriceData.value)) {
  294. window.$message?.error('请填写完整数据');
  295. return;
  296. }
  297. setSubModalLoding(true);
  298. const form = {
  299. shopId: selectData.value?.shopId,
  300. skuId: selectData.value?.skuId,
  301. purchasePrice: selectData.value?.channelProdList?.length ? selectData.value.channelProdList[0].purchasePrice : null,
  302. deliveryPrice: selectData.value?.channelProdList?.length ? selectData.value.channelProdList[0].deliveryPrice : null,
  303. setChannelPriceDtoList: PriceData.value.map(item => {
  304. return {
  305. channelId: item.channelId,
  306. channelProdPrice: item.channelProdPrice
  307. };
  308. })
  309. };
  310. const { error } = await fetchSetUpChannels(form);
  311. if (!error) {
  312. closePriceModal();
  313. refresh();
  314. PriceData.value = [];
  315. options.value.map(it => (it.disabled = false));
  316. } else {
  317. setSubModalLoding(false);
  318. }
  319. console.log(PriceData.value, 'asdsad');
  320. }
  321. async function getData() {
  322. const { data, error } = await fetchGetAllChannelList();
  323. if (!error) {
  324. options.value = data;
  325. }
  326. }
  327. getData();
  328. async function handleExport() {
  329. const form = getSeachForm();
  330. // 将form对象转换为查询参数字符串
  331. const queryParams = new URLSearchParams(form).toString();
  332. const baseUrl = `${import.meta.env.VITE_SERVICE_BASE_URL}/platform/channelProd/export`;
  333. const fullUrl = queryParams ? `${baseUrl}?${queryParams}` : baseUrl;
  334. window.open(fullUrl, '_blank');
  335. }
  336. </script>
  337. <template>
  338. <LayoutTable>
  339. <ZTable :columns="columns" :api="fetchGetGoodsList" @register="registerTable">
  340. <template #op="{ row }">
  341. <NButton size="small" ghost type="primary" @click="handleModalPrice(row)">设置渠道及价格</NButton>
  342. </template>
  343. <template #prefix>
  344. <NSpace>
  345. <NButton size="small" @click="openImportModal">导入商品销售渠道及价格</NButton>
  346. <NButton size="small" :disabled="tableData.length == 0" @click="handleExport">导出全部</NButton>
  347. <NButton size="small" :disabled="isDisabledExport">导出选中数据</NButton>
  348. <NButton size="small">修改记录</NButton>
  349. </NSpace>
  350. </template>
  351. </ZTable>
  352. <ZImportTemplate
  353. ref="importTemplateRef"
  354. url="/platform/channelProd/template/download"
  355. template-text="商品渠道及价格导入模版.xlsx"
  356. modal-text="导入商品销售渠道及价格"
  357. @submit="handleSubmitImport"
  358. ></ZImportTemplate>
  359. <BasicModal @register="registerModalPrice" @ok="handleSubmitPrice">
  360. <NDataTable :columns="PriceColumns" :data="PriceData" :row-key="row => row.id" />
  361. </BasicModal>
  362. </LayoutTable>
  363. </template>
  364. <style scoped></style>