提交
This commit is contained in:
1 parent
c3be9d1dce
commit
299609706c
328 files changed
+144899
-13873
No files matched your search
@@ -32,6 +32,15 @@ export function statusTone(status: number | string | null | undefined): string {
|
||||
return map[status] ?? 'info'
|
||||
}
|
||||
|
||||
/** 业务类型 → Element Plus Tag type(报销单/备用金) */
|
||||
export function bizTypeTagType(
|
||||
bizType: string | undefined | null
|
||||
): 'success' | 'warning' | 'danger' | 'info' | 'primary' {
|
||||
if (bizType === '报销单') return 'primary'
|
||||
if (bizType === '备用金') return 'warning'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
/** bx 状态 → Element Plus Tag type(对齐 OpenSpec 语义色) */
|
||||
export function statusTagType(
|
||||
status: number | string | null | undefined
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { ViewField } from '@/components/business/system-view-dialog/types'
|
||||
|
||||
type BxKvOpts = Partial<Pick<ViewField, 'long' | 'fullSpan' | 'span' | 'money' | 'tagType'>>
|
||||
|
||||
/** 构建报销模块查看弹窗 KV 字段(空值统一为 —) */
|
||||
export function bxViewKv(label: string, value: unknown, opts: BxKvOpts = {}): ViewField {
|
||||
const text =
|
||||
value === null || value === undefined || value === ''
|
||||
? undefined
|
||||
: typeof value === 'boolean'
|
||||
? value
|
||||
? '是'
|
||||
: '否'
|
||||
: String(value)
|
||||
return { label, value: text, ...opts }
|
||||
}
|
||||
|
||||
/** 长文本 / 备注 / 地址等 */
|
||||
export function bxViewKvLong(label: string, value: unknown, opts: BxKvOpts = {}): ViewField {
|
||||
return bxViewKv(label, value, { long: true, span: true, ...opts })
|
||||
}
|
||||
|
||||
/** 金额类(红色加粗由 money 样式控制) */
|
||||
export function bxViewKvMoney(label: string, display: string, opts: BxKvOpts = {}): ViewField {
|
||||
return { label, value: display || '—', money: true, ...opts }
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { TagType, ViewField } from '@/components/business/system-view-dialog/types'
|
||||
import { formatViewValue } from '@/components/business/system-view-dialog/types'
|
||||
|
||||
type DictStoreLike = {
|
||||
getLabel: (dictType: string, value: string) => string
|
||||
getTagType: (dictType: string, value: string) => TagType
|
||||
}
|
||||
|
||||
export function crmField(label: string, value: unknown, extra?: Partial<ViewField>): ViewField {
|
||||
return { label, value: formatViewValue(value), ...extra }
|
||||
}
|
||||
|
||||
export function crmLongField(label: string, value: unknown, extra?: Partial<ViewField>): ViewField {
|
||||
return { label, value: formatViewValue(value), long: true, span: true, ...extra }
|
||||
}
|
||||
|
||||
export function crmDictField(
|
||||
label: string,
|
||||
dictType: string,
|
||||
value: string | null | undefined,
|
||||
dictStore: DictStoreLike,
|
||||
extra?: Partial<ViewField>
|
||||
): ViewField {
|
||||
return {
|
||||
label,
|
||||
value: value ? String(dictStore.getLabel(dictType, value)) : '—',
|
||||
tagType: value ? dictStore.getTagType(dictType, value) : undefined,
|
||||
...extra
|
||||
}
|
||||
}
|
||||
|
||||
export function crmTagField(
|
||||
label: string,
|
||||
display: string,
|
||||
tagType?: TagType,
|
||||
extra?: Partial<ViewField>
|
||||
): ViewField {
|
||||
return { label, value: display || '—', tagType, ...extra }
|
||||
}
|
||||
@@ -5,7 +5,6 @@
|
||||
import axios, { type AxiosRequestConfig, type InternalAxiosRequestConfig } from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { applyPortalAuthHeader } from '@/utils/http/portal-auth'
|
||||
|
||||
export interface BxApiResponse<T = unknown> {
|
||||
code: number
|
||||
@@ -13,14 +12,80 @@ export interface BxApiResponse<T = unknown> {
|
||||
data?: T
|
||||
}
|
||||
|
||||
/** 解析 bx / 运输模块响应体:兼容直出 JSON 与 { code, data } 信封 */
|
||||
export function pickBxData<T>(res: unknown): T {
|
||||
if (res == null) return res as T
|
||||
if (Array.isArray(res)) return res as T
|
||||
if (typeof res !== 'object') return res as T
|
||||
|
||||
const obj = res as BxApiResponse<T> & Record<string, unknown>
|
||||
if ('data' in obj && obj.data !== undefined) {
|
||||
return obj.data as T
|
||||
}
|
||||
return res as T
|
||||
}
|
||||
|
||||
/** 解析列表类响应,保证始终返回数组 */
|
||||
export function pickBxList<T>(res: unknown): T[] {
|
||||
const payload = pickBxData<T[] | { list?: T[]; rows?: T[] }>(res)
|
||||
if (Array.isArray(payload)) return payload
|
||||
if (payload && typeof payload === 'object') {
|
||||
const record = payload as { list?: T[]; rows?: T[] }
|
||||
if (Array.isArray(record.list)) return record.list
|
||||
if (Array.isArray(record.rows)) return record.rows
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
const bxAxios = axios.create({
|
||||
baseURL: '/api',
|
||||
timeout: 60000,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
|
||||
bxAxios.interceptors.request.use((config: InternalAxiosRequestConfig) => {
|
||||
applyPortalAuthHeader(config)
|
||||
/** FastAPI @router.get("/") 集合根须带尾斜杠,否则 307 重定向到绝对 URL 会丢 Authorization */
|
||||
const BX_COLLECTION_ROOT = /^\/(transport\/(order|plan|freight)|reimburses|advances|payments|workflows|expense-items)$/
|
||||
|
||||
function normalizeBxUrl(url?: string): string | undefined {
|
||||
if (!url || url.endsWith('/')) return url
|
||||
return BX_COLLECTION_ROOT.test(url) ? `${url}/` : url
|
||||
}
|
||||
|
||||
/** 从 localStorage 兜底读取 accessToken(兼容 Pinia persist 的版本化 key) */
|
||||
function readTokenFallback(): string {
|
||||
if (typeof localStorage === 'undefined') return ''
|
||||
try {
|
||||
// 尝试 Pinia persist 的版本化 key(如 sys-v3.0.2-user)
|
||||
for (const k of Object.keys(localStorage)) {
|
||||
if (!k.startsWith('sys-') || !k.includes('-user')) continue
|
||||
const raw = localStorage.getItem(k)
|
||||
if (!raw) continue
|
||||
const parsed = JSON.parse(raw) as { accessToken?: unknown }
|
||||
const token = parsed?.accessToken
|
||||
if (typeof token === 'string' && token.trim()) return token.trim()
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
bxAxios.interceptors.request.use(async (config: InternalAxiosRequestConfig) => {
|
||||
if (config.url) {
|
||||
config.url = normalizeBxUrl(config.url)
|
||||
}
|
||||
const store = useUserStore()
|
||||
let token = store.accessToken as unknown as string
|
||||
if (!token) {
|
||||
token = readTokenFallback()
|
||||
}
|
||||
if (token) {
|
||||
if (typeof config.headers.set === 'function') {
|
||||
config.headers.set('Authorization', `Bearer ${token}`)
|
||||
} else {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
return config
|
||||
})
|
||||
|
||||
@@ -76,6 +141,9 @@ const bxRequest = {
|
||||
put<T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig) {
|
||||
return request<T>({ ...config, url, method: 'PUT', data })
|
||||
},
|
||||
patch<T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig) {
|
||||
return request<T>({ ...config, url, method: 'PATCH', data })
|
||||
},
|
||||
delete<T = unknown>(url: string, config?: AxiosRequestConfig) {
|
||||
return request<T>({ ...config, url, method: 'DELETE' })
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import axios, { type AxiosRequestConfig, type InternalAxiosRequestConfig } from
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { fetchRefreshToken } from '@/api/auth'
|
||||
import { applyPortalAuthHeader } from '@/utils/http/portal-auth'
|
||||
|
||||
type RetryableConfig = InternalAxiosRequestConfig & {
|
||||
_retry?: boolean
|
||||
@@ -51,11 +50,40 @@ function onRefreshFailed(error: unknown) {
|
||||
pendingRequests = []
|
||||
}
|
||||
|
||||
dbAxios.interceptors.request.use((config: InternalAxiosRequestConfig) => {
|
||||
/** 从 localStorage 兜底读取 accessToken(兼容 Pinia persist 的版本化 key) */
|
||||
function readTokenFallback(): string {
|
||||
if (typeof localStorage === 'undefined') return ''
|
||||
try {
|
||||
for (const k of Object.keys(localStorage)) {
|
||||
if (!k.startsWith('sys-') || !k.includes('-user')) continue
|
||||
const raw = localStorage.getItem(k)
|
||||
if (!raw) continue
|
||||
const parsed = JSON.parse(raw) as { accessToken?: unknown }
|
||||
const token = parsed?.accessToken
|
||||
if (typeof token === 'string' && token.trim()) return token.trim()
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
dbAxios.interceptors.request.use(async (config: InternalAxiosRequestConfig) => {
|
||||
if (config.url) {
|
||||
config.url = normalizeDbUrl(config.url)
|
||||
}
|
||||
applyPortalAuthHeader(config)
|
||||
const store = useUserStore()
|
||||
let token = store.accessToken as unknown as string
|
||||
if (!token) {
|
||||
token = readTokenFallback()
|
||||
}
|
||||
if (token) {
|
||||
if (typeof config.headers.set === 'function') {
|
||||
config.headers.set('Authorization', `Bearer ${token}`)
|
||||
} else {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
return config
|
||||
})
|
||||
|
||||
@@ -74,7 +102,11 @@ dbAxios.interceptors.response.use(
|
||||
return new Promise((resolve, reject) => {
|
||||
subscribeTokenRefresh(
|
||||
(newToken: string) => {
|
||||
applyPortalAuthHeader(config)
|
||||
if (typeof config.headers.set === 'function') {
|
||||
config.headers.set('Authorization', `Bearer ${newToken}`)
|
||||
} else {
|
||||
config.headers.Authorization = `Bearer ${newToken}`
|
||||
}
|
||||
config._retry = true
|
||||
resolve(dbAxios.request(config))
|
||||
},
|
||||
@@ -89,7 +121,11 @@ dbAxios.interceptors.response.use(
|
||||
const result = await fetchRefreshToken(storedRefreshToken)
|
||||
userStore.setToken(result.token, result.refreshToken)
|
||||
onRefreshed(result.token)
|
||||
applyPortalAuthHeader(config)
|
||||
if (typeof config.headers.set === 'function') {
|
||||
config.headers.set('Authorization', `Bearer ${result.token}`)
|
||||
} else {
|
||||
config.headers.Authorization = `Bearer ${result.token}`
|
||||
}
|
||||
return dbAxios.request(config)
|
||||
} catch (refreshError) {
|
||||
onRefreshFailed(refreshError)
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import axios from 'axios'
|
||||
import type { BaseResponse } from '@/types'
|
||||
|
||||
const publicPlRequest = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_URL || '/api',
|
||||
timeout: 60000
|
||||
})
|
||||
|
||||
publicPlRequest.interceptors.response.use(response => {
|
||||
const payload = response.data as BaseResponse<unknown>
|
||||
if (payload && typeof payload.code === 'number' && payload.code !== 200) {
|
||||
return Promise.reject(new Error(payload.message || '请求失败'))
|
||||
}
|
||||
return response
|
||||
})
|
||||
|
||||
export async function publicPlGet<T>(url: string): Promise<T> {
|
||||
const res = await publicPlRequest.get<BaseResponse<T>>(url)
|
||||
return (res.data?.data ?? res.data) as T
|
||||
}
|
||||
|
||||
export async function publicPlPost<T>(url: string, data?: unknown): Promise<T> {
|
||||
const res = await publicPlRequest.post<BaseResponse<T>>(url, data)
|
||||
return (res.data?.data ?? res.data) as T
|
||||
}
|
||||
|
||||
export async function publicPlUpload<T>(url: string, file: File): Promise<T> {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
const res = await publicPlRequest.post<BaseResponse<T>>(url, formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
return (res.data?.data ?? res.data) as T
|
||||
}
|
||||
|
||||
export default publicPlRequest
|
||||
@@ -0,0 +1,160 @@
|
||||
export function getPlVehicleStatusTagType(status: string) {
|
||||
const map: Record<string, string> = {
|
||||
available: 'success',
|
||||
maintaining: 'warning',
|
||||
scrapped: 'danger'
|
||||
}
|
||||
return map[status] || 'info'
|
||||
}
|
||||
|
||||
export function getPlVehicleStatusLabel(status: string) {
|
||||
const map: Record<string, string> = {
|
||||
available: '可用',
|
||||
maintaining: '维修中',
|
||||
scrapped: '报废'
|
||||
}
|
||||
return map[status] || status
|
||||
}
|
||||
|
||||
export function getPlVehicleTypeLabel(type: string) {
|
||||
return type === 'self' ? '自有' : type === 'carrier' ? '承运商' : type
|
||||
}
|
||||
|
||||
export function getPlVehicleTypeTagType(type: string) {
|
||||
return type === 'self' ? 'success' : 'warning'
|
||||
}
|
||||
|
||||
export function getPlCarrierStatusLabel(status: string) {
|
||||
return status === '1' ? '合作中' : '已终止'
|
||||
}
|
||||
|
||||
export function getPlCarrierStatusTagType(status: string) {
|
||||
return status === '1' ? 'success' : 'danger'
|
||||
}
|
||||
|
||||
export function getPlanProductSpec(product: { model?: string; packageSpec?: string }) {
|
||||
return product.model || product.packageSpec || '—'
|
||||
}
|
||||
|
||||
export function getPlanProductQuantity(product: {
|
||||
quantity?: number | string | null
|
||||
actualQty?: number | string | null
|
||||
inboundEstimateQty?: number | string | null
|
||||
outboundEstimateQty?: number | string | null
|
||||
}) {
|
||||
const qty =
|
||||
product.actualQty ??
|
||||
product.quantity ??
|
||||
product.inboundEstimateQty ??
|
||||
product.outboundEstimateQty
|
||||
if (qty === undefined || qty === null || qty === '') return '—'
|
||||
return String(qty)
|
||||
}
|
||||
|
||||
export function formatPlanProductListText(
|
||||
product: {
|
||||
productName?: string
|
||||
model?: string
|
||||
packageSpec?: string
|
||||
unit?: string
|
||||
quantity?: number | string | null
|
||||
actualQty?: number | string | null
|
||||
inboundEstimateQty?: number | string | null
|
||||
outboundEstimateQty?: number | string | null
|
||||
},
|
||||
getUnitLabel?: (unit: string) => string
|
||||
) {
|
||||
const name = product.productName || '—'
|
||||
const spec = getPlanProductSpec(product)
|
||||
const qty = getPlanProductQuantity(product)
|
||||
const unitRaw = product.unit || ''
|
||||
const unit = unitRaw ? (getUnitLabel ? getUnitLabel(unitRaw) || unitRaw : unitRaw) : ''
|
||||
|
||||
const specPart = spec && spec !== '—' ? `(${spec})` : ''
|
||||
let qtyUnit = '—'
|
||||
if (qty !== '—' && unit) {
|
||||
qtyUnit = `${qty}${unit}`
|
||||
} else if (qty !== '—') {
|
||||
qtyUnit = qty
|
||||
} else if (unit) {
|
||||
qtyUnit = unit
|
||||
}
|
||||
|
||||
return `${name}${specPart},${qtyUnit}`
|
||||
}
|
||||
|
||||
export function getPlPlanCompanyLabel(company?: string) {
|
||||
const map: Record<string, string> = { shanxia: '安庆山峡', yundong: '安徽云栋' }
|
||||
return company ? map[company] || company : '—'
|
||||
}
|
||||
|
||||
export function getPlPlanTypeTagType(type?: string) {
|
||||
const map: Record<string, string> = { direct: 'primary', inbound: 'success', outbound: 'warning' }
|
||||
return map[type || ''] || 'info'
|
||||
}
|
||||
|
||||
export function getPlPlanTypeLabel(type?: string) {
|
||||
const map: Record<string, string> = { direct: '直销单', inbound: '入库单', outbound: '出库单' }
|
||||
return type ? map[type] || type : '—'
|
||||
}
|
||||
|
||||
export function getPlPlanFollowStatusTagType(status?: string) {
|
||||
const map: Record<string, string> = {
|
||||
draft: 'info',
|
||||
processing: 'warning',
|
||||
completed: 'success',
|
||||
cancelled: 'danger'
|
||||
}
|
||||
return map[status || ''] || 'info'
|
||||
}
|
||||
|
||||
export function getPlPlanFollowStatusLabel(status?: string) {
|
||||
const map: Record<string, string> = {
|
||||
draft: '草稿',
|
||||
processing: '进行中',
|
||||
completed: '已完成',
|
||||
cancelled: '已取消'
|
||||
}
|
||||
return status ? map[status] || status : '—'
|
||||
}
|
||||
|
||||
export function getPlPlanArchiveStatusTagType(status?: string) {
|
||||
return status === 'archived' ? 'info' : 'success'
|
||||
}
|
||||
|
||||
export function getPlPlanArchiveStatusLabel(status?: string) {
|
||||
return status === 'archived' ? '已归档' : '未归档'
|
||||
}
|
||||
|
||||
export function getPlPlanCompleteStatusLabel(status?: string) {
|
||||
return status === 'yes' ? '是' : '否'
|
||||
}
|
||||
|
||||
export function getPlFreightTypeLabel(type?: string) {
|
||||
const map: Record<string, string> = { by_weight: '按重量', by_trip: '按趟' }
|
||||
return type ? map[type] || type : '—'
|
||||
}
|
||||
|
||||
export function getPlYesNoLabel(value?: number | boolean | null) {
|
||||
return value === 1 || value === true ? '是' : '否'
|
||||
}
|
||||
|
||||
/** 解析运输单司机回传附件(JSON 数组或逗号分隔) */
|
||||
export function parsePlFeedbackImages(raw?: string | string[] | null): string[] {
|
||||
if (!raw) return []
|
||||
if (Array.isArray(raw)) return raw.map((item) => String(item).trim()).filter(Boolean)
|
||||
const text = String(raw).trim()
|
||||
if (!text) return []
|
||||
try {
|
||||
const parsed = JSON.parse(text)
|
||||
if (Array.isArray(parsed)) {
|
||||
return parsed.map((item) => String(item).trim()).filter(Boolean)
|
||||
}
|
||||
} catch {
|
||||
// fall through
|
||||
}
|
||||
return text
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import type { AppRouteRecord } from '@/types/router'
|
||||
|
||||
/** 目录型菜单缺省图标 */
|
||||
export const DEFAULT_SUBMENU_ICON = 'ri:folder-2-line'
|
||||
|
||||
/** 叶子菜单缺省图标 */
|
||||
export const DEFAULT_MENU_ITEM_ICON = 'ri:file-list-2-line'
|
||||
|
||||
/**
|
||||
* 侧栏菜单图标映射(按路由 name;未在 meta 配置 icon 时使用)
|
||||
* 键与 router/modules 中 name 一致,便于前后端菜单对齐。
|
||||
*/
|
||||
export const SIDEBAR_MENU_ICON_MAP: Record<string, string> = {
|
||||
// ── 报销管理 ──
|
||||
BxManagement: 'ri:wallet-3-line',
|
||||
BxExpenseItem: 'ri:price-tag-3-line',
|
||||
BxWorkflowAdvance: 'ri:git-branch-line',
|
||||
BxWorkflowReimburse: 'ri:git-merge-line',
|
||||
BxAdvanceApply: 'ri:money-cny-circle-line',
|
||||
BxAdvanceApprove: 'ri:check-double-line',
|
||||
BxAdvanceIssue: 'ri:hand-coin-line',
|
||||
BxReimburseApply: 'ri:file-edit-line',
|
||||
BxReimburseApprove: 'ri:checkbox-circle-line',
|
||||
BxPayment: 'ri:bank-card-line',
|
||||
BxViewMine: 'ri:user-line',
|
||||
BxViewTodo: 'ri:time-line',
|
||||
BxViewAll: 'ri:file-list-3-line',
|
||||
|
||||
// ── CRM ──
|
||||
CrmManagement: 'goods',
|
||||
CrmProductCategory: 'ri:folder-3-line',
|
||||
CrmProduct: 'ri:box-3-line',
|
||||
CrmClue: 'ri:seedling-line',
|
||||
CrmOpportunity: 'ri:trophy-line',
|
||||
CrmQuote: 'ri:file-text-line',
|
||||
CrmContract: 'ri:file-paper-2-line',
|
||||
CrmCustomer: 'ri:team-line',
|
||||
CrmCustomerPool: 'ri:global-line',
|
||||
|
||||
// ── 地磅 ──
|
||||
DbManagement: 'ri:scales-3-line',
|
||||
DbDashboard: 'ri:dashboard-line',
|
||||
DbPlans: 'ri:calendar-schedule-line',
|
||||
DbInventory: 'ri:archive-line',
|
||||
DbWeighing: 'ri:scales-line',
|
||||
DbVehicles: 'ri:car-line',
|
||||
|
||||
// ── 工作流 ──
|
||||
Workflow: 'workflow',
|
||||
WorkflowProcess: 'workflow',
|
||||
WorkflowPending: 'ri:inbox-line',
|
||||
WorkflowCompleted: 'ri:checkbox-circle-line',
|
||||
WorkflowMyApplications: 'ri:file-user-line',
|
||||
LeaveRequest: 'ri:calendar-event-line',
|
||||
|
||||
// ── 系统 ──
|
||||
SystemManage: 'ri:settings-3-line',
|
||||
Tenant: 'ri:building-line',
|
||||
Dict: 'ri:book-2-line',
|
||||
Menu: 'ri:menu-line',
|
||||
Dept: 'ri:organization-chart',
|
||||
Role: 'ri:shield-user-line',
|
||||
User: 'user',
|
||||
Post: 'ri:briefcase-line',
|
||||
|
||||
// ── 运输 ──
|
||||
TransportPlan: 'ri:truck-line',
|
||||
PlCarrier: 'ri:building-2-line',
|
||||
PlVehicle: 'ri:truck-line',
|
||||
PlSelfVehicle: 'ri:car-line',
|
||||
PlPersonnel: 'ri:user-settings-line',
|
||||
PlPlan: 'ri:route-line',
|
||||
PlPlanCert: 'ri:shield-check-line',
|
||||
PlFreight: 'ri:money-cny-box-line',
|
||||
PlDocumentReminder: 'ri:alarm-warning-line'
|
||||
}
|
||||
|
||||
/** 按 menuCode 补充映射(后端菜单可能仅有 code) */
|
||||
export const SIDEBAR_MENU_ICON_BY_CODE: Record<string, string> = {
|
||||
'bx:expenseItem:list': 'ri:price-tag-3-line',
|
||||
'bx:workflowAdvance:list': 'ri:git-branch-line',
|
||||
'bx:workflowReimburse:list': 'ri:git-merge-line',
|
||||
'bx:advanceApply:list': 'ri:money-cny-circle-line',
|
||||
'bx:advanceApprove:list': 'ri:check-double-line',
|
||||
'bx:advanceIssue:list': 'ri:hand-coin-line',
|
||||
'bx:reimburseApply:list': 'ri:file-edit-line',
|
||||
'bx:reimburseApprove:list': 'ri:checkbox-circle-line',
|
||||
'bx:payment:list': 'ri:bank-card-line',
|
||||
'bx:viewMine:list': 'ri:user-line',
|
||||
'bx:viewTodo:list': 'ri:time-line',
|
||||
'bx:viewAll:list': 'ri:file-list-3-line',
|
||||
'crm:productCategory:list': 'ri:folder-3-line',
|
||||
'crm:product:list': 'ri:box-3-line',
|
||||
'crm:clue:list': 'ri:seedling-line',
|
||||
'crm:opportunity:list': 'ri:trophy-line',
|
||||
'crm:quote:list': 'ri:file-text-line',
|
||||
'crm:contract:list': 'ri:file-paper-2-line',
|
||||
'crm:customer:list': 'ri:team-line',
|
||||
'crm:customerPool:list': 'ri:global-line',
|
||||
'db:dashboard:list': 'ri:dashboard-line',
|
||||
'db:plan:list': 'ri:calendar-schedule-line',
|
||||
'db:inventory:list': 'ri:archive-line',
|
||||
'db:weighing:list': 'ri:scales-line',
|
||||
'db:vehicle:list': 'ri:car-line',
|
||||
'workflow:process': 'workflow',
|
||||
'workflow:pending': 'ri:inbox-line',
|
||||
'workflow:completed': 'ri:checkbox-circle-line',
|
||||
'workflow:myApplications': 'ri:file-user-line',
|
||||
'workflow:leave': 'ri:calendar-event-line',
|
||||
'system:tenant:list': 'ri:building-line',
|
||||
'system:dict:list': 'ri:book-2-line',
|
||||
'system:menu:list': 'ri:menu-line',
|
||||
'system:dept:list': 'ri:organization-chart',
|
||||
'system:role:list': 'ri:shield-user-line',
|
||||
'system:user:list': 'user',
|
||||
'system:post:list': 'ri:briefcase-line',
|
||||
'pl:carrier:list': 'ri:building-2-line',
|
||||
'pl:vehicle:list': 'ri:truck-line',
|
||||
'pl:self-vehicle:list': 'ri:car-line',
|
||||
'pl:driver:list': 'ri:user-settings-line',
|
||||
'pl:plan:list': 'ri:route-line',
|
||||
'pl:plan-cert:list': 'ri:shield-check-line',
|
||||
'pl:freight:list': 'ri:money-cny-box-line',
|
||||
'pl:document:list': 'ri:alarm-warning-line'
|
||||
}
|
||||
|
||||
function lookupMappedIcon(item: AppRouteRecord): string {
|
||||
const metaIcon = String(item.meta?.icon ?? '').trim()
|
||||
if (metaIcon) return metaIcon
|
||||
|
||||
const name = String(item.name ?? '').trim()
|
||||
if (name && SIDEBAR_MENU_ICON_MAP[name]) {
|
||||
return SIDEBAR_MENU_ICON_MAP[name]
|
||||
}
|
||||
|
||||
const menuCode = String((item.meta as { menuCode?: string })?.menuCode ?? '').trim()
|
||||
if (menuCode && SIDEBAR_MENU_ICON_BY_CODE[menuCode]) {
|
||||
return SIDEBAR_MENU_ICON_BY_CODE[menuCode]
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析侧栏菜单图标:meta.icon → 路由 name / menuCode 映射 → 缺省图标。
|
||||
*/
|
||||
export function resolveSidebarMenuIcon(
|
||||
item: AppRouteRecord,
|
||||
hasChildren = false
|
||||
): string {
|
||||
const mapped = lookupMappedIcon(item)
|
||||
if (mapped) return mapped
|
||||
return hasChildren ? DEFAULT_SUBMENU_ICON : DEFAULT_MENU_ITEM_ICON
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import type { AppRouteRecord } from '@/types/router'
|
||||
import { getFirstMenuPath } from '@/utils/navigation/route'
|
||||
import { filterSidebarRoutes } from '@/utils/router/sidebar-menu-filter'
|
||||
|
||||
/** 门户顶栏一级模块展示顺序(与路由注册顺序无关) */
|
||||
export const PORTAL_TOP_MODULE_ORDER = [
|
||||
'/crm',
|
||||
'/bx',
|
||||
'/db',
|
||||
'/pl',
|
||||
'/workflow',
|
||||
'/system'
|
||||
] as const
|
||||
|
||||
export type PortalTopModulePath = (typeof PORTAL_TOP_MODULE_ORDER)[number]
|
||||
|
||||
export function isDashboardMenuItem(item: AppRouteRecord): boolean {
|
||||
const path = String(item.path ?? '')
|
||||
const title = String(item.meta?.title ?? '')
|
||||
const name = String(item.name ?? '')
|
||||
return (
|
||||
path === '/dashboard' ||
|
||||
title === '控制台' ||
|
||||
name === 'Dashboard' ||
|
||||
title === 'menus.dashboard.title'
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤并排序门户一级模块(排除控制台与 isHide)
|
||||
*/
|
||||
export function filterPortalTopModules(menuList: AppRouteRecord[]): AppRouteRecord[] {
|
||||
const visible = (menuList || []).filter(
|
||||
(item) => !item.meta?.isHide && !isDashboardMenuItem(item)
|
||||
)
|
||||
const orderMap = new Map<string, number>(
|
||||
PORTAL_TOP_MODULE_ORDER.map((path, index) => [path, index])
|
||||
)
|
||||
|
||||
return visible.sort((a, b) => {
|
||||
const orderA = orderMap.get(String(a.path ?? '')) ?? 999
|
||||
const orderB = orderMap.get(String(b.path ?? '')) ?? 999
|
||||
return orderA - orderB
|
||||
})
|
||||
}
|
||||
|
||||
export function resolveTopModulePath(routePath: string): string {
|
||||
const segment = routePath.split('/').filter(Boolean)[0]
|
||||
return segment ? `/${segment}` : ''
|
||||
}
|
||||
|
||||
export function resolveActivePortalModule(
|
||||
menuList: AppRouteRecord[],
|
||||
routePath: string
|
||||
): AppRouteRecord | undefined {
|
||||
const tops = filterPortalTopModules(menuList)
|
||||
const activeTopPath = resolveTopModulePath(routePath)
|
||||
return tops.find((menu) => menu.path === activeTopPath)
|
||||
}
|
||||
|
||||
function isRouteInMenuTree(item: AppRouteRecord, targetPath: string): boolean {
|
||||
const activePath = String(targetPath)
|
||||
const itemPath = String(item.meta?.activePath || item.path || '')
|
||||
|
||||
if (itemPath && (activePath === itemPath || activePath.startsWith(`${itemPath}/`))) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (item.children?.length) {
|
||||
return item.children.some((child) => isRouteInMenuTree(child, activePath))
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/** 判断一级模块卡片是否为当前路由所属模块 */
|
||||
export function isPortalModuleActive(item: AppRouteRecord, routePath: string): boolean {
|
||||
const modulePath = String(item.path || '')
|
||||
const activePath = String(routePath)
|
||||
|
||||
if (modulePath && (activePath === modulePath || activePath.startsWith(`${modulePath}/`))) {
|
||||
return true
|
||||
}
|
||||
|
||||
return isRouteInMenuTree(item, activePath)
|
||||
}
|
||||
|
||||
/** 模块首个可访问叶子路径(用于点击卡片跳转) */
|
||||
export function getFirstAccessiblePath(module: AppRouteRecord): string {
|
||||
if (module.redirect) {
|
||||
return String(module.redirect)
|
||||
}
|
||||
|
||||
const children = module.children ? filterSidebarRoutes(module.children) : []
|
||||
if (children.length) {
|
||||
const childPath = getFirstMenuPath(children, module.path)
|
||||
if (childPath) {
|
||||
return childPath
|
||||
}
|
||||
}
|
||||
|
||||
return module.path || '/'
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import type { ViewField } from '@/components/business/system-view-dialog/types'
|
||||
|
||||
/** 显示文本达到此长度视为长值(一行占 2 格,4 列栅格下每行 2 个) */
|
||||
export const VIEW_KV_LONG_MIN_LEN = 17
|
||||
|
||||
/** 显示文本达到此长度占满整行 */
|
||||
export const VIEW_KV_FULL_MIN_LEN = 80
|
||||
|
||||
const LONG_LABEL_RE =
|
||||
/备注|地址|描述|说明|条款|原因|摘要|网站|路径|意向|竞争|决策|需求|付款|交付|关联人员/
|
||||
|
||||
export function getViewFieldDisplayText(
|
||||
field: ViewField,
|
||||
formatValue: (val: unknown) => string
|
||||
): string {
|
||||
if (field.items?.length) return field.items.join('、')
|
||||
return formatValue(field.value)
|
||||
}
|
||||
|
||||
/** 是否长值字段(4 列栅格下占 2 列,即一行 2 个) */
|
||||
export function isViewFieldLong(field: ViewField, displayText: string): boolean {
|
||||
if (field.long === false) return false
|
||||
if (field.long === true || field.span === true) return true
|
||||
if (field.span === 'full') return true
|
||||
if (field.items?.length) {
|
||||
const joined = field.items.join('、')
|
||||
if (joined.length >= VIEW_KV_LONG_MIN_LEN) return true
|
||||
}
|
||||
if (displayText === '—') return false
|
||||
if (displayText.includes(' / ')) return true
|
||||
if (LONG_LABEL_RE.test(field.label)) return true
|
||||
return displayText.length >= VIEW_KV_LONG_MIN_LEN
|
||||
}
|
||||
|
||||
/** 是否占满整行(备注超长、显式 fullSpan / span:'full') */
|
||||
export function isViewFieldFullSpan(field: ViewField, displayText: string): boolean {
|
||||
if (field.fullSpan === true || field.span === 'full') return true
|
||||
if (displayText === '—') return false
|
||||
return displayText.length >= VIEW_KV_FULL_MIN_LEN
|
||||
}
|
||||
|
||||
/**
|
||||
* 查看弹窗 KV 栅格跨列 class。
|
||||
* - 4 列:短值 1 列(4 个/行),长值 span 2(2 个/行),超长 span full
|
||||
* - 2 列:长值占满一行
|
||||
*/
|
||||
export function getViewKvSpanClass(
|
||||
field: ViewField,
|
||||
gridColumns: number,
|
||||
formatValue: (val: unknown) => string
|
||||
): string {
|
||||
const text = getViewFieldDisplayText(field, formatValue)
|
||||
if (isViewFieldFullSpan(field, text)) return 'view-kv-span-full'
|
||||
if (!isViewFieldLong(field, text)) return ''
|
||||
if (gridColumns <= 1) return ''
|
||||
if (gridColumns === 2) return 'view-kv-span-full'
|
||||
return 'view-kv-span-2'
|
||||
}
|
||||
|
||||
export type ViewKvTableRow =
|
||||
| { type: 'long'; field: ViewField }
|
||||
| { type: 'dual'; left: ViewField; right?: ViewField }
|
||||
|
||||
/** KV 表格模式(双组四列):长字段独占行,短字段两两一行 */
|
||||
export function buildViewKvTableDualRows(
|
||||
fields: ViewField[],
|
||||
formatValue: (val: unknown) => string
|
||||
): ViewKvTableRow[] {
|
||||
const rows: ViewKvTableRow[] = []
|
||||
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 next = fields[i + 1]
|
||||
if (next) {
|
||||
const nextText = getViewFieldDisplayText(next, formatValue)
|
||||
if (!isViewFieldLong(next, nextText) && !isViewFieldFullSpan(next, nextText)) {
|
||||
rows.push({ type: 'dual', left: field, right: next })
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
}
|
||||
rows.push({ type: 'dual', left: field })
|
||||
i += 1
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
export type ViewKvTableTripleRow =
|
||||
| { type: 'long'; field: ViewField }
|
||||
| { type: 'triple'; fields: ViewField[] }
|
||||
|
||||
/** KV 表格模式(三组六列):长字段独占行,短字段最多三个一组 */
|
||||
export function buildViewKvTableTripleRows(
|
||||
fields: ViewField[],
|
||||
formatValue: (val: unknown) => string
|
||||
): ViewKvTableTripleRow[] {
|
||||
const rows: ViewKvTableTripleRow[] = []
|
||||
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 < 3) {
|
||||
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: 'triple', fields: group })
|
||||
}
|
||||
return rows
|
||||
}
|
||||
Reference in new issue
Block a user