From 9ea8ffd7fd311cac525d349359191dce1cac3fcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E4=BC=A0=E9=BE=99?= Date: Wed, 31 Aug 2022 10:16:38 +0800 Subject: [PATCH 1/5] wip: crud table --- src/components/query-bar/QueryBar.vue | 12 +++++ src/components/query-bar/QueryBarItem.vue | 29 +++++++++++ src/components/table/CrudModal.vue | 44 +++++++++++++++++ src/components/table/CrudTable.vue | 16 +++++++ src/composables/useCRUD.js | 5 ++ src/views/examples/table/post/index.vue | 48 +++++++++++++------ src/views/examples/table/post/usePostTable.js | 32 +++++++++++-- 7 files changed, 168 insertions(+), 18 deletions(-) create mode 100644 src/components/query-bar/QueryBar.vue create mode 100644 src/components/query-bar/QueryBarItem.vue create mode 100644 src/components/table/CrudModal.vue create mode 100644 src/components/table/CrudTable.vue create mode 100644 src/composables/useCRUD.js diff --git a/src/components/query-bar/QueryBar.vue b/src/components/query-bar/QueryBar.vue new file mode 100644 index 0000000..4062389 --- /dev/null +++ b/src/components/query-bar/QueryBar.vue @@ -0,0 +1,12 @@ + 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..40404f4 --- /dev/null +++ b/src/components/table/CrudModal.vue @@ -0,0 +1,44 @@ + + + diff --git a/src/components/table/CrudTable.vue b/src/components/table/CrudTable.vue new file mode 100644 index 0000000..195f559 --- /dev/null +++ b/src/components/table/CrudTable.vue @@ -0,0 +1,16 @@ + + + diff --git a/src/composables/useCRUD.js b/src/composables/useCRUD.js new file mode 100644 index 0000000..e683ac5 --- /dev/null +++ b/src/composables/useCRUD.js @@ -0,0 +1,5 @@ +const ACTIONS = { + view: '查看', + edit: '编辑', + add: '新增', +} diff --git a/src/views/examples/table/post/index.vue b/src/views/examples/table/post/index.vue index b1ffd6b..38e88bd 100644 --- a/src/views/examples/table/post/index.vue +++ b/src/views/examples/table/post/index.vue @@ -1,24 +1,44 @@ diff --git a/src/components/table/CrudTable.vue b/src/components/table/CrudTable.vue index 195f559..95635df 100644 --- a/src/components/table/CrudTable.vue +++ b/src/components/table/CrudTable.vue @@ -1,16 +1,106 @@ diff --git a/src/views/examples/table/post/index.vue b/src/views/examples/table/post/index.vue index 38e88bd..f7d70bb 100644 --- a/src/views/examples/table/post/index.vue +++ b/src/views/examples/table/post/index.vue @@ -6,51 +6,172 @@ - + - - - - - 内容 + + 内容 From 661aed1a94dc099fd0ac80649727d8e2371dc79b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E4=BC=A0=E9=BE=99?= Date: Sat, 3 Sep 2022 17:32:30 +0800 Subject: [PATCH 3/5] style: add annotation --- src/utils/is.js | 1 + 1 file changed, 1 insertion(+) 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 From d1dd58215d25b0ef70f9b31dc52632fcf0058618 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E4=BC=A0=E9=BE=99?= Date: Sat, 3 Sep 2022 17:33:20 +0800 Subject: [PATCH 4/5] wip: crud table --- mock/api/post.js | 10 +++-- src/components/table/CrudTable.vue | 57 +++++++++++++++++-------- src/views/examples/table/post/index.vue | 26 ++++------- 3 files changed, 54 insertions(+), 39 deletions(-) diff --git a/mock/api/post.js b/mock/api/post.js index 46c786c..9195bfa 100644 --- a/mock/api/post.js +++ b/mock/api/post.js @@ -65,10 +65,14 @@ export default [ const { title, pageNo, pageSize } = data.query let pageData = [] let total = 60 - const filterData = posts.filter((item) => item.title.includes(title)) + const filterData = posts.filter((item) => item.title.includes(title) || (!title && title !== 0)) if (filterData.length) { - while (pageData.length < pageSize) { - pageData.push(filterData[Math.round(Math.random() * (filterData.length - 1))]) + 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, diff --git a/src/components/table/CrudTable.vue b/src/components/table/CrudTable.vue index 95635df..3d88e70 100644 --- a/src/components/table/CrudTable.vue +++ b/src/components/table/CrudTable.vue @@ -44,31 +44,52 @@ const props = defineProps({ type: Array, required: true, }, - queryForm: { + /** queryBar中的参数 */ + queryItems: { type: Object, - required: true, + default() { + return {} + }, }, + /** 补充参数(可选) */ + extraParams: { + type: Object, + default() { + return {} + }, + }, + /** + * ! 约定接口入参出参 + * * 分页模式需约定分页接口入参 + * @pageSize 分页参数:一页展示多少条,默认10 + * @pageNo 分页参数:页码,默认1 + * * 需约定接口出参 + * @pageData 分页模式必须,非分页模式如果没有pageData则取上一层data + * @total 分页模式必须,非分页模式如果没有total则取上一层data.length + */ getData: { type: Function, required: true, }, }) -const emit = defineEmits(['update:queryForm', 'onChecked']) +const emit = defineEmits(['update:queryItems', 'onChecked']) const loading = ref(false) -const initQuery = { ...props.queryForm } +const initQuery = { ...props.queryItems } const tableData = ref([]) -const pagination = reactive({ page: 1, pageSize: 10, itemCount: 100 }) +const pagination = reactive({ page: 1, pageSize: 10 }) -async function handleQuery(extraParams = {}) { +async function handleQuery() { try { loading.value = true - const res = await props.getData( - { ...props.queryForm, ...extraParams }, - { pageNo: pagination.page, pageSize: pagination.pageSize } - ) - tableData.value = res?.pageData || res - pagination.itemCount = res.total ?? res.length + let paginationParams = {} + // 如果非分页模式或者使用前端分页,则无需传分页参数 + if (props.isPagination && props.remote) { + paginationParams = { pageNo: pagination.page, pageSize: pagination.pageSize } + } + const { data } = await props.getData({ ...props.queryItems, ...props.extraParams, ...paginationParams }) + tableData.value = data?.pageData || data + pagination.itemCount = data.total ?? data.length } catch (error) { tableData.value = [] pagination.itemCount = 0 @@ -82,22 +103,22 @@ function handleSearch() { handleQuery() } async function handleReset() { - emit('update:queryForm', { ...initQuery }) + emit('update:queryItems', { ...initQuery }) await nextTick() pagination.page = 1 handleQuery() } -function onChecked(rowKeys) { - if (props.columns.some((item) => item.type === 'selection')) { - emit('onChecked', rowKeys) - } -} function onPageChange(currentPage) { pagination.page = currentPage if (props.remote) { handleQuery() } } +function onChecked(rowKeys) { + if (props.columns.some((item) => item.type === 'selection')) { + emit('onChecked', rowKeys) + } +} defineExpose({ handleSearch, diff --git a/src/views/examples/table/post/index.vue b/src/views/examples/table/post/index.vue index f7d70bb..7615c40 100644 --- a/src/views/examples/table/post/index.vue +++ b/src/views/examples/table/post/index.vue @@ -8,15 +8,16 @@ @@ -32,22 +33,11 @@ import { renderIcon } from '@/utils/icon' import api from './api' const $table = ref(null) -const queryForm = ref({ - title: '', -}) +/** queryBar参数 */ +const queryItems = ref({}) +/** 可选,用于补充参数 */ +const extraParams = ref({}) -async function getTableData(query = {}, pagination = {}) { - const { pageSize = 10, pageNo = 1 } = pagination - try { - // * 参数可自定义,如不需要后端分页则可以不传 pagination 相关参数 - const res = await api.getPosts({ ...query, pageSize, pageNo }) - if (res.code === 0) { - return Promise.resolve(res.data) - } - } catch (error) { - return Promise.reject(error) - } -} // 选中事件 function onChecked(rowKeys) { if (rowKeys.length) $message.info(`选中${rowKeys.join(' ')}`) From b59e47b5dd496cfc197e3425aadd7bfa007d0054 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E4=BC=A0=E9=BE=99?= Date: Sat, 3 Sep 2022 22:28:37 +0800 Subject: [PATCH 5/5] feat: finish curd table --- mock/api/post.js | 44 ++++- src/components/table/CrudModal.vue | 6 +- src/composables/index.js | 1 + src/composables/useCRUD.js | 99 +++++++++++ src/utils/http/interceptors.js | 9 +- src/views/examples/table/post/api.js | 8 +- src/views/examples/table/post/index.vue | 216 +++++++++++++++--------- 7 files changed, 292 insertions(+), 91 deletions(-) create mode 100644 src/composables/index.js diff --git a/mock/api/post.js b/mock/api/post.js index 9195bfa..579c533 100644 --- a/mock/api/post.js +++ b/mock/api/post.js @@ -12,7 +12,7 @@ const posts = [ }, { title: 'Vue2&Vue3项目风格指南', - author: '大脸怪', + author: 'Ronnie', category: 'Vue', description: '总结的Vue2和Vue3的项目风格', content: '### 1. 命名风格\n\n> 文件夹如果是由多个单词组成,应该始终是横线连接 ', @@ -93,4 +93,46 @@ export default [ } }, }, + { + 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, + }, + } + } + }, + }, ] diff --git a/src/components/table/CrudModal.vue b/src/components/table/CrudModal.vue index 40404f4..431eb15 100644 --- a/src/components/table/CrudModal.vue +++ b/src/components/table/CrudModal.vue @@ -5,7 +5,7 @@
取消 - 保存 + 保存
@@ -30,6 +30,10 @@ const props = defineProps({ type: Boolean, required: true, }, + loading: { + type: Boolean, + default: false, + }, }) const emit = defineEmits(['update:visible', 'onSave']) 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 index e683ac5..e2ea6f7 100644 --- a/src/composables/useCRUD.js +++ b/src/composables/useCRUD.js @@ -1,5 +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/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 7615c40..7af4293 100644 --- a/src/views/examples/table/post/index.vue +++ b/src/views/examples/table/post/index.vue @@ -1,7 +1,7 @@ @@ -30,29 +84,42 @@ import { NButton, NSwitch } from 'naive-ui' import { formatDateTime } from '@/utils' import { renderIcon } from '@/utils/icon' +import { useCRUD } from '@/composables' import api from './api' +import { isNullOrUndef } from '@/utils/is' const $table = ref(null) -/** queryBar参数 */ -const queryItems = ref({}) -/** 可选,用于补充参数 */ +/** QueryBar筛选参数(可选) */ +const queryItems = ref({ + title: '', +}) +/** 补充参数(可选) */ const extraParams = ref({}) -// 选中事件 -function onChecked(rowKeys) { - if (rowKeys.length) $message.info(`选中${rowKeys.join(' ')}`) -} +onMounted(() => { + $table.value?.handleSearch() +}) const columns = [ - { type: 'selection' }, + { type: 'selection', fixed: 'left' }, + { + title: '发布', + key: 'isPublish', + width: 60, + align: 'center', + fixed: 'left', + render(row) { + return h(NSwitch, { + size: 'small', + rubberBand: false, + value: row['isPublish'], + loading: !!row.publishing, + onUpdateValue: () => handlePublish(row), + }) + }, + }, { title: '标题', key: 'title', width: 150, ellipsis: { tooltip: true } }, { title: '分类', key: 'category', width: 80, ellipsis: { tooltip: true } }, - { - title: '描述', - key: 'description', - width: 200, - ellipsis: { tooltip: true }, - }, { title: '创建人', key: 'author', width: 80 }, { title: '创建时间', @@ -70,42 +137,10 @@ const columns = [ return h('span', formatDateTime(row['updateDate'])) }, }, - { - title: '推荐', - key: 'isRecommend', - width: 120, - align: 'center', - fixed: 'right', - render(row) { - return h(NSwitch, { - size: 'small', - value: row['isRecommend'], - rubberBand: false, - loading: !!row.recommending, - onUpdateValue: () => handleRecommend(row), - }) - }, - }, - { - title: '发布', - key: 'isPublish', - width: 120, - align: 'center', - fixed: 'right', - render(row) { - return h(NSwitch, { - size: 'small', - rubberBand: false, - value: row['isPublish'], - loading: !!row.publishing, - onUpdateValue: () => handlePublish(row), - }) - }, - }, { title: '操作', key: 'actions', - width: 200, + width: 240, align: 'center', fixed: 'right', render(row) { @@ -115,17 +150,29 @@ const columns = [ { size: 'small', type: 'primary', + secondary: true, + onClick: () => handleView(row), + }, + { default: () => '查看', icon: renderIcon('majesticons:eye-line', { size: 14 }) } + ), + h( + NButton, + { + size: 'small', + type: 'primary', + style: 'margin-left: 15px;', onClick: () => handleEdit(row), }, { default: () => '编辑', icon: renderIcon('material-symbols:edit-outline', { size: 14 }) } ), + h( NButton, { size: 'small', type: 'error', style: 'margin-left: 15px;', - onClick: () => handleDelete(row), + onClick: () => handleDelete(row.id), }, { default: () => '删除', icon: renderIcon('material-symbols:delete-outline', { size: 14 }) } ), @@ -134,34 +181,41 @@ const columns = [ }, ] -onMounted(() => { - $table.value?.handleSearch() +// 选中事件 +function onChecked(rowKeys) { + if (rowKeys.length) $message.info(`选中${rowKeys.join(' ')}`) +} + +// 发布 +function handlePublish(row) { + if (isNullOrUndef(row.id)) return + + row.publishing = true + setTimeout(() => { + row.isPublish = !row.isPublish + row.publishing = false + $message?.success(row.isPublish ? '已发布' : '已取消发布') + }, 1000) +} + +const { + modalVisible, + modalAction, + modalTitle, + modalLoading, + handleAdd, + handleDelete, + handleEdit, + handleView, + handleSave, + modalForm, + modalFormRef, +} = useCRUD({ + name: '文章', + initForm: { author: '大脸怪' }, + doCreate: api.addPost, + doDelete: api.deletePost, + doUpdate: api.updatePost, + refresh: () => $table.value?.handleSearch(), }) - -const modalVisible = ref(false) -const modalTitle = ref('新增文章') - -function handleDelete(row) { - if (row && row.id) { - $dialog.confirm({ - content: '确定删除?', - confirm() { - $message.success('删除成功') - initTableData() - }, - cancel() { - $message.success('已取消') - }, - }) - } -} - -function handleEdit(row) { - modalTitle.value = '编辑文章' - modalVisible.value = true -} - -function handleSave() { - modalVisible.value = false -}