diff --git a/mock/api/post.js b/mock/api/post.js index b86d50e..579c533 100644 --- a/mock/api/post.js +++ b/mock/api/post.js @@ -1,74 +1,137 @@ +const posts = [ + { + title: '使用纯css优雅配置移动端rem布局', + author: '大脸怪', + category: 'Css', + description: '通常配置rem布局会使用js进行处理,比如750的设计稿会这样...', + content: '通常配置rem布局会使用js进行处理,比如750的设计稿会这样', + isRecommend: true, + isPublish: true, + createDate: '2021-11-04T04:03:36.000Z', + updateDate: '2021-11-04T04:03:36.000Z', + }, + { + title: 'Vue2&Vue3项目风格指南', + author: 'Ronnie', + category: 'Vue', + description: '总结的Vue2和Vue3的项目风格', + content: '### 1. 命名风格\n\n> 文件夹如果是由多个单词组成,应该始终是横线连接 ', + isRecommend: true, + isPublish: true, + createDate: '2021-10-25T08:57:47.000Z', + updateDate: '2022-02-28T04:02:39.000Z', + }, + { + title: '如何优雅的给图片添加水印', + author: '大脸怪', + category: 'JavaScript', + description: '优雅的给图片添加水印', + content: '我之前写过一篇文章记录了一次上传图片的优化史', + isRecommend: true, + isPublish: true, + createDate: '2021-06-24T18:46:19.000Z', + updateDate: '2021-09-23T07:51:22.000Z', + }, + + { + title: '前端缓存的理解', + author: '大脸怪', + category: 'Http', + description: '谈谈前端缓存的理解', + content: '> 背景\n\n公司有个vue-cli3移动端web项目发版更新后发现部分用户手机在钉钉内置浏览器打开出现了缓存', + isRecommend: true, + isPublish: true, + createDate: '2021-06-10T18:51:19.000Z', + updateDate: '2021-09-17T09:33:24.000Z', + }, + { + title: 'Promise的五个静态方法', + author: '大脸怪', + category: 'JavaScript', + description: '简单介绍下在 Promise 类中,有5 种静态方法及它们的使用场景', + content: '## 1. Promise.all\n\n并行执行多个 promise,并等待所有 promise 都准备就绪。再对它们进行处理。', + isRecommend: true, + isPublish: true, + createDate: '2021-02-22T22:37:06.000Z', + updateDate: '2021-09-17T09:33:24.000Z', + }, +] + export default [ { url: '/api/posts', method: 'get', - response: () => { + response: (data = {}) => { + const { title, pageNo, pageSize } = data.query + let pageData = [] + let total = 60 + const filterData = posts.filter((item) => item.title.includes(title) || (!title && title !== 0)) + if (filterData.length) { + if (pageSize) { + while (pageData.length < pageSize) { + pageData.push(filterData[Math.round(Math.random() * (filterData.length - 1))]) + } + } else { + pageData = filterData + } + pageData = pageData.map((item, index) => ({ + id: pageSize * (pageNo - 1) + index + 1, + ...item, + })) + } else { + total = 0 + } return { code: 0, message: 'ok', - data: [ - { - id: 36, - title: '使用纯css优雅配置移动端rem布局', - author: 'Ronnie', - category: '移动端,Css', - description: '通常配置rem布局会使用js进行处理,比如750的设计稿会这样...', - content: '通常配置rem布局会使用js进行处理,比如750的设计稿会这样', - isRecommend: true, - isPublish: true, - createDate: '2021-11-04T04:03:36.000Z', - updateDate: '2021-11-04T04:03:36.000Z', + data: { + pageData, + total, + pageNo, + pageSize, + }, + } + }, + }, + { + url: '/api/post', + method: 'post', + response: ({ body }) => { + return { + code: 0, + message: 'ok', + data: body, + } + }, + }, + { + url: '/api/post/:id', + method: 'put', + response: ({ query, body }) => { + return { + code: 0, + message: 'ok', + data: { + id: query.id, + body, + }, + } + }, + }, + { + url: '/api/post/:id', + method: 'delete', + response: ({ query }) => { + if (!query.id) { + return { code: -1, message: '删除失败,id不能为空' } + } else { + return { + code: 0, + message: 'ok', + data: { + id: query.id, }, - { - id: 35, - title: 'Vue2&Vue3项目风格指南', - author: 'Ronnie', - category: 'Vue', - description: '总结的Vue2和Vue3的项目风格', - content: '### 1. 命名风格\n\n> 文件夹如果是由多个单词组成,应该始终是横线连接 ', - isRecommend: true, - isPublish: true, - createDate: '2021-10-25T08:57:47.000Z', - updateDate: '2022-02-28T04:02:39.000Z', - }, - { - id: 28, - title: '如何优雅的给图片添加水印', - author: '大脸怪', - category: 'JavaScript', - description: '优雅的给图片添加水印', - content: '我之前写过一篇文章记录了一次上传图片的优化史', - isRecommend: true, - isPublish: true, - createDate: '2021-06-24T18:46:19.000Z', - updateDate: '2021-09-23T07:51:22.000Z', - }, - - { - id: 26, - title: '前端缓存的理解', - author: '大脸怪', - category: 'Http', - description: '谈谈前端缓存的理解', - content: '> 背景\n\n公司有个vue-cli3移动端web项目发版更新后发现部分用户手机在钉钉内置浏览器打开出现了缓存', - isRecommend: true, - isPublish: true, - createDate: '2021-06-10T18:51:19.000Z', - updateDate: '2021-09-17T09:33:24.000Z', - }, - { - id: 18, - title: 'Promise的五个静态方法', - author: '大脸怪', - category: 'JavaScript', - description: '简单介绍下在 Promise 类中,有5 种静态方法及它们的使用场景', - content: '## 1. Promise.all\n\n并行执行多个 promise,并等待所有 promise 都准备就绪。再对它们进行处理。', - isRecommend: true, - isPublish: true, - createDate: '2021-02-22T22:37:06.000Z', - updateDate: '2021-09-17T09:33:24.000Z', - }, - ], + } } }, }, diff --git a/src/components/query-bar/QueryBar.vue b/src/components/query-bar/QueryBar.vue new file mode 100644 index 0000000..1f6eb0f --- /dev/null +++ b/src/components/query-bar/QueryBar.vue @@ -0,0 +1,16 @@ + + + diff --git a/src/components/query-bar/QueryBarItem.vue b/src/components/query-bar/QueryBarItem.vue new file mode 100644 index 0000000..c0e03cf --- /dev/null +++ b/src/components/query-bar/QueryBarItem.vue @@ -0,0 +1,29 @@ + + + diff --git a/src/components/table/CrudModal.vue b/src/components/table/CrudModal.vue new file mode 100644 index 0000000..431eb15 --- /dev/null +++ b/src/components/table/CrudModal.vue @@ -0,0 +1,48 @@ + + + diff --git a/src/components/table/CrudTable.vue b/src/components/table/CrudTable.vue new file mode 100644 index 0000000..3d88e70 --- /dev/null +++ b/src/components/table/CrudTable.vue @@ -0,0 +1,127 @@ + + + diff --git a/src/composables/index.js b/src/composables/index.js new file mode 100644 index 0000000..e410b83 --- /dev/null +++ b/src/composables/index.js @@ -0,0 +1 @@ +export { default as useCRUD } from './useCRUD' diff --git a/src/composables/useCRUD.js b/src/composables/useCRUD.js new file mode 100644 index 0000000..e2ea6f7 --- /dev/null +++ b/src/composables/useCRUD.js @@ -0,0 +1,104 @@ +import { isNullOrWhitespace } from '../utils/is' + +const ACTIONS = { + view: '查看', + edit: '编辑', + add: '新增', +} + +export default function ({ name, initForm = {}, doCreate, doDelete, doUpdate, refresh }) { + const modalVisible = ref(false) + const modalAction = ref('') + const modalTitle = computed(() => ACTIONS[modalAction.value] + name) + const modalLoading = ref(false) + const modalFormRef = ref(null) + const modalForm = ref({ ...initForm }) + + /** 新增 */ + function handleAdd() { + modalAction.value = 'add' + modalVisible.value = true + modalForm.value = { ...initForm } + } + + /** 修改 */ + function handleEdit(row) { + modalAction.value = 'edit' + modalVisible.value = true + modalForm.value = { ...row } + } + + /** 查看 */ + function handleView(row) { + modalAction.value = 'view' + modalVisible.value = true + modalForm.value = { ...row } + } + + /** 保存 */ + function handleSave() { + if (!['edit', 'add'].includes(modalAction.value)) { + modalVisible.value = false + return + } + modalFormRef.value?.validate(async (err) => { + if (err) return + const actions = { + add: { + api: () => doCreate(modalForm.value), + cb: () => $message.success('新增成功'), + }, + edit: { + api: () => doUpdate(modalForm.value), + cb: () => $message.success('编辑成功'), + }, + } + const action = actions[modalAction.value] + + try { + modalLoading.value = true + const data = await action.api() + action.cb() + modalLoading.value = modalVisible.value = false + data && refresh(data) + } catch (error) { + $message.error('操作失败') + modalLoading.value = false + } + }) + } + + /** 删除 */ + function handleDelete(id, confirmOptions) { + if (isNullOrWhitespace(id)) return + $dialog.confirm({ + content: '确定删除?', + async confirm() { + try { + modalLoading.value = true + const data = await doDelete(id) + $message.success('删除成功') + modalLoading.value = false + refresh(data) + } catch (error) { + modalLoading.value = false + } + }, + ...confirmOptions, + }) + } + + return { + modalVisible, + modalAction, + modalTitle, + modalLoading, + handleAdd, + handleDelete, + handleEdit, + handleView, + handleSave, + modalForm, + modalFormRef, + } +} diff --git a/src/utils/http/interceptors.js b/src/utils/http/interceptors.js index 160941a..51ce7d8 100644 --- a/src/utils/http/interceptors.js +++ b/src/utils/http/interceptors.js @@ -35,7 +35,11 @@ export function reqReject(error) { } export function repResolve(response) { - return response?.data + if (response?.data?.code !== 0) { + $message.error(response?.data?.message || '操作异常') + return Promise.reject(response?.data) + } + return Promise.resolve(response?.data) } export function repReject(error) { @@ -67,5 +71,6 @@ export function repReject(error) { } } console.error(`【${code}】 ${error}`) - return Promise.resolve({ code, message, error }) + $message.error(message || '操作异常') + return Promise.reject({ code, message, error }) } diff --git a/src/utils/is.js b/src/utils/is.js index 876c99f..5651c79 100644 --- a/src/utils/is.js +++ b/src/utils/is.js @@ -72,6 +72,7 @@ export function isNullOrWhitespace(val) { return isNullOrUndef(val) || isWhitespace(val) } +/** 空数组 | 空字符串 | 空对象 | 空Map | 空Set */ export function isEmpty(val) { if (isArray(val) || isString(val)) { return val.length === 0 diff --git a/src/views/examples/table/post/api.js b/src/views/examples/table/post/api.js index 9416f24..d17fbdf 100644 --- a/src/views/examples/table/post/api.js +++ b/src/views/examples/table/post/api.js @@ -3,11 +3,7 @@ import request from '@/utils/http' export default { getPosts: (params = {}) => request.get('posts', { params }), getPostById: (id) => request.get(`/post/${id}`), - savePost: (id, data = {}) => { - if (id) { - return request.put(`/post/${id}`, data) - } - return request.post('/post', data) - }, + addPost: (data) => request.post('/post', data), + updatePost: (data) => request.put(`/post/${data.id}`, data), deletePost: (id) => request.delete(`/post/${id}`), } diff --git a/src/views/examples/table/post/index.vue b/src/views/examples/table/post/index.vue index b1ffd6b..7af4293 100644 --- a/src/views/examples/table/post/index.vue +++ b/src/views/examples/table/post/index.vue @@ -1,36 +1,221 @@ + + + :get-data="api.getPosts" + @on-checked="onChecked" + > + + + + + + + + + + + + + + + + diff --git a/src/views/examples/table/post/usePostTable.js b/src/views/examples/table/post/usePostTable.js index b051d30..d1548b9 100644 --- a/src/views/examples/table/post/usePostTable.js +++ b/src/views/examples/table/post/usePostTable.js @@ -1,8 +1,8 @@ import { h } from 'vue' import { NButton, NSwitch } from 'naive-ui' import { formatDateTime } from '@/utils' -import api from './api' import { renderIcon } from '@/utils/icon' +import api from './api' export const usePostTable = () => { // refs @@ -31,6 +31,21 @@ export const usePostTable = () => { } } + function handleEdit(row) { + if (row && row.id) { + $dialog.confirm({ + content: '确定删除?', + confirm() { + $message.success('删除成功') + initTableData() + }, + cancel() { + $message.success('已取消') + }, + }) + } + } + async function handleRecommend(row) { if (row && row.id) { row.recommending = true @@ -84,7 +99,7 @@ export const usePostTable = () => { { title: '推荐', key: 'isRecommend', - width: 100, + width: 120, align: 'center', fixed: 'right', render(row) { @@ -100,7 +115,7 @@ export const usePostTable = () => { { title: '发布', key: 'isPublish', - width: 100, + width: 120, align: 'center', fixed: 'right', render(row) { @@ -116,11 +131,20 @@ export const usePostTable = () => { { title: '操作', key: 'actions', - width: 120, + width: 200, align: 'center', fixed: 'right', render(row) { return [ + h( + NButton, + { + size: 'small', + type: 'primary', + onClick: () => handleEdit(row), + }, + { default: () => '编辑', icon: renderIcon('material-symbols:edit-outline', { size: 14 }) } + ), h( NButton, {