GoodsSelectDialog.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  1. <template>
  2. <ele-modal
  3. :width="width"
  4. v-model="visible"
  5. :title="title"
  6. position="center"
  7. :body-style="{ padding: '0 20px 20px' }"
  8. >
  9. <!-- Search -->
  10. <div class="p-0">
  11. <el-form :inline="true" :model="searchForm" label-width="0px">
  12. <el-form-item>
  13. <el-input
  14. v-model="searchForm.bookName"
  15. placeholder="请输入商品名称"
  16. clearable
  17. />
  18. </el-form-item>
  19. <el-form-item>
  20. <el-input
  21. v-model="searchForm.isbn"
  22. placeholder="请输入ISBN"
  23. clearable
  24. />
  25. </el-form-item>
  26. <el-form-item>
  27. <el-button type="primary" @click="handleSearch"
  28. >查询</el-button
  29. >
  30. <el-button @click="handleReset">重置</el-button>
  31. </el-form-item>
  32. </el-form>
  33. </div>
  34. <div class="flex justify-between items-end mb-4">
  35. <!-- Tabs -->
  36. <div class="flex space-x-6 border-b border-gray-200 w-64 mr-4">
  37. <div
  38. class="cursor-pointer pb-2 px-2 whitespace-nowrap"
  39. :class="
  40. activeTab === 'all'
  41. ? 'text-primary border-b-2 border-primary font-medium'
  42. : 'text-gray-600'
  43. "
  44. @click="activeTab = 'all'"
  45. >全部</div
  46. >
  47. <div
  48. class="cursor-pointer pb-2 px-2 whitespace-nowrap"
  49. :class="
  50. activeTab === 'selected'
  51. ? 'text-primary border-b-2 border-primary font-medium'
  52. : 'text-gray-600'
  53. "
  54. @click="activeTab = 'selected'"
  55. >已选择 ({{ selectedCount }})</div
  56. >
  57. </div>
  58. <!-- Pagination -->
  59. <el-pagination
  60. v-if="activeTab === 'all'"
  61. v-model:current-page="currentPage"
  62. v-model:page-size="pageSize"
  63. :page-sizes="[10, 20, 50, 100]"
  64. layout="total, prev, pager, next, jumper"
  65. :total="total"
  66. @size-change="handleSizeChange"
  67. @current-change="handleCurrentChange"
  68. class="shrink-0"
  69. />
  70. <!-- Empty div to maintain flex-between layout when pagination is hidden -->
  71. <div v-else class="shrink-0"></div>
  72. </div>
  73. <!-- Table for All -->
  74. <el-table
  75. v-show="activeTab === 'all'"
  76. ref="tableRef"
  77. :data="tableData"
  78. row-key="isbn"
  79. v-loading="loading"
  80. @selection-change="handleSelectionChange"
  81. border
  82. height="400px"
  83. >
  84. <el-table-column
  85. type="selection"
  86. width="55"
  87. align="center"
  88. :reserve-selection="true"
  89. />
  90. <el-table-column label="图示" width="80" align="center">
  91. <template #default="{ row }">
  92. <el-image
  93. :src="row.cover"
  94. class="w-10 h-10 object-cover"
  95. :preview-src-list="[row.cover]"
  96. preview-teleported
  97. >
  98. <template #error>
  99. <div
  100. class="w-10 h-10 bg-gray-100 flex items-center justify-center text-gray-400"
  101. >
  102. <el-icon><Picture /></el-icon>
  103. </div>
  104. </template>
  105. </el-image>
  106. </template>
  107. </el-table-column>
  108. <el-table-column
  109. prop="isbn"
  110. label="ISBN"
  111. width="140"
  112. align="center"
  113. />
  114. <el-table-column
  115. prop="bookName"
  116. label="书名"
  117. min-width="200"
  118. show-overflow-tooltip
  119. />
  120. <template v-if="showDescription">
  121. <el-table-column label="商品描述" min-width="200">
  122. <template #default="{ row }">
  123. <div
  124. v-if="!row._editingDesc"
  125. class="cursor-pointer flex items-center"
  126. @click="row._editingDesc = true"
  127. >
  128. <span
  129. class="truncate flex-1"
  130. :title="row.bookDesc || '点击输入描述'"
  131. >{{ row.bookDesc || '点击输入描述' }}</span
  132. >
  133. <el-icon class="ml-1 text-gray-400"
  134. ><Edit
  135. /></el-icon>
  136. </div>
  137. <el-input
  138. v-else
  139. v-model="row.bookDesc"
  140. placeholder="请输入商品描述"
  141. size="small"
  142. @blur="row._editingDesc = false"
  143. @keyup.enter="row._editingDesc = false"
  144. @input="(val) => updateSelectionDesc(row, val)"
  145. autofocus
  146. />
  147. </template>
  148. </el-table-column>
  149. </template>
  150. <template v-else>
  151. <el-table-column
  152. prop="author"
  153. label="作者"
  154. width="120"
  155. show-overflow-tooltip
  156. />
  157. <el-table-column
  158. prop="price"
  159. label="价格"
  160. width="100"
  161. align="center"
  162. />
  163. </template>
  164. </el-table>
  165. <!-- Table for Selected -->
  166. <el-table
  167. v-show="activeTab === 'selected'"
  168. :data="currentSelections"
  169. row-key="isbn"
  170. border
  171. height="400px"
  172. >
  173. <el-table-column width="55" align="center">
  174. <template #header>
  175. <el-checkbox
  176. :model-value="true"
  177. @change="handleClearAllSelections"
  178. />
  179. </template>
  180. <template #default="{ row }">
  181. <el-checkbox
  182. :model-value="true"
  183. @change="() => handleRemoveSelection(row)"
  184. />
  185. </template>
  186. </el-table-column>
  187. <el-table-column label="图示" width="80" align="center">
  188. <template #default="{ row }">
  189. <el-image
  190. :src="row.cover"
  191. class="w-10 h-10 object-cover"
  192. :preview-src-list="[row.cover]"
  193. preview-teleported
  194. >
  195. <template #error>
  196. <div
  197. class="w-10 h-10 bg-gray-100 flex items-center justify-center text-gray-400"
  198. >
  199. <el-icon><Picture /></el-icon>
  200. </div>
  201. </template>
  202. </el-image>
  203. </template>
  204. </el-table-column>
  205. <el-table-column
  206. prop="isbn"
  207. label="ISBN"
  208. width="140"
  209. align="center"
  210. />
  211. <el-table-column
  212. prop="bookName"
  213. label="书名"
  214. min-width="200"
  215. show-overflow-tooltip
  216. />
  217. <template v-if="showDescription">
  218. <el-table-column label="商品描述" min-width="200">
  219. <template #default="{ row }">
  220. <div
  221. v-if="!row._editingDesc"
  222. class="cursor-pointer flex items-center"
  223. @click="row._editingDesc = true"
  224. >
  225. <span
  226. class="truncate flex-1"
  227. :title="row.bookDesc || '点击输入描述'"
  228. >{{ row.bookDesc || '点击输入描述' }}</span
  229. >
  230. <el-icon class="ml-1 text-gray-400"
  231. ><Edit
  232. /></el-icon>
  233. </div>
  234. <el-input
  235. v-else
  236. v-model="row.bookDesc"
  237. placeholder="请输入商品描述"
  238. size="small"
  239. @blur="row._editingDesc = false"
  240. @keyup.enter="row._editingDesc = false"
  241. @input="(val) => updateSelectionDesc(row, val)"
  242. autofocus
  243. />
  244. </template>
  245. </el-table-column>
  246. </template>
  247. <template v-else>
  248. <el-table-column
  249. prop="author"
  250. label="作者"
  251. width="120"
  252. show-overflow-tooltip
  253. />
  254. <el-table-column
  255. prop="price"
  256. label="价格"
  257. width="100"
  258. align="center"
  259. />
  260. </template>
  261. </el-table>
  262. <template #footer>
  263. <div class="flex justify-end items-center">
  264. <el-button @click="handleCancel">取消</el-button>
  265. <el-button type="primary" @click="handleConfirm"
  266. >确定</el-button
  267. >
  268. </div>
  269. </template>
  270. </ele-modal>
  271. </template>
  272. <script setup>
  273. import { ref, reactive, watch, computed, nextTick } from 'vue';
  274. import { Picture, Edit } from '@element-plus/icons-vue';
  275. import request from '@/utils/request';
  276. const props = defineProps({
  277. title: { type: String, default: '添加商品' },
  278. width: { type: String, default: '900px' },
  279. defaultSelected: { type: Array, default: () => [] },
  280. showDescription: { type: Boolean, default: false }
  281. });
  282. const emit = defineEmits(['update:modelValue', 'confirm']);
  283. const visible = defineModel({ type: Boolean });
  284. const tableRef = ref(null);
  285. const searchForm = reactive({
  286. bookName: '',
  287. isbn: ''
  288. });
  289. const activeTab = ref('all');
  290. const currentSelections = ref([]);
  291. const selectedCount = computed(() => currentSelections.value.length);
  292. // Pagination for 'all'
  293. const tableData = ref([]);
  294. const total = ref(0);
  295. const currentPage = ref(1);
  296. const pageSize = ref(10);
  297. const loading = ref(false);
  298. const handleSearch = async () => {
  299. if (activeTab.value === 'selected') {
  300. return;
  301. }
  302. loading.value = true;
  303. try {
  304. const params = {
  305. ...searchForm,
  306. pageNum: currentPage.value,
  307. pageSize: pageSize.value
  308. };
  309. console.log('Search params:', params);
  310. const res = await request.get('/book/bookInfo/list', { params });
  311. console.log('API response:', res);
  312. if (res.data.code === 200) {
  313. const data = res.data.data || res.data;
  314. const rows = data.rows || data;
  315. const updateRowDesc = (row) => {
  316. const selectedItem = currentSelections.value.find(
  317. (s) => s.isbn === row.isbn
  318. );
  319. if (selectedItem && selectedItem.bookDesc !== undefined) {
  320. row.bookDesc = selectedItem.bookDesc;
  321. return;
  322. }
  323. const defaultItem = props.defaultSelected.find(
  324. (d) => d.isbn === row.isbn
  325. );
  326. if (defaultItem) {
  327. row.bookDesc = defaultItem.bookDesc;
  328. }
  329. };
  330. rows.forEach(updateRowDesc);
  331. tableData.value = rows;
  332. total.value = data.total || rows.length;
  333. console.log(
  334. 'Updated tableData:',
  335. tableData.value.length,
  336. 'rows, total:',
  337. total.value
  338. );
  339. // Sync selections to table
  340. nextTick(() => {
  341. if (tableRef.value) {
  342. tableRef.value.clearSelection();
  343. rows.forEach((row) => {
  344. const isSelected = currentSelections.value.some(
  345. (s) => s.isbn === row.isbn
  346. );
  347. if (isSelected) {
  348. tableRef.value.toggleRowSelection(row, true);
  349. }
  350. });
  351. }
  352. });
  353. }
  354. } catch (e) {
  355. console.error('Search error:', e);
  356. } finally {
  357. loading.value = false;
  358. }
  359. };
  360. const handleSizeChange = (val) => {
  361. pageSize.value = val;
  362. handleSearch();
  363. };
  364. const handleCurrentChange = (val) => {
  365. currentPage.value = val;
  366. handleSearch();
  367. };
  368. const handleReset = () => {
  369. searchForm.bookName = '';
  370. searchForm.isbn = '';
  371. currentPage.value = 1;
  372. handleSearch();
  373. };
  374. const reset = () => {
  375. searchForm.bookName = '';
  376. searchForm.isbn = '';
  377. activeTab.value = 'all';
  378. currentSelections.value = [];
  379. currentPage.value = 1;
  380. if (tableRef.value) {
  381. tableRef.value.clearSelection();
  382. }
  383. handleSearch();
  384. };
  385. const handleSelectionChange = (rows) => {
  386. // Get all selected rows from the table (only current page)
  387. const currentPageSelectedIsbns = new Set(rows.map((row) => row.isbn));
  388. // Remove rows that are no longer selected on current page
  389. currentSelections.value = currentSelections.value.filter(
  390. (s) =>
  391. currentPageSelectedIsbns.has(s.isbn) ||
  392. !tableData.value.some((r) => r.isbn === s.isbn)
  393. );
  394. // Add newly selected rows from current page
  395. rows.forEach((row) => {
  396. if (!currentSelections.value.some((s) => s.isbn === row.isbn)) {
  397. currentSelections.value.push({ ...row });
  398. }
  399. });
  400. };
  401. const handleClearAllSelections = (val) => {
  402. if (!val) {
  403. // User clicked to uncheck
  404. currentSelections.value = [];
  405. if (tableRef.value) {
  406. tableRef.value.clearSelection();
  407. }
  408. }
  409. };
  410. const handleRemoveSelection = (row) => {
  411. const idx = currentSelections.value.findIndex(
  412. (s) => s.isbn === row.isbn
  413. );
  414. if (idx !== -1) {
  415. currentSelections.value.splice(idx, 1);
  416. }
  417. // Also untoggle in table if it's currently loaded
  418. if (tableRef.value) {
  419. const tableRow = tableData.value.find((r) => r.isbn === row.isbn);
  420. if (tableRow) {
  421. tableRef.value.toggleRowSelection(tableRow, false);
  422. }
  423. }
  424. };
  425. const updateSelectionDesc = (row, val) => {
  426. const selected = currentSelections.value.find(
  427. (s) => s.isbn === row.isbn
  428. );
  429. if (selected) {
  430. selected.bookDesc = val;
  431. }
  432. // Also update the row in tableData if it exists
  433. const tableRow = tableData.value.find((r) => r.isbn === row.isbn);
  434. if (tableRow) {
  435. tableRow.bookDesc = val;
  436. }
  437. };
  438. const handleCancel = () => {
  439. visible.value = false;
  440. };
  441. const handleConfirm = () => {
  442. emit('confirm', currentSelections.value);
  443. visible.value = false;
  444. };
  445. watch(visible, (val) => {
  446. if (val) {
  447. nextTick(() => {
  448. if (props.defaultSelected && props.defaultSelected.length > 0) {
  449. currentSelections.value = JSON.parse(
  450. JSON.stringify(props.defaultSelected)
  451. );
  452. } else {
  453. currentSelections.value = [];
  454. }
  455. currentPage.value = 1;
  456. handleSearch();
  457. });
  458. }
  459. });
  460. defineExpose({
  461. reset
  462. });
  463. </script>