590 lines
18 KiB
Vue
590 lines
18 KiB
Vue
<script setup lang="ts">
|
||
import { useI18n } from 'vue-i18n';
|
||
import { $baseMessage } from '@/utils';
|
||
import { useUserStore } from '@/store';
|
||
|
||
import { postGetList as postGetListTrajectoryContent } from '@/api/TrackFlag/trackFlag';
|
||
|
||
import { postGetList as postGetListTrajectoryState } from '@/api/TrackState/trackStatus';
|
||
import { inputEnNum } from '@/utils/formatter';
|
||
import {
|
||
postGetList,
|
||
postEdit,
|
||
getGetDetail,
|
||
} from '@/api/TrackContent/trackContent';
|
||
import { getAppGetList } from '@/api/ApplicationServices/apps';
|
||
import PaginationPro from '@/components/PaginationPro/index.vue';
|
||
import HandsontablePro from '@/components/HandsontablePro/index.vue';
|
||
import { useTableContainerHeight } from '@/composables/useTableContainerHeight';
|
||
import { IconSearch } from '@arco-design/web-vue/es/icon';
|
||
|
||
const { t } = useI18n();
|
||
const { tableContainerRef, tableHeight } = useTableContainerHeight();
|
||
const handsontableHeight = computed(() => Math.max(tableHeight.value || 0, 400));
|
||
|
||
type IDialogConfig = {
|
||
show: boolean;
|
||
type: 'add' | 'edit';
|
||
title: string;
|
||
};
|
||
defineOptions({ name: 'TrackContent' });
|
||
const userStore = useUserStore();
|
||
|
||
const modalFormRef = ref();
|
||
const editFormParams = reactive<any>({
|
||
options: 1,
|
||
id: undefined,
|
||
appKey: userStore.appKey,
|
||
app: userStore.appKey,
|
||
});
|
||
const searchKeyword = ref('');
|
||
|
||
const tableData = ref<any[]>([]);
|
||
const totalCount = ref(0);
|
||
const currentPage = ref(1);
|
||
const pageSize = ref(50);
|
||
|
||
const appMap = ref<Record<string, string>>({});
|
||
|
||
const loadAppMap = async () => {
|
||
try {
|
||
const res = await getAppGetList({
|
||
PageIndex: 1,
|
||
PageSize: 1000,
|
||
}) as any;
|
||
const apps = res?.data?.items || res?.items || res?.data || [];
|
||
const map: Record<string, string> = {};
|
||
apps.forEach((app: any) => {
|
||
if (app.appKey && app.name) {
|
||
map[app.appKey] = app.name;
|
||
}
|
||
});
|
||
appMap.value = map;
|
||
} catch (error) {
|
||
console.error('加载应用列表失败', error);
|
||
}
|
||
};
|
||
|
||
const columns = computed(() => [
|
||
{ key: 'code', label: t('track.content.table.trajectoryCode'), width: 120 },
|
||
{ key: 'content', label: t('track.content.table.trajectoryContent'), width: 150 },
|
||
{ key: 'enContent', label: t('track.content.table.trajectoryContentEn'), width: 150 },
|
||
{ key: 'trackStatus', label: t('track.content.table.status'), width: 120 },
|
||
{ key: 'trackFlag', label: t('track.content.table.flag'), width: 120 },
|
||
{ key: 'isExternal', label: t('track.content.table.isExternal'), width: 100 },
|
||
{ key: 'isRepeatable', label: t('track.content.table.isRepeatable'), width: 100 },
|
||
{ key: 'isAllowManuallyUpdate', label: t('track.content.table.allowManualModify'), width: 120 },
|
||
{ key: 'isAllowManuallyCreate', label: t('track.content.table.allowManualCreate'), width: 120 },
|
||
{ key: 'createTime', label: t('track.content.table.createTime'), width: 150 },
|
||
{ key: 'createUser', label: t('track.content.table.creator'), width: 100 },
|
||
{ key: 'updateTime', label: t('track.content.table.modifyTime'), width: 150 },
|
||
{ key: 'updateUser', label: t('track.content.table.modifier'), width: 100 },
|
||
{ key: 'app', label: t('track.content.table.application'), width: 120 },
|
||
]);
|
||
|
||
function customCellRenderer(row: number, col: number, value: any, rowData: any) {
|
||
const dataRow = tableData.value[row];
|
||
if (!dataRow) return null;
|
||
|
||
const yes = t('track.content.common.yes');
|
||
const no = t('track.content.common.no');
|
||
// 物理列索引:序号列占 col 0,数据列从 col 1 起,isExternal/isRepeatable/isAllowManuallyUpdate/isAllowManuallyCreate 为 col 6/7/8/9
|
||
const colorMap: Record<number, boolean | undefined> = {
|
||
6: dataRow.isExternal,
|
||
7: dataRow.isRepeatable,
|
||
8: dataRow.isAllowManuallyUpdate,
|
||
9: dataRow.isAllowManuallyCreate,
|
||
};
|
||
if (col in colorMap) {
|
||
const val = colorMap[col];
|
||
const text = val ? yes : no;
|
||
return `<span style="color: ${val ? '#00b42a' : '#f53f3f'};">${text}</span>`;
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
function actionRenderer(row: any, rowIndex: number) {
|
||
const editText = t('track.content.actions.edit');
|
||
const deleteText = t('track.content.actions.delete');
|
||
return `
|
||
<div class="action-buttons" style="display: flex; align-items: center; justify-content: center; gap: 8px;">
|
||
<span data-action="edit" data-row-index="${rowIndex}" style="cursor: pointer; color: var(--color-text-2); padding: 4px; border-radius: 4px;" title="${editText}">
|
||
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M11.7 2.3a1 1 0 0 1 1.4 0l1 1a1 1 0 0 1 0 1.4l-8 8a1 1 0 0 1-.7.3H4a1 1 0 0 1-1-1v-1.4a1 1 0 0 1 .3-.7l8-8z"/></svg>
|
||
</span>
|
||
<span data-action="delete" data-row-index="${rowIndex}" style="cursor: pointer; color: #ff4d4f; padding: 4px; border-radius: 4px;" title="${deleteText}">
|
||
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/><path d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1z"/></svg>
|
||
</span>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function handleActionClick(row: any, _rowIndex: number, action?: string) {
|
||
if (action === 'edit') {
|
||
handleOpenModal('edit', row);
|
||
} else if (action === 'delete') {
|
||
handleDelData(row.id);
|
||
}
|
||
}
|
||
|
||
const loading = ref(false);
|
||
|
||
const loadTableData = async () => {
|
||
loading.value = true;
|
||
try {
|
||
const res = await postGetList({
|
||
appKey: userStore.appKey,
|
||
keywords: searchKeyword.value,
|
||
pageIndex: currentPage.value,
|
||
pageSize: pageSize.value,
|
||
}) as API.GetTrackContentListDtoPagedResult;
|
||
totalCount.value = Number(res?.totalCount ?? 0);
|
||
const items = res?.items || [];
|
||
tableData.value = items.map((item: any) => ({
|
||
...item,
|
||
app: item.appKey
|
||
? (appMap.value[item.appKey] || item.appKey)
|
||
: (typeof item.app === 'object' && item.app?.name ? item.app.name : item.app),
|
||
}));
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
};
|
||
|
||
const handlePageChange = (page: number) => {
|
||
currentPage.value = page;
|
||
loadTableData();
|
||
};
|
||
|
||
const handlePageSizeChange = (size: number) => {
|
||
pageSize.value = size;
|
||
currentPage.value = 1;
|
||
loadTableData();
|
||
};
|
||
|
||
const formProps = computed(() => {
|
||
return {
|
||
formLayout: 'vertical',
|
||
defaultSpan: 12,
|
||
title: t('track.content.modal.addTitle'),
|
||
itemList: [
|
||
{
|
||
label: t('track.content.form.status'),
|
||
field: 'trackStatusId',
|
||
comp: 'SelectPro',
|
||
compProps: ({ formModel }: any) => {
|
||
return {
|
||
placeholder: t('track.content.form.statusPlaceholder'),
|
||
showHeader: false,
|
||
titleStrs: [t('track.content.titleStrs.statusCode'), t('track.content.titleStrs.status')],
|
||
labelKeys: ['code', 'trackStatus'],
|
||
idKey: 'id',
|
||
isPage: true,
|
||
immediate: true,
|
||
api: postGetListTrajectoryState,
|
||
extra: {
|
||
appKey: userStore.appKey,
|
||
selectIds: formModel?.trackStatusId
|
||
? [formModel.trackStatusId]
|
||
: [],
|
||
},
|
||
};
|
||
},
|
||
|
||
rules: [
|
||
{
|
||
required: true,
|
||
message: t('track.content.form.statusRequired'),
|
||
trigger: 'blur',
|
||
},
|
||
],
|
||
span: 12,
|
||
},
|
||
{
|
||
label: t('track.content.form.trajectoryCode'),
|
||
field: 'code',
|
||
comp: 'AInput',
|
||
|
||
compProps: ({ formModel }: any) => {
|
||
return {
|
||
disabled: !!formModel?.id,
|
||
placeholder: t('track.content.form.trajectoryCodePlaceholder'),
|
||
size: 'large',
|
||
formatter: (e: string) => inputEnNum(e, true),
|
||
parser: (e: string) => inputEnNum(e, true),
|
||
};
|
||
},
|
||
rules: [
|
||
{
|
||
required: true,
|
||
message: t('track.content.form.trajectoryCodeRequired'),
|
||
trigger: 'blur',
|
||
},
|
||
],
|
||
span: 12,
|
||
},
|
||
{
|
||
field: 'content',
|
||
label: t('track.content.form.trajectoryContent'),
|
||
comp: 'AInput',
|
||
compProps: () => {
|
||
return {
|
||
placeholder: t('track.content.form.trajectoryContentPlaceholder'),
|
||
size: 'large',
|
||
};
|
||
},
|
||
rules: [
|
||
{
|
||
required: true,
|
||
message: t('track.content.form.trajectoryContentRequired'),
|
||
trigger: 'blur',
|
||
},
|
||
],
|
||
span: 12,
|
||
},
|
||
{
|
||
field: 'enContent',
|
||
label: t('track.content.form.trajectoryContentEn'),
|
||
comp: 'AInput',
|
||
compProps: () => {
|
||
return {
|
||
placeholder: t('track.content.form.trajectoryContentEnPlaceholder'),
|
||
size: 'large',
|
||
};
|
||
},
|
||
rules: [
|
||
{
|
||
required: true,
|
||
message: t('track.content.form.trajectoryContentEnRequired'),
|
||
trigger: 'blur',
|
||
},
|
||
{
|
||
required: true,
|
||
pattern: /^[^\u4E00-\u9FA5]{0,3000}$/,
|
||
message: t('track.content.form.trajectoryContentEnNoChinese'),
|
||
trigger: 'change',
|
||
},
|
||
],
|
||
|
||
span: 12,
|
||
},
|
||
|
||
{
|
||
comp: 'SelectPro',
|
||
field: 'trackFlagIds',
|
||
label: t('track.content.form.flag'),
|
||
compProps: ({ formModel }: any) => {
|
||
return {
|
||
placeholder: t('track.content.form.flagPlaceholder'),
|
||
showHeader: false,
|
||
titleStrs: [t('track.content.titleStrs.flag'), t('track.content.titleStrs.code'), t('track.content.titleStrs.isBroadcast')],
|
||
labelKeys: ['flag', 'code', 'isToMq'],
|
||
idKey: 'id',
|
||
isPage: true,
|
||
immediate: true,
|
||
api: postGetListTrajectoryContent,
|
||
multiple: true,
|
||
extra: {
|
||
appKey: userStore.appKey,
|
||
selectIds: formModel?.trackFlagIds
|
||
? formModel.trackFlagIds
|
||
: [],
|
||
},
|
||
};
|
||
},
|
||
|
||
colProps: {
|
||
span: 12,
|
||
},
|
||
},
|
||
|
||
{
|
||
label: t('track.content.form.isExternal'),
|
||
field: 'isExternal',
|
||
comp: 'a-select',
|
||
compProps: {
|
||
placeholder: t('track.content.form.isExternalPlaceholder'),
|
||
size: 'large',
|
||
defaultValue: false,
|
||
options: [
|
||
{ label: t('track.content.common.yes'), value: true },
|
||
{ label: t('track.content.common.no'), value: false },
|
||
],
|
||
},
|
||
span: 12,
|
||
},
|
||
{
|
||
label: t('track.content.form.isRepeatable'),
|
||
field: 'isRepeatable',
|
||
comp: 'a-select',
|
||
compProps: {
|
||
placeholder: t('track.content.form.isRepeatablePlaceholder'),
|
||
size: 'large',
|
||
defaultValue: true,
|
||
options: [
|
||
{ label: t('track.content.common.yes'), value: true },
|
||
{ label: t('track.content.common.no'), value: false },
|
||
],
|
||
},
|
||
span: 12,
|
||
},
|
||
{
|
||
label: t('track.content.form.allowManualCreate'),
|
||
field: 'isAllowManuallyCreate',
|
||
comp: 'a-select',
|
||
compProps: {
|
||
placeholder: t('track.content.form.allowManualCreatePlaceholder'),
|
||
size: 'large',
|
||
defaultValue: true,
|
||
options: [
|
||
{ label: t('track.content.common.yes'), value: true },
|
||
{ label: t('track.content.common.no'), value: false },
|
||
],
|
||
},
|
||
span: 12,
|
||
},
|
||
{
|
||
label: t('track.content.form.allowManualModify'),
|
||
field: 'isAllowManuallyUpdate',
|
||
comp: 'a-select',
|
||
compProps: {
|
||
placeholder: t('track.content.form.allowManualModifyPlaceholder'),
|
||
size: 'large',
|
||
defaultValue: true,
|
||
options: [
|
||
{ label: t('track.content.common.yes'), value: true },
|
||
{ label: t('track.content.common.no'), value: false },
|
||
],
|
||
},
|
||
span: 12,
|
||
},
|
||
{
|
||
field: 'appKey',
|
||
label: t('track.content.form.application'),
|
||
comp: 'SelectPro',
|
||
rules: [
|
||
{
|
||
required: true,
|
||
message: t('track.content.form.applicationRequired'),
|
||
},
|
||
],
|
||
compProps: {
|
||
placeholder: t('track.content.form.applicationPlaceholder'),
|
||
size: 'large',
|
||
api: getAppGetList,
|
||
labelKeys: ['name', 'appKey'],
|
||
separator: '+',
|
||
idKey: 'appKey',
|
||
isPage: true,
|
||
immediate: true,
|
||
onChange: handleAppChange,
|
||
},
|
||
span: 12,
|
||
},
|
||
],
|
||
};
|
||
});
|
||
|
||
function handleAppChange(appKey: string) {
|
||
editFormParams.appKey = appKey;
|
||
editFormParams.app = appKey;
|
||
}
|
||
|
||
function handleOpenModal(type: IDialogConfig['type'], row?: any) {
|
||
const titleObj: { [x in IDialogConfig['type']]: string } = {
|
||
add: t('track.content.actions.add'),
|
||
edit: t('track.content.actions.edit'),
|
||
};
|
||
editFormParams.options = 1;
|
||
editFormParams.id = undefined;
|
||
|
||
if (type === 'edit') {
|
||
unref(modalFormRef)?.openModalFn({
|
||
id: row?.id,
|
||
title: titleObj[type],
|
||
});
|
||
editFormParams.options = 3;
|
||
editFormParams.id = row?.id;
|
||
editFormParams.appKey = userStore.appKey;
|
||
return;
|
||
}
|
||
unref(modalFormRef)?.openModalFn({
|
||
id: editFormParams.id,
|
||
title: titleObj[type],
|
||
formData: {
|
||
appKey: userStore.appKey,
|
||
},
|
||
});
|
||
}
|
||
|
||
function onSubmit() {
|
||
loadTableData();
|
||
}
|
||
|
||
function handleDelData(id: number) {
|
||
$baseConfirm({
|
||
content: t('track.content.modal.deleteConfirm'),
|
||
title: t('track.content.modal.warningTitle'),
|
||
onOk: async () => {
|
||
await postEdit({ options: 2, id, appKey: userStore.appKey });
|
||
await loadTableData();
|
||
$baseMessage(t('track.content.message.deleteSuccess'), 'success');
|
||
},
|
||
});
|
||
}
|
||
|
||
function handleSearch() {
|
||
currentPage.value = 1;
|
||
loadTableData();
|
||
}
|
||
|
||
function handleRefresh() {
|
||
searchKeyword.value = '';
|
||
currentPage.value = 1;
|
||
loadTableData();
|
||
}
|
||
|
||
onMounted(() => {
|
||
loadAppMap();
|
||
loadTableData();
|
||
});
|
||
</script>
|
||
|
||
<template>
|
||
<div class="track-content-page">
|
||
<div class="track-content-page-card">
|
||
<a-input
|
||
v-model="searchKeyword"
|
||
:placeholder="$t('track.content.search.placeholder')"
|
||
allow-clear
|
||
size="small"
|
||
class="search-input"
|
||
@change="handleSearch"
|
||
@clear="handleSearch"
|
||
@keyup.enter="handleSearch"
|
||
>
|
||
<template #prefix>
|
||
<icon-search />
|
||
</template>
|
||
</a-input>
|
||
|
||
<div class="table-toolbar">
|
||
<div class="toolbar-left">
|
||
<TableButtons
|
||
:actions="[
|
||
{
|
||
label: $t('track.content.actions.add'),
|
||
type: 'primary',
|
||
onClick: () => handleOpenModal('add'),
|
||
},
|
||
]"
|
||
/>
|
||
</div>
|
||
<div class="toolbar-right">
|
||
<PaginationPro
|
||
:total-count="totalCount"
|
||
v-model:current-page="currentPage"
|
||
v-model:page-size="pageSize"
|
||
@page-change="handlePageChange"
|
||
@page-size-change="handlePageSizeChange"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div ref="tableContainerRef" class="list-wrap">
|
||
<HandsontablePro
|
||
:columns="columns"
|
||
:data="tableData"
|
||
:total="totalCount"
|
||
:current-page="currentPage"
|
||
:page-size="pageSize"
|
||
:search-params="{}"
|
||
:show-action-column="true"
|
||
:action-column-position="'end'"
|
||
:action-column-width="110"
|
||
:custom-cell-renderer="customCellRenderer"
|
||
:action-renderer="actionRenderer"
|
||
:height="handsontableHeight"
|
||
:read-only="true"
|
||
:stretch-columns="true"
|
||
@action-click="handleActionClick"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<ModalForm
|
||
ref="modalFormRef"
|
||
:edit-api="postEdit"
|
||
:add-api="postEdit"
|
||
:info-api="getGetDetail"
|
||
:api-params="editFormParams"
|
||
:form-props="formProps"
|
||
@on-submit="onSubmit"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped lang="less">
|
||
.track-content-page {
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
padding: 16px 16px 24px;
|
||
background-color: #f0f3f4;
|
||
flex: 1;
|
||
min-height: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow-x: hidden;
|
||
overflow-y: hidden;
|
||
}
|
||
|
||
.track-content-page-card {
|
||
background: #fff;
|
||
border-radius: 8px;
|
||
padding: 0;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||
box-sizing: border-box;
|
||
flex: 1;
|
||
min-height: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.search-input {
|
||
width: 20%;
|
||
max-width: 320px;
|
||
margin: 16px 16px 16px;
|
||
}
|
||
|
||
.table-toolbar {
|
||
padding: 0 16px;
|
||
margin-bottom: 16px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
flex-wrap: nowrap;
|
||
}
|
||
|
||
.list-wrap {
|
||
width: 100%;
|
||
flex: 1;
|
||
min-height: 0;
|
||
padding: 0 16px 16px;
|
||
box-sizing: border-box;
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overscroll-behavior: contain;
|
||
touch-action: pan-x pan-y;
|
||
}
|
||
|
||
.list-wrap :deep(.handsontable-pro-container) {
|
||
flex: 1;
|
||
min-height: 0;
|
||
width: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
</style>
|