feat(admin): 使用@tmagic/table重构活动列表

This commit is contained in:
roymondchen 2022-12-30 15:37:12 +08:00
parent 032ec81b86
commit 8fa1d4a5c3
10 changed files with 100 additions and 397 deletions

View File

@ -14,14 +14,13 @@
"@tmagic/form": "1.2.0", "@tmagic/form": "1.2.0",
"@tmagic/schema": "1.2.0", "@tmagic/schema": "1.2.0",
"@tmagic/stage": "1.2.0", "@tmagic/stage": "1.2.0",
"@tmagic/table": "1.2.0",
"@tmagic/utils": "1.2.0", "@tmagic/utils": "1.2.0",
"axios": "^0.27.2", "axios": "^0.27.2",
"axios-jsonp": "^1.0.4", "axios-jsonp": "^1.0.4",
"core-js": "^3.20.0", "core-js": "^3.20.0",
"element-plus": "^2.2.19", "element-plus": "^2.2.19",
"js-cookie": "^3.0.0", "js-cookie": "^3.0.0",
"moment": "^2.29.3",
"moment-timezone": "^0.5.34",
"serialize-javascript": "^6.0.0", "serialize-javascript": "^6.0.0",
"vue": "^3.2.26", "vue": "^3.2.26",
"vue-router": "^4.0.3" "vue-router": "^4.0.3"
@ -3068,6 +3067,25 @@
"node": ">=14" "node": ">=14"
} }
}, },
"node_modules/@tmagic/table": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@tmagic/table/-/table-1.2.0.tgz",
"integrity": "sha512-sXUeR/wkjAeeFo1XU/v4LVbzWCSNJ5ZtzmSPuZvWbDTUEDvsHNuujoEizGngsL0h+qYkAt+ZkzFdV95M++Y1fA==",
"dependencies": {
"@tmagic/design": "1.2.0",
"@tmagic/form": "1.2.0",
"@tmagic/utils": "1.2.0",
"lodash-es": "^4.17.21",
"vue": "^3.2.37"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"@tmagic/form": "1.2.0",
"vue": "^3.2.37"
}
},
"node_modules/@tmagic/utils": { "node_modules/@tmagic/utils": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/@tmagic/utils/-/utils-1.2.0.tgz", "resolved": "https://registry.npmjs.org/@tmagic/utils/-/utils-1.2.0.tgz",
@ -12273,25 +12291,6 @@
"integrity": "sha512-A/78XjoX2EmNvppVWEhM2oGk3x4lLxnkEA4jTbaK97QKSDjkIoOsKQlfylt/d3kKKi596Qy3NP5XrXJ6fZIC9Q==", "integrity": "sha512-A/78XjoX2EmNvppVWEhM2oGk3x4lLxnkEA4jTbaK97QKSDjkIoOsKQlfylt/d3kKKi596Qy3NP5XrXJ6fZIC9Q==",
"dev": true "dev": true
}, },
"node_modules/moment": {
"version": "2.29.3",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz",
"integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==",
"engines": {
"node": "*"
}
},
"node_modules/moment-timezone": {
"version": "0.5.34",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.34.tgz",
"integrity": "sha512-3zAEHh2hKUs3EXLESx/wsgw6IQdusOT8Bxm3D9UrHPQR7zlMmzwybC8zHEM1tQ4LJwP7fcxrWr8tuBg05fFCbg==",
"dependencies": {
"moment": ">= 2.9.0"
},
"engines": {
"node": "*"
}
},
"node_modules/monaco-editor": { "node_modules/monaco-editor": {
"version": "0.34.0", "version": "0.34.0",
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.34.0.tgz", "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.34.0.tgz",
@ -18982,6 +18981,18 @@
"moveable-helper": "^0.4.0" "moveable-helper": "^0.4.0"
} }
}, },
"@tmagic/table": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@tmagic/table/-/table-1.2.0.tgz",
"integrity": "sha512-sXUeR/wkjAeeFo1XU/v4LVbzWCSNJ5ZtzmSPuZvWbDTUEDvsHNuujoEizGngsL0h+qYkAt+ZkzFdV95M++Y1fA==",
"requires": {
"@tmagic/design": "1.2.0",
"@tmagic/form": "1.2.0",
"@tmagic/utils": "1.2.0",
"lodash-es": "^4.17.21",
"vue": "^3.2.37"
}
},
"@tmagic/utils": { "@tmagic/utils": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/@tmagic/utils/-/utils-1.2.0.tgz", "resolved": "https://registry.npmjs.org/@tmagic/utils/-/utils-1.2.0.tgz",
@ -26062,19 +26073,6 @@
"integrity": "sha512-A/78XjoX2EmNvppVWEhM2oGk3x4lLxnkEA4jTbaK97QKSDjkIoOsKQlfylt/d3kKKi596Qy3NP5XrXJ6fZIC9Q==", "integrity": "sha512-A/78XjoX2EmNvppVWEhM2oGk3x4lLxnkEA4jTbaK97QKSDjkIoOsKQlfylt/d3kKKi596Qy3NP5XrXJ6fZIC9Q==",
"dev": true "dev": true
}, },
"moment": {
"version": "2.29.3",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz",
"integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw=="
},
"moment-timezone": {
"version": "0.5.34",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.34.tgz",
"integrity": "sha512-3zAEHh2hKUs3EXLESx/wsgw6IQdusOT8Bxm3D9UrHPQR7zlMmzwybC8zHEM1tQ4LJwP7fcxrWr8tuBg05fFCbg==",
"requires": {
"moment": ">= 2.9.0"
}
},
"monaco-editor": { "monaco-editor": {
"version": "0.34.0", "version": "0.34.0",
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.34.0.tgz", "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.34.0.tgz",

View File

@ -15,14 +15,13 @@
"@tmagic/form": "1.2.0", "@tmagic/form": "1.2.0",
"@tmagic/schema": "1.2.0", "@tmagic/schema": "1.2.0",
"@tmagic/stage": "1.2.0", "@tmagic/stage": "1.2.0",
"@tmagic/table": "1.2.0",
"@tmagic/utils": "1.2.0", "@tmagic/utils": "1.2.0",
"axios": "^0.27.2", "axios": "^0.27.2",
"axios-jsonp": "^1.0.4", "axios-jsonp": "^1.0.4",
"core-js": "^3.20.0", "core-js": "^3.20.0",
"element-plus": "^2.2.19", "element-plus": "^2.2.19",
"js-cookie": "^3.0.0", "js-cookie": "^3.0.0",
"moment": "^2.29.3",
"moment-timezone": "^0.5.34",
"serialize-javascript": "^6.0.0", "serialize-javascript": "^6.0.0",
"vue": "^3.2.26", "vue": "^3.2.26",
"vue-router": "^4.0.3" "vue-router": "^4.0.3"

View File

@ -41,7 +41,14 @@
</el-row> </el-row>
</template> </template>
<m-table :data="tableData.res" :config="columns" @sort-change="sortChange" /> <magic-table
rowkey-name="actId"
:data="tableData.res.data"
:loading="!tableData.res.fetch"
:empty-text="tableData.res.errorMsg"
:columns="columns"
@sort-change="sortChange"
/>
<div class="bottom clearfix" style="margin-top: 10px; text-align: right"> <div class="bottom clearfix" style="margin-top: 10px; text-align: right">
<el-pagination <el-pagination
@ -57,15 +64,13 @@
</div> </div>
</el-card> </el-card>
<form-dialog <MFormDialog
ref="dialog"
title="新建活动" title="新建活动"
:visible="formDialogVisible"
:values="actValues" :values="actValues"
:action="action"
:config="formConfig" :config="formConfig"
@afterAction="afterAction" @submit="submitHandler"
@close="closeFormDialogHandler" ></MFormDialog>
></form-dialog>
</div> </div>
</template> </template>
@ -76,22 +81,25 @@ import { defineComponent, onMounted, watch } from '@vue/runtime-core';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import actApi, { ActListItem, ActListQuery, ActListRes, CopyInfo, OrderItem } from '@src/api/act'; import { MFormDialog } from '@tmagic/form';
import FormDialog from '@src/components/form-dialog.vue'; import { MagicTable } from '@tmagic/table';
import MTable from '@src/components/table.vue';
import actApi, { ActInfoDetail, ActListItem, ActListQuery, ActListRes, CopyInfo, OrderItem } from '@src/api/act';
import { getActListFormConfig } from '@src/config/act-list-config'; import { getActListFormConfig } from '@src/config/act-list-config';
import { BlankActFormConfig } from '@src/config/blank-act-config'; import { BlankActFormConfig } from '@src/config/blank-act-config';
import { ActStatus } from '@src/config/status'; import { ActStatus } from '@src/config/status';
import type { ActFormValue, ColumnItem } from '@src/typings'; import type { ActFormValue } from '@src/typings';
import { status } from '@src/use/use-status'; import { status } from '@src/use/use-status';
import { Res } from '@src/util/request'; import { Res } from '@src/util/request';
import { datetimeFormatter } from '@src/util/utils'; import { datetimeFormatter } from '@src/util/utils';
export default defineComponent({ export default defineComponent({
name: 'act-list', name: 'act-list',
components: { FormDialog, MTable }, components: { MFormDialog, MagicTable },
setup() { setup() {
const dialog = ref<InstanceType<typeof MFormDialog>>();
const actStatus = [...status.actStatus]; const actStatus = [...status.actStatus];
const pageStatus = [...status.pageStatus]; const pageStatus = [...status.pageStatus];
const router = useRouter(); const router = useRouter();
@ -119,7 +127,6 @@ export default defineComponent({
const tableData = reactive<{ res: ActListRes }>({ const tableData = reactive<{ res: ActListRes }>({
res: { data: [], fetch: false, errorMsg: '', total: 0 }, res: { data: [], fetch: false, errorMsg: '', total: 0 },
}); });
const formDialogVisible = ref<boolean>(false);
// //
const getActs = async () => { const getActs = async () => {
const res = await actApi.getList({ const res = await actApi.getList({
@ -165,7 +172,7 @@ export default defineComponent({
const copyActAfterHandler = async () => { const copyActAfterHandler = async () => {
await getActs(); await getActs();
}; };
const columns: ColumnItem[] = getActListFormConfig( const columns = getActListFormConfig(
pageStatusFormatter, pageStatusFormatter,
actStatusFormatter, actStatusFormatter,
router, router,
@ -240,7 +247,7 @@ export default defineComponent({
actBeginTime: datetimeFormatter(new Date()), actBeginTime: datetimeFormatter(new Date()),
actEndTime: datetimeFormatter(new Date()), actEndTime: datetimeFormatter(new Date()),
}; };
formDialogVisible.value = true; dialog.value && (dialog.value.dialogVisible = true);
}; };
const afterAction = (res: Res<{ actId: number }>) => { const afterAction = (res: Res<{ actId: number }>) => {
@ -248,28 +255,30 @@ export default defineComponent({
router.push(`/editor/${actId}`); router.push(`/editor/${actId}`);
}; };
const closeFormDialogHandler = () => { const submitHandler = async (info: ActInfoDetail) => {
formDialogVisible.value = false; const res = await actApi.saveAct({
data: info,
});
afterAction(res);
}; };
return { return {
dialog,
actStatus, actStatus,
columns, columns,
query, query,
tableData, tableData,
actValues: toRefs(actValues).data, actValues: toRefs(actValues).data,
formDialogVisible,
formConfig: BlankActFormConfig, formConfig: BlankActFormConfig,
action: actApi.saveAct,
searchChangeHandler, searchChangeHandler,
actStatusChangeHandle, actStatusChangeHandle,
pageTitleChangeHandler, pageTitleChangeHandler,
sortChange, sortChange,
handleSizeChange, handleSizeChange,
handleCurrentChange, handleCurrentChange,
afterAction,
closeFormDialogHandler,
newHandler, newHandler,
submitHandler,
}; };
}, },
}); });

View File

@ -1,144 +0,0 @@
<!-- 新建活动对话框 -->
<template>
<el-dialog
custom-class="m-dialog"
top="10%"
:title="title"
:model-value="dialogVisible"
:appendToBody="true"
:close-on-click-modal="false"
:before-close="closeHandler"
>
<div class="m-dialog-body" :style="`max-height: ${bodyHeight}; overflow-y: auto; overflow-x: hidden;`">
<m-form v-if="dialogVisible" ref="form" :config="config" :init-values="formInitValues"></m-form>
<slot></slot>
</div>
<template #footer>
<el-row class="dialog-footer">
<el-col :span="12" style="text-align: left">
<div style="min-height: 1px">
<slot name="left"></slot>
</div>
</el-col>
<el-col :span="12">
<slot name="footer">
<el-button @click="$emit('close')" size="small"> </el-button>
<el-button type="primary" size="small" :loading="saveFetch" @click="save">确定</el-button>
</slot>
</el-col>
</el-row>
</template>
</el-dialog>
</template>
<script lang="ts">
import { computed, defineComponent, PropType, ref } from 'vue';
import { ElMessage } from 'element-plus';
import { MForm } from '@tmagic/form';
import type { ActFormValue, FormConfigItem } from '@src/typings';
import { Res } from '@src/util/request';
export default defineComponent({
name: 'm-form-dialog',
props: {
values: {
type: Object as PropType<ActFormValue>,
default: () => ({}),
},
title: String,
config: {
type: Array as PropType<FormConfigItem[]>,
default: () => [],
},
visible: {
type: Boolean,
default: () => false,
},
action: {
type: Function as PropType<(options: { data: any }) => Res>,
},
},
emits: ['afterAction', 'close'],
setup(props, { emit }) {
const form = ref<InstanceType<typeof MForm>>();
const saveFetch = ref(false);
const dialogVisible = computed(() => props.visible);
const formInitValues = computed(() => props.values);
//
const closeHandler = () => {
emit('close');
form.value?.resetForm();
};
//
const save = async () => {
if (saveFetch.value) {
return;
}
saveFetch.value = true;
try {
const values = await form.value?.submitForm();
if (!values) {
emit('close');
return;
}
const res = await props.action?.({ data: values });
if (res) {
if (res.ret === 0) {
ElMessage.success(res.msg || '保存成功');
emit('close');
emit('afterAction', res);
} else {
ElMessage({
type: 'error',
duration: 10000,
showClose: true,
dangerouslyUseHTMLString: true,
message: res.msg || '保存失败',
});
}
} else {
emit('close');
}
} catch (e) {
ElMessage({
type: 'error',
duration: 10000,
showClose: true,
message: (e as Error).message,
dangerouslyUseHTMLString: true,
});
}
saveFetch.value = false;
};
return {
dialogVisible,
saveFetch,
form,
formInitValues,
bodyHeight: ref(`${document.body.clientHeight - 194}px`),
closeHandler,
save,
};
},
});
</script>
<style>
.m-dialog .el-dialog__body {
padding: 0 !important;
}
.m-dialog .m-dialog-body {
padding: 0 20px;
}
.el-table .m-form-item .el-form-item {
margin-bottom: 0;
}
</style>

View File

@ -1,155 +0,0 @@
<!-- 表格组件 -->
<template>
<el-table
:data="tableData?.data"
:empty-text="tableData?.errorMsg || '暂无数据'"
@sort-change="sortChange"
v-loading="!tableData?.fetch"
border
>
<!-- 解析表格配置 -->
<template v-for="(item, columnIndex) in columns">
<!-- 操作栏 -->
<el-table-column
:key="columnIndex + '1'"
v-if="item.actions"
:prop="item.prop"
:label="item.label"
:width="item.width"
:fixed="item.fixed"
>
<template #default="{ row, $index }">
<el-button
class="action-btn"
v-for="(action, actionIndex) in item.actions"
:key="actionIndex"
@click="actionHandler(action, row, $index)"
text
size="small"
v-html="action.text"
></el-button>
</template>
</el-table-column>
<!-- 数据展示栏 -->
<el-table-column
v-else
:key="columnIndex + '2'"
:prop="item.prop"
:label="item.label"
:width="item.width"
:fixed="item.fixed"
:sortable="item.sortable ? item.sortable : false"
show-overflow-tooltip
:type="item.type"
>
<template #default="{ row }">
<!-- 展示为文字链接 -->
<el-button v-if="item.action === 'actionLink'" text @click="item.handler(row)">
{{ formatter(item, row) }}
</el-button>
<!-- 展示为标签 -->
<el-tag v-else-if="item.action === 'tag'" :type="statusTagType[row[item.prop]]" close-transition>
{{ formatter(item, row) }}
</el-tag>
<!-- 扩展表格子表 -->
<el-table
v-else-if="item.table"
:data="row.pages"
empty-text="暂无数据"
border
size="small"
class="sub-table"
>
<!-- 解析子表 -->
<el-table-column
v-for="(column, columnIndex) in item.table"
:key="columnIndex"
:prop="column.prop"
:label="column.label"
>
<template #default="page">
{{ formatter(column, page.row) }}
</template>
</el-table-column>
</el-table>
<div v-else v-html="formatter(item, row)"></div>
</template>
</el-table-column>
</template>
</el-table>
</template>
<script lang="ts">
import { computed, defineComponent, PropType } from 'vue';
import { ActListItem, ActListRes } from '@src/api/act';
import type { ActionItem, ColumnItem } from '@src/typings';
import { status } from '@src/use/use-status';
export default defineComponent({
name: 'm-table',
props: {
data: {
type: Object as PropType<ActListRes>,
default: () => ({ data: [], fetch: true, errorMsg: '', total: 0 }),
},
config: {
type: Array,
default: () => [],
},
},
emits: ['sort-change'],
setup(props, { emit }) {
const tableData = computed(() => props.data);
const columns = computed(() => props.config);
const isValidProp = (row: object, prop: string) => prop && prop in row;
return {
tableData,
columns,
statusTagType: [...status.statusTagType],
//
actionHandler: async (action: ActionItem, row: ActListItem) => {
await action.handler?.(row);
action.after?.();
},
//
formatter: (item: ColumnItem, row: ActListItem) => {
if (!isValidProp(row, item.prop)) {
return '';
}
if (item.formatter) {
try {
return item.formatter(row[item.prop], row);
} catch (e) {
console.log((e as Error).message);
return row[item.prop];
}
} else {
return row[item.prop];
}
},
//
sortChange: (column: { prop: string; order: string }) => {
emit('sort-change', column);
},
};
},
});
</script>
<style lang="scss">
.sub-table {
margin-top: 10px;
margin-bottom: 10px;
th {
background-color: #ffffff;
color: #909399;
}
}
</style>

View File

@ -18,6 +18,8 @@
import type { Router } from 'vue-router'; import type { Router } from 'vue-router';
import { MagicTable } from '@tmagic/table';
import type { ActListItem } from '@src/api/act'; import type { ActListItem } from '@src/api/act';
import type { ColumnItem } from '@src/typings'; import type { ColumnItem } from '@src/typings';
import { datetimeFormatter } from '@src/util/utils'; import { datetimeFormatter } from '@src/util/utils';
@ -29,11 +31,15 @@ export const getActListFormConfig = (
router: Router, router: Router,
copyActHandler: ColumnItem['handler'], copyActHandler: ColumnItem['handler'],
copyActAfterHandler: ColumnItem['handler'], copyActAfterHandler: ColumnItem['handler'],
): ColumnItem[] => [ ) => [
{ {
prop: '', prop: '',
type: 'expand', type: 'expand',
table: [ component: MagicTable,
props: (row: ActListItem) => ({
data: row.pages,
border: true,
columns: [
{ {
prop: 'pageTitle', prop: 'pageTitle',
label: '页面标题', label: '页面标题',
@ -46,6 +52,8 @@ export const getActListFormConfig = (
{ {
prop: 'pagePublishStatus', prop: 'pagePublishStatus',
label: '页面状态', label: '页面状态',
action: 'tag',
type: (v: number) => ['', 'success'][v],
formatter: pageStatusFormatter, formatter: pageStatusFormatter,
}, },
{ {
@ -54,6 +62,7 @@ export const getActListFormConfig = (
formatter: (v: string | number | Date) => (v as string) || '-', formatter: (v: string | number | Date) => (v as string) || '-',
}, },
], ],
}),
}, },
{ {
prop: 'actId', prop: 'actId',
@ -86,6 +95,7 @@ export const getActListFormConfig = (
prop: 'actStatus', prop: 'actStatus',
label: '活动状态', label: '活动状态',
action: 'tag', action: 'tag',
type: (v: number) => ['info', '', 'success'][v],
formatter: actStatusFormatter, formatter: actStatusFormatter,
}, },
{ {

View File

@ -19,9 +19,9 @@
import { ElMessage, ElMessageBox } from 'element-plus'; import { ElMessage, ElMessageBox } from 'element-plus';
import type { MNode } from '@tmagic/schema'; import type { MNode } from '@tmagic/schema';
import { isPage } from '@tmagic/utils';
import actApi from '@src/api/act'; import actApi from '@src/api/act';
import { isPage } from '@tmagic/utils';
export default { export default {
/** /**

View File

@ -134,8 +134,8 @@ export interface EditorInfo {
// 新建活动的初始值类型 // 新建活动的初始值类型
export interface ActFormValue { export interface ActFormValue {
operator: string; operator: string;
actBeginTime: string; actBeginTime: string | number;
actEndTime: string; actEndTime: string | number;
} }
// 侧边栏配置 // 侧边栏配置
export interface AsideState { export interface AsideState {

View File

@ -16,25 +16,11 @@
* limitations under the License. * limitations under the License.
*/ */
import momentTimezone from 'moment-timezone';
import serialize from 'serialize-javascript'; import serialize from 'serialize-javascript';
import { EditorInfo } from '@src/typings'; import { EditorInfo } from '@src/typings';
export const datetimeFormatter = function (v: string | number | Date): string { export { datetimeFormatter } from '@tmagic/utils';
if (v) {
let time = null;
time = momentHandler(v);
// 格式化为北京时间
if (time !== 'Invalid date') {
return time;
}
return '-';
}
return '-';
};
const momentHandler = (v: string | number | Date) =>
momentTimezone.tz(v, 'Asia/Shanghai').format('YYYY-MM-DD HH:mm:ss');
export const serializeConfig = function (value: EditorInfo): string { export const serializeConfig = function (value: EditorInfo): string {
return serialize(value, { return serialize(value, {

View File

@ -236,7 +236,7 @@ describe('List', () => {
const wrapper = getWrapper(); const wrapper = getWrapper();
const createButton = wrapper.find('#create'); const createButton = wrapper.find('#create');
await createButton.trigger('click'); await createButton.trigger('click');
expect(wrapper.vm.formDialogVisible).toBe(true); expect(wrapper.vm.dialog?.dialogVisible).toBe(true);
}); });
it('路由的活动状态值缺省', () => { it('路由的活动状态值缺省', () => {