front/src/views/track/trackMapping/Push.vue
2026-01-31 15:32:12 +08:00

1690 lines
46 KiB
Vue

<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { $baseMessage } from '@/utils';
import { IconPlus, IconEdit, IconDelete, IconSearch } from '@arco-design/web-vue/es/icon';
import { useUserStore } from '@/store';
const { t } = useI18n();
import {
postEditContent,
getGetDetail,
postGetContentPage,
postGetPage,
postEdit,
getGetContentDetail,
} from '@/api/TrackPush/trackPush';
import { postGetList } from '@/api/TrackContent/trackContent';
import { getGetList } from '@/api/ApplicationServicesProvider/dictionary';
import { getAppGetList } from '@/api/ApplicationServices/apps';
import { getServiceProviderGetServiceProviderDropdownList } from '@/api/ServiceProviderInterface/serviceProvider';
import CustomLeftMenu from '@/components/CustomLeftMenu/index.vue';
import dayjs from 'dayjs';
import ModalForm from '@/components/ModalForm/index.vue';
import SelectPro from '@/components/SelectPro/index.vue';
import ModalPro from '@/components/ModalPro/index.vue';
import useModal from '@/components/ModalPro/useModal';
import CustomList from '@/components/CustomList/index.vue';
type IDialogConfig = {
show: boolean;
type: 'add' | 'edit';
title: string;
};
const userStore = useUserStore();
/** ???????? */
const editFormParams = reactive<any>({
options: 1,
id: undefined,
});
/** ???????????? */
const editFormRightParams = reactive<any>({
options: 1,
id: undefined,
appKey: userStore.appKey,
});
const topFormRef = ref();
const currentId = ref<API.GetTrackPushConfigPageOutputVO | undefined>();
const modalFormLeftRef = ref();
const modalFormRightRef = ref();
const refHeight = ref();
const { contentHeight, calcContentHeight } = useContentHeight(
refHeight,
true,
12
);
/** ???? */
const searchModel = reactive<any>({});
/** ??????*/
const searchKeyword = ref('');
const pushListData = ref<any[]>([]);
const pushLoading = ref(false);
const appMap = ref<Record<string, string>>({});
async function loadAppMap() {
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 (e) {
console.error('加载应用列表失败', e);
}
}
async function loadPushList() {
pushLoading.value = true;
try {
const params: any = {
pageIndex: 1,
pageSize: 200,
...searchModel,
};
if (userStore.appKey) params.appKey = userStore.appKey;
const res = (await postGetPage(params)) as any;
const items = res?.data?.items ?? res?.items ?? res?.data ?? [];
pushListData.value = Array.isArray(items) ? items : [];
} catch (e) {
$baseMessage('加载推送配置列表失败', 'error');
pushListData.value = [];
} finally {
pushLoading.value = false;
}
}
const pushTreeData = computed(() => {
const list = pushListData.value || [];
if (list.length === 0) return [];
return [
{
key: 'category-1',
title: '推送配置',
children: list.map((item: any) => {
const time = item.createTime ? dayjs(item.createTime).format('YYYY-MM-DD HH:mm') : '';
const app = item.appName || (item.appKey != null ? (appMap.value?.[item.appKey] ?? '') : '') || item.appKey || '-';
const appStr = app ? `【${app}】` : '';
const customerStr = item.customer ? `【${item.customer}】` : '';
const codeLine = [time, appStr, customerStr].filter(Boolean).join(' ');
return {
key: `item-${item.id}`,
title: item.customer || item.serviceProviderName || item.serviceProvider || '-',
isLeaf: true,
id: item.id,
code: codeLine,
...item,
};
}),
},
];
});
const pushSelectedKeys = computed(() =>
currentId.value?.id ? [`item-${currentId.value.id}`] : []
);
const pushExpandedKeys = ref(['category-1']);
function handlePushSelectFromTree(keys: string[], info: any) {
const node = info?.node;
if (!node || !node.isLeaf) return;
const key = node.key;
const id = key.startsWith('item-') ? Number(key.slice(5)) : null;
if (id == null) return;
const item = pushListData.value.find((x: any) => x.id === id);
if (item) cellClick(item);
}
// 明细列表数据(用于新增空白行后重载)
const detailListData = ref<any[]>([]);
// 自定义表头列(与 CustomList 双向绑定)
const customColumns = ref<Array<{ field: string; title: string; isEditing?: boolean; isNew?: boolean }>>([]);
const customListRef = ref<InstanceType<typeof CustomList>>();
// ?????????
const totalCount = ref(0);
const currentPage = ref(1);
const pageSize = ref(30);
const jumpPage = ref('');
// 自定义列表:左侧固定列(序号、系统标准轨迹)
const pushFixedLeftColumns = computed(() => [
{
field: 'index',
title: t('track.push.table.serialNo'),
width: 60,
align: 'center',
fixed: 'left',
formatter: ({ rowIndex }: any) => rowIndex + 1,
},
{
field: 'standardTrack',
title: t('track.push.table.systemStandardTrack'),
width: 280,
align: 'left',
fixed: 'left',
slots: { default: 'standardTrack' },
showOverflow: 'tooltip',
},
]);
// 自定义列表:右侧固定列(操作)
const pushFixedRightColumns = computed(() => [
{
field: 'action',
title: t('track.push.table.actions'),
width: 100,
align: 'left',
fixed: 'right',
slots: { default: 'action' },
},
]);
const pushTableConfigExtra = {
autoHeight: true,
height: undefined,
rowConfig: { useKey: true, keyField: 'id' },
size: 'small',
};
const leftFormProps = computed(() => {
return {
formLayout: 'vertical',
defaultSpan: 12,
colProps: {
span: 12,
style: { marginBottom: '16px' }
},
itemList: [
{
label: t('track.push.form.application'),
field: 'appKey',
comp: 'SelectPro',
rules: [
{
required: true,
message: t('track.push.form.applicationRequired'),
},
],
compProps: {
placeholder: t('track.push.form.applicationPlaceholder'),
size: 'large',
api: getAppGetList,
labelKeys: ['name', 'appKey'],
separator: '+',
idKey: 'appKey',
isPage: true,
immediate: true,
defaultValue: userStore.appKey,
style: { width: '100%', height: '40px' },
},
span: 12,
},
{
label: t('track.push.form.customer'),
field: 'customer',
comp: 'AInput',
compProps: {
placeholder: t('track.push.form.customerPlaceholder'),
style: { width: '100%', height: '40px' },
},
span: 12,
rules: [{ required: true, message: t('track.push.form.customerRequired') }],
},
{
label: t('track.push.form.serviceProvider'),
field: 'serviceProviderCode',
comp: 'SelectPro',
compProps: ({ formValue }: { formValue: any }) => {
return {
immediate: true,
clearable: true,
labelKeys: ['name'],
titleStrs: ['name'],
placeholder: t('track.push.form.serviceProviderPlaceholder'),
style: { width: '100%', height: '40px' },
idKey: 'code',
isPage: false,
api: getServiceProviderGetServiceProviderDropdownList,
extra: { appKey: formValue?.appKey || userStore.appKey },
allowSearch: true,
allowCreate: false,
trigger: 'click',
};
},
span: 12,
rules: [
{
required: true,
message: t('track.push.form.serviceProviderRequired'),
},
],
},
{
label: t('track.push.form.product'),
field: 'product',
comp: 'AInput',
compProps: {
placeholder: t('track.push.form.productPlaceholder'),
style: { width: '100%', height: '40px' },
},
span: 12,
},
{
label: t('track.push.form.isAutoPush'),
field: 'isAutoPush',
comp: 'ASelect',
compProps: () => {
return {
placeholder: t('track.push.form.isAutoPushPlaceholder'),
size: 'large',
defaultValue: true,
style: { width: '100%', height: '40px' },
options: [
{ label: t('track.push.common.yes'), value: true },
{ label: t('track.push.common.no'), value: false },
],
};
},
span: 12,
},
{
label: t('track.push.form.supportRepush'),
field: 'isCanUpdate',
comp: 'ASelect',
compProps: () => {
return {
placeholder: t('track.push.form.supportRepushPlaceholder'),
size: 'large',
defaultValue: false,
style: { width: '100%', height: '40px' },
options: [
{ label: t('track.push.common.yes'), value: true },
{ label: t('track.push.common.no'), value: false },
],
};
},
span: 12,
},
{
label: t('track.push.form.supportDelete'),
field: 'isCanDelete',
comp: 'ASelect',
compProps: () => {
return {
placeholder: t('track.push.form.supportDeletePlaceholder'),
size: 'large',
defaultValue: false,
style: { width: '100%', height: '40px' },
options: [
{ label: t('track.push.common.yes'), value: true },
{ label: t('track.push.common.no'), value: false },
],
};
},
span: 12,
},
{
label: t('track.push.form.isFullPush'),
field: 'isAllPush',
comp: 'ASelect',
compProps: () => {
return {
placeholder: t('track.push.form.isFullPushPlaceholder'),
size: 'large',
defaultValue: false,
style: { width: '100%', height: '40px' },
options: [
{ label: t('track.push.common.yes'), value: true },
{ label: t('track.push.common.no'), value: false },
],
};
},
span: 12,
},
{
label: t('track.push.form.isLatestFullPush'),
field: 'isAllPushWithLatest',
comp: 'ASelect',
compProps: () => {
return {
placeholder: t('track.push.form.isLatestFullPushPlaceholder'),
size: 'large',
defaultValue: false,
style: { width: '100%', height: '40px' },
options: [
{ label: t('track.push.common.yes'), value: true },
{ label: t('track.push.common.no'), value: false },
],
};
},
span: 12,
},
{
label: t('track.push.form.pushTimeStyle'),
field: 'dateTimeFormatType',
comp: 'SelectPro',
compProps: {
idKey: 'value',
labelKeys: ['label'],
style: { width: '100%', height: '40px' },
defaultValue: 0,
options: [
{ label: t('track.push.form.pushTimeStyle.none'), value: 0 },
{ label: t('track.push.form.pushTimeStyle.timestamp'), value: 1 },
{ label: t('track.push.form.pushTimeStyle.timezone'), value: 2 },
],
},
span: 12,
},
{
label: t('track.push.form.time'),
field: 'dateTimeFormat',
comp: 'AInput',
compProps: {
placeholder: t('track.push.form.timePlaceholder'),
style: { width: '100%', height: '40px' },
},
span: 12,
ifShow: ({ model }: { model: any }) => model?.dateTimeFormatType === 2,
},
{
label: t('track.push.form.timezone'),
field: 'timezoneFormat',
comp: 'AInput',
compProps: {
placeholder: t('track.push.form.timezonePlaceholder'),
style: { width: '100%', height: '40px' },
},
span: 12,
ifShow: ({ model }: { model: any }) => model?.dateTimeFormatType === 2,
},
],
};
});
const rightFormProps = computed(() => {
return {
formLayout: 'vertical',
itemList: [
{
label: t('track.push.form.trackCode'),
field: 'trackContentId',
comp: 'SelectPro',
compProps: ({ formValue }: { formValue: any }) => {
return {
clearable: true,
immediate: true,
labelKeys: ['code', 'content', 'enContent'],
titleStrs: [t('track.push.titleStrs.trackCode'), t('track.push.titleStrs.trackContent'), t('track.push.table.trackContentEn')],
separator: '-',
idKey: 'id',
api: postGetList,
searchKey: 'keywords',
extra: { appKey: useUserStore().appKey, pageSize: 100 },
};
},
span: 12,
rules: [{ required: true, message: t('track.push.form.trackCodeRequired') }],
},
{
label: t('track.push.form.pushTrackStatus'),
field: 'trackStatusValue',
comp: 'AInput',
compProps: () => {
return {
placeholder: t('track.push.form.pushTrackStatusPlaceholder'),
};
},
span: 12,
},
{
label: t('track.push.form.pushTrackCode'),
field: 'trackContentCodeValue',
comp: 'AInput',
compProps: () => {
return {
placeholder: t('track.push.form.pushTrackStatusPlaceholder'),
};
},
span: 12,
},
{
label: t('track.push.form.pushTrackContent'),
field: 'trackContentValue',
comp: 'AInput',
compProps: () => {
return {
placeholder: t('track.push.form.pushTrackStatusPlaceholder'),
};
},
span: 12,
},
{
label: t('track.push.form.pushTrackContentEn'),
field: 'trackEnContentValue',
comp: 'AInput',
compProps: () => {
return {
placeholder: t('track.push.form.pushTrackStatusPlaceholder'),
};
},
span: 12,
dynamicRules: () => {
return [
{
pattern: /^[^\u4E00-\u9FA5]{0,3000}$/,
message: t('track.content.form.trackContentEnNoChinese'),
trigger: 'change',
},
];
},
},
{
label: t('track.push.form.pushExtendedField'),
field: 'expandFieldsValue',
comp: 'AInput',
compProps: () => {
return {
placeholder: t('track.push.form.pushTrackStatusPlaceholder'),
};
},
span: 12,
},
{
label: t('track.push.form.timezone'),
field: 'trackTimeZone',
comp: 'Timezone',
span: 12,
},
{
label: t('track.push.form.swapTrackContent'),
field: 'swapTrackContentId',
comp: 'SelectPro',
compProps: ({ formValue }: { formValue: any }) => {
return {
clearable: true,
labelKeys: ['code', 'content'],
titleStrs: [t('track.push.titleStrs.trackCode'), t('track.push.titleStrs.trackContent')],
idKey: 'contentId',
api: postGetContentPage,
disableFn: (id: number) => {
if (id === formValue?.id) return true;
return false;
},
searchKey: 'keywords',
extra: { trackPushConfigId: currentId.value?.id },
};
},
span: 12,
},
],
};
});
// ??????
type TabItem = {
key: number;
value: string;
};
// ????? Tab??????
const activeTab = ref<number | undefined>(0);
const tabs = ref<TabItem[]>([]);
// ??????
type OrderTypeDict = {
[key: number]: string;
};
// ?????????? // function getUnusedOrderTypes(
// allOrderTypes: TabItem[],
// usedOrderTypes: TabItem[]
// ): TabItem[] {
// return allOrderTypes.filter(
// (orderType) =>
// !usedOrderTypes.some((usedType) => usedType.key === orderType.key)
// );
// }
async function cellClick(rowId: API.GetTrackPushConfigPageOutputVO) {
currentId.value = rowId;
const configTagsResponse = await getGetList({
type: 'TrackOrderType',
});
tabs.value = configTagsResponse as unknown as any[];
(configTagsResponse as unknown as any[]).length > 0
? (activeTab.value = (configTagsResponse as unknown as any[])[0].key as number)
: (activeTab.value = 0);
currentPage.value = 1;
jumpPage.value = '1';
loadTableData();
}
function handleTabClick(tab: any) {
activeTab.value = tab;
const targetTab = tabs.value.find((t: any) => t.key === tab);
if (targetTab) {
currentPage.value = 1;
jumpPage.value = '1';
loadTableData();
}
nextTick(() => {
calcContentHeight();
});
}
// ???? ????????
function handleOpenModal(type: IDialogConfig['type'], row?: any) {
const titleObj: { [x in IDialogConfig['type']]: string } = {
add: t('track.push.actions.add'),
edit: t('track.push.actions.edit'),
};
editFormParams.options = 1;
editFormParams.id = undefined;
editFormParams.options = 1;
if (type === 'edit') {
unref(modalFormLeftRef)?.openModalFn({
title: titleObj[type],
id: row?.id,
});
editFormParams.options = 3;
editFormParams.id = row?.id;
return;
}
unref(modalFormLeftRef)?.openModalFn({
title: titleObj[type],
id: editFormParams.id,
});
nextTick(() => {
unref(modalFormLeftRef)?.setFormValue({
appKey: userStore.appKey,
isAutoPush: true,
isCanUpdate: false,
isAllPush: false,
isCanDelete: false,
isAllPushWithLatest: false,
dateTimeFormatType: 0,
});
});
}
// ???? ????????
function handleOpenRightModal(type: IDialogConfig['type'], row?: any) {
const titleObj: { [x in IDialogConfig['type']]: string } = {
add: t('track.push.actions.add'),
edit: t('track.push.actions.edit'),
};
editFormRightParams.options = 1;
editFormRightParams.id = undefined;
editFormRightParams.options = 1;
if (type === 'edit') {
unref(modalFormRightRef)?.openModalFn({
title: titleObj[type],
id: row?.id,
});
editFormRightParams.options = 3;
editFormRightParams.id = row?.id;
return;
}
unref(modalFormRightRef)?.openModalFn({
title: titleObj[type],
id: editFormRightParams.id,
formData: {
trackPushConfigId: currentId.value?.id,
orderNoType: activeTab.value,
},
});
}
// ????
function handleDelData(row: any) {
const id = row?.id ?? row;
$baseConfirm({
content: t('track.warningConfig.modal.deletePushConfirm'),
title: t('track.push.modal.warningTitle'),
onOk: async () => {
await postEdit({ options: 2, id, appKey: userStore.appKey });
onFormSubmitClick();
$baseMessage(t('track.push.message.deleteSuccess'), 'success');
if (id === currentId.value?.id) currentId.value = undefined;
},
});
}
// // ??????
function handleDelRightData(id: number) {
$baseConfirm({
content: t('track.push.modal.deleteConfirm'),
title: t('track.push.modal.warningTitle'),
onOk: async () => {
await postEditContent({ options: 2, id });
unref(customListRef)?.tableRef?.reload?.();
$baseMessage(t('track.push.message.deleteSuccess'), 'success');
},
});
}
/** ?? */
function onFormSubmitClick() {
loadPushList();
currentId.value = undefined;
customListRef.value?.reloadData([]);
}
/** ???? */
function handleSearch() {
Object.assign(searchModel, { keywords: searchKeyword.value });
loadPushList();
currentId.value = undefined;
customListRef.value?.reloadData([]);
}
/** ???? */
function handleRefresh() {
searchKeyword.value = '';
Object.assign(searchModel, { keywords: '' });
loadPushList();
currentId.value = undefined;
customListRef.value?.reloadData([]);
}
// ??????
function onSubmitLeft() {
onFormSubmitClick();
}
// ??????
function onSubmitRight() {
loadTableData();
}
function caclHeiht() {
setTimeout(() => {
calcContentHeight();
}, 100);
}
watch(
() => currentId.value,
(val) => {
if (val) cellClick(val);
},
{
deep: true,
}
);
onMounted(() => {
loadAppMap();
loadPushList();
});
const loading = ref(false);
const loadTableData = async () => {
if (!currentId.value?.id || !activeTab.value) {
detailListData.value = [];
customListRef.value?.reloadData?.([]);
totalCount.value = 0;
return;
}
loading.value = true;
try {
const res = await postGetContentPage({
trackPushConfigId: currentId.value.id,
orderNoType: activeTab.value as API.TrackOrderType | undefined,
pageIndex: currentPage.value,
pageSize: pageSize.value,
}) as API.GetTrackPushContentPageOutputVOPagedResult;
totalCount.value = res?.totalCount || 0;
const items = res?.items || [];
items.forEach((r: any) => {
r.trackContentId = r.trackContentId ?? r.contentId;
});
detailListData.value = items;
customListRef.value?.reloadData?.(items);
} finally {
loading.value = false;
}
};
// 自定义列下拉建议:取当前列在本表数据中的去重非空值,供 a-auto-complete 使用
function getCustomColumnSuggestions(field: string): { value: string }[] {
const set = new Set<string>();
for (const row of detailListData.value) {
const v = row[field];
if (v != null && String(v).trim() !== '') set.add(String(v).trim());
}
return Array.from(set).map((v) => ({ value: v }));
}
// 将轨迹内容 id 规范为数字,便于 a-select 与 option value 匹配并正确显示 label
function normalizeTrackContentId(val: any): number | undefined {
if (val == null || val === '') return undefined;
const n = Number(val);
return Number.isNaN(n) ? undefined : n;
}
// 新增:增加一行空白数据(不打开弹窗)
function handleAddBlankRow() {
if (!currentId.value?.id || activeTab.value == null) return;
const rowKey = `new_${Date.now()}_${Math.random().toString(36).slice(2)}`;
const blankRow: any = {
__isNew: true,
_rowKey: rowKey,
id: rowKey,
trackPushConfigId: currentId.value.id,
orderNoType: activeTab.value,
};
customColumns.value.forEach((col) => {
blankRow[col.field] = '';
});
const next = [...detailListData.value, blankRow];
detailListData.value = next;
customListRef.value?.reloadData?.(next);
$baseMessage(t('track.push.actions.add') + ' 一行空白数据');
}
// 删除新增的空白行(未保存的)
function handleDelNewRow(row: any) {
const idx = detailListData.value.findIndex((r: any) => r === row || (r._rowKey && r._rowKey === row._rowKey));
if (idx > -1) {
detailListData.value.splice(idx, 1);
const next = [...detailListData.value];
detailListData.value = next;
customListRef.value?.reloadData?.(next);
}
}
const handlePageChange = (page: number) => {
if (page < 1) return;
const max = Math.max(1, Math.ceil(totalCount.value / pageSize.value));
if (page > max) return;
currentPage.value = page;
jumpPage.value = String(page);
loadTableData();
};
const handlePageSizeChange = (size: number) => {
pageSize.value = size;
currentPage.value = 1;
jumpPage.value = '1';
loadTableData();
};
const handleJumpPage = () => {
const val = Number(jumpPage.value);
if (!val || val < 1) return;
const max = Math.max(1, Math.ceil(totalCount.value / pageSize.value));
handlePageChange(Math.min(val, max));
};
defineExpose({
caclHeiht,
});
</script>
<template>
<div ref="refHeight" class="track-layout">
<BasicLayoutLeftRight :left-width="280">
<template #left>
<CustomLeftMenu
:tree-data="pushTreeData"
:selected-keys="pushSelectedKeys"
v-model:expanded-keys="pushExpandedKeys"
:loading="pushLoading"
v-model:search-keyword="searchKeyword"
:search-placeholder="$t('track.push.search.placeholder')"
:refresh-title="$t('track.push.actions.refresh')"
:add-title="$t('track.push.actions.addPush')"
loading-text="加载中..."
:delete-label="$t('track.push.actions.delete')"
:show-method="false"
:show-map-count="false"
@search="handleSearch"
@refresh="handleRefresh"
@add="handleOpenModal('add')"
@select="handlePushSelectFromTree"
@delete-node="(nodeData) => handleDelData(nodeData)"
@node-click="(key) => { const id = key.startsWith('item-') ? Number(key.slice(5)) : null; if (id != null) { const item = pushListData.find((x: any) => x.id === id); if (item) cellClick(item); } }"
>
<template #node-menu="{ nodeData }">
<a-doption @click="handleOpenModal('edit', nodeData)">
<IconEdit style="margin-right: 8px;" />
修改
</a-doption>
<a-doption @click="handleDelData(nodeData)">
<IconDelete style="margin-right: 8px;" />
删除
</a-doption>
</template>
</CustomLeftMenu>
<ModalForm
ref="modalFormLeftRef"
:form-props="leftFormProps"
:edit-api="postEdit"
:add-api="postEdit"
:info-api="getGetDetail"
:api-params="editFormParams"
@on-submit="onSubmitLeft"
/>
</template>
<template #right>
<div class="right-content-panel">
<a-tabs
v-model="activeTab"
type="line"
size="large"
class="left-aligned-tabs"
:class="{ 'visibility-hidden': !currentId }"
@tab-click="handleTabClick"
>
<a-tab-pane
v-for="item in tabs"
:key="item?.key"
:title="item.value"
:name="item.key"
/>
</a-tabs>
<!-- ?? Tab???? + ??????+ ?? -->
<div v-if="currentId && activeTab" class="detail-tab-wrapper">
<CustomList
ref="customListRef"
v-model="customColumns"
:data="detailListData"
mode="modal"
:section-title="$t('track.push.table.customHeader')"
:add-placeholder="$t('track.push.table.customColumnPlaceholder')"
:fixed-left-columns="pushFixedLeftColumns"
:fixed-right-columns="pushFixedRightColumns"
:table-config-extra="pushTableConfigExtra"
:table-height="400"
columns-key="trackPushDetail"
:custom-header-disabled="!currentId || !currentId.id"
>
<template #toolbar-left>
<TableButtons
:actions="[
{
label: $t('track.push.actions.add'),
type: 'primary',
disabled: !currentId || !currentId.id,
onClick: handleAddBlankRow,
},
]"
class="big-btn"
/>
</template>
<template #toolbar-right>
<div class="custom-list-toolbar-right-inner">
<div class="pagination-controls">
<span class="total-records">
{{ $t('track.order.pagination.total', { count: totalCount }) }}
</span>
<a-button
size="small"
:disabled="currentPage <= 1"
@click="handlePageChange(currentPage - 1)"
>
&lt;
</a-button>
<span class="current-page">{{ currentPage }}</span>
<a-button
size="small"
:disabled="currentPage >= Math.ceil(totalCount / pageSize)"
@click="handlePageChange(currentPage + 1)"
>
&gt;
</a-button>
<a-select
v-model="pageSize"
size="small"
class="page-size-select"
@change="handlePageSizeChange"
>
<a-option :value="10">10</a-option>
<a-option :value="30">30</a-option>
<a-option :value="50">50</a-option>
<a-option :value="100">100</a-option>
</a-select>
<span class="page-size-text">{{ $t('track.order.pagination.perPage') }}</span>
<span class="go-to-text">{{ $t('track.order.pagination.goTo') }}</span>
<a-input
v-model="jumpPage"
size="small"
class="jump-input"
@keyup.enter="handleJumpPage"
/>
<span class="page-text">{{ $t('track.order.pagination.page') }}</span>
</div>
</div>
</template>
<template #standardTrack="{ row }">
<SelectPro
:model-value="normalizeTrackContentId(row.trackContentId ?? row.contentId)"
:api="postGetList"
:label-keys="['code', 'content', 'enContent']"
:title-strs="[$t('track.push.titleStrs.trackCode'), $t('track.push.titleStrs.trackContent'), $t('track.push.table.trackContentEn')]"
separator="-"
id-key="id"
search-key="keywords"
:extra="{ appKey: userStore.appKey, pageSize: 100 }"
:is-page="false"
immediate
clearable
class="standard-track-select"
@update:model-value="(val: number) => { row.trackContentId = val; row.contentId = val; }"
@select-change="(payload: any) => { if (payload?.item) { row.code = payload.item.code; row.content = payload.item.content; row.enContent = payload.item.enContent; } }"
/>
</template>
<template
v-for="col in customColumns"
:key="col.field"
#[col.field]="{ row }"
>
<a-auto-complete
v-model="row[col.field]"
:data="getCustomColumnSuggestions(col.field)"
:filter-option="(inputValue: string, option: { value: string }) => (option.value || '').toLowerCase().includes((inputValue || '').toLowerCase())"
allow-clear
placeholder="请输入或选择"
class="custom-column-autocomplete"
/>
</template>
<template #action="{ row }">
<div class="action-buttons">
<a-button
type="text"
size="small"
class="action-btn edit-btn"
@click="row.__isNew ? handleOpenRightModal('add') : handleOpenRightModal('edit', row)"
:title="$t('track.push.actions.edit')"
>
<IconEdit />
</a-button>
<a-button
type="text"
size="small"
class="action-btn delete-btn"
@click="row.__isNew ? handleDelNewRow(row) : handleDelRightData(row.id)"
:title="$t('track.push.actions.delete')"
>
<IconDelete />
</a-button>
</div>
</template>
</CustomList>
</div>
</div>
<ModalForm
ref="modalFormRightRef"
:edit-api="postEditContent"
:add-api="postEditContent"
:info-api="getGetContentDetail"
:api-params="editFormRightParams"
:form-props="rightFormProps"
@on-submit="onSubmitRight"
/>
</template>
</BasicLayoutLeftRight>
</div>
</template>
<style lang="less" scoped>
/* ?????? - ??????interface-layout?????*/
.track-layout {
display: flex;
height: calc(100vh - 86px);
max-height: calc(100vh - 86px);
padding: 16px;
gap: 16px;
background-color: var(--color-fill-2);
overflow: hidden;
box-sizing: border-box;
width: 100%;
}
/* ?????? - ????????????????*/
:deep(.trb-transform-left) {
background: var(--app-sidebar-bg) !important;
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
border: 1px solid rgba(255, 255, 255, 0.18) !important;
border-top: 1px solid rgba(255, 255, 255, 0.25) !important;
border-radius: 24px !important;
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.4),
0 2px 8px rgba(0, 0, 0, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.1),
inset 0 -1px 0 rgba(0, 0, 0, 0.2);
position: relative;
color: #fff;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(90deg,
transparent 0%,
rgba(255, 255, 255, 0.3) 50%,
transparent 100%);
pointer-events: none;
z-index: 1;
}
}
.mx-8px {
padding: 0;
height: 100%;
display: flex;
flex-direction: column;
}
.w-full {
padding: 0;
}
.pb-10px {
padding-bottom: 0 !important;
}
.serach_header {
border-bottom: 1px solid rgba(255, 255, 255, 0.08) !important;
padding: 10px 12px !important;
min-height: 48px !important;
margin: 0;
}
/* ?????? - ??????interface-tree?????*/
:deep(.scrollbarplus) {
padding: 8px 0 0;
padding-right: 4px;
flex: 1;
overflow-y: auto;
}
.h-full {
.visibility-hidden {
visibility: hidden;
}
:deep(.a-tabs) {
.a-tabs__content {
display: none;
}
.a-tabs__nav-extra {
display: none;
}
}
/* ????????tab */
:deep(.a-tab-pane) {
background-color: #f0f1f2;
}
:deep(.arco-tabs-content) {
padding: 0;
}
:deep(.arco-tabs-nav) {
background-color: #fff;
}
}
.search-box {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
:deep(.arco-input-wrapper) {
background: rgba(255, 255, 255, 0.1) !important;
border: 1px solid rgba(255, 255, 255, 0.2) !important;
border-radius: 4px !important;
&:hover, &:focus-within {
background: rgba(255, 255, 255, 0.15) !important;
border-color: rgba(255, 255, 255, 0.3) !important;
}
}
:deep(.arco-input) {
background: transparent !important;
border: none !important;
color: #fff !important;
&::placeholder {
color: rgba(255, 255, 255, 0.5) !important;
}
}
:deep(.arco-input-prefix) {
color: rgba(255, 255, 255, 0.6) !important;
}
}
.search-actions {
display: flex;
gap: 4px;
}
.action-icon-btn {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
&.refresh-btn {
color: #fff;
border-radius: 4px;
&:hover {
color: var(--app-primary-color);
}
}
&.add-btn {
color: var(--app-primary-color) !important;
background-color: rgba(73, 186, 179, 0.1) !important;
border: 1px solid var(--app-primary-color) !important;
border-radius: 50% !important;
width: 28px !important;
height: 28px !important;
min-width: 28px !important;
max-width: 28px !important;
min-height: 28px !important;
max-height: 28px !important;
padding: 0 !important;
line-height: 28px !important;
&:hover {
color: #ffffff !important;
background-color: var(--app-primary-color) !important;
border-color: var(--app-primary-color) !important;
}
}
}
.action-buttons {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
.action-btn {
padding: 4px;
border-radius: 4px;
transition: all 0.2s ease;
&.edit-btn {
color: var(--color-text-2);
&:hover {
background-color: var(--color-fill-2);
color: var(--color-text-1);
}
}
&.delete-btn {
color: #ff4d4f;
&:hover {
background-color: #fff2f0;
color: #ff7875;
}
}
}
}
/* ?????????? */
:deep(.arco-btn[disabled]) {
opacity: 0.5 !important;
cursor: not-allowed !important;
background-color: #f5f5f5 !important;
color: #bfbfbf !important;
border-color: #d9d9d9 !important;
}
/* ?????? - ?????*/
.right-content-panel {
height: 100%;
background-color: #fff;
border-radius: 24px;
overflow: hidden;
display: flex;
flex-direction: column;
padding: 0;
margin: 0;
}
.left-aligned-tabs {
flex-shrink: 0;
:deep(.arco-tabs-nav) {
border-bottom: 1px solid var(--color-border-2);
margin: 0;
padding: 0;
justify-content: flex-start !important;
}
:deep(.arco-tabs-content) {
flex: 1;
padding: 0;
overflow: hidden;
background: #fff;
display: flex;
flex-direction: column;
min-height: 0;
}
:deep(.arco-tabs-pane) {
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
min-height: 0;
}
}
.table-toolbar {
margin-bottom: 8px;
margin-left: 0;
margin-right: 0;
display: flex;
justify-content: flex-start;
align-items: center;
padding: 0 16px;
width: 100%;
.toolbar-left {
flex: 0 0 auto;
margin-left: 0;
padding-left: 0;
.left-aligned-buttons {
justify-content: flex-start;
margin-left: 0;
padding-left: 0;
}
}
.toolbar-right {
margin-left: auto;
flex: 0 0 auto;
}
}
/* CustomList 工具栏右侧:新增 + 分页,整体在列表右上角 */
.custom-list-toolbar-right-inner {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: nowrap;
}
.toolbar-right .pagination-controls,
.custom-list-toolbar-right-inner .pagination-controls {
display: flex;
align-items: center;
gap: 8px;
white-space: nowrap;
flex-wrap: nowrap;
.total-records,
.go-to-text,
.page-size-text,
.page-text {
font-size: 12px;
color: #4e5969;
white-space: nowrap;
}
.current-page {
font-size: 12px;
font-weight: 500;
color: #1d2129;
white-space: nowrap;
}
.arco-btn-size-small {
padding: 0 8px;
height: 24px;
min-width: 24px;
border-radius: 4px;
color: #4e5969;
border: 1px solid #e5e6eb;
background-color: #fff;
&:hover {
background-color: #f2f3f5;
}
&:disabled {
cursor: not-allowed;
opacity: 0.6;
}
}
.arco-select-size-small {
.arco-select-view {
height: 24px;
line-height: 24px;
font-size: 12px;
}
}
.arco-input-size-small {
.arco-input-wrapper {
height: 24px;
line-height: 24px;
font-size: 12px;
}
}
}
.page-size-select {
width: 110px;
}
.jump-input {
width: 60px;
}
.detail-tab-wrapper {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
padding: 0;
margin: 0;
min-height: 0;
:deep(.vxe-table) {
overflow-x: hidden !important;
.vxe-table--body-wrapper {
overflow-x: hidden !important;
}
.vxe-table--header-wrapper {
overflow-x: hidden !important;
}
.vxe-body--column:first-child,
.vxe-header--column:first-child {
&,
& .vxe-cell,
& .vxe-cell--title {
text-align: center !important;
justify-content: center !important;
padding-left: 0 !important;
}
.vxe-cell--title {
display: flex !important;
align-items: center !important;
justify-content: center !important;
}
}
.vxe-body--column:not(:first-child) .vxe-cell,
.vxe-header--column:not(:first-child) .vxe-cell {
text-align: left !important;
justify-content: flex-start !important;
padding-left: 12px !important;
}
.vxe-cell {
text-align: left !important;
justify-content: flex-start !important;
padding-left: 12px !important;
}
.vxe-header--column {
text-align: left !important;
justify-content: flex-start !important;
padding-left: 12px !important;
}
.vxe-cell--title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
}
.vxe-table--body-wrapper,
.vxe-table--header-wrapper {
width: 100% !important;
}
.vxe-table--main-wrapper {
width: 100% !important;
}
.vxe-table--body,
.vxe-table--header {
width: 100% !important;
}
.vxe-table--body .vxe-body--column:last-child,
.vxe-table--header .vxe-header--column:last-child {
padding-right: 0 !important;
}
}
:deep(.vxe-table--container) {
overflow-x: hidden !important;
width: 100% !important;
padding: 0 !important;
margin: 0 !important;
}
:deep(.vxe-table) {
width: 100% !important;
margin: 0 !important;
}
:deep(.vxe-table--main-wrapper) {
padding: 0 !important;
margin: 0 !important;
}
}
.standard-track-cell {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
cursor: pointer;
}
.standard-track-select {
width: 100%;
}
.custom-column-autocomplete {
width: 100%;
}
/* 自定义表头弹窗 - 标签高度与输入框一致,整体美化 */
.custom-header-modal :deep(.arco-modal-body) {
padding: 16px 20px 20px;
}
.column-management-section {
padding: 16px 0;
min-width: 320px;
.column-list {
display: flex;
flex-wrap: wrap;
gap: 10px;
max-height: 320px;
overflow-y: auto;
padding: 4px 0;
.column-item {
display: flex;
align-items: center;
min-height: 28px; /* 与 a-input size="small" 高度一致 */
.column-edit {
display: flex;
align-items: center;
min-height: 28px;
:deep(.arco-input-wrapper) {
height: 28px;
min-height: 28px;
}
.arco-input {
width: 160px;
height: 28px;
}
}
.column-display {
display: flex;
align-items: center;
min-height: 28px;
height: 28px;
padding: 0 10px;
background-color: var(--color-fill-2);
border: 1px solid var(--color-border-2);
border-radius: 6px;
box-sizing: border-box;
transition: border-color 0.2s, background-color 0.2s;
&:hover {
border-color: var(--color-border-3);
background-color: var(--color-fill-3);
}
.column-name {
cursor: pointer;
margin-right: 6px;
font-size: 13px;
line-height: 28px;
color: var(--color-text-1);
&:hover {
color: rgb(var(--primary-6));
}
}
.column-delete-btn {
display: flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
min-width: 18px;
min-height: 18px;
padding: 0;
margin: 0;
color: rgb(var(--danger-6));
font-size: 14px;
line-height: 1;
border-radius: 4px;
opacity: 0.75;
transition: opacity 0.2s, background-color 0.2s;
&:hover {
opacity: 1;
background-color: rgb(var(--danger-6) / 0.12);
}
}
}
}
.column-add-item {
display: flex;
align-items: center;
gap: 10px;
min-height: 28px;
.column-add-input {
width: 220px;
:deep(.arco-input-wrapper) {
height: 28px;
min-height: 28px;
}
}
.arco-btn {
height: 28px;
min-width: 28px;
padding: 0 8px;
}
}
}
}
/* 左侧面板与轨迹拉取一致 */
:deep(.trb-transform-left) {
background: transparent !important;
border: none !important;
box-shadow: none !important;
height: 100%;
min-height: 0;
display: flex;
flex-direction: column;
position: relative;
padding: 0 !important;
margin-right: 16px !important;
}
:deep(.trb-transform-left > *:first-child) {
flex: 1;
min-height: 0;
height: 100%;
}
:deep(.node-code-cell) {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
:deep(.node-second-row-extra-cell) {
display: none;
}
/* ??BasicLayoutLeftRight???margin */
:deep(.trb-transform) {
width: 100%;
height: 100%;
}
</style>