This commit is contained in:
2026-07-27 17:19:36 +08:00
parent e50d1b8fda
commit 1c7532e0de
132 changed files with 7066 additions and 1241 deletions

54
src/utils/coordinate.js Normal file
View File

@@ -0,0 +1,54 @@
export const coordinateInputPattern = /^-?\d+(\.\d{0,6})?$/;
const coordinateRanges = {
longitude: {
label: '经度',
min: -180,
max: 180,
},
latitude: {
label: '纬度',
min: -90,
max: 90,
},
};
export const normalizeCoordinateInput = value => {
const rawValue = String(value ?? '').replace(/[^\d.-]/g, '');
const isNegative = rawValue.startsWith('-');
const unsignedValue = rawValue.replace(/-/g, '');
if (!unsignedValue) {
return isNegative ? '-' : '';
}
const [integerPart, ...decimalParts] = unsignedValue.split('.');
const sign = isNegative ? '-' : '';
if (!decimalParts.length) {
return `${sign}${integerPart}`;
}
return `${sign}${integerPart || '0'}.${decimalParts.join('').slice(0, 6)}`;
};
export const isCoordinateInput = value => {
if (value === undefined || value === null || value === '') {
return true;
}
return coordinateInputPattern.test(String(value));
};
export const getCoordinateValidationMessage = (value, type, required = false) => {
const range = coordinateRanges[type];
if (!range) {
return '';
}
if (value === undefined || value === null || value === '') {
return required ? `${range.label}不能为空` : '';
}
if (!isCoordinateInput(value)) {
return `${range.label}最多保留6位小数`;
}
const numberValue = Number(value);
if (numberValue < range.min || numberValue > range.max) {
return `${range.label}范围为 ${range.min}${range.max}`;
}
return '';
};

View File

@@ -0,0 +1,136 @@
const FORM_SELECTOR = '.el-dialog .el-form';
const ITEM_SELECTOR = '.el-form-item';
const COLUMN_TOLERANCE = 8;
const REQUIRED_MARK_WIDTH = 12;
let textMeasureEl;
const isVisible = element => {
if (!element) return false;
const rect = element.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
};
const getDirectLabel = item => {
return Array.from(item.children).find(child => child.classList.contains('el-form-item__label'));
};
const getColumnLeft = item => {
const column = item.closest('.el-col');
const target = column || item;
return Math.round(target.getBoundingClientRect().left);
};
const findColumnGroup = (groups, left) => {
return groups.find(group => Math.abs(group.left - left) <= COLUMN_TOLERANCE);
};
const getTextMeasureEl = () => {
if (textMeasureEl) return textMeasureEl;
textMeasureEl = document.createElement('span');
textMeasureEl.style.position = 'fixed';
textMeasureEl.style.left = '-9999px';
textMeasureEl.style.top = '-9999px';
textMeasureEl.style.visibility = 'hidden';
textMeasureEl.style.whiteSpace = 'nowrap';
textMeasureEl.style.pointerEvents = 'none';
document.body.appendChild(textMeasureEl);
return textMeasureEl;
};
const measureLabelWidth = (label, item) => {
const text = String(label.textContent || '').trim();
if (!text) return 0;
const style = window.getComputedStyle(label);
const measurer = getTextMeasureEl();
measurer.style.font = style.font;
measurer.style.letterSpacing = style.letterSpacing;
measurer.textContent = text;
const paddingLeft = Number.parseFloat(style.paddingLeft) || 0;
const paddingRight = Number.parseFloat(style.paddingRight) || 0;
const requiredWidth = item.classList.contains('is-required') ? REQUIRED_MARK_WIDTH : 0;
return Math.ceil(measurer.getBoundingClientRect().width + paddingLeft + paddingRight + requiredWidth);
};
const collectLabelGroups = form => {
const groups = [];
const items = Array.from(form.querySelectorAll(ITEM_SELECTOR));
items.forEach(item => {
if (!isVisible(item) || item.closest('.avue-form__menu')) return;
const label = getDirectLabel(item);
if (!label || !isVisible(label)) return;
const width = measureLabelWidth(label, item);
if (!width) return;
const left = getColumnLeft(item);
let group = findColumnGroup(groups, left);
if (!group) {
group = {
left,
labels: [],
width: 0,
};
groups.push(group);
}
group.labels.push(label);
group.width = Math.max(group.width, width);
});
return groups;
};
const alignFormLabelsByColumn = form => {
const groups = collectLabelGroups(form);
groups.forEach(group => {
group.labels.forEach(label => {
const width = `${group.width}px`;
if (label.style.width !== width) {
label.style.width = width;
}
if (label.style.flexBasis !== width) {
label.style.flexBasis = width;
}
if (label.style.justifyContent !== 'flex-end') {
label.style.justifyContent = 'flex-end';
}
if (label.style.textAlign !== 'right') {
label.style.textAlign = 'right';
}
});
});
};
const alignAllDialogFormLabels = () => {
document.querySelectorAll(FORM_SELECTOR).forEach(alignFormLabelsByColumn);
};
export const setupDialogFormLabelAlign = () => {
if (typeof window === 'undefined' || typeof document === 'undefined') return;
let frameId = 0;
const scheduleAlign = () => {
if (frameId) return;
frameId = window.requestAnimationFrame(() => {
frameId = 0;
alignAllDialogFormLabels();
});
};
scheduleAlign();
window.addEventListener('resize', scheduleAlign);
const observer = new MutationObserver(scheduleAlign);
observer.observe(document.body, {
attributes: true,
attributeFilter: ['class', 'style'],
childList: true,
subtree: true,
});
};

