提交
This commit is contained in:
1 parent
33c038b8d9
commit
54b27a3f0e
253 files changed
+30060
-14298
No files matched your search
@@ -13,6 +13,41 @@ export function buildAmapNavigationUrl(
|
||||
return `https://uri.amap.com/navigation?to=${lngText},${latText},${toName}&mode=car&coordinate=gaode&callnative=1`
|
||||
}
|
||||
|
||||
/** 构建高德 URI 标记点链接(Web/App 查看位置) */
|
||||
export function buildAmapMarkerUrl(
|
||||
lng?: number | null,
|
||||
lat?: number | null,
|
||||
name?: string
|
||||
): string | null {
|
||||
if (lng == null || lat == null || Number.isNaN(Number(lng)) || Number.isNaN(Number(lat))) {
|
||||
return null
|
||||
}
|
||||
const lngText = Number(lng).toFixed(6)
|
||||
const latText = Number(lat).toFixed(6)
|
||||
const markerName = encodeURIComponent((name || '位置').trim() || '位置')
|
||||
return `https://uri.amap.com/marker?position=${lngText},${latText}&name=${markerName}&coordinate=gaode&callnative=0`
|
||||
}
|
||||
|
||||
/** 按地址关键词打开高德地图搜索(无坐标时的兜底) */
|
||||
export function buildAmapSearchUrl(keyword?: string | null): string | null {
|
||||
const text = keyword?.trim()
|
||||
if (!text) return null
|
||||
return `https://www.amap.com/search?query=${encodeURIComponent(text)}`
|
||||
}
|
||||
|
||||
/** 优先坐标标记,否则按地址搜索 */
|
||||
export function buildAmapLocationUrl(options: {
|
||||
lng?: number | null
|
||||
lat?: number | null
|
||||
name?: string
|
||||
address?: string
|
||||
}): string | null {
|
||||
return (
|
||||
buildAmapMarkerUrl(options.lng, options.lat, options.name || options.address) ||
|
||||
buildAmapSearchUrl(options.address || options.name)
|
||||
)
|
||||
}
|
||||
|
||||
export function appendAmapNavLine(lines: string[], label: string, url: string | null) {
|
||||
if (url) {
|
||||
lines.push(`${label}:${url}`)
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { formatSumMoney } from './format'
|
||||
|
||||
export interface SummaryColumn {
|
||||
property?: string
|
||||
type?: string
|
||||
}
|
||||
|
||||
export interface PageSummaryParams {
|
||||
columns: SummaryColumn[]
|
||||
data: Record<string, unknown>[]
|
||||
}
|
||||
|
||||
export interface BuildPageSummaryOptions {
|
||||
/** 放置合计标签的列 prop,默认 applyCode;默认不展示标签文案 */
|
||||
labelProp?: string
|
||||
/** 自定义标签文案;默认返回空字符串 */
|
||||
labelFormatter?: (count: number) => string
|
||||
/** 列 prop -> 合计值(已格式化字符串) */
|
||||
sumByProp?: Record<string, (data: Record<string, unknown>[]) => string>
|
||||
}
|
||||
|
||||
/** 对数值字段求和并格式化为合计金额 */
|
||||
export function sumField(
|
||||
data: Record<string, unknown>[],
|
||||
field: string,
|
||||
optional = false
|
||||
): string {
|
||||
const total = data.reduce((sum, row) => {
|
||||
const value = row[field]
|
||||
if (optional && (value == null || value === '')) return sum
|
||||
return sum + Number(value || 0)
|
||||
}, 0)
|
||||
return formatSumMoney(total)
|
||||
}
|
||||
|
||||
/** 按业务类型(备用金 / 报销单)分列求和,用于合并列表合计行 */
|
||||
export function sumFieldByBizType(
|
||||
data: Record<string, unknown>[],
|
||||
field: string,
|
||||
bizTypeField = 'bizType'
|
||||
): string {
|
||||
let advanceTotal = 0
|
||||
let reimburseTotal = 0
|
||||
for (const row of data) {
|
||||
const amount = Number(row[field] || 0)
|
||||
const bizType = row[bizTypeField]
|
||||
if (bizType === '备用金') advanceTotal += amount
|
||||
else if (bizType === '报销单') reimburseTotal += amount
|
||||
}
|
||||
return `备用金总额 ${formatSumMoney(advanceTotal)} / 报销单总额 ${formatSumMoney(reimburseTotal)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 Element Plus 表格合计行方法;按当前可见 columns 动态对齐,兼容列显隐。
|
||||
*/
|
||||
export function buildPageSummaryMethod(options: BuildPageSummaryOptions = {}) {
|
||||
const {
|
||||
labelProp = 'applyCode',
|
||||
labelFormatter = () => '',
|
||||
sumByProp = {}
|
||||
} = options
|
||||
|
||||
return ({ columns, data }: PageSummaryParams) => {
|
||||
const label = labelFormatter(data.length)
|
||||
return columns.map((col) => {
|
||||
const prop = col.property
|
||||
if (prop === labelProp) return label
|
||||
if (prop && sumByProp[prop]) return sumByProp[prop](data)
|
||||
return ''
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { TagType, ViewField } from '@/components/business/system-view-dialog/types'
|
||||
import type { TagType, ViewField, ViewHighlight } from '@/components/business/system-view-dialog/types'
|
||||
import { formatViewValue } from '@/components/business/system-view-dialog/types'
|
||||
|
||||
type DictStoreLike = {
|
||||
@@ -37,3 +37,11 @@ export function crmTagField(
|
||||
): ViewField {
|
||||
return { label, value: display || '—', tagType, ...extra }
|
||||
}
|
||||
|
||||
export function crmHighlight(
|
||||
label: string,
|
||||
value: unknown,
|
||||
extra?: Partial<ViewHighlight>
|
||||
): ViewHighlight {
|
||||
return { label, value: formatViewValue(value), ...extra }
|
||||
}
|
||||
@@ -112,6 +112,29 @@ export function formatPlanProductListText(
|
||||
return `${name}${specPart},${qtyUnit}`
|
||||
}
|
||||
|
||||
/** 司机公开页产品一行展示(含运输计划产品备注) */
|
||||
export function formatPublicTransportProductLine(product: {
|
||||
productName?: string
|
||||
model?: string
|
||||
packageSpec?: string
|
||||
unit?: string
|
||||
quantity?: number | string | null
|
||||
remark?: string
|
||||
}) {
|
||||
const name = product.productName?.trim() || '—'
|
||||
const parts: string[] = []
|
||||
const model = product.model?.trim()
|
||||
const packageSpec = product.packageSpec?.trim()
|
||||
if (model) parts.push(model)
|
||||
if (packageSpec) parts.push(packageSpec)
|
||||
const qty = getPlanProductPlannedQuantity(product)
|
||||
const unitRaw = product.unit || '吨'
|
||||
parts.push(qty !== '—' ? `${qty} ${unitRaw}` : unitRaw)
|
||||
const remark = product.remark?.trim()
|
||||
parts.push(`备注:${remark || '—'}`)
|
||||
return `${name}(${parts.join(',')})`
|
||||
}
|
||||
|
||||
export function getPlPlanCompanyLabel(company?: string) {
|
||||
const map: Record<string, string> = { shanxia: '安庆山峡', yundong: '安徽云栋' }
|
||||
return company ? map[company] || company : '—'
|
||||
@@ -142,7 +165,7 @@ export function getPlPlanFollowStatusLabel(status?: string) {
|
||||
draft: '草稿',
|
||||
processing: '进行中',
|
||||
completed: '已完成',
|
||||
cancelled: '已取消'
|
||||
cancelled: '已作废'
|
||||
}
|
||||
return status ? map[status] || status : '—'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import type { CertRow } from '@/api/pl'
|
||||
|
||||
export const PL_PERSONNEL_CERT_MISSING_SUFFIX = '(证书缺失)'
|
||||
|
||||
export const PL_PERSONNEL_DRIVER_REQUIRED_CERT_NAMES = [
|
||||
'身份证',
|
||||
'驾驶证',
|
||||
'从业资格证'
|
||||
] as const
|
||||
|
||||
export const PL_PERSONNEL_ESCORT_REQUIRED_CERT_NAMES = ['身份证', '从业资格证'] as const
|
||||
|
||||
export type PersonnelCertRole = 'driver' | 'escort'
|
||||
|
||||
export function matchRequiredCertName(
|
||||
actualName: string | undefined,
|
||||
requiredName: string
|
||||
): boolean {
|
||||
const name = actualName?.trim() || ''
|
||||
if (!name) return false
|
||||
if (requiredName === '从业资格证') {
|
||||
return (
|
||||
name === '从业资格证' ||
|
||||
name === '危化品从业资格证' ||
|
||||
name.includes('从业资格证')
|
||||
)
|
||||
}
|
||||
return name === requiredName
|
||||
}
|
||||
|
||||
export function isPersonnelCertUploaded(cert?: Pick<CertRow, 'certName' | 'attachmentUrls'>) {
|
||||
return Boolean(cert?.certName?.trim() && cert?.attachmentUrls?.trim())
|
||||
}
|
||||
|
||||
export function getRequiredPersonnelCertNames(role: PersonnelCertRole): readonly string[] {
|
||||
return role === 'driver'
|
||||
? PL_PERSONNEL_DRIVER_REQUIRED_CERT_NAMES
|
||||
: PL_PERSONNEL_ESCORT_REQUIRED_CERT_NAMES
|
||||
}
|
||||
|
||||
export function getMissingPersonnelCertNames(
|
||||
role: PersonnelCertRole,
|
||||
certList: CertRow[] = []
|
||||
): string[] {
|
||||
return getRequiredPersonnelCertNames(role).filter(requiredName => {
|
||||
const matched = certList.find(cert => matchRequiredCertName(cert.certName, requiredName))
|
||||
return !isPersonnelCertUploaded(matched)
|
||||
})
|
||||
}
|
||||
|
||||
export function hasPersonnelCertMissing(
|
||||
role: PersonnelCertRole,
|
||||
certList: CertRow[] = []
|
||||
): boolean {
|
||||
return getMissingPersonnelCertNames(role, certList).length > 0
|
||||
}
|
||||
|
||||
/** 人员主数据页:至少满足押运员必备证书(身份证 + 从业资格证) */
|
||||
export function hasPersonnelMasterCertMissing(certList: CertRow[] = []): boolean {
|
||||
return hasPersonnelCertMissing('escort', certList)
|
||||
}
|
||||
|
||||
export function getPersonnelCertMissingSuffix(
|
||||
certList: CertRow[] = [],
|
||||
role?: PersonnelCertRole
|
||||
): string {
|
||||
if (role) {
|
||||
return hasPersonnelCertMissing(role, certList) ? PL_PERSONNEL_CERT_MISSING_SUFFIX : ''
|
||||
}
|
||||
return hasPersonnelMasterCertMissing(certList) ? PL_PERSONNEL_CERT_MISSING_SUFFIX : ''
|
||||
}
|
||||
@@ -3,8 +3,24 @@ import type { CertRow } from '@/api/pl'
|
||||
export const PL_VEHICLE_CERT_PART_HEAD = 'head'
|
||||
export const PL_VEHICLE_CERT_PART_TRAILER = 'trailer'
|
||||
|
||||
export const PL_VEHICLE_CERT_MISSING_SUFFIX = '(证书缺失)'
|
||||
|
||||
/** @deprecated 保存不再阻断,仅保留供历史引用 */
|
||||
export const PL_VEHICLE_REQUIRED_CERT_NAMES = ['行驶证', '营运证'] as const
|
||||
|
||||
export const PL_SELF_VEHICLE_HEAD_REQUIRED_CERT_NAMES = [
|
||||
'行驶证',
|
||||
'营运证',
|
||||
'保险单'
|
||||
] as const
|
||||
|
||||
export const PL_SELF_VEHICLE_TRAILER_REQUIRED_CERT_NAMES = [
|
||||
'行驶证',
|
||||
'营运证',
|
||||
'保险单',
|
||||
'罐检报告'
|
||||
] as const
|
||||
|
||||
export const PL_VEHICLE_CERT_NAME_OPTIONS = [
|
||||
'行驶证',
|
||||
'营运证',
|
||||
@@ -12,7 +28,8 @@ export const PL_VEHICLE_CERT_NAME_OPTIONS = [
|
||||
'驾驶证',
|
||||
'押运员证',
|
||||
'危化品从业资格证',
|
||||
'保险单'
|
||||
'保险单',
|
||||
'罐检报告'
|
||||
]
|
||||
|
||||
export type VehicleCertPart = typeof PL_VEHICLE_CERT_PART_HEAD | typeof PL_VEHICLE_CERT_PART_TRAILER
|
||||
@@ -53,16 +70,61 @@ export function mergeVehicleCertsByPart<T extends CertRow>(
|
||||
return [...normalizedHead, ...normalizedTrailer]
|
||||
}
|
||||
|
||||
function isCertFilled(cert?: CertRow) {
|
||||
if (!cert) return false
|
||||
return Boolean(
|
||||
cert.certName?.trim() &&
|
||||
cert.certNo?.trim() &&
|
||||
cert.expireDate &&
|
||||
cert.attachmentUrls?.trim()
|
||||
)
|
||||
export function isVehicleCertUploaded(cert?: Pick<CertRow, 'certName' | 'attachmentUrls'>) {
|
||||
return Boolean(cert?.certName?.trim() && cert?.attachmentUrls?.trim())
|
||||
}
|
||||
|
||||
export function getMissingSelfVehiclePartCertNames(
|
||||
certList: CertRow[] = [],
|
||||
part: VehicleCertPart
|
||||
): string[] {
|
||||
const requiredNames =
|
||||
part === PL_VEHICLE_CERT_PART_TRAILER
|
||||
? PL_SELF_VEHICLE_TRAILER_REQUIRED_CERT_NAMES
|
||||
: PL_SELF_VEHICLE_HEAD_REQUIRED_CERT_NAMES
|
||||
|
||||
return requiredNames.filter(requiredName => {
|
||||
const matched = certList.find(cert => cert.certName?.trim() === requiredName)
|
||||
return !isVehicleCertUploaded(matched)
|
||||
})
|
||||
}
|
||||
|
||||
export function hasSelfVehicleCertMissing(options: {
|
||||
certList?: CertRow[]
|
||||
trailerNo?: string | null
|
||||
}): boolean {
|
||||
const { certList = [], trailerNo } = options
|
||||
const { headCerts, trailerCerts } = splitVehicleCertsByPart(certList)
|
||||
const hasTrailer = Boolean(trailerNo?.trim())
|
||||
|
||||
if (getMissingSelfVehiclePartCertNames(headCerts, PL_VEHICLE_CERT_PART_HEAD).length > 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (hasTrailer && getMissingSelfVehiclePartCertNames(trailerCerts, PL_VEHICLE_CERT_PART_TRAILER).length > 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export function getSelfVehicleCertMissingSuffix(options: {
|
||||
certList?: CertRow[]
|
||||
trailerNo?: string | null
|
||||
}): string {
|
||||
return hasSelfVehicleCertMissing(options) ? PL_VEHICLE_CERT_MISSING_SUFFIX : ''
|
||||
}
|
||||
|
||||
export function getSelfVehiclePartCertMissingSuffix(
|
||||
certList: CertRow[] = [],
|
||||
part: VehicleCertPart
|
||||
): string {
|
||||
return getMissingSelfVehiclePartCertNames(certList, part).length > 0
|
||||
? PL_VEHICLE_CERT_MISSING_SUFFIX
|
||||
: ''
|
||||
}
|
||||
|
||||
/** @deprecated 保存不再阻断证书完整性,仅校验证书名称非空等基础项 */
|
||||
export function validateVehicleRequiredCerts(options: {
|
||||
headCerts: CertRow[]
|
||||
trailerCerts: CertRow[]
|
||||
@@ -71,28 +133,12 @@ export function validateVehicleRequiredCerts(options: {
|
||||
trailerNo: string
|
||||
onError: (message: string) => void
|
||||
}) {
|
||||
const { headCerts, trailerCerts, hasTrailer, licensePlate, trailerNo, onError } = options
|
||||
const parts: Array<{ label: string; certs: CertRow[] }> = [
|
||||
{ label: licensePlate?.trim() || '主车', certs: headCerts }
|
||||
]
|
||||
if (hasTrailer) {
|
||||
parts.push({ label: trailerNo?.trim() || '挂车', certs: trailerCerts })
|
||||
}
|
||||
|
||||
for (const part of parts) {
|
||||
for (const certName of PL_VEHICLE_REQUIRED_CERT_NAMES) {
|
||||
const matched = part.certs.find(item => item.certName?.trim() === certName)
|
||||
if (!isCertFilled(matched)) {
|
||||
onError(`请完善「${part.label}」的${certName}(证号、有效期、附件均必填)`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
const { headCerts, trailerCerts, hasTrailer } = options
|
||||
|
||||
for (const certs of [headCerts, ...(hasTrailer ? [trailerCerts] : [])]) {
|
||||
for (const cert of certs) {
|
||||
if (!cert.certName?.trim()) {
|
||||
onError('证书名称不能为空')
|
||||
options.onError('证书名称不能为空')
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -108,3 +154,18 @@ export function filterVehicleCertsForSubmit<T extends CertRow>(
|
||||
) {
|
||||
return mergeVehicleCertsByPart(headCerts, trailerCerts, hasTrailer)
|
||||
}
|
||||
|
||||
export function validateVehicleCertNamesNotEmpty(
|
||||
certLists: CertRow[][],
|
||||
onError: (message: string) => void
|
||||
): boolean {
|
||||
for (const certs of certLists) {
|
||||
for (const cert of certs) {
|
||||
if (!cert.certName?.trim()) {
|
||||
onError('证书名称不能为空')
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
const TOOLBAR_SELECTOR = '.page-container .list-toolbar, .console-container .list-toolbar'
|
||||
const FILTER_AUTO_TOGGLE_CLASS = 'btn-filter-toggle-auto'
|
||||
const FILTER_TOGGLE_ICON_CLASS = 'btn-list-filter-toggle'
|
||||
const QUICK_SEARCH_GROUP_CLASS = 'quick-search-group'
|
||||
|
||||
const ensureQuickSearchGroup = (quickSearchInput: HTMLElement): HTMLElement => {
|
||||
const parent = quickSearchInput.parentElement
|
||||
if (parent?.classList.contains(QUICK_SEARCH_GROUP_CLASS)) {
|
||||
return parent
|
||||
}
|
||||
|
||||
const group = document.createElement('div')
|
||||
group.className = QUICK_SEARCH_GROUP_CLASS
|
||||
quickSearchInput.replaceWith(group)
|
||||
group.appendChild(quickSearchInput)
|
||||
return group
|
||||
}
|
||||
|
||||
const findPrecedingFilterBlock = (toolbar: HTMLElement): HTMLElement | null => {
|
||||
if (toolbar.closest('.el-dialog')) return null
|
||||
|
||||
let prev = toolbar.previousElementSibling as HTMLElement | null
|
||||
while (prev) {
|
||||
if (prev.classList.contains('filter-block')) return prev
|
||||
if (
|
||||
prev.classList.contains('list-body') ||
|
||||
prev.classList.contains('list-toolbar') ||
|
||||
prev.classList.contains('panel-top') ||
|
||||
prev.classList.contains('stat-grid')
|
||||
) {
|
||||
return null
|
||||
}
|
||||
prev = prev.previousElementSibling as HTMLElement | null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const setFilterBlockExpanded = (
|
||||
filterBlock: HTMLElement,
|
||||
button: HTMLButtonElement,
|
||||
expanded: boolean
|
||||
) => {
|
||||
filterBlock.classList.toggle('is-filter-collapsed', !expanded)
|
||||
filterBlock.classList.toggle('is-filter-expanded', expanded)
|
||||
button.setAttribute('aria-expanded', expanded ? 'true' : 'false')
|
||||
button.setAttribute('aria-label', expanded ? '收起检索' : '展开检索')
|
||||
button.setAttribute('title', expanded ? '收起检索' : '展开检索')
|
||||
}
|
||||
|
||||
const createToggleButton = (filterBlock: HTMLElement): HTMLButtonElement => {
|
||||
const button = document.createElement('button')
|
||||
button.type = 'button'
|
||||
button.className = FILTER_TOGGLE_ICON_CLASS
|
||||
button.setAttribute('aria-expanded', 'false')
|
||||
button.setAttribute('aria-label', '展开检索')
|
||||
button.setAttribute('title', '展开检索')
|
||||
button.addEventListener('click', (event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
const expanded = filterBlock.classList.contains('is-filter-expanded')
|
||||
setFilterBlockExpanded(filterBlock, button, !expanded)
|
||||
})
|
||||
return button
|
||||
}
|
||||
|
||||
const resolveFilterToggleAnchor = (
|
||||
toolbar: HTMLElement
|
||||
): { anchor: HTMLElement; quickSearchInput: HTMLElement | null } | null => {
|
||||
const quickSearchInput = toolbar.querySelector<HTMLElement>('.quick-search-input')
|
||||
if (quickSearchInput) {
|
||||
return { anchor: ensureQuickSearchGroup(quickSearchInput), quickSearchInput }
|
||||
}
|
||||
|
||||
const selectedAnchor = toolbar.querySelector<HTMLElement>('.list-toolbar-selected')
|
||||
if (selectedAnchor) {
|
||||
return { anchor: selectedAnchor, quickSearchInput: null }
|
||||
}
|
||||
|
||||
const rowAnchor = toolbar.querySelector<HTMLElement>('.list-toolbar-row')
|
||||
if (rowAnchor) {
|
||||
return { anchor: rowAnchor, quickSearchInput: null }
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const ensureToolbarFilterToggle = (toolbar: HTMLElement, filterBlock: HTMLElement) => {
|
||||
if (!filterBlock.classList.contains('is-filter-expanded')) {
|
||||
filterBlock.classList.add('is-filter-collapsed')
|
||||
}
|
||||
|
||||
const resolved = resolveFilterToggleAnchor(toolbar)
|
||||
if (!resolved) return
|
||||
|
||||
const { anchor } = resolved
|
||||
const existingToggle = toolbar.querySelector<HTMLButtonElement>(`.${FILTER_TOGGLE_ICON_CLASS}`)
|
||||
|
||||
if (existingToggle && existingToggle.parentElement !== anchor) {
|
||||
existingToggle.remove()
|
||||
}
|
||||
|
||||
let toggle = anchor.querySelector<HTMLButtonElement>(`:scope > .${FILTER_TOGGLE_ICON_CLASS}`)
|
||||
if (!toggle) {
|
||||
toggle = createToggleButton(filterBlock)
|
||||
anchor.insertBefore(toggle, anchor.firstChild)
|
||||
}
|
||||
|
||||
setFilterBlockExpanded(
|
||||
filterBlock,
|
||||
toggle,
|
||||
filterBlock.classList.contains('is-filter-expanded')
|
||||
)
|
||||
}
|
||||
|
||||
const removeAutoFilterToggles = (): void => {
|
||||
document.querySelectorAll<HTMLElement>(`.${FILTER_AUTO_TOGGLE_CLASS}`).forEach((node) => {
|
||||
node.remove()
|
||||
})
|
||||
}
|
||||
|
||||
export const refreshFilterCollapse = (): void => {
|
||||
removeAutoFilterToggles()
|
||||
|
||||
document.querySelectorAll<HTMLElement>(TOOLBAR_SELECTOR).forEach((toolbar) => {
|
||||
const filterBlock = findPrecedingFilterBlock(toolbar)
|
||||
if (!filterBlock) return
|
||||
ensureToolbarFilterToggle(toolbar, filterBlock)
|
||||
})
|
||||
}
|
||||
|
||||
export const setupFilterCollapseObserver = (): MutationObserver => {
|
||||
refreshFilterCollapse()
|
||||
const observer = new MutationObserver(() => {
|
||||
refreshFilterCollapse()
|
||||
})
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
})
|
||||
return observer
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
const INTERACTIVE_SELECTOR =
|
||||
'button, a, .el-link, .el-dropdown, input, textarea, select, .el-switch, .el-radio, .el-select, .btn-op-item, .el-table__expand-icon'
|
||||
|
||||
/**
|
||||
* 主列表行单击切换多选:点击行即选中/取消,点击链接、按钮、展开图标等交互元素时不触发。
|
||||
*/
|
||||
export function setupListTableRowSelect() {
|
||||
const handler = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement | null
|
||||
if (!target) return
|
||||
|
||||
if (target.closest('.el-table-column--selection')) return
|
||||
|
||||
const row = target.closest('.list-table .el-table__body-wrapper tbody tr.el-table__row')
|
||||
if (!row) return
|
||||
|
||||
if (target.closest(INTERACTIVE_SELECTOR)) return
|
||||
|
||||
const checkboxInput = row.querySelector(
|
||||
'.el-table-column--selection .el-checkbox__input'
|
||||
) as HTMLElement | null
|
||||
if (!checkboxInput) return
|
||||
|
||||
checkboxInput.click()
|
||||
}
|
||||
|
||||
document.body.addEventListener('click', handler)
|
||||
return () => document.body.removeEventListener('click', handler)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
export function syncViewRowFromFlatList<T extends { id: string | number }>(
|
||||
viewRow: Ref<T | null>,
|
||||
list: T[]
|
||||
) {
|
||||
const current = viewRow.value
|
||||
if (!current?.id) return
|
||||
const updated = list.find(row => row.id === current.id)
|
||||
if (updated) viewRow.value = updated
|
||||
}
|
||||
|
||||
export function findNodeInTreeById<T extends { id: string | number; children?: T[] }>(
|
||||
nodes: T[],
|
||||
id: string | number
|
||||
): T | undefined {
|
||||
for (const node of nodes) {
|
||||
if (node.id === id) return node
|
||||
if (node.children?.length) {
|
||||
const found = findNodeInTreeById(node.children, id)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function syncViewRowFromTree<T extends { id: string | number; children?: T[] }>(
|
||||
viewRow: Ref<T | null>,
|
||||
roots: T[]
|
||||
) {
|
||||
const current = viewRow.value
|
||||
if (!current?.id) return
|
||||
const updated = findNodeInTreeById(roots, current.id)
|
||||
if (updated) viewRow.value = updated
|
||||
}
|
||||
@@ -34,6 +34,7 @@ export function isViewFieldLong(field: ViewField, displayText: string): boolean
|
||||
|
||||
/** 是否占满整行(备注超长、显式 fullSpan / span:'full') */
|
||||
export function isViewFieldFullSpan(field: ViewField, displayText: string): boolean {
|
||||
if (field.fullSpan === false) return false
|
||||
if (field.fullSpan === true || field.span === 'full') return true
|
||||
if (displayText === '—') return false
|
||||
return displayText.length >= VIEW_KV_FULL_MIN_LEN
|
||||
@@ -156,3 +157,36 @@ export function buildViewKvTableQuadRows(
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
export type ViewKvTableQuintRow =
|
||||
| { type: 'long'; field: ViewField }
|
||||
| { type: 'quint'; fields: ViewField[] }
|
||||
|
||||
/** KV 表格模式(五组十列):长字段独占行,短字段最多五个一组 */
|
||||
export function buildViewKvTableQuintRows(
|
||||
fields: ViewField[],
|
||||
formatValue: (val: unknown) => string
|
||||
): ViewKvTableQuintRow[] {
|
||||
const rows: ViewKvTableQuintRow[] = []
|
||||
let i = 0
|
||||
while (i < fields.length) {
|
||||
const field = fields[i]
|
||||
const text = getViewFieldDisplayText(field, formatValue)
|
||||
if (isViewFieldLong(field, text) || isViewFieldFullSpan(field, text)) {
|
||||
rows.push({ type: 'long', field })
|
||||
i += 1
|
||||
continue
|
||||
}
|
||||
const group: ViewField[] = [field]
|
||||
i += 1
|
||||
while (i < fields.length && group.length < 5) {
|
||||
const next = fields[i]
|
||||
const nextText = getViewFieldDisplayText(next, formatValue)
|
||||
if (isViewFieldLong(next, nextText) || isViewFieldFullSpan(next, nextText)) break
|
||||
group.push(next)
|
||||
i += 1
|
||||
}
|
||||
rows.push({ type: 'quint', fields: group })
|
||||
}
|
||||
return rows
|
||||
}
|
||||
Reference in new issue
Block a user