index.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  1. <template>
  2. <div class="app-container">
  3. <el-card shadow="never">
  4. <!-- 搜索表单 -->
  5. <el-form class="-mb-10px" :model="queryParams" ref="queryFormRef" :inline="true" label-width="80px">
  6. <TenantSelect v-model="queryParams.tenantIds" placeholder="请选择机构名称" prop="tenantIds" />
  7. <el-form-item>
  8. <el-button type="primary" @click="handleQuery">搜索</el-button>
  9. <el-button @click="resetQuery">重置</el-button>
  10. </el-form-item>
  11. </el-form>
  12. <template #header>
  13. <div class="card-header">
  14. <span>物资分类信息</span>
  15. <div class="right-actions">
  16. <el-button v-hasPermi="['warehouses:classifyMessage:add']" type="primary" @click="handleAddRoot"><Icon icon="ep:plus" class="mr-5px" />新增根节点</el-button>
  17. <!-- <el-button type="success" @click="openImport"><Icon icon="ep:download" class="mr-5px" />导入</el-button> -->
  18. </div>
  19. </div>
  20. </template>
  21. <div class="main-layout">
  22. <!-- 左侧树 -->
  23. <div class="left-tree">
  24. <el-tree
  25. ref="treeRef"
  26. :data="treeData"
  27. node-key="id"
  28. :props="treeProps"
  29. default-expand-all
  30. highlight-current
  31. :expand-on-click-node="false"
  32. @node-click="handleTreeNodeClick"
  33. >
  34. <template #default="{ node, data }">
  35. <span class="tree-node">
  36. <Icon icon="ep:folder" v-if="data.childrenList && data.childrenList.length" class="mr-5px" />
  37. <Icon icon="ep:document" v-else class="mr-5px" />
  38. <span>{{ node.label }}</span>
  39. </span>
  40. </template>
  41. </el-tree>
  42. </div>
  43. <!-- 右侧树表格 -->
  44. <div class="right-table">
  45. <el-table
  46. :data="tableData"
  47. row-key="id"
  48. border
  49. default-expand-all
  50. :tree-props="{ children: 'childrenList', hasChildren: 'hasChildren' }"
  51. :header-cell-style="tableHeaderColor"
  52. >
  53. <el-table-column prop="categoryName" label="类别名称" min-width="300" />
  54. <el-table-column label="操作" width="360" align="center">
  55. <template #default="{ row }">
  56. <el-button v-hasPermi="['warehouses:classifyMessage:edit']" link type="primary" @click="handleEdit(row)">
  57. <Icon icon="ep:edit" class="mr-5px" />编辑
  58. </el-button>
  59. <el-button v-hasPermi="['warehouses:classifyMessage:add']" link type="primary" @click="handleAddChild(row)">
  60. <Icon icon="ep:plus" class="mr-5px" />添加下级节点
  61. </el-button>
  62. <el-button v-hasPermi="['warehouses:classifyMessage:delete']" link type="danger" @click="handleDelete(row)">
  63. <Icon icon="ep:delete" class="mr-5px" />删除
  64. </el-button>
  65. </template>
  66. </el-table-column>
  67. </el-table>
  68. </div>
  69. </div>
  70. <!-- 新增/编辑弹窗 -->
  71. <Dialog v-model="dialog.visible" :title="dialog.title" width="600px" @close="handleDialogClose">
  72. <el-form ref="formRef" :model="dialog.form" :rules="rules" label-width="90px">
  73. <el-form-item label="上级节点">
  74. <el-input v-model="dialog.form.parentName" disabled />
  75. </el-form-item>
  76. <el-row :gutter="20">
  77. <el-col :span="12">
  78. <el-form-item label="分类编号" prop="categoryNumber">
  79. <el-input v-model="dialog.form.categoryNumber" placeholder="请输入分类编号" />
  80. </el-form-item>
  81. </el-col>
  82. <el-col :span="12">
  83. <el-form-item label="分类名称" prop="categoryName">
  84. <el-input v-model="dialog.form.categoryName" placeholder="请输入分类名称" />
  85. </el-form-item>
  86. </el-col>
  87. </el-row>
  88. </el-form>
  89. <template #footer>
  90. <el-button @click="dialog.visible = false">关闭</el-button>
  91. <el-button type="primary" :loading="dialog.loading" @click="handleSubmit">保存</el-button>
  92. </template>
  93. </Dialog>
  94. <!-- 导入弹窗(复用 ImportFile 组件) -->
  95. <Import ref="importRef" :config="importConfig" @success="handleImportSuccess" />
  96. </el-card>
  97. </div>
  98. </template>
  99. <script setup>
  100. import { onMounted, reactive, ref } from 'vue'
  101. import { ElMessage } from 'element-plus'
  102. import Import from '@/components/ImportFile/index.vue'
  103. import {
  104. getMaterialCategoryTree,
  105. createMaterialCategory,
  106. updateMaterialCategory,
  107. getMaterialCategory,
  108. deleteMaterialCategory
  109. } from '@/api/material/classifyMessage'
  110. import { ElMessageBox } from 'element-plus'
  111. import { useUserStore } from '@/store/modules/user'
  112. const userStore = useUserStore()
  113. const queryFormRef = ref()
  114. const queryParams = reactive({
  115. tenantIds: userStore.orgTenantId,
  116. })
  117. // -------------------- 左侧树 --------------------
  118. const treeRef = ref()
  119. const treeData = ref([])
  120. const treeProps = { children: 'childrenList', label: 'categoryName' }
  121. const currentNodeId = ref(null)
  122. const handleTreeNodeClick = (data) => {
  123. currentNodeId.value = data.id
  124. tableData.value = [JSON.parse(JSON.stringify(data))]
  125. }
  126. // -------------------- 右侧树表格 --------------------
  127. const tableData = ref([])
  128. const tableHeaderColor = ({ rowIndex }) => {
  129. if (rowIndex === 0) {
  130. return {
  131. backgroundColor: '#f8f8f9',
  132. color: '#666666',
  133. fontWeight: 'bold'
  134. }
  135. }
  136. }
  137. // -------------------- API:获取树 --------------------
  138. const getTree = async () => {
  139. try {
  140. const res = await getMaterialCategoryTree({
  141. tenantIds: queryParams.tenantIds?.length ? queryParams.tenantIds : undefined
  142. })
  143. const data = res?.data ?? res
  144. treeData.value = Array.isArray(data) ? data : []
  145. // 默认选中根节点
  146. currentNodeId.value = treeData.value?.[0]?.id ?? null
  147. if (currentNodeId.value) {
  148. treeRef.value?.setCurrentKey?.(currentNodeId.value)
  149. tableData.value = [JSON.parse(JSON.stringify(treeData.value[0]))]
  150. } else {
  151. tableData.value = []
  152. }
  153. } catch (e) {
  154. console.error(e)
  155. ElMessage.error(e?.message || '获取分类树失败')
  156. treeData.value = []
  157. tableData.value = []
  158. }
  159. }
  160. const handleQuery = () => {
  161. if (queryParams.tenantIds.length == 0) {
  162. ElMessage.error('请选择机构')
  163. return
  164. }
  165. getTree()
  166. }
  167. const resetQuery = () => {
  168. queryParams.tenantIds = userStore.orgTenantId
  169. getTree()
  170. }
  171. // -------------------- 弹窗:新增/编辑 --------------------
  172. const dialog = reactive({
  173. visible: false,
  174. mode: 'add', // add/edit
  175. loading: false,
  176. form: {
  177. id: null,
  178. superior: 0,
  179. parentName: '',
  180. categoryNumber: '',
  181. categoryName: ''
  182. },
  183. title: ''
  184. })
  185. const formRef = ref()
  186. const rules = {
  187. categoryNumber: [{ required: true, message: '请输入分类编号', trigger: 'blur' }],
  188. categoryName: [{ required: true, message: '请输入分类名称', trigger: 'blur' }]
  189. }
  190. const handleDialogClose = () => {
  191. formRef.value?.resetFields?.()
  192. }
  193. // 通用:树查找(childrenList)
  194. const findNodeById = (nodes, id) => {
  195. if (!Array.isArray(nodes)) return null
  196. for (const n of nodes) {
  197. if (n.id === id) return n
  198. if (n.childrenList?.length) {
  199. const found = findNodeById(n.childrenList, id)
  200. if (found) return found
  201. }
  202. }
  203. return null
  204. }
  205. const openDialog = async ({ mode, parentNode, row }) => {
  206. // 新增/编辑:只能选择一个院区(参考 check-in-request 的判断风格)
  207. if (queryParams.tenantIds.length == 0 || (queryParams.tenantIds.length > 1 && !row?.tenantId)) {
  208. ElMessage.error(mode === 'add' ? '新增只能选择一个机构' : '编辑只能选择一个机构')
  209. return
  210. }
  211. dialog.mode = mode
  212. dialog.visible = true
  213. dialog.loading = false
  214. dialog.title = mode === 'add' ? '物资类别-新增' : '物资类别-编辑'
  215. if (mode === 'add') {
  216. dialog.form = {
  217. id: null,
  218. superior: parentNode?.id ?? 0,
  219. parentName: parentNode?.categoryName ?? '',
  220. categoryNumber: '',
  221. categoryName: ''
  222. }
  223. } else {
  224. // 编辑:拉详情再赋值
  225. try {
  226. const res = await getMaterialCategory(row.id)
  227. const data = res?.data ?? res
  228. dialog.form = {
  229. id: data?.id,
  230. superior: data?.superior ?? 0,
  231. parentName: findNodeById(treeData.value, data?.superior)?.categoryName || '',
  232. categoryNumber: data?.categoryNumber ?? '',
  233. categoryName: data?.categoryName ?? ''
  234. }
  235. } catch (e) {
  236. console.error(e)
  237. ElMessage.error(e?.message || '获取详情失败')
  238. dialog.visible = false
  239. }
  240. }
  241. }
  242. const handleAddRoot = () => {
  243. // 新增根节点:只能选择一个院区
  244. if (queryParams.tenantIds.length == 0 || queryParams.tenantIds.length > 1) {
  245. ElMessage.error('新增只能选择一个机构')
  246. return
  247. }
  248. openDialog({ mode: 'add', parentNode: { id: 0, categoryName: '根节点' } })
  249. }
  250. const handleAddChild = (row) => {
  251. openDialog({ mode: 'add', parentNode: row })
  252. }
  253. const handleEdit = (row) => {
  254. openDialog({ mode: 'edit', row })
  255. }
  256. const handleSubmit = async () => {
  257. await formRef.value?.validate?.()
  258. // 新增/编辑提交:必须选且只能选一个机构
  259. if (queryParams.tenantIds.length == 0 || queryParams.tenantIds.length > 1) {
  260. ElMessage.error('只能选择一个机构进行操作')
  261. return
  262. }
  263. dialog.loading = true
  264. try {
  265. const tenantId = queryParams.tenantIds[0]
  266. const payload = {
  267. id: dialog.form.id || undefined,
  268. tenantId,
  269. superior: dialog.form.superior,
  270. categoryNumber: dialog.form.categoryNumber,
  271. categoryName: dialog.form.categoryName
  272. }
  273. if (dialog.mode === 'add') {
  274. delete payload.id
  275. await createMaterialCategory(payload)
  276. ElMessage.success('新增成功')
  277. } else {
  278. if (!payload.id) {
  279. ElMessage.error('缺少 id,无法编辑')
  280. return
  281. }
  282. await updateMaterialCategory(payload)
  283. ElMessage.success('编辑成功')
  284. }
  285. dialog.visible = false
  286. await getTree()
  287. } catch (e) {
  288. console.error(e)
  289. ElMessage.error(e?.message || '保存失败')
  290. } finally {
  291. dialog.loading = false
  292. }
  293. }
  294. // 删除
  295. const handleDelete = async (row) => {
  296. // 删除:必须选且只能选一个机构;并且如果多选机构,要求当前行存在 tenantId
  297. if (queryParams.tenantIds.length == 0 || (queryParams.tenantIds.length > 1 && !row?.tenantId)) {
  298. ElMessage.error('删除只能选择一个机构')
  299. return
  300. }
  301. try {
  302. await ElMessageBox.confirm(
  303. `确定要删除分类【${row?.categoryName || '-'}】吗?删除后该分类下的所有子分类以及所属物资信息将一并删除,此操作不可恢复!`,
  304. '删除确认',
  305. {
  306. confirmButtonText: '确定',
  307. cancelButtonText: '取消',
  308. type: 'warning'
  309. }
  310. )
  311. await deleteMaterialCategory(row.id)
  312. ElMessage.success('删除成功')
  313. await getTree()
  314. } catch (e) {
  315. // 用户取消不提示错误
  316. if (e !== 'cancel' && e !== 'close') {
  317. console.error(e)
  318. ElMessage.error(e?.message || '删除失败')
  319. }
  320. }
  321. }
  322. // -------------------- 导入 --------------------
  323. const importRef = ref()
  324. const importConfig = reactive({
  325. title: '物资分类信息导入',
  326. importUrl: '/warehouses/classify/import',
  327. excelTempName: '物资分类信息导入模板',
  328. downloadUrl: '/warehouses/classify/template',
  329. desc: '物资分类信息',
  330. failExportUrl: ''
  331. })
  332. const openImport = () => {
  333. importRef.value?.open?.()
  334. }
  335. const handleImportSuccess = () => {
  336. ElMessage.success('导入成功')
  337. getTree()
  338. }
  339. // -------------------- init --------------------
  340. onMounted(() => {
  341. getTree()
  342. })
  343. </script>
  344. <style scoped>
  345. .card-header {
  346. display: flex;
  347. align-items: center;
  348. justify-content: space-between;
  349. }
  350. .right-actions {
  351. display: flex;
  352. gap: 10px;
  353. }
  354. .main-layout {
  355. display: flex;
  356. height: calc(100vh - 240px);
  357. min-height: 520px;
  358. }
  359. .left-tree {
  360. width: 342px;
  361. /* border-right: 1px solid #ebeef5; */
  362. padding: 10px;
  363. overflow: auto;
  364. }
  365. .right-table {
  366. flex: 1;
  367. padding: 10px;
  368. overflow: auto;
  369. }
  370. .tree-node {
  371. display: inline-flex;
  372. align-items: center;
  373. }
  374. .mr-5px {
  375. margin-right: 5px;
  376. }
  377. </style>