76
src/utils/import-excel.js Normal file
View File

@@ -0,0 +1,76 @@
import { importBlob } from '@/api/common';
import { downloadXls } from '@/utils/util';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
const excelPattern = /\.(xls|xlsx)$/;
const isExcelResponse = res => {
const contentType = res.headers['content-type'] || res.data.type || '';
return contentType.includes('application/vnd.ms-excel');
};
const parseBlobJson = async blob => {
const text = await blob.text();
return text ? JSON.parse(text) : {};
};
export const openImportDialog = (vm, businessName, onSuccess) => {
const column = vm.findColumn(vm.excelOption.column, 'excelFile');
column.httpRequest = (option, uploadColumn) =>
handleImportExcel(vm, option, uploadColumn, businessName, onSuccess);
vm.excelBox = true;
};
export const handleImportExcel = async (vm, option, column, businessName, onSuccess) => {
const file = option.file;
const fileName = file && file.name ? file.name.toLowerCase() : '';
if (!excelPattern.test(fileName)) {
vm.$message.error('请上传 .xls,.xlsx 标准格式文件');
if (typeof option.onError === 'function') {
option.onError(new Error('请上传 .xls,.xlsx 标准格式文件'));
}
return;
}
NProgress.start();
try {
const res = await importBlob(column.action, file);
if (isExcelResponse(res)) {
downloadXls(
res.data,
`${businessName}导入失败明细${vm.$dayjs().format('YYYY-MM-DD HH:mm:ss')}.xlsx`
);
vm.$message.warning('部分数据导入失败,已下载失败明细');
} else {
const result = await parseBlobJson(res.data);
if (result.code !== 200) {
vm.$message.error(result.msg || '导入失败');
if (typeof option.onError === 'function') {
option.onError(new Error(result.msg || '导入失败'));
}
return;
}
vm.$message.success(result.msg || '导入数据成功');
}
vm.excelBox = false;
vm.excelForm = {};
if (typeof onSuccess === 'function') {
onSuccess();
} else if (typeof vm.onLoad === 'function') {
vm.onLoad(vm.page, vm.query);
} else if (typeof vm.initTree === 'function') {
vm.initTree();
}
if (typeof option.onSuccess === 'function') {
option.onSuccess(res);
}
} catch (error) {
vm.$message.error(error.message || '导入失败');
if (typeof option.onError === 'function') {
option.onError(error);
}
} finally {
NProgress.done();
}
};

167
src/utils/pagination.js Normal file
View File

