list.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  1. <template>
  2. <CommonPage back :title="detail.title">
  3. <template #action>
  4. <n-space>
  5. <NButton type="tertiary" @click="handleTopMenuEdit">
  6. 编辑
  7. </NButton>
  8. <NButton type="primary" @click="handleSubAdd()">
  9. <i class="i-material-symbols:add mr-4 text-18" />
  10. 新增子菜单
  11. </NButton>
  12. </n-space>
  13. </template>
  14. <MeCrud ref="$table" v-model:query-items="queryItems" :scroll-x="1200" :columns="columns" :get-data="api.read">
  15. <MeQueryItem label="标题" :label-width="50">
  16. <n-input v-model:value="queryItems.title" type="text" placeholder="请输入标题名" clearable>
  17. <template #password-visible-icon />
  18. </n-input>
  19. </MeQueryItem>
  20. <MeQueryItem label="状态" :label-width="50">
  21. <n-select
  22. v-model:value="queryItems.enable" clearable :options="[
  23. { label: '启用', value: 1 },
  24. { label: '停用', value: 0 },
  25. ]"
  26. />
  27. </MeQueryItem>
  28. </MeCrud>
  29. <MeModal ref="modalRef" width="520px">
  30. <n-form ref="modalFormRef" label-placement="left" label-align="left" :label-width="95" :model="modalForm">
  31. <n-form-item
  32. label="名称" path="title" :rule="{
  33. required: true,
  34. message: '请输入名称',
  35. trigger: ['input', 'blur'],
  36. }"
  37. >
  38. <n-input v-model:value="modalForm.title" />
  39. </n-form-item>
  40. <!-- <n-form-item label="描述" path="description" :rule="{
  41. required: false,
  42. message: '请输入描述',
  43. trigger: ['input', 'blur'],
  44. }">
  45. <n-input v-model:value="modalForm.description" type="textarea" />
  46. </n-form-item> -->
  47. <n-form-item v-if="modalForm.level !== 0" label="封面" path="cover">
  48. <n-upload
  49. :multiple="false" :default-upload="true" list-type="image-card" :custom-request="uploadCover"
  50. :max="1" :default-file-list="previewFileList" @preview="handlePreview" @remove="handleCoverRemove"
  51. />
  52. <n-modal v-model:show="showModal" preset="card" style="width: 600px" title="">
  53. <img :src="previewImageUrl" style="width: 100%">
  54. </n-modal>
  55. </n-form-item>
  56. <n-form-item
  57. label="分类" path="categoryId" :rule="{
  58. required: modalForm.level === 0 ? false : true,
  59. type: 'number',
  60. trigger: ['change', 'blur'],
  61. message: '请输入分类',
  62. }"
  63. >
  64. <n-tree-select
  65. v-model:value="modalForm.categoryId" :options="allCategory" label-field="title" key-field="id"
  66. placeholder="根分类" clearable
  67. />
  68. </n-form-item>
  69. <n-form-item
  70. v-if="modalForm.level === 0" label="样式类型" path="styleType" :rule="{
  71. required: true,
  72. type: 'number',
  73. message: '请输入样式类型',
  74. trigger: ['input', 'blur'],
  75. }"
  76. >
  77. <n-select v-model:value="modalForm.styleType" :options="styleEnum" clearable filterable tag />
  78. </n-form-item>
  79. <n-form-item
  80. v-if="isShowOtherCol" label="其他类别" path="otherType" :rule="{
  81. required: true,
  82. type: 'number',
  83. message: '请输入其他类别',
  84. trigger: ['input', 'blur'],
  85. }"
  86. >
  87. <n-select v-model:value="modalForm.otherType" :options="otherstyleEnum" clearable />
  88. </n-form-item>
  89. <n-form-item
  90. label="文章链接" path="articleId" :rule="{
  91. required: false,
  92. type: 'number',
  93. trigger: ['change', 'blur'],
  94. message: '请输入文章链接',
  95. }"
  96. >
  97. <n-select v-model:value="modalForm.articleId" :options="allArticle" clearable filterable tag />
  98. </n-form-item>
  99. <n-form-item v-if="modalForm.level === 0" label="一行显示数" path="grid">
  100. <n-input-number v-model:value="modalForm.grid" style="width:100%" />
  101. </n-form-item>
  102. <n-form-item
  103. label="排序" path="order" :rule="{
  104. type: 'number',
  105. required: true,
  106. message: '此为必填项',
  107. trigger: ['blur', 'change'],
  108. }"
  109. >
  110. <n-input-number v-model:value="modalForm.order" />
  111. </n-form-item>
  112. <!-- {{ modalForm.translations }} -->
  113. <n-tabs v-if="modalForm.translations.length > 0" type="line" animated>
  114. <template v-for="(lang, index) in langs" :key="lang">
  115. <n-tab-pane :name="lang" :tab="langLabel[lang]" :index="index">
  116. <n-form-item
  117. label="名称" path="title" :rule="{
  118. required: true,
  119. message: '请输入名称',
  120. trigger: ['input', 'blur'],
  121. }"
  122. >
  123. <n-input v-model:value="modalForm.translations.find(i => i.locale === lang).title">
  124. <template #password-invisible-icon />
  125. </n-input>
  126. </n-form-item>
  127. <n-form-item
  128. label="描述" path="description" :rule="{
  129. required: false,
  130. message: '请输入描述',
  131. trigger: ['input', 'blur'],
  132. }"
  133. >
  134. <n-input
  135. v-model:value="modalForm.translations.find(i => i.locale === lang).description"
  136. type="textarea"
  137. />
  138. </n-form-item>
  139. </n-tab-pane>
  140. </template>
  141. </n-tabs>
  142. <n-form-item label="是否显示" path="isPublish">
  143. <NSwitch v-model:value="modalForm.isPublish">
  144. <template #checked>
  145. 启用
  146. </template>
  147. <template #unchecked>
  148. 停用
  149. </template>
  150. </NSwitch>
  151. </n-form-item>
  152. <n-form-item label="状态" path="enable">
  153. <NSwitch v-model:value="modalForm.enable">
  154. <template #checked>
  155. 启用
  156. </template>
  157. <template #unchecked>
  158. 停用
  159. </template>
  160. </NSwitch>
  161. </n-form-item>
  162. </n-form>
  163. </MeModal>
  164. </CommonPage>
  165. </template>
  166. <script setup>
  167. import { MeCrud, MeModal, MeQueryItem } from '@/components'
  168. import { CommonPage } from '@/components/index.js'
  169. import { useCrud } from '@/composables'
  170. import { useUserStore } from '@/store/index.js'
  171. import { formatDateTime } from '@/utils'
  172. import { otherstyleEnum, styleEnum } from '@/utils/enum.js'
  173. import { initTranslations, langLabel, langs } from '@/utils/translations'
  174. import { NButton, NImage, NSwitch } from 'naive-ui'
  175. import { ref, watchEffect } from 'vue'
  176. import { useRoute, useRouter } from 'vue-router'
  177. import articleApi from '../article/api'
  178. import categoryApi from '../category/api'
  179. import api from './api.js'
  180. const $table = ref(null)
  181. const router = useRouter()
  182. const { userId } = useUserStore()
  183. const previewFileList = ref([])
  184. const showModal = ref(false)
  185. const previewImageUrl = ref('')
  186. const allCategory = ref([])
  187. const allArticle = ref([])
  188. const route = useRoute()
  189. const detail = ref({
  190. title: '',
  191. id: null,
  192. })
  193. /** QueryBar筛选参数(可选) */
  194. const queryItems = ref({
  195. parentId: route.params.id,
  196. })
  197. const isShowOtherCol = ref(false)
  198. // const modalForm = ref({})
  199. const { modalRef, modalFormRef, modalAction, modalForm, handleAdd, handleDelete, handleEdit }
  200. = useCrud({
  201. name: '子菜单',
  202. doCreate: api.create,
  203. doDelete: api.delete,
  204. doUpdate: api.update,
  205. initForm: {
  206. enable: true,
  207. isPublish: true,
  208. order: 0,
  209. translations: initTranslations({}, ['title', 'description']).translations,
  210. },
  211. refresh: (_, keepCurrentPage) => $table.value?.handleSearch(keepCurrentPage),
  212. })
  213. watchEffect(() => {
  214. // if (newmodalForm.value) {
  215. // modalForm.value = {
  216. // ...modalForm.value,
  217. // ...newmodalForm.value,
  218. // }
  219. // initTranslations(modalForm.value, ['title', 'description'])
  220. // console.log('modalForm', modalForm.value)
  221. // }
  222. })
  223. async function getMenuDetail() {
  224. const { data } = await api.getOne(route.params.id)
  225. if (data) {
  226. console.log('data', data)
  227. detail.value = data
  228. isShowOtherCol.value = data.styleType === 4
  229. }
  230. getAllType()
  231. }
  232. const columns = [
  233. { title: '标题名', key: 'title', width: '200' },
  234. { title: '分类', key: 'category.title' },
  235. {
  236. title: '封面图',
  237. key: 'cover',
  238. render: row => row.cover
  239. ? h(NImage, {
  240. src: row.cover,
  241. height: 60,
  242. width: 80,
  243. })
  244. : null,
  245. },
  246. {
  247. title: '创建时间',
  248. key: 'createTime',
  249. render: row => h('span', formatDateTime(row.createTime)),
  250. },
  251. {
  252. title: '状态',
  253. key: 'enable',
  254. render: row =>
  255. h(
  256. NSwitch,
  257. {
  258. size: 'small',
  259. rubberBand: false,
  260. value: row.enable,
  261. loading: !!row.enableLoading,
  262. disabled: row.code === 'SUPER_ADMIN',
  263. onUpdateValue: () => handleEnable(row),
  264. },
  265. {
  266. checked: () => '启用',
  267. unchecked: () => '停用',
  268. },
  269. ),
  270. },
  271. {
  272. title: '操作',
  273. key: 'actions',
  274. width: 200,
  275. align: 'center',
  276. fixed: 'right',
  277. render(row) {
  278. return [
  279. h(
  280. NButton,
  281. {
  282. size: 'small',
  283. type: 'primary',
  284. style: 'margin-left: 12px;',
  285. disabled: row.code === 'SUPER_ADMIN',
  286. onClick: () => handleFormEdit(row),
  287. },
  288. {
  289. default: () => '编辑',
  290. icon: () => h('i', { class: 'i-material-symbols:edit-outline text-14' }),
  291. },
  292. ),
  293. h(
  294. NButton,
  295. {
  296. size: 'small',
  297. type: 'error',
  298. style: 'margin-left: 12px;',
  299. disabled: row.code === 'SUPER_ADMIN',
  300. onClick: () => handleDelete(row.id),
  301. },
  302. {
  303. default: () => '删除',
  304. icon: () => h('i', { class: 'i-material-symbols:delete-outline text-14' }),
  305. },
  306. ),
  307. ]
  308. },
  309. },
  310. ]
  311. async function handleEnable(row) {
  312. row.enableLoading = true
  313. try {
  314. await api.update({ id: row.id, enable: !row.enable })
  315. row.enableLoading = false
  316. $message.success('操作成功')
  317. $table.value?.handleSearch()
  318. }
  319. catch (error) {
  320. console.error(error)
  321. row.enableLoading = false
  322. }
  323. }
  324. watchEffect(() => {
  325. if (userId) {
  326. modalForm.value.userId = userId
  327. if (modalForm.value.level !== 0) {
  328. modalForm.value.parentId = detail.value.id
  329. }
  330. }
  331. })
  332. onMounted(() => {
  333. $table.value?.handleSearch()
  334. getMenuDetail()
  335. })
  336. async function uploadCover({ file }) {
  337. const data = new FormData()
  338. data.append('file', file.file)
  339. const res = await api.uploadImage(data)
  340. modalForm.value.cover = res.data
  341. file.url = res.data
  342. file.thumbnailUrl = res.data
  343. previewFileList.value = [{
  344. id: '0',
  345. status: 'finished',
  346. url: res.data,
  347. }]
  348. }
  349. function handlePreview(file) {
  350. const { url } = file
  351. previewImageUrl.value = url
  352. showModal.value = true
  353. }
  354. async function handleFormEdit(data = {}) {
  355. modalForm.value = {
  356. ...data,
  357. userId,
  358. }
  359. console.log('handleFormEdit', modalForm.value)
  360. if (modalForm.value.cover) {
  361. previewFileList.value = [{
  362. id: '0',
  363. status: 'finished',
  364. url: modalForm.value.cover,
  365. }]
  366. }
  367. else {
  368. previewFileList.value = []
  369. }
  370. if (data.translations.length === 0) {
  371. initTranslations(data, ['title', 'description'])
  372. }
  373. handleEdit(data)
  374. }
  375. function handleCoverRemove() {
  376. modalForm.value.cover = ''
  377. previewFileList.value = []
  378. previewImageUrl.value = ''
  379. }
  380. function handleSubAdd() {
  381. modalForm.value.level = 1
  382. modalForm.value.cover = ''
  383. previewFileList.value = []
  384. handleAdd({ level: 1, cover: '' })
  385. }
  386. function handleTopMenuEdit() {
  387. modalForm.value = {
  388. cover: '',
  389. ...detail.value,
  390. userId,
  391. level: 0,
  392. parentId: null,
  393. }
  394. if (modalForm.value.translations.length === 0) {
  395. initTranslations(modalForm.value, ['title', 'description'])
  396. }
  397. handleEdit(modalForm.value, '编辑顶层菜单', () => {
  398. getMenuDetail()
  399. })
  400. }
  401. function getAllType() {
  402. categoryApi.getAll().then(({ data = [] }) => (allCategory.value = data))
  403. articleApi.getAll().then(({ data = [] }) => (allArticle.value = data.map(item => ({
  404. label: item.title,
  405. value: +item.id,
  406. }))))
  407. }
  408. </script>