goodsList-edit.vue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583
  1. <template>
  2. <div class="pc-edit">
  3. <!-- 内容 -->
  4. <div class="content-wrap" v-show="diyStore.editTab == 'content'">
  5. <div class="edit-attr-item-wrap">
  6. <h3 class="mb-[10px]">购买按钮</h3>
  7. <el-form label-width="90px" class="px-[10px]">
  8. <el-form-item label="是否显示">
  9. <el-switch v-model="diyStore.editComponent.btnShow" />
  10. </el-form-item>
  11. <el-form-item label="样式">
  12. <div class="flex-row-start">
  13. <div
  14. @click="diyStore.editComponent.btnStyle = 1"
  15. class="btnStyle flex-row-center"
  16. :class="{ 'btnStyle1': diyStore.editComponent.btnStyle == 1 }"
  17. >
  18. <div class="btn1">购买</div>
  19. </div>
  20. <div
  21. @click="diyStore.editComponent.btnStyle = 2"
  22. class="btnStyle flex-row-center"
  23. :class="{ 'btnStyle1': diyStore.editComponent.btnStyle == 2 }"
  24. >
  25. <div class="btn2 flex-row-center">
  26. <el-icon size="14"><Plus /></el-icon>
  27. </div>
  28. </div>
  29. <div
  30. @click="diyStore.editComponent.btnStyle = 3"
  31. class="btnStyle flex-row-center"
  32. :class="{ 'btnStyle1': diyStore.editComponent.btnStyle == 3 }"
  33. >
  34. <div class="btn2 flex-row-center">
  35. <icon name="iconfont icongouwuche" size="14px" />
  36. </div>
  37. </div>
  38. </div>
  39. </el-form-item>
  40. <el-form-item label="按钮文字">
  41. <el-input v-model="diyStore.editComponent.btnText" placeholder="请输入按钮文字" :maxlength="4" show-word-limit />
  42. </el-form-item>
  43. </el-form>
  44. </div>
  45. <div class="edit-attr-item-wrap">
  46. <h3 class="mb-[10px]">商品数据</h3>
  47. <el-form label-width="90px" class="px-[10px]">
  48. <el-form-item label="显示内容">
  49. <el-checkbox-group v-model="diyStore.editComponent.goodsShow">
  50. <el-checkbox label="商品名称" :value="1" />
  51. <el-checkbox label="销售价格" :value="2" />
  52. <el-checkbox label="划线价格" :value="3" />
  53. </el-checkbox-group>
  54. </el-form-item>
  55. <el-form-item label="商品数量">
  56. <div>
  57. <el-input-number v-model="diyStore.editComponent.goodsNumber" :min="0" />
  58. <div class="annotation3 mt-[5px]">(0代表不限)</div>
  59. </div>
  60. </el-form-item>
  61. <el-form-item label="商品排序">
  62. <el-radio-group v-model="diyStore.editComponent.goodsSort" fill="#409eff">
  63. <el-radio-button label="综合" :value="1" />
  64. <el-radio-button label="销量" :value="2" />
  65. <el-radio-button label="价格" :value="3" />
  66. </el-radio-group>
  67. </el-form-item>
  68. </el-form>
  69. </div>
  70. <div class="edit-attr-item-wrap">
  71. <div class="edit-attr-title flex-row-between">
  72. <div>
  73. <span>选项卡</span>
  74. <span class="title2">鼠标拖拽可以改变顺序</span>
  75. </div>
  76. </div>
  77. <el-form label-width="80px" class="px-[10px]">
  78. <draggable v-model="diyStore.editComponent.tabList" item-key="id">
  79. <template #item="{ element, index }">
  80. <div class="edit-attr-box" @click="diyStore.editComponent.tabIndex = index">
  81. <el-icon @click.stop="onDel(index)" color="#F56C6C" size="18px" class="circleClose">
  82. <CircleCloseFilled />
  83. </el-icon>
  84. <el-form-item label="名称">
  85. <el-input v-model="element.title" placeholder="请输入选项卡名称" :maxlength="10" show-word-limit />
  86. </el-form-item>
  87. <el-form-item label="选择方式">
  88. <el-radio-group v-model="element.goodsType">
  89. <el-radio :value="1">指定商品</el-radio>
  90. <el-radio :value="2">商品分类</el-radio>
  91. </el-radio-group>
  92. </el-form-item>
  93. <el-form-item label="指定商品" v-if="element.goodsType == 1">
  94. <div class="data-num">
  95. <span @click="openDialog(index)" v-if="element.goodsIds.length == 0">请选择</span>
  96. <span @click="openDialog(index)" v-else>已选择{{ element.goodsIds.length }}个</span>
  97. <el-icon><ArrowRight /></el-icon>
  98. </div>
  99. </el-form-item>
  100. <el-form-item label="商品分类" v-if="element.goodsType == 2">
  101. <el-tree-select
  102. v-model="element.goodsClassify"
  103. :data="categoryOptions1"
  104. :props="treeProps"
  105. value-key="id"
  106. placeholder="请选择商品分类"
  107. clearable
  108. check-strictly
  109. @change="(res: any) => goodsClassifyChange(res, element)"
  110. />
  111. </el-form-item>
  112. </div>
  113. </template>
  114. </draggable>
  115. <el-button @click="onAdd" style="width: 100%; margin-top: 10px">新增选项卡</el-button>
  116. </el-form>
  117. </div>
  118. </div>
  119. <!-- 样式 -->
  120. <div class="style-wrap" v-show="diyStore.editTab == 'style'">
  121. <div class="edit-attr-item-wrap">
  122. <h3 class="mb-[10px]">选项卡样式</h3>
  123. <el-form label-width="80px" class="px-[10px]">
  124. <el-form-item label="背景">
  125. <span class="mr-[10px]">{{ diyStore.editComponent.tabbackgroundColor1 }}</span>
  126. <el-color-picker class="mr-[10px]" v-model="diyStore.editComponent.tabbackgroundColor1" />
  127. <el-button @click="diyStore.editComponent.tabbackgroundColor1 = '#ffffff'" size="small">重置</el-button>
  128. </el-form-item>
  129. <el-form-item label="选中背景">
  130. <span class="mr-[10px]">{{ diyStore.editComponent.tabbackgroundColor2 }}</span>
  131. <el-color-picker class="mr-[10px]" v-model="diyStore.editComponent.tabbackgroundColor2" />
  132. <el-button @click="diyStore.editComponent.tabbackgroundColor2 = '#E7000B'" size="small">重置</el-button>
  133. </el-form-item>
  134. <el-form-item label="文字颜色">
  135. <span class="mr-[10px]">{{ diyStore.editComponent.tabColor1 }}</span>
  136. <el-color-picker class="mr-[10px]" v-model="diyStore.editComponent.tabColor1" />
  137. <el-button @click="diyStore.editComponent.tabColor1 = '#333333'" size="small">重置</el-button>
  138. </el-form-item>
  139. <el-form-item label="选中文字">
  140. <span class="mr-[10px]">{{ diyStore.editComponent.tabColor2 }}</span>
  141. <el-color-picker class="mr-[10px]" v-model="diyStore.editComponent.tabColor2" />
  142. <el-button @click="diyStore.editComponent.tabColor2 = '#ffffff'" size="small">重置</el-button>
  143. </el-form-item>
  144. <el-form-item label="圆角">
  145. <el-slider size="small" v-model="diyStore.editComponent.tabRadius" show-input :min="1" :max="50" />
  146. </el-form-item>
  147. </el-form>
  148. </div>
  149. <div class="edit-attr-item-wrap">
  150. <h3 class="mb-[10px]">商品样式</h3>
  151. <el-form label-width="80px" class="px-[10px]">
  152. <el-form-item label="商品背景">
  153. <span class="mr-[10px]">{{ diyStore.editComponent.goodsbackgroundColor }}</span>
  154. <el-color-picker class="mr-[10px]" v-model="diyStore.editComponent.goodsbackgroundColor" />
  155. <el-button @click="diyStore.editComponent.goodsbackgroundColor = '#ffffff'" size="small">重置</el-button>
  156. </el-form-item>
  157. <el-form-item label="商品名称">
  158. <el-radio-group v-model="diyStore.editComponent.goodsTitleType">
  159. <el-radio :value="1">加粗</el-radio>
  160. <el-radio :value="2">单行</el-radio>
  161. <el-radio :value="3">多行</el-radio>
  162. </el-radio-group>
  163. </el-form-item>
  164. <el-form-item label="名称颜色">
  165. <span class="mr-[10px]">{{ diyStore.editComponent.goodsTitleColor }}</span>
  166. <el-color-picker class="mr-[10px]" v-model="diyStore.editComponent.goodsTitleColor" />
  167. <el-button @click="diyStore.editComponent.goodsTitleColor = '#101828'" size="small">重置</el-button>
  168. </el-form-item>
  169. <el-form-item label="图片圆角">
  170. <el-slider size="small" v-model="diyStore.editComponent.imageRadius" show-input :min="1" :max="50" />
  171. </el-form-item>
  172. <el-form-item label="销售价">
  173. <span class="mr-[10px]">{{ diyStore.editComponent.priceColor }}</span>
  174. <el-color-picker class="mr-[10px]" v-model="diyStore.editComponent.priceColor" />
  175. <el-button @click="diyStore.editComponent.priceColor = '#E7000B'" size="small">重置</el-button>
  176. </el-form-item>
  177. <el-form-item label="上圆角">
  178. <el-slider size="small" v-model="diyStore.editComponent.goodstopRounded" show-input :min="1" :max="50" />
  179. </el-form-item>
  180. <el-form-item label="下圆角">
  181. <el-slider size="small" v-model="diyStore.editComponent.goodsbottomRounded" show-input :min="1" :max="50" />
  182. </el-form-item>
  183. </el-form>
  184. </div>
  185. <div class="edit-attr-item-wrap">
  186. <h3 class="mb-[10px]">商品样式</h3>
  187. <el-form label-width="80px" class="px-[10px]">
  188. <el-form-item label="购买按钮">
  189. <span class="mr-[10px]">{{ diyStore.editComponent.btnColor }}</span>
  190. <el-color-picker class="mr-[10px]" v-model="diyStore.editComponent.btnColor" />
  191. <el-button @click="diyStore.editComponent.btnColor = '#ffffff'" size="small">重置</el-button>
  192. </el-form-item>
  193. <el-form-item label="背景颜色">
  194. <span class="mr-[10px]">{{ diyStore.editComponent.btnbackgroundColor }}</span>
  195. <el-color-picker class="mr-[10px]" v-model="diyStore.editComponent.btnbackgroundColor" />
  196. <el-button @click="diyStore.editComponent.btnbackgroundColor = '#E7000B'" size="small">重置</el-button>
  197. </el-form-item>
  198. </el-form>
  199. </div>
  200. <!-- 组件样式 -->
  201. <slot name="style"></slot>
  202. </div>
  203. <!-- 手动选择 -->
  204. <el-dialog v-model="showDialog" title="选择商品" width="1400">
  205. <div class="dialog-bos">
  206. <el-input v-model="queryParams.itemName" placeholder="请输入商品名称" clearable style="width: 300px; margin-bottom: 10px">
  207. <template #append>
  208. <el-button :icon="Search" @click="handleQuery" />
  209. </template>
  210. </el-input>
  211. <div class="flex">
  212. <div class="tree-bos">
  213. <el-tree :data="categoryOptions" :props="defaultProps" @node-click="handleNodeClick" :highlight-current="true" />
  214. </div>
  215. <el-table ref="multipleTableRef" v-loading="loading" :data="tableData" border @selection-change="handleSelectionChange">
  216. <el-table-column type="selection" width="55" />
  217. <el-table-column label="商品图片" align="center" prop="productImage" width="100">
  218. <template #default="scope">
  219. <image-preview :src="scope.row.productImage" :width="60" :height="60" />
  220. </template>
  221. </el-table-column>
  222. <el-table-column label="商品信息" align="center" minWidth="250" show-overflow-tooltip>
  223. <template #default="scope">
  224. <div class="text-left">
  225. <div>{{ scope.row.itemName }}</div>
  226. <div class="text-gray-500" style="font-size: 12px">品牌: {{ scope.row.brandName || '-' }}</div>
  227. </div>
  228. </template>
  229. </el-table-column>
  230. <el-table-column label="SKU价格" align="center" width="180">
  231. <template #default="scope">
  232. <div class="text-left" style="font-size: 12px">
  233. <div>
  234. <span class="text-gray-500">市场价:</span>
  235. <span class="text-red-500">¥{{ scope.row.marketPrice || '0.00' }}</span>
  236. </div>
  237. <div>
  238. <span class="text-gray-500">会员价:</span>
  239. <span class="text-red-500">¥{{ scope.row.memberPrice || '0.00' }}</span>
  240. </div>
  241. <div>
  242. <span class="text-gray-500">最低价:</span>
  243. <span class="text-red-500">¥{{ scope.row.minSellingPrice || '0.00' }}</span>
  244. </div>
  245. </div>
  246. </template>
  247. </el-table-column>
  248. <el-table-column label="成本情况" align="center" width="150">
  249. <template #default="scope">
  250. <div class="text-left" style="font-size: 12px">
  251. <div>
  252. <span class="text-gray-500">采购价:</span>
  253. <span>¥{{ scope.row.purchasingPrice || '0.00' }}</span>
  254. </div>
  255. <div>
  256. <span class="text-gray-500">暂估毛利率:</span>
  257. <span>{{ scope.row.tempGrossMargin || '0.0000' }}%</span>
  258. </div>
  259. </div>
  260. </template>
  261. </el-table-column>
  262. </el-table>
  263. </div>
  264. <pagination v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList">
  265. <template #slotDiv>
  266. <div class="selected">已选择 {{ multipleSelection.length }} 个</div>
  267. </template>
  268. </pagination>
  269. </div>
  270. <template #footer>
  271. <span class="dialog-footer">
  272. <el-button @click="showDialog = false">取消</el-button>
  273. <el-button type="primary" @click="onConfirm">确认</el-button>
  274. </span>
  275. </template>
  276. </el-dialog>
  277. </div>
  278. </template>
  279. <script lang="ts" setup>
  280. import draggable from 'vuedraggable';
  281. import { categoryTree, listBase } from '@/api/pmsProduct/base';
  282. import usePcdiyStore from '@/store/modules/pcdiy';
  283. import { Search } from '@element-plus/icons-vue';
  284. import type { TableInstance } from 'element-plus';
  285. const diyStore = usePcdiyStore();
  286. const multipleTableRef = ref<TableInstance>();
  287. const showDialog = ref(false);
  288. const loading = ref(false);
  289. const tableData = ref<any[]>([]);
  290. const multipleSelection: any = ref([]); // 选中数据
  291. const total = ref(0);
  292. const queryParams = reactive({
  293. pageNum: 1,
  294. pageSize: 10,
  295. itemName: '',
  296. topCategoryId: '',
  297. mediumCategoryId: '',
  298. bottomCategoryId: ''
  299. });
  300. const resultList = ref<any>([]); //单页之前被选中的数据
  301. const categoryOptions = ref<any>([]);
  302. const categoryOptions1 = ref<any>([]);
  303. const navIndex = ref<any>(0);
  304. const defaultProps = {
  305. children: 'children',
  306. label: 'label'
  307. };
  308. const treeProps = {
  309. value: 'id',
  310. label: 'label',
  311. children: 'children'
  312. };
  313. onMounted(() => {
  314. getCategoryTree();
  315. });
  316. /** 搜索 */
  317. const handleQuery = () => {
  318. queryParams.pageNum = 1;
  319. getList();
  320. };
  321. /** 获取列表 */
  322. const getList = async () => {
  323. loading.value = true;
  324. try {
  325. const res = await listBase(queryParams);
  326. tableData.value = res.rows || [];
  327. const result = tableData.value.filter((item: any) => diyStore.editComponent.tabList[navIndex.value].goodsIds.includes(item.id));
  328. resultList.value = result;
  329. nextTick(() => {
  330. result.forEach((item: any) => {
  331. multipleTableRef.value?.toggleRowSelection(item, true);
  332. });
  333. });
  334. total.value = res.total || 0;
  335. } finally {
  336. loading.value = false;
  337. }
  338. };
  339. /** 查询分类树 */
  340. const getCategoryTree = async () => {
  341. categoryOptions.value = [];
  342. categoryOptions1.value = [];
  343. const res = await categoryTree();
  344. const list = res.data || [];
  345. categoryOptions.value = [...list];
  346. categoryOptions1.value = [...list];
  347. categoryOptions.value.unshift({
  348. id: '',
  349. label: '全部'
  350. });
  351. };
  352. //打开弹窗
  353. const openDialog = (res: any) => {
  354. navIndex.value = res;
  355. showDialog.value = true;
  356. getList();
  357. };
  358. const handleNodeClick = (data: any) => {
  359. queryParams.topCategoryId = '';
  360. queryParams.mediumCategoryId = '';
  361. queryParams.bottomCategoryId = '';
  362. if (data.parentId == 0) {
  363. queryParams.topCategoryId = data.id;
  364. } else if (data.children) {
  365. queryParams.mediumCategoryId = data.id;
  366. } else {
  367. queryParams.bottomCategoryId = data.id;
  368. }
  369. handleQuery();
  370. };
  371. // 监听表格单行选中
  372. const handleSelectionChange = (val: []) => {
  373. multipleSelection.value = val;
  374. };
  375. //确定
  376. const onConfirm = () => {
  377. const newIds = calculateNewIds(diyStore.editComponent.tabList[navIndex.value].goodsIds, tableData.value, multipleSelection.value);
  378. diyStore.editComponent.tabList[navIndex.value].goodsIds = newIds;
  379. showDialog.value = false;
  380. };
  381. const calculateNewIds = (cacheIds: any, allPageItems: any, selectedItems: any) => {
  382. // 1. 获取当前页所有存在的 ID 集合 (用于识别哪些旧数据属于当前页)
  383. const currentPageIdSet = new Set(allPageItems.map((item) => item.id));
  384. // 2. 获取最终选中项的 ID 集合
  385. const selectedIdSet = new Set(selectedItems.map((item) => item.id));
  386. // 3. 过滤旧的缓存 IDs
  387. const retainedOldIds = cacheIds.filter((id) => {
  388. // 情况 A: 该 ID 不在当前页数据中 (说明是其他页的数据,必须无条件保留)
  389. if (!currentPageIdSet.has(id)) {
  390. return true;
  391. }
  392. // 情况 B: 该 ID 在当前页数据中,且也在最终选中列表中 (说明用户保持了选中)
  393. if (selectedIdSet.has(id)) {
  394. return true;
  395. }
  396. // 情况 C: 该 ID 在当前页数据中,但不在最终选中列表中 (说明用户取消了选中,如 ID 4)
  397. // 返回 false,将其剔除
  398. return false;
  399. });
  400. // 4. 合并:保留的旧数据 + 当前页新选中的数据
  401. // 使用 Set 去重,虽然逻辑上 retainedOldIds 和 selectedIdSet 不会有交集,但以防万一
  402. const newIdsSet = new Set([...retainedOldIds, ...selectedIdSet]);
  403. // 转回数组 (如果需要保持原有顺序或特定排序,可以在此处调整)
  404. // 这里简单转为数组,通常建议按数字大小排序以便阅读,或者保持插入顺序
  405. return Array.from(newIdsSet).sort((a, b) => a - b);
  406. };
  407. //选择商品分类
  408. const goodsClassifyChange = (res: any, element: any) => {
  409. const foundNode = findNodeByKey(categoryOptions1.value, res);
  410. element.goodsClassify = res;
  411. element.topCategoryId = '';
  412. element.mediumCategoryId = '';
  413. element.bottomCategoryId = '';
  414. if (foundNode.parentId == 0) {
  415. element.topCategoryId = foundNode.id;
  416. } else if (foundNode.children) {
  417. element.mediumCategoryId = foundNode.id;
  418. } else {
  419. element.bottomCategoryId = foundNode.id;
  420. }
  421. };
  422. // 递归查找节点的辅助函数
  423. const findNodeByKey = (nodes: any, key: any) => {
  424. for (const node of nodes) {
  425. if (node.id === key) {
  426. return node;
  427. }
  428. if (node.children) {
  429. const found = findNodeByKey(node.children, key);
  430. if (found) return found;
  431. }
  432. }
  433. return null;
  434. };
  435. const onAdd = () => {
  436. diyStore.editComponent.tabList.push({
  437. title: '',
  438. goodsType: 1,
  439. goodsIds: [],
  440. goodsClassify: '',
  441. topCategoryId: '',
  442. mediumCategoryId: '',
  443. bottomCategoryId: '',
  444. id: Date.now()
  445. });
  446. };
  447. const onDel = (index: any) => {
  448. diyStore.editComponent.tabList.splice(index, 1);
  449. };
  450. </script>
  451. <style lang="scss" scoped>
  452. .pc-edit {
  453. .edit-attr-item-wrap {
  454. border-top: 2px solid var(--el-color-info-light-8);
  455. padding-top: 20px;
  456. &:first-of-type {
  457. border-top: none;
  458. padding-top: 0;
  459. }
  460. .edit-attr-box {
  461. padding: 18px 10px 0 10px;
  462. border: 1px solid #e5e6eb;
  463. border-radius: 4px;
  464. position: relative;
  465. margin-top: 18px;
  466. .images-bos {
  467. flex: 1;
  468. height: 98px;
  469. padding: 5px 0;
  470. }
  471. .images-box {
  472. font-size: 13px;
  473. color: #666;
  474. }
  475. .circleClose {
  476. position: absolute;
  477. top: -9px;
  478. right: -9px;
  479. cursor: pointer;
  480. }
  481. .annotation3 {
  482. font-size: 12px;
  483. color: #666;
  484. line-height: 14px;
  485. }
  486. }
  487. .edit-attr-title {
  488. display: flex;
  489. .title2 {
  490. font-size: 12px;
  491. color: #666;
  492. margin-left: 6px;
  493. }
  494. }
  495. .btnStyle {
  496. min-width: 60px;
  497. padding: 5px;
  498. cursor: pointer;
  499. border-radius: 4px;
  500. margin-right: 4px;
  501. &.btnStyle1 {
  502. border: 1px solid var(--el-color-primary);
  503. }
  504. .btn1 {
  505. background-color: var(--el-color-primary);
  506. padding: 5px 15px;
  507. border-radius: 15px;
  508. font-size: 12px;
  509. color: #ffffff;
  510. line-height: 1;
  511. }
  512. .btn2 {
  513. color: var(--el-color-primary);
  514. border: 1px solid var(--el-color-primary);
  515. height: 26px;
  516. width: 26px;
  517. border-radius: 50%;
  518. }
  519. }
  520. .data-num {
  521. width: 100%;
  522. font-size: 14px;
  523. display: flex;
  524. align-items: center;
  525. justify-content: flex-end;
  526. color: var(--el-color-primary);
  527. cursor: pointer;
  528. }
  529. }
  530. .selected {
  531. line-height: 32px;
  532. position: absolute;
  533. left: 0px;
  534. }
  535. :deep(.el-form-item__label) {
  536. font-weight: 400;
  537. }
  538. .annotation3 {
  539. font-size: 12px;
  540. color: #666;
  541. line-height: 14px;
  542. }
  543. .dialog-bos {
  544. .tree-bos {
  545. max-height: 900px;
  546. overflow: auto;
  547. width: 240px;
  548. margin-right: 10px;
  549. }
  550. }
  551. }
  552. </style>