@@ -0,0 +1,167 @@
const FIRST_BUTTON_CLASS = 'erp-pagination-first';
const LAST_BUTTON_CLASS = 'erp-pagination-last';
const JUMP_BUTTON_CLASS = 'erp-pagination-jump';
const DEFAULT_PAGE_SIZE = 10;
const DEFAULT_PAGE_SIZES = [10, 20, 50, 100];
const PAGE_KEY_PATTERN = /(^page$|Page$|^page[A-Z])/;
const toNumber = value => {
const number = Number(value);
return Number.isFinite(number) ? number : 0;
};
const getNativeInputValueSetter = input => {
const prototype = Object.getPrototypeOf(input);
const descriptor = Object.getOwnPropertyDescriptor(prototype, 'value');
return descriptor && descriptor.set;
};
const setInputValue = (input, value) => {
const setter = getNativeInputValueSetter(input);
if (setter) {
setter.call(input, value);
} else {
input.value = value;
}
input.dispatchEvent(new Event('input', { bubbles: true }));
};
const getPaginationState = pagination => {
const active = pagination.querySelector('.el-pager li.is-active');
const jumpInput = pagination.querySelector('.el-pagination__editor input');
const currentPage = toNumber(
active && active.textContent ? active.textContent.trim() : jumpInput?.value
);
const pageCount = toNumber(jumpInput?.getAttribute('max'));
return {
currentPage: currentPage || 1,
pageCount: pageCount || 1,
jumpInput,
};
};
const changePage = (pagination, page) => {
const { currentPage, pageCount, jumpInput } = getPaginationState(pagination);
const targetPage = Math.min(Math.max(toNumber(page), 1), pageCount);
if (!targetPage || targetPage === currentPage) return;
const targetPager = Array.from(pagination.querySelectorAll('.el-pager li.number')).find(
item => toNumber(item.textContent && item.textContent.trim()) === targetPage
);
if (targetPager) {
targetPager.click();
return;
}
if (jumpInput) {
setInputValue(jumpInput, targetPage);
jumpInput.dispatchEvent(new Event('change', { bubbles: true }));
jumpInput.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
}
};
const createPaginationButton = (className, text, onClick) => {
const button = document.createElement('button');
button.type = 'button';
button.className = `erp-pagination-button ${className}`;
button.textContent = text;
button.addEventListener('click', onClick);
return button;
};
const setButtonDisabled = (button, disabled) => {
if (!button || button.disabled === disabled) return;
button.disabled = disabled;
};
const isPageConfig = value =>
value &&
typeof value === 'object' &&
('pageSize' in value || 'currentPage' in value || 'total' in value);
const normalizePageConfig = page => {
if (!isPageConfig(page)) return;
page.pageSize = DEFAULT_PAGE_SIZES.includes(toNumber(page.pageSize))
? toNumber(page.pageSize)
: DEFAULT_PAGE_SIZE;
page.pageSizes = DEFAULT_PAGE_SIZES;
};
const normalizeComponentPageConfigs = pagination => {
let component = pagination.__vueParentComponent;
while (component) {
[component.data, component.setupState].forEach(state => {
if (!state) return;
Object.keys(state)
.filter(key => PAGE_KEY_PATTERN.test(key))
.forEach(key => normalizePageConfig(state[key]));
});
component = component.parent;
}
};
const updatePaginationButtons = pagination => {
const { currentPage, pageCount, jumpInput } = getPaginationState(pagination);
const firstButton = pagination.querySelector(`.${FIRST_BUTTON_CLASS}`);
const lastButton = pagination.querySelector(`.${LAST_BUTTON_CLASS}`);
const jumpButton = pagination.querySelector(`.${JUMP_BUTTON_CLASS}`);
setButtonDisabled(firstButton, currentPage <= 1);
setButtonDisabled(lastButton, currentPage >= pageCount);
setButtonDisabled(jumpButton, !jumpInput || jumpInput.disabled);
};
const enhancePagination = pagination => {
const prevButton = pagination.querySelector('.btn-prev');
const nextButton = pagination.querySelector('.btn-next');
const jumpWrapper = pagination.querySelector('.el-pagination__jump');
normalizeComponentPageConfigs(pagination);
if (prevButton && !pagination.querySelector(`.${FIRST_BUTTON_CLASS}`)) {
const firstButton = createPaginationButton(FIRST_BUTTON_CLASS, '首页', () => {
changePage(pagination, 1);
});
prevButton.before(firstButton);
}
if (nextButton && !pagination.querySelector(`.${LAST_BUTTON_CLASS}`)) {
const lastButton = createPaginationButton(LAST_BUTTON_CLASS, '尾页', () => {
const { pageCount } = getPaginationState(pagination);
changePage(pagination, pageCount);
});
nextButton.after(lastButton);
}
if (jumpWrapper && !pagination.querySelector(`.${JUMP_BUTTON_CLASS}`)) {
const jumpButton = createPaginationButton(JUMP_BUTTON_CLASS, '跳转', () => {
const { jumpInput } = getPaginationState(pagination);
if (!jumpInput) return;
changePage(pagination, jumpInput.value);
});
jumpWrapper.after(jumpButton);
}
updatePaginationButtons(pagination);
};
const enhanceAllPaginations = () => {
document.querySelectorAll('.el-pagination').forEach(enhancePagination);
};
export const setupPaginationEnhancer = () => {
if (typeof window === 'undefined' || typeof document === 'undefined') return;
const scheduleEnhance = () => {
window.requestAnimationFrame(enhanceAllPaginations);
};
scheduleEnhance();
const observer = new MutationObserver(scheduleEnhance);
observer.observe(document.body, {
attributes: true,
attributeFilter: ['class', 'disabled'],
childList: true,
subtree: true,
});
};