index.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  1. <template>
  2. <!-- 搜索工作栏 -->
  3. <ContentWrap>
  4. <el-form
  5. ref="queryFormRef"
  6. :inline="true"
  7. :model="queryParams"
  8. class="-mb-15px"
  9. label-width="68px"
  10. >
  11. <el-form-item label="商品名称" prop="name">
  12. <el-input
  13. v-model="queryParams.name"
  14. class="!w-240px"
  15. clearable
  16. placeholder="请输入商品名称"
  17. @keyup.enter="handleQuery"
  18. />
  19. </el-form-item>
  20. <el-form-item label="商品分类" prop="categoryId">
  21. <el-cascader
  22. v-model="queryParams.categoryId"
  23. :options="categoryList"
  24. :props="defaultProps"
  25. class="w-1/1"
  26. clearable
  27. filterable
  28. placeholder="请选择商品分类"
  29. />
  30. </el-form-item>
  31. <el-form-item label="创建时间" prop="createTime">
  32. <el-date-picker
  33. v-model="queryParams.createTime"
  34. :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
  35. class="!w-240px"
  36. end-placeholder="结束日期"
  37. start-placeholder="开始日期"
  38. type="daterange"
  39. value-format="YYYY-MM-DD HH:mm:ss"
  40. />
  41. </el-form-item>
  42. <el-form-item>
  43. <el-button @click="handleQuery">
  44. <Icon class="mr-5px" icon="ep:search" />
  45. 搜索
  46. </el-button>
  47. <el-button @click="resetQuery">
  48. <Icon class="mr-5px" icon="ep:refresh" />
  49. 重置
  50. </el-button>
  51. <el-button
  52. v-hasPermi="['product:spu:create']"
  53. plain
  54. type="primary"
  55. @click="openForm(undefined)"
  56. >
  57. <Icon class="mr-5px" icon="ep:plus" />
  58. 新增
  59. </el-button>
  60. <el-button
  61. v-hasPermi="['product:spu:export']"
  62. :loading="exportLoading"
  63. plain
  64. type="success"
  65. @click="handleExport"
  66. >
  67. <Icon class="mr-5px" icon="ep:download" />
  68. 导出
  69. </el-button>
  70. </el-form-item>
  71. </el-form>
  72. </ContentWrap>
  73. <!-- 列表 -->
  74. <ContentWrap>
  75. <el-tabs v-model="queryParams.tabType" @tab-click="handleTabClick">
  76. <el-tab-pane
  77. v-for="item in tabsData"
  78. :key="item.type"
  79. :label="item.name + '(' + item.count + ')'"
  80. :name="item.type"
  81. />
  82. </el-tabs>
  83. <el-table v-loading="loading" :data="list">
  84. <el-table-column type="expand">
  85. <template #default="{ row }">
  86. <el-form class="spu-table-expand" label-position="left">
  87. <el-row>
  88. <el-col :span="24">
  89. <el-row>
  90. <el-col :span="8">
  91. <el-form-item label="商品分类:">
  92. <span>{{ formatCategoryName(row.categoryId) }}</span>
  93. </el-form-item>
  94. </el-col>
  95. <el-col :span="8">
  96. <el-form-item label="市场价:">
  97. <span>{{ fenToYuan(row.marketPrice) }}</span>
  98. </el-form-item>
  99. </el-col>
  100. <el-col :span="8">
  101. <el-form-item label="成本价:">
  102. <span>{{ fenToYuan(row.costPrice) }}</span>
  103. </el-form-item>
  104. </el-col>
  105. </el-row>
  106. </el-col>
  107. </el-row>
  108. <el-row>
  109. <el-col :span="24">
  110. <el-row>
  111. <el-col :span="8">
  112. <el-form-item label="浏览量:">
  113. <span>{{ row.browseCount }}</span>
  114. </el-form-item>
  115. </el-col>
  116. <el-col :span="8">
  117. <el-form-item label="虚拟销量:">
  118. <span>{{ row.virtualSalesCount }}</span>
  119. </el-form-item>
  120. </el-col>
  121. </el-row>
  122. </el-col>
  123. </el-row>
  124. </el-form>
  125. </template>
  126. </el-table-column>
  127. <el-table-column align="center" label="商品编号" min-width="60" prop="id" />
  128. <el-table-column label="商品图" min-width="80">
  129. <template #default="{ row }">
  130. <el-image :src="row.picUrl" class="h-30px w-30px" @click="imagePreview(row.picUrl)" />
  131. </template>
  132. </el-table-column>
  133. <el-table-column :show-overflow-tooltip="true" label="商品名称" min-width="300" prop="name" />
  134. <el-table-column align="center" label="商品售价" min-width="90" prop="price">
  135. <template #default="{ row }"> {{ fenToYuan(row.price) }}元</template>
  136. </el-table-column>
  137. <el-table-column align="center" label="销量" min-width="90" prop="salesCount" />
  138. <el-table-column align="center" label="库存" min-width="90" prop="stock" />
  139. <el-table-column align="center" label="排序" min-width="70" prop="sort" />
  140. <el-table-column
  141. :formatter="dateFormatter"
  142. align="center"
  143. label="创建时间"
  144. prop="createTime"
  145. width="180"
  146. />
  147. <el-table-column align="center" label="状态" min-width="80">
  148. <template #default="{ row }">
  149. <template v-if="row.status >= 0">
  150. <el-switch
  151. v-model="row.status"
  152. :active-value="1"
  153. :inactive-value="0"
  154. active-text="上架"
  155. inactive-text="下架"
  156. inline-prompt
  157. @change="handleStatusChange(row)"
  158. />
  159. </template>
  160. <template v-else>
  161. <el-tag type="info">回收站</el-tag>
  162. </template>
  163. </template>
  164. </el-table-column>
  165. <el-table-column align="center" fixed="right" label="操作" min-width="200">
  166. <template #default="{ row }">
  167. <el-button
  168. v-hasPermi="['product:spu:update']"
  169. link
  170. type="primary"
  171. @click="openDetail(row.id)"
  172. >
  173. 详情
  174. </el-button>
  175. <el-button
  176. v-hasPermi="['product:spu:update']"
  177. link
  178. type="primary"
  179. @click="openForm(row.id)"
  180. >
  181. 修改
  182. </el-button>
  183. <template v-if="queryParams.tabType === 4">
  184. <el-button
  185. v-hasPermi="['product:spu:delete']"
  186. link
  187. type="danger"
  188. @click="handleDelete(row.id)"
  189. >
  190. 删除
  191. </el-button>
  192. <el-button
  193. v-hasPermi="['product:spu:update']"
  194. link
  195. type="primary"
  196. @click="handleStatus02Change(row, ProductSpuStatusEnum.DISABLE.status)"
  197. >
  198. 恢复到仓库
  199. </el-button>
  200. </template>
  201. <template v-else>
  202. <el-button
  203. v-hasPermi="['product:spu:update']"
  204. link
  205. type="primary"
  206. @click="handleStatus02Change(row, ProductSpuStatusEnum.RECYCLE.status)"
  207. >
  208. 加入回收站
  209. </el-button>
  210. </template>
  211. </template>
  212. </el-table-column>
  213. </el-table>
  214. <!-- 分页 -->
  215. <Pagination
  216. v-model:limit="queryParams.pageSize"
  217. v-model:page="queryParams.pageNo"
  218. :total="total"
  219. @pagination="getList"
  220. />
  221. </ContentWrap>
  222. </template>
  223. <script lang="ts" setup>
  224. import { TabsPaneContext } from 'element-plus'
  225. import { createImageViewer } from '@/components/ImageViewer'
  226. import { dateFormatter } from '@/utils/formatTime'
  227. import { defaultProps, handleTree, treeToString } from '@/utils/tree'
  228. import { ProductSpuStatusEnum } from '@/utils/constants'
  229. import { fenToYuan } from '@/utils'
  230. import download from '@/utils/download'
  231. import * as ProductSpuApi from '@/api/mall/product/spu'
  232. import * as ProductCategoryApi from '@/api/mall/product/category'
  233. defineOptions({ name: 'ProductSpu' })
  234. const message = useMessage() // 消息弹窗
  235. const { t } = useI18n() // 国际化
  236. const { currentRoute, push } = useRouter() // 路由跳转
  237. const loading = ref(false) // 列表的加载中
  238. const exportLoading = ref(false) // 导出的加载中
  239. const total = ref(0) // 列表的总页数
  240. const list = ref<any[]>([]) // 列表的数据
  241. // tabs 数据
  242. const tabsData = ref([
  243. {
  244. count: 0,
  245. name: '出售中商品',
  246. type: 0
  247. },
  248. {
  249. count: 0,
  250. name: '仓库中商品',
  251. type: 1
  252. },
  253. {
  254. count: 0,
  255. name: '已售罄商品',
  256. type: 2
  257. },
  258. {
  259. count: 0,
  260. name: '警戒库存',
  261. type: 3
  262. },
  263. {
  264. count: 0,
  265. name: '商品回收站',
  266. type: 4
  267. }
  268. ])
  269. /** 获得每个 Tab 的数量 */
  270. const getTabsCount = async () => {
  271. const res = await ProductSpuApi.getTabsCount()
  272. for (let objName in res) {
  273. tabsData.value[Number(objName)].count = res[objName]
  274. }
  275. }
  276. const queryParams = ref({
  277. pageNo: 1,
  278. pageSize: 10,
  279. tabType: 0,
  280. name: '',
  281. categoryId: undefined,
  282. createTime: undefined
  283. }) // 查询参数
  284. const queryFormRef = ref() // 搜索的表单Ref
  285. const handleTabClick = (tab: TabsPaneContext) => {
  286. queryParams.value.tabType = tab.paneName as number
  287. getList()
  288. }
  289. /** 查询列表 */
  290. const getList = async () => {
  291. loading.value = true
  292. try {
  293. const data = await ProductSpuApi.getSpuPage(queryParams.value)
  294. list.value = data.list
  295. total.value = data.total
  296. } finally {
  297. loading.value = false
  298. }
  299. }
  300. /** 添加到仓库 / 回收站的状态 */
  301. const handleStatus02Change = async (row, newStatus: number) => {
  302. try {
  303. // 二次确认
  304. const text = newStatus === ProductSpuStatusEnum.RECYCLE.status ? '加入到回收站' : '恢复到仓库'
  305. await message.confirm(`确认要"${row.name}"${text}吗?`)
  306. // 发起修改
  307. await ProductSpuApi.updateStatus({ id: row.id, status: newStatus })
  308. message.success(text + '成功')
  309. // 刷新 tabs 数据
  310. await getTabsCount()
  311. // 刷新列表
  312. await getList()
  313. } catch {}
  314. }
  315. /** 更新上架/下架状态 */
  316. const handleStatusChange = async (row) => {
  317. try {
  318. // 二次确认
  319. const text = row.status ? '上架' : '下架'
  320. await message.confirm(`确认要${text}"${row.name}"吗?`)
  321. // 发起修改
  322. await ProductSpuApi.updateStatus({ id: row.id, status: row.status })
  323. message.success(text + '成功')
  324. // 刷新 tabs 数据
  325. await getTabsCount()
  326. // 刷新列表
  327. await getList()
  328. } catch {
  329. // 异常时,需要重置回之前的值
  330. row.status =
  331. row.status === ProductSpuStatusEnum.DISABLE.status
  332. ? ProductSpuStatusEnum.ENABLE.status
  333. : ProductSpuStatusEnum.DISABLE.status
  334. }
  335. }
  336. /** 删除按钮操作 */
  337. const handleDelete = async (id: number) => {
  338. try {
  339. // 删除的二次确认
  340. await message.delConfirm()
  341. // 发起删除
  342. await ProductSpuApi.deleteSpu(id)
  343. message.success(t('common.delSuccess'))
  344. // 刷新tabs数据
  345. await getTabsCount()
  346. // 刷新列表
  347. await getList()
  348. } catch {}
  349. }
  350. /** 商品图预览 */
  351. const imagePreview = (imgUrl: string) => {
  352. createImageViewer({
  353. urlList: [imgUrl]
  354. })
  355. }
  356. /** 搜索按钮操作 */
  357. const handleQuery = () => {
  358. getList()
  359. }
  360. /** 重置按钮操作 */
  361. const resetQuery = () => {
  362. queryFormRef.value.resetFields()
  363. handleQuery()
  364. }
  365. /** 新增或修改 */
  366. const openForm = (id?: number) => {
  367. // 修改
  368. if (typeof id === 'number') {
  369. push({ name: 'ProductSpuEdit', params: { id } })
  370. return
  371. }
  372. // 新增
  373. push({ name: 'ProductSpuAdd' })
  374. }
  375. /** 查看商品详情 */
  376. const openDetail = (id: number) => {
  377. push({ name: 'ProductSpuDetail', params: { id } })
  378. }
  379. /** 导出按钮操作 */
  380. const handleExport = async () => {
  381. try {
  382. // 导出的二次确认
  383. await message.exportConfirm()
  384. // 发起导出
  385. exportLoading.value = true
  386. const data = await ProductSpuApi.exportSpu(queryParams)
  387. download.excel(data, '商品列表.xls')
  388. } catch {
  389. } finally {
  390. exportLoading.value = false
  391. }
  392. }
  393. const categoryList = ref() // 分类树
  394. /** 获取分类的节点的完整结构 */
  395. const formatCategoryName = (categoryId) => {
  396. return treeToString(categoryList.value, categoryId)
  397. }
  398. // 监听路由变化更新列表,解决商品保存后,列表不刷新的问题。
  399. watch(
  400. () => currentRoute.value,
  401. () => {
  402. getList()
  403. }
  404. )
  405. /** 初始化 **/
  406. onMounted(async () => {
  407. await getTabsCount()
  408. await getList()
  409. // 获得分类树
  410. const data = await ProductCategoryApi.getCategoryList({})
  411. categoryList.value = handleTree(data, 'id', 'parentId')
  412. })
  413. </script>
  414. <style lang="scss" scoped>
  415. .spu-table-expand {
  416. padding-left: 42px;
  417. :deep(.el-form-item__label) {
  418. width: 82px;
  419. font-weight: bold;
  420. color: #99a9bf;
  421. }
  422. }
  423. </style>