front/src/views/track/trackContent/index.vue
2026-03-23 18:03:05 +08:00

590 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>