index.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574
  1. <template>
  2. <div class="floor-ad-page">
  3. <div class="floor-ad-container">
  4. <!-- 搜索区域 -->
  5. <div class="search-card">
  6. <el-form :model="queryParams" :inline="true">
  7. <el-form-item label="楼层名称">
  8. <el-input v-model="queryParams.name" placeholder="请输入楼层名称" clearable style="width: 240px" />
  9. </el-form-item>
  10. <el-form-item>
  11. <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
  12. <el-button icon="Refresh" @click="resetQuery">重置</el-button>
  13. <el-button type="primary" icon="Plus" @click="handleAdd">添加楼层</el-button>
  14. </el-form-item>
  15. </el-form>
  16. </div>
  17. <!-- 表格区域 -->
  18. <div class="table-card">
  19. <div class="table-header">
  20. <span class="table-title">楼层信息列表</span>
  21. <el-button icon="Refresh" circle size="small" @click="getList" />
  22. </div>
  23. <el-table v-loading="loading" :data="floorList" border>
  24. <el-table-column label="楼层名称" align="center" prop="name" min-width="120" />
  25. <el-table-column label="链接地址" align="center" prop="link" min-width="280">
  26. <template #default="scope">
  27. <el-link v-if="scope.row.link" type="primary" :href="scope.row.link" target="_blank">{{ scope.row.link }}</el-link>
  28. <span v-else>-</span>
  29. </template>
  30. </el-table-column>
  31. <el-table-column label="状态" align="center" width="80">
  32. <template #default="scope">
  33. <el-tag :type="scope.row.isShow === '1' ? 'primary' : 'info'" size="small">
  34. {{ scope.row.isShow === '1' ? '显示' : '隐藏' }}
  35. </el-tag>
  36. </template>
  37. </el-table-column>
  38. <el-table-column label="排序" align="center" prop="sort" width="80" />
  39. <el-table-column label="更新时间" align="center" width="180">
  40. <template #default="scope">
  41. {{ scope.row.updateTime || scope.row.createTime || '-' }}
  42. </template>
  43. </el-table-column>
  44. <el-table-column label="操作" align="center" width="200">
  45. <template #default="scope">
  46. <span class="action-link primary" @click="handleEdit(scope.row)">编辑</span>
  47. <span class="action-link primary" @click="handleConfigProduct(scope.row)">配置商品</span>
  48. <span class="action-link danger" @click="handleDelete(scope.row)">删除</span>
  49. </template>
  50. </el-table-column>
  51. </el-table>
  52. <pagination
  53. v-show="total > 0"
  54. v-model:page="queryParams.pageNum"
  55. v-model:limit="queryParams.pageSize"
  56. :total="total"
  57. @pagination="getList"
  58. />
  59. </div>
  60. </div>
  61. <!-- 添加/编辑对话框 -->
  62. <el-dialog v-model="dialog.visible" :title="dialog.title" width="700px" append-to-body>
  63. <el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
  64. <el-row :gutter="20">
  65. <el-col :span="12">
  66. <el-form-item label="楼层名称" prop="name">
  67. <el-input v-model="form.name" placeholder="请输入楼层名称" />
  68. </el-form-item>
  69. </el-col>
  70. <el-col :span="12">
  71. <el-form-item label="链接地址" prop="link">
  72. <el-input v-model="form.link" placeholder="请输入链接地址" />
  73. </el-form-item>
  74. </el-col>
  75. </el-row>
  76. <el-row :gutter="20">
  77. <el-col :span="12">
  78. <el-form-item label="排序" prop="sort">
  79. <el-input-number v-model="form.sort" :min="0" :max="999" controls-position="right" style="width: 100%" />
  80. </el-form-item>
  81. </el-col>
  82. <el-col :span="12">
  83. <el-form-item label="状态" prop="isShow">
  84. <el-switch v-model="form.isShow" active-value="1" inactive-value="0" active-text="显示" />
  85. </el-form-item>
  86. </el-col>
  87. </el-row>
  88. <el-form-item label="封面图片" prop="mainImg">
  89. <upload-image v-model="form.mainImg" :limit="1" />
  90. </el-form-item>
  91. </el-form>
  92. <template #footer>
  93. <el-button type="primary" @click="submitForm">确 认</el-button>
  94. <el-button @click="dialog.visible = false">取 消</el-button>
  95. </template>
  96. </el-dialog>
  97. <!-- 配置商品对话框 -->
  98. <el-dialog v-model="productDialog.visible" title="推荐商品" width="900px" append-to-body>
  99. <div class="product-dialog-header">
  100. <el-button type="primary" @click="handleAddProduct">新增商品</el-button>
  101. <el-button type="primary" @click="handleImportProduct">导入商品</el-button>
  102. <el-button icon="Refresh" circle size="small" @click="getLinkedProducts" style="margin-left: auto" />
  103. </div>
  104. <el-table v-loading="productDialog.loading" :data="linkedProducts" border>
  105. <el-table-column label="商品编号" align="center" prop="productNo" width="120" />
  106. <el-table-column label="商品名称" align="center" prop="itemName" min-width="200" show-overflow-tooltip />
  107. <el-table-column label="商品图片" align="center" width="100">
  108. <template #default="scope">
  109. <el-image v-if="scope.row.productImage" :src="scope.row.productImage" fit="cover" style="width: 60px; height: 60px" />
  110. <span v-else>-</span>
  111. </template>
  112. </el-table-column>
  113. <el-table-column label="价格" align="center" width="100">
  114. <template #default="scope">
  115. {{ scope.row.minSellingPrice || scope.row.marketPrice || '-' }}
  116. </template>
  117. </el-table-column>
  118. <el-table-column label="操作" align="center" width="80">
  119. <template #default="scope">
  120. <span class="action-link danger" @click="handleRemoveLinked(scope.row)">删除</span>
  121. </template>
  122. </el-table-column>
  123. </el-table>
  124. <template #footer>
  125. <el-button type="primary" @click="productDialog.visible = false">确 认</el-button>
  126. <el-button @click="productDialog.visible = false">取 消</el-button>
  127. </template>
  128. </el-dialog>
  129. <!-- 导入商品对话框 -->
  130. <el-dialog v-model="importProductDialog.visible" title="导入商品" width="500px" append-to-body>
  131. <el-form label-width="100px">
  132. <el-form-item label="商品编号">
  133. <el-input
  134. v-model="importProductDialog.productNos"
  135. type="textarea"
  136. :rows="5"
  137. placeholder="请输入商品编号,多个用逗号隔开"
  138. />
  139. </el-form-item>
  140. </el-form>
  141. <template #footer>
  142. <el-button type="primary" @click="confirmImportProducts">确 定</el-button>
  143. <el-button @click="importProductDialog.visible = false">取 消</el-button>
  144. </template>
  145. </el-dialog>
  146. <!-- 选择商品对话框 -->
  147. <el-dialog v-model="selectDialog.visible" title="商品信息" width="900px" append-to-body>
  148. <div class="select-dialog-header">
  149. <el-input v-model="selectDialog.keyword" placeholder="请输入商品编号名称输入搜索" style="width: 300px" />
  150. <el-button type="primary" @click="searchProducts">搜 索</el-button>
  151. </div>
  152. <el-table v-loading="selectDialog.loading" :data="productList" border @selection-change="handleSelectionChange">
  153. <el-table-column type="selection" width="50" align="center" />
  154. <el-table-column label="商品编号" align="center" prop="productNo" width="120" />
  155. <el-table-column label="商品名称" align="center" prop="itemName" min-width="180" show-overflow-tooltip />
  156. <el-table-column label="商品图片" align="center" width="100">
  157. <template #default="scope">
  158. <el-image v-if="scope.row.productImage" :src="scope.row.productImage" fit="cover" style="width: 60px; height: 60px" />
  159. <span v-else>-</span>
  160. </template>
  161. </el-table-column>
  162. <el-table-column label="价格" align="center" width="100">
  163. <template #default="scope">
  164. {{ scope.row.minSellingPrice || scope.row.marketPrice || '-' }}
  165. </template>
  166. </el-table-column>
  167. <el-table-column label="排序" align="center" width="100">
  168. <template #default="scope">
  169. <el-input-number v-model="scope.row.sort" :min="0" size="small" controls-position="right" style="width: 80px" />
  170. </template>
  171. </el-table-column>
  172. </el-table>
  173. <pagination
  174. v-show="selectDialog.total > 0"
  175. v-model:page="selectDialog.pageNum"
  176. v-model:limit="selectDialog.pageSize"
  177. :total="selectDialog.total"
  178. @pagination="getProductList"
  179. />
  180. <template #footer>
  181. <el-button type="primary" @click="confirmSelectProducts">确 定</el-button>
  182. <el-button @click="selectDialog.visible = false">取 消</el-button>
  183. </template>
  184. </el-dialog>
  185. </div>
  186. </template>
  187. <script setup name="GiftFloorAd" lang="ts">
  188. import { ref, reactive, onMounted, getCurrentInstance } from 'vue';
  189. import type { ComponentInternalInstance } from 'vue';
  190. import type { FormInstance } from 'element-plus';
  191. import { listGiftFloor, addGiftFloor, updateGiftFloor, delGiftFloor } from '@/api/product/giftFloor';
  192. import { listGiftFloorLinkWithProduct, addGiftFloorLink, delGiftFloorLink } from '@/api/product/giftFloorLink';
  193. import { listProduct } from '@/api/product/productBase';
  194. const { proxy } = getCurrentInstance() as ComponentInternalInstance;
  195. // 楼层类型:1=楼层广告
  196. const FLOOR_TYPE = 1;
  197. const queryParams = reactive({
  198. name: '',
  199. type: FLOOR_TYPE,
  200. pageNum: 1,
  201. pageSize: 10
  202. });
  203. const floorList = ref<any[]>([]);
  204. const loading = ref(false);
  205. const total = ref(0);
  206. const dialog = reactive({
  207. visible: false,
  208. title: ''
  209. });
  210. const formRef = ref<FormInstance>();
  211. const initForm = {
  212. id: undefined as number | undefined,
  213. name: '',
  214. link: '',
  215. mainImg: '',
  216. sort: 0,
  217. isShow: '1',
  218. type: FLOOR_TYPE
  219. };
  220. const form = ref({ ...initForm });
  221. const rules = {
  222. name: [{ required: true, message: '请输入楼层名称', trigger: 'blur' }]
  223. };
  224. // 配置商品弹框
  225. const productDialog = reactive({
  226. visible: false,
  227. loading: false,
  228. floorId: null as number | null,
  229. floorName: ''
  230. });
  231. const linkedProducts = ref<any[]>([]);
  232. // 导入商品弹框
  233. const importProductDialog = reactive({
  234. visible: false,
  235. productNos: ''
  236. });
  237. // 选择商品弹框
  238. const selectDialog = reactive({
  239. visible: false,
  240. loading: false,
  241. keyword: '',
  242. pageNum: 1,
  243. pageSize: 10,
  244. total: 0
  245. });
  246. const productList = ref<any[]>([]);
  247. const selectedProducts = ref<any[]>([]);
  248. const getList = async () => {
  249. loading.value = true;
  250. try {
  251. const res = await listGiftFloor(queryParams);
  252. floorList.value = res.rows || [];
  253. total.value = res.total || 0;
  254. } catch (error) {
  255. console.error('加载列表失败', error);
  256. } finally {
  257. loading.value = false;
  258. }
  259. };
  260. const reset = () => {
  261. form.value = { ...initForm };
  262. formRef.value?.resetFields();
  263. };
  264. const handleQuery = () => {
  265. queryParams.pageNum = 1;
  266. getList();
  267. };
  268. const resetQuery = () => {
  269. queryParams.name = '';
  270. handleQuery();
  271. };
  272. const handleAdd = () => {
  273. reset();
  274. dialog.title = '添加楼层';
  275. dialog.visible = true;
  276. };
  277. const handleEdit = (row: any) => {
  278. reset();
  279. form.value = {
  280. id: row.id,
  281. name: row.name || '',
  282. link: row.link || '',
  283. mainImg: row.mainImg || '',
  284. sort: row.sort || 0,
  285. isShow: row.isShow || '1',
  286. type: FLOOR_TYPE
  287. };
  288. dialog.title = '编辑楼层';
  289. dialog.visible = true;
  290. };
  291. const handleConfigProduct = (row: any) => {
  292. productDialog.floorId = row.id;
  293. productDialog.floorName = row.name;
  294. productDialog.visible = true;
  295. getLinkedProducts();
  296. };
  297. // 获取已关联的商品(调用后端联表查询接口)
  298. const getLinkedProducts = async () => {
  299. if (!productDialog.floorId) return;
  300. productDialog.loading = true;
  301. try {
  302. const res = await listGiftFloorLinkWithProduct(productDialog.floorId);
  303. linkedProducts.value = res.data || [];
  304. } catch (error) {
  305. console.error('加载关联商品失败', error);
  306. } finally {
  307. productDialog.loading = false;
  308. }
  309. };
  310. // 删除关联商品
  311. const handleRemoveLinked = (row: any) => {
  312. proxy?.$modal.confirm('是否确认删除该商品?').then(() => {
  313. delGiftFloorLink(row.id).then(() => {
  314. proxy?.$modal.msgSuccess('删除成功');
  315. getLinkedProducts();
  316. });
  317. });
  318. };
  319. // 新增商品
  320. const handleAddProduct = () => {
  321. selectDialog.keyword = '';
  322. selectDialog.pageNum = 1;
  323. selectDialog.visible = true;
  324. getProductList();
  325. };
  326. // 导入商品(打开导入弹框)
  327. const handleImportProduct = () => {
  328. importProductDialog.productNos = '';
  329. importProductDialog.visible = true;
  330. };
  331. // 确认导入商品
  332. const confirmImportProducts = async () => {
  333. const input = importProductDialog.productNos.trim();
  334. if (!input) {
  335. proxy?.$modal.msgWarning('请输入商品编号');
  336. return;
  337. }
  338. // 解析商品编号(支持逗号、中文逗号、空格、换行分隔)
  339. const productNos = input.split(/[,,\s\n]+/).map((s: string) => s.trim()).filter(Boolean);
  340. if (productNos.length === 0) {
  341. proxy?.$modal.msgWarning('请输入有效的商品编号');
  342. return;
  343. }
  344. // 检查重复
  345. const existingProductNos = linkedProducts.value.map((item: any) => item.productNo);
  346. const newProductNos = productNos.filter((no: string) => !existingProductNos.includes(no));
  347. if (newProductNos.length === 0) {
  348. proxy?.$modal.msgWarning('所有商品编号已存在,请勿重复添加');
  349. return;
  350. }
  351. try {
  352. // 先查询商品信息获取 productId
  353. const productRes = await listProduct({ productNos: newProductNos.join(','), pageSize: 1000 });
  354. const productMap = new Map<string, any>((productRes.rows || []).map((p: any) => [p.productNo, p]));
  355. for (const productNo of newProductNos) {
  356. const product = productMap.get(productNo);
  357. if (!product) {
  358. console.warn(`商品编号 ${productNo} 不存在`);
  359. continue;
  360. }
  361. await addGiftFloorLink({
  362. floorId: productDialog.floorId,
  363. productId: product.id,
  364. productNo: productNo,
  365. sort: 0,
  366. status: '0'
  367. });
  368. }
  369. const skipped = productNos.length - newProductNos.length;
  370. if (skipped > 0) {
  371. proxy?.$modal.msgSuccess(`导入成功,${skipped}个商品已存在被跳过`);
  372. } else {
  373. proxy?.$modal.msgSuccess('导入成功');
  374. }
  375. importProductDialog.visible = false;
  376. getLinkedProducts();
  377. } catch (error) {
  378. proxy?.$modal.msgError('导入失败');
  379. }
  380. };
  381. // 获取商品列表
  382. const getProductList = async () => {
  383. selectDialog.loading = true;
  384. try {
  385. const res = await listProduct({
  386. keyword: selectDialog.keyword,
  387. pageNum: selectDialog.pageNum,
  388. pageSize: selectDialog.pageSize
  389. });
  390. productList.value = (res.rows || []).map((item: any) => ({ ...item, sort: 0 }));
  391. selectDialog.total = res.total || 0;
  392. } catch (error) {
  393. console.error('加载商品列表失败', error);
  394. } finally {
  395. selectDialog.loading = false;
  396. }
  397. };
  398. // 搜索商品
  399. const searchProducts = () => {
  400. selectDialog.pageNum = 1;
  401. getProductList();
  402. };
  403. // 选择变化
  404. const handleSelectionChange = (selection: any[]) => {
  405. selectedProducts.value = selection;
  406. };
  407. // 确认选择商品
  408. const confirmSelectProducts = async () => {
  409. if (selectedProducts.value.length === 0) {
  410. proxy?.$modal.msgWarning('请选择商品');
  411. return;
  412. }
  413. // 检查是否有重复商品
  414. const existingProductNos = linkedProducts.value.map((item: any) => item.productNo);
  415. const duplicates = selectedProducts.value.filter((p: any) => existingProductNos.includes(p.productNo));
  416. if (duplicates.length > 0) {
  417. const duplicateNames = duplicates.map((p: any) => p.itemName || p.productNo).join('、');
  418. proxy?.$modal.msgWarning(`商品 ${duplicateNames} 已存在,请勿重复添加`);
  419. return;
  420. }
  421. try {
  422. for (const product of selectedProducts.value) {
  423. await addGiftFloorLink({
  424. floorId: productDialog.floorId,
  425. productId: product.id,
  426. productNo: product.productNo,
  427. sort: product.sort || 0,
  428. status: '0'
  429. });
  430. }
  431. proxy?.$modal.msgSuccess('添加成功');
  432. selectDialog.visible = false;
  433. getLinkedProducts();
  434. } catch (error) {
  435. proxy?.$modal.msgError('添加失败');
  436. }
  437. };
  438. const handleDelete = (row: any) => {
  439. proxy?.$modal.confirm(`是否确认删除楼层"${row.name}"?`).then(() => {
  440. delGiftFloor(row.id).then(() => {
  441. proxy?.$modal.msgSuccess('删除成功');
  442. getList();
  443. });
  444. });
  445. };
  446. const submitForm = () => {
  447. formRef.value?.validate((valid: boolean) => {
  448. if (valid) {
  449. const api = form.value.id ? updateGiftFloor : addGiftFloor;
  450. api(form.value).then(() => {
  451. proxy?.$modal.msgSuccess(form.value.id ? '修改成功' : '添加成功');
  452. dialog.visible = false;
  453. getList();
  454. });
  455. }
  456. });
  457. };
  458. onMounted(() => {
  459. getList();
  460. });
  461. </script>
  462. <style scoped lang="scss">
  463. .floor-ad-page {
  464. min-height: 100vh;
  465. background: #f5f5f5;
  466. padding: 20px;
  467. }
  468. .floor-ad-container {
  469. max-width: 1200px;
  470. margin: 0 auto;
  471. }
  472. .search-card {
  473. background: #fff;
  474. border-radius: 4px;
  475. padding: 20px;
  476. margin-bottom: 12px;
  477. }
  478. .table-card {
  479. background: #fff;
  480. border-radius: 4px;
  481. padding: 20px;
  482. }
  483. .table-header {
  484. display: flex;
  485. justify-content: space-between;
  486. align-items: center;
  487. margin-bottom: 15px;
  488. }
  489. .table-title {
  490. font-size: 16px;
  491. font-weight: 600;
  492. color: #303133;
  493. }
  494. .action-link {
  495. cursor: pointer;
  496. margin: 0 6px;
  497. &.primary {
  498. color: #409eff;
  499. &:hover { color: #66b1ff; }
  500. }
  501. &.danger {
  502. color: #f56c6c;
  503. &:hover { color: #f78989; }
  504. }
  505. }
  506. .product-dialog-header {
  507. display: flex;
  508. align-items: center;
  509. gap: 10px;
  510. margin-bottom: 15px;
  511. }
  512. .select-dialog-header {
  513. display: flex;
  514. align-items: center;
  515. gap: 10px;
  516. margin-bottom: 15px;
  517. }
  518. </style>