list.vue 11 KB

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