This commit is contained in:
shaoleiliu-netizen123 2026-06-06 16:44:40 +08:00
commit 57c7eb92eb
13 changed files with 776 additions and 238 deletions

View File

@ -44,7 +44,12 @@ export interface OrderItem {
assingWorkOperationTime?: string assingWorkOperationTime?: string
starter?: number starter?: number
orderCode?: string
routeCode?: string
proWorkshop?: string
} }
// 生产订单 API // 生产订单 API
@ -93,11 +98,23 @@ export const orderItemApi = {
}, },
// 导出 // 导出
export(params?: { ids?: string[]; code?: string; projectId?: number; beginTime?: string; endTime?: string }) { export(params?: {
ids?: string[]
code?: string
projectName?: string
orderCode?: string
routeCode?: string
proWorkshop?: string
beginTime?: string
endTime?: string
}) {
const p: Record<string, any> = {} const p: Record<string, any> = {}
if (params?.ids?.length) p.ids = params.ids.join(',') if (params?.ids?.length) p.ids = params.ids.join(',')
if (params?.code !== undefined && params?.code !== null) p.code = params.code if (params?.code !== undefined && params?.code !== null) p.code = params.code
if (params?.projectId !== undefined && params?.projectId !== null) p.projectId = params.projectId if (params?.projectName !== undefined && params?.projectName !== null) p.projectName = params.projectName
if (params?.orderCode !== undefined && params?.orderCode !== null) p.orderCode = params.orderCode
if (params?.routeCode !== undefined && params?.routeCode !== null) p.routeCode = params.routeCode
if (params?.proWorkshop !== undefined && params?.proWorkshop !== null) p.proWorkshop = params.proWorkshop
if (params?.beginTime !== undefined && params?.beginTime !== null) p.beginTime = params.beginTime if (params?.beginTime !== undefined && params?.beginTime !== null) p.beginTime = params.beginTime
if (params?.endTime !== undefined && params?.endTime !== null) p.endTime = params.endTime if (params?.endTime !== undefined && params?.endTime !== null) p.endTime = params.endTime
return request({ url: `/biz/orderItem/export`, method: 'get', params: p, responseType: 'blob' }) return request({ url: `/biz/orderItem/export`, method: 'get', params: p, responseType: 'blob' })

View File

@ -69,18 +69,57 @@ export interface KingdeePrdMo {
/** 金蝶工艺路线工序(树子节点) */ /** 金蝶工艺路线工序(树子节点) */
export interface KingdeeProcessRoute { export interface KingdeeProcessRoute {
/** 工序号 FOperNumber */
operNumber: number | null operNumber: number | null
/** 工作中心 FWorkCenterId.FName */
workCenterName: string | null workCenterName: string | null
/** 生产车间 FDepartmentId.FName */
departmentName: string | null departmentName: string | null
processProperty: string | null /** 工序名称 FProcessProperty */
processName: string | null
/** 工序说明 FOperDescription */
operDescription: string | null operDescription: string | null
/** 活动单位 FActivity1UnitID.FName */
activityUnit: string | null activityUnit: string | null
/** 工序控制码 FOptCtrlCodeId.FName */
optCtrlCodeName: string | null optCtrlCodeName: string | null
/** 活动量 FActivity1Qty */
activityQty: number | null activityQty: number | null
/** 计划开始时间 yyyy-MM-dd HH:mm:ss */
planStartTime: string | null planStartTime: string | null
/** 计划结束时间 yyyy-MM-dd HH:mm:ss */
planFinishTime: string | null planFinishTime: string | null
} }
/** 解析工序名称,兼容旧字段及后端误映射 */
export function resolveProcessName(route: Partial<KingdeeProcessRoute> & { processProperty?: string | null }) {
const legacyName = typeof route.processProperty === 'string' ? route.processProperty.trim() : ''
const processName = route.processName?.trim() || legacyName || ''
const operDescription = route.operDescription?.trim() || ''
if (processName) return processName
// 兼容FProcessProperty 被写入 operDescription 而 processName 为空
if (operDescription) return operDescription
return ''
}
/** 归一化金蝶工序字段,保证 processName / operDescription 各归其位 */
export function normalizeKingdeeProcessRoute(
route: KingdeeProcessRoute & { processProperty?: string | null }
): KingdeeProcessRoute {
const legacyName = typeof route.processProperty === 'string' ? route.processProperty.trim() : ''
const processName = route.processName?.trim() || legacyName || ''
const operDescription = route.operDescription?.trim() || ''
if (processName) {
return { ...route, processName, operDescription: operDescription || null }
}
if (operDescription) {
return { ...route, processName: operDescription, operDescription: null }
}
return { ...route, processName: null, operDescription: null }
}
// 项目表 API // 项目表 API
export const orderProjectApi = { export const orderProjectApi = {
// 分页查询 // 分页查询

View File

@ -0,0 +1,36 @@
<!--
站点 Logo 展示优先显示图片 URL 或加载失败时回退为站点名称首字母
用于 layoutloginregister 等页面src 不传则使用 siteStore.siteLogo
-->
<template>
<img
v-if="showSiteLogoImg"
:src="siteLogo"
:class="imgClass"
alt="Logo"
@error="handleLogoError"
/>
<div v-else :class="iconClass" :style="iconStyle">{{ fallbackLetter }}</div>
</template>
<script setup lang="ts">
import { useSiteLogo } from '@/composables/useSiteLogo'
const props = withDefaults(defineProps<{
/** 自定义 Logo 地址,默认取 siteStore.siteLogo */
src?: string
/** 图片模式下的 class */
imgClass?: string
/** 首字母 fallback 模式下的 class */
iconClass?: string
/** 首字母 fallback 的内联样式 */
iconStyle?: Record<string, string>
}>(), {
imgClass: 'logo-img',
iconClass: 'logo-icon',
})
const { siteLogo, showSiteLogoImg, fallbackLetter, handleLogoError } = useSiteLogo(
() => props.src
)
</script>

View File

@ -0,0 +1,30 @@
import { computed, ref, watch } from 'vue'
import { useSiteStore } from '@/stores/site'
/**
* Logo退
*/
export function useSiteLogo(src?: () => string | undefined) {
const siteStore = useSiteStore()
const siteName = computed(() => siteStore.siteName || 'MES系统')
const siteLogo = computed(() => src?.() ?? siteStore.siteLogo)
const logoLoadFailed = ref(false)
const showSiteLogoImg = computed(() => !!siteLogo.value && !logoLoadFailed.value)
const fallbackLetter = computed(() => siteName.value.charAt(0) || 'M')
watch(siteLogo, () => {
logoLoadFailed.value = false
})
function handleLogoError() {
logoLoadFailed.value = true
}
return {
siteName,
siteLogo,
showSiteLogoImg,
fallbackLetter,
handleLogoError,
}
}

View File

@ -17,8 +17,11 @@
> >
<!-- Logo --> <!-- Logo -->
<div class="logo" :class="{ 'logo-collapsed': collapsed, 'logo-primary': themeStore.headerUsePrimaryColor }" :style="themeStore.headerUsePrimaryColor ? { background: themeStore.primaryColor, borderBottomColor: themeStore.primaryColor } : {}"> <div class="logo" :class="{ 'logo-collapsed': collapsed, 'logo-primary': themeStore.headerUsePrimaryColor }" :style="themeStore.headerUsePrimaryColor ? { background: themeStore.primaryColor, borderBottomColor: themeStore.primaryColor } : {}">
<img v-if="siteLogo" :src="siteLogo" class="logo-img" alt="Logo" /> <SiteLogoMark
<div v-else class="logo-icon" :style="{ background: themeStore.headerUsePrimaryColor ? '#fff' : themeStore.primaryColor, color: themeStore.headerUsePrimaryColor ? themeStore.primaryColor : '#fff' }">{{ siteName.charAt(0) }}</div> img-class="logo-img"
icon-class="logo-icon"
:icon-style="logoIconStyle"
/>
<transition name="fade"> <transition name="fade">
<span v-if="!collapsed" class="logo-text">{{ siteName }}</span> <span v-if="!collapsed" class="logo-text">{{ siteName }}</span>
<!-- <span v-if="!collapsed" class="logo-text">伊特智造MES</span> --> <!-- <span v-if="!collapsed" class="logo-text">伊特智造MES</span> -->
@ -43,8 +46,11 @@
<n-layout-header bordered class="layout-header" :class="[`theme-${layoutConfig.theme}`, { 'header-primary': themeStore.headerUsePrimaryColor }]" :style="headerStyle"> <n-layout-header bordered class="layout-header" :class="[`theme-${layoutConfig.theme}`, { 'header-primary': themeStore.headerUsePrimaryColor }]" :style="headerStyle">
<!-- 顶部菜单模式下的Logo --> <!-- 顶部菜单模式下的Logo -->
<div v-if="layoutConfig.siderPosition === 'top'" class="header-logo"> <div v-if="layoutConfig.siderPosition === 'top'" class="header-logo">
<img v-if="siteLogo" :src="siteLogo" class="logo-img" alt="Logo" /> <SiteLogoMark
<div v-else class="logo-icon" :style="{ background: themeStore.headerUsePrimaryColor ? '#fff' : themeStore.primaryColor, color: themeStore.headerUsePrimaryColor ? themeStore.primaryColor : '#fff' }">{{ siteName.charAt(0) }}</div> img-class="logo-img"
icon-class="logo-icon"
:icon-style="logoIconStyle"
/>
<span class="logo-text">{{ siteName }}</span> <span class="logo-text">{{ siteName }}</span>
</div> </div>
@ -373,6 +379,7 @@ import ProfileModal from '@/components/ProfileModal.vue'
import PasswordModal from '@/components/PasswordModal.vue' import PasswordModal from '@/components/PasswordModal.vue'
import MessageNotification from '@/components/MessageNotification.vue' import MessageNotification from '@/components/MessageNotification.vue'
import TabBar from '@/components/TabBar.vue' import TabBar from '@/components/TabBar.vue'
import SiteLogoMark from '@/components/SiteLogoMark.vue'
import { noticeApi, chatApi, type SysNotice, type ChatMessage } from '@/api/message' import { noticeApi, chatApi, type SysNotice, type ChatMessage } from '@/api/message'
import { iconMap as externalIconMap } from '@/utils/icons' import { iconMap as externalIconMap } from '@/utils/icons'
@ -387,7 +394,10 @@ const themeStore = useThemeStore()
// //
const siteName = computed(() => siteStore.siteName || 'CSY Admin') const siteName = computed(() => siteStore.siteName || 'CSY Admin')
const siteLogo = computed(() => siteStore.siteLogo) const logoIconStyle = computed(() => ({
background: themeStore.headerUsePrimaryColor ? '#fff' : themeStore.primaryColor,
color: themeStore.headerUsePrimaryColor ? themeStore.primaryColor : '#fff',
}))
// message // message
window.$message = message window.$message = message

View File

@ -141,6 +141,18 @@ const routes: RouteRecordRaw[] = [
component: () => import('@/views/biz/order/index.vue'), component: () => import('@/views/biz/order/index.vue'),
meta: { title: '订单管理', icon: 'ListOutline' } meta: { title: '订单管理', icon: 'ListOutline' }
}, },
{
path: 'biz/orderProject',
name: 'orderProject',
component: () => import('@/views/biz/orderProject/index.vue'),
meta: { title: '项目管理', icon: 'ListOutline' }
},
{
path: 'biz/orderProject/gantt',
name: 'OrderProjectGantt',
component: () => import('@/views/biz/orderProject/components/GanttSchedule.vue'),
meta: { title: '甘特图排产', icon: 'CalendarOutline', activeMenu: '/biz/orderProject' }
},
{ {
path: 'biz/code', path: 'biz/code',
name: 'code', name: 'code',

View File

@ -7,7 +7,7 @@ import { configGroupApi } from '@/api/org'
*/ */
export const useSiteStore = defineStore('site', () => { export const useSiteStore = defineStore('site', () => {
// 站点名称 // 站点名称
const siteName = ref('CSY Admin') const siteName = ref('MES系统')
// 站点描述 // 站点描述
const siteDescription = ref('伊特智造MES') const siteDescription = ref('伊特智造MES')
// 站点 Logo // 站点 Logo
@ -33,7 +33,7 @@ export const useSiteStore = defineStore('site', () => {
try { try {
const config = await configGroupApi.getPublicConfig() const config = await configGroupApi.getPublicConfig()
if (config.system) { if (config.system) {
siteName.value = config.system.siteName || 'CSY Admin' siteName.value = config.system.siteName || 'MES系统'
siteDescription.value = config.system.siteDescription || '伊特智造MES' siteDescription.value = config.system.siteDescription || '伊特智造MES'
siteLogo.value = config.system.siteLogo || '' siteLogo.value = config.system.siteLogo || ''
copyright.value = config.system.copyright || '' copyright.value = config.system.copyright || ''

View File

@ -176,6 +176,21 @@
<n-input v-else v-model:value="formData.quantity" placeholder="请输入生产数量" disabled /> <n-input v-else v-model:value="formData.quantity" placeholder="请输入生产数量" disabled />
</n-form-item> </n-form-item>
</n-gi> </n-gi>
<n-gi>
<n-form-item label="订单编号" path="orderCode">
<n-input v-model:value="formData.orderCode" placeholder="订单编号" :disabled="!formData.id" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="工艺路线编码" path="routeCode">
<n-input v-model:value="formData.routeCode" placeholder="工艺路线编码" :disabled="!formData.id" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="生产车间" path="proWorkshop">
<n-input v-model:value="formData.proWorkshop" placeholder="生产车间" :disabled="!formData.id" />
</n-form-item>
</n-gi>
<n-gi> <n-gi>
<n-form-item label="是否半成品" path="sfProduct"> <n-form-item label="是否半成品" path="sfProduct">
@ -425,6 +440,9 @@ const hasPermission = (permission: string) => userStore.hasPermission(permission
const searchForm = reactive<any>({ const searchForm = reactive<any>({
code: '', code: '',
projectName: '', projectName: '',
orderCode: '',
routeCode: '',
proWorkshop: '',
beginTime: null as number | null, beginTime: null as number | null,
endTime: null as number | null, endTime: null as number | null,
}) })
@ -471,6 +489,9 @@ const defaultFormData = {
assingWorkOperationId: '', assingWorkOperationId: '',
assingWorkOperationTime: null, assingWorkOperationTime: null,
starter: '', starter: '',
orderCode: '',
routeCode: '',
proWorkshop: '',
} }
const formData = reactive<any>({ ...defaultFormData }) const formData = reactive<any>({ ...defaultFormData })
@ -503,6 +524,24 @@ const columns = [
title: '物料编码', title: '物料编码',
key: 'code' key: 'code'
}, },
{
align:'center',
minWidth: 150,
title: '订单编号',
key: 'orderCode',
},
{
align:'center',
minWidth: 150,
title: '工艺路线编码',
key: 'routeCode',
},
{
align:'center',
minWidth: 150,
title: '生产车间',
key: 'proWorkshop',
},
{ {
align:'center', align:'center',
minWidth: 150, minWidth: 150,
@ -642,7 +681,10 @@ async function loadData() {
page: pagination.page, page: pagination.page,
pageSize: pagination.pageSize, pageSize: pagination.pageSize,
code: searchForm.code, code: searchForm.code,
projectId: searchForm.projectId, projectName: searchForm.projectName,
orderCode: searchForm.orderCode,
routeCode: searchForm.routeCode,
proWorkshop: searchForm.proWorkshop,
beginTime: searchForm.beginTime, beginTime: searchForm.beginTime,
endTime: searchForm.endTime endTime: searchForm.endTime
}) })
@ -662,13 +704,12 @@ function handleSearch() {
// //
function handleReset() { function handleReset() {
searchForm.code = '' searchForm.code = ''
searchForm.projectName = ''
searchForm.projectId = '' searchForm.orderCode = ''
searchForm.routeCode = ''
searchForm.proWorkshop = ''
searchForm.beginTime = null searchForm.beginTime = null
searchForm.endTime = null searchForm.endTime = null
handleSearch() handleSearch()
} }
@ -696,14 +737,20 @@ let planList = ref<any>([])
if(formData.code){ if(formData.code){
planList.value.splice(0) planList.value.splice(0)
orderItemApi.preCreation({ orderItemApi.preCreation({
//productionCode:formData.productionCode
code:formData.code, code:formData.code,
projectCode:formData.projectCode projectCode:formData.projectCode
}).then((rps:any) => { }).then((rps:any) => {
planList.value = rps.planList?rps.planList:[] planList.value = rps.planList ? rps.planList : []
formData.orderCode = rps.orderCode ?? ''
formData.routeCode = rps.routeCode ?? ''
formData.proWorkshop = rps.proWorkshop ?? ''
if (rps.quantity != null) formData.quantity = rps.quantity
}) })
}else{ }else{
planList.value.splice(0) planList.value.splice(0)
formData.orderCode = ''
formData.routeCode = ''
formData.proWorkshop = ''
} }
} }
@ -760,6 +807,9 @@ function xmselupdate(v:any,opt:any) {
planList.value.splice(0) planList.value.splice(0)
wllist.value.splice(0) wllist.value.splice(0)
formData.code = '' formData.code = ''
formData.orderCode = ''
formData.routeCode = ''
formData.proWorkshop = ''
formData.projectName = opt.label formData.projectName = opt.label
if(v){ if(v){
wlhand(opt.value) wlhand(opt.value)
@ -1000,7 +1050,10 @@ async function handleExport() {
const params: Record<string, any> = {} const params: Record<string, any> = {}
if (selectedIds.value.length > 0) params.ids = selectedIds.value if (selectedIds.value.length > 0) params.ids = selectedIds.value
if (searchForm.code) params.code = searchForm.code if (searchForm.code) params.code = searchForm.code
if (searchForm.projectId != null) params.projectId = searchForm.projectId if (searchForm.projectName) params.projectName = searchForm.projectName
if (searchForm.orderCode) params.orderCode = searchForm.orderCode
if (searchForm.routeCode) params.routeCode = searchForm.routeCode
if (searchForm.proWorkshop) params.proWorkshop = searchForm.proWorkshop
if (searchForm.beginTime != null) params.beginTime = searchForm.beginTime if (searchForm.beginTime != null) params.beginTime = searchForm.beginTime
if (searchForm.endTime != null) params.endTime = searchForm.endTime if (searchForm.endTime != null) params.endTime = searchForm.endTime
const blob = await orderItemApi.export(params) const blob = await orderItemApi.export(params)

View File

@ -1,25 +1,43 @@
<template> <template>
<div class="gantt-schedule-page"> <div class="gantt-schedule-page">
<div class="gantt-header"> <div class="gantt-header">
<n-space justify="space-between" align="center"> <div class="gantt-header-left">
<n-h2 style="margin: 0">甘特图排产</n-h2> <h1 class="page-title">甘特图排产</h1>
<n-space> <p class="page-desc">拖拽工序条块调整计划时间完成后请保存排产草稿</p>
<n-button type="primary" :loading="saving" @click="handleSaveDraft">保存排产草稿</n-button> </div>
<n-button @click="handleBack">返回</n-button> <n-space>
</n-space> <n-button type="primary" :loading="saving" @click="handleSaveDraft">保存排产草稿</n-button>
<n-button @click="handleBack">返回</n-button>
</n-space> </n-space>
</div> </div>
<div class="gantt-container" ref="ganttContainer"></div>
<div class="gantt-meta">
<span>生产订单 {{ scheduleStats.moCount }} </span>
<span class="meta-divider">|</span>
<span>工序 {{ scheduleStats.routeCount }} </span>
<span class="meta-divider">|</span>
<span>今日 {{ todayLabel }}</span>
<span class="meta-divider">|</span>
<span class="meta-legend"><i class="legend-today-line" />今日时间轴</span>
</div>
<div class="gantt-panel">
<div class="gantt-panel-head">
<span class="panel-title">排产明细</span>
<span class="panel-tip">主产品行展示物料编码与名称子行展示工序号与工序名称</span>
</div>
<div class="gantt-container" ref="ganttContainer" />
</div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, onUnmounted, toRaw } from 'vue' import { ref, onMounted, onUnmounted, toRaw, computed } from 'vue'
import { useMessage } from 'naive-ui' import { useMessage } from 'naive-ui'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { gantt } from 'dhtmlx-gantt' import { gantt } from 'dhtmlx-gantt'
import 'dhtmlx-gantt/codebase/dhtmlxgantt.css' import 'dhtmlx-gantt/codebase/dhtmlxgantt.css'
import { orderProjectApi, type KingdeePrdMo } from '@/api/orderProject' import { orderProjectApi, normalizeKingdeeProcessRoute, resolveProcessName, type KingdeePrdMo } from '@/api/orderProject'
const router = useRouter() const router = useRouter()
const ganttContainer = ref<HTMLElement | null>(null) const ganttContainer = ref<HTMLElement | null>(null)
@ -30,6 +48,42 @@ const tableData = ref<any[]>([])
const projectId = ref<number | null>(null) const projectId = ref<number | null>(null)
const saving = ref(false) const saving = ref(false)
const PROCESS_COLORS = [
'#409eff', '#67c23a', '#e6a23c', '#909399', '#f56c6c',
'#36cfc9', '#597ef7', '#9254de', '#73d13d',
]
const WEEKDAY_LABELS = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
function getRouteColorIndex(index: number) {
return index % PROCESS_COLORS.length
}
function getRouteColor(index: number) {
return PROCESS_COLORS[getRouteColorIndex(index)]
}
function formatDayScale(date: Date) {
return `${date.getDate()}${WEEKDAY_LABELS[date.getDay()]}`
}
const scheduleStats = computed(() => {
let routeCount = 0
tableData.value.forEach((mo) => {
routeCount += mo.processRoutes?.length ?? 0
})
return {
moCount: tableData.value.length,
routeCount,
}
})
const todayLabel = computed(() => {
const d = new Date()
const pad = (n: number) => String(n).padStart(2, '0')
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
})
function handleBack() { function handleBack() {
router.back() router.back()
} }
@ -39,22 +93,17 @@ async function handleSaveDraft() {
message.warning('缺少项目ID无法保存') message.warning('缺少项目ID无法保存')
return return
} }
saving.value = true saving.value = true
try { try {
// processRoutes children
const draftData = tableData.value.map(mo => { const draftData = tableData.value.map(mo => {
const { processRoutes, ...rest } = mo const { processRoutes, ...rest } = mo
return { ...rest, children: processRoutes } as KingdeePrdMo return { ...rest, children: processRoutes } as KingdeePrdMo
}) })
await orderProjectApi.saveKingdeeOrderDraft(projectId.value, draftData) await orderProjectApi.saveKingdeeOrderDraft(projectId.value, draftData)
message.success('排产草稿保存成功') message.success('排产草稿保存成功')
// session
sessionStorage.setItem('gantt_schedule_data', JSON.stringify(tableData.value)) sessionStorage.setItem('gantt_schedule_data', JSON.stringify(tableData.value))
} catch (error) {
//
} finally { } finally {
saving.value = false saving.value = false
} }
@ -62,25 +111,26 @@ async function handleSaveDraft() {
onMounted(async () => { onMounted(async () => {
try { try {
initGantt() initGantt()
const stateStr = sessionStorage.getItem('gantt_schedule_data') const stateStr = sessionStorage.getItem('gantt_schedule_data')
const idStr = sessionStorage.getItem('gantt_project_id') const idStr = sessionStorage.getItem('gantt_project_id')
if (idStr) { if (idStr) {
projectId.value = Number(idStr) projectId.value = Number(idStr)
} }
if (stateStr) { if (stateStr) {
const parsedData = JSON.parse(stateStr) const parsedData = JSON.parse(stateStr)
tableData.value = parsedData tableData.value = (Array.isArray(parsedData) ? parsedData : []).map((mo: any) => ({
...mo,
processRoutes: (mo.processRoutes ?? []).map(normalizeKingdeeProcessRoute),
}))
renderGanttData() renderGanttData()
} else { } else {
message.warning('未获取到排产数据') message.warning('未获取到排产数据')
} }
} catch (error) { } catch {
message.error('加载排产数据失败') message.error('加载排产数据失败')
} }
}) })
@ -90,204 +140,283 @@ function formatGanttDate(date: Date) {
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}` return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
} }
function formatGridDate(value: Date | string | undefined) {
if (!value) return ''
const date = value instanceof Date ? value : new Date(String(value).replace(' ', 'T'))
if (isNaN(date.getTime())) return ''
const pad = (n: number) => String(n).padStart(2, '0')
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
}
function formatMoMaterialLabel(mo: any) {
const code = mo.materialCode?.trim() || ''
const name = mo.materialName?.trim() || ''
if (code && name) return `${code} ${name}`
return code || name
}
function initGantt() { function initGantt() {
if (!ganttContainer.value) return if (!ganttContainer.value) return
//
ganttEventIds.forEach(id => gantt.detachEvent(id)) ganttEventIds.forEach(id => gantt.detachEvent(id))
ganttEventIds = [] ganttEventIds = []
gantt.clearAll() gantt.clearAll()
// gantt.config.date_format = '%Y-%m-%d %H:%i:%s'
gantt.config.date_format = "%Y-%m-%d %H:%i:%s" gantt.config.scale_height = 60
gantt.config.scale_height = 50 gantt.config.row_height = 40
gantt.config.bar_height = 26
gantt.config.scales = [ gantt.config.scales = [
{ unit: "month", step: 1, format: "%Y年 %m月" }, { unit: 'month', step: 1, format: '%Y年 %m月' },
{ unit: "day", step: 1, format: "%d日" } { unit: 'day', step: 1, format: formatDayScale },
] ]
gantt.config.columns = [ gantt.config.columns = [
{ name: "text", label: "单号 / 序号", tree: true, width: 200, resize: true }, {
{ name: "materialCode", label: "物料编码", align: "center", width: 140, resize: true }, name: 'text',
{ name: "name", label: "名称", align: "center", width: 160, resize: true }, label: '工序号 / 物料编码',
{ name: "start_date", label: "计划开始", align: "center", width: 130 }, tree: true,
{ name: "end_date", label: "计划结束", align: "center", width: 130 }, width: 200,
resize: true,
template(task: any) {
if (task._type === 'route') return task.operNumber ?? ''
return task.materialCode ?? ''
},
},
{
name: 'routeProcessName',
label: '工序名称 / 物料名称',
align: 'center',
width: 168,
resize: true,
template(task: any) {
if (task._type === 'route') {
return task.routeProcessName ?? resolveProcessName(task._raw ?? {}) ?? ''
}
return task.materialName ?? ''
},
},
{
name: 'start_date',
label: '计划开始',
align: 'center',
width: 118,
template(task: any) {
return formatGridDate(task.start_date)
},
},
{
name: 'end_date',
label: '计划结束',
align: 'center',
width: 118,
template(task: any) {
return formatGridDate(task.end_date)
},
},
] ]
//??
gantt.config.layout = { gantt.config.layout = {
css: "gantt_container", css: 'gantt_container',
cols: [ cols: [
{ {
width: 300, width: 620,
minWidth: 200, minWidth: 420,
maxWidth: 600, maxWidth: 680,
rows: [ rows: [
{ view: "grid", scrollX: "gridScroll", scrollable: true, { view: 'grid', scrollX: 'gridScroll', scrollable: true, scrollY: 'scrollVer' },
scrollY: "scrollVer" { view: 'scrollbar', id: 'gridScroll', group: 'horizontal' },
}, ],
{ view: "scrollbar", id: "gridScroll", group: "horizontal" } /*!*/ },
] { resizer: true, width: 1 },
}, {
{ resizer: true, width: 1 }, rows: [
{ { view: 'timeline', scrollX: 'scrollHor', scrollY: 'scrollVer' },
rows: [ { view: 'scrollbar', id: 'scrollHor', group: 'horizontal' },
{ view: "timeline", scrollX: "scrollHor", scrollY: "scrollVer" }, ],
{ view: "scrollbar", id: "scrollHor", group: "horizontal" } /*!*/ },
] { view: 'scrollbar', id: 'scrollVer' },
}, ],
{ view: "scrollbar", id: "scrollVer" } }
]
}
//??
// gantt.config.drag_progress = false
gantt.config.drag_progress = false // gantt.config.drag_links = false
gantt.config.drag_links = false // 线 // 使
gantt.config.work_time = true // gantt.config.work_time = false
// gantt.config.auto_scheduling gantt.config.correct_work_time = false
gantt.config.skip_off_time = false
gantt.i18n.setLocale("cn") gantt.config.duration_unit = 'day'
gantt.config.smart_rendering = true
// marker gantt.i18n.setLocale('cn')
gantt.plugins({ gantt.plugins({ marker: true })
marker: true
})
// gantt.templates.task_text = function(_start, _end, task) {
gantt.templates.task_text = function(start, end, task) {
if (task._type === 'route') { if (task._type === 'route') {
return task.name; // return task.routeProcessName || resolveProcessName(task._raw ?? {}) || ''
} }
return task.text; return task.materialName || task.materialLabel || ''
}; }
gantt.templates.task_class = function(_start, _end, task) {
if (task._type === 'mo') return 'gantt-task-mo'
if (task._type === 'route') {
return `gantt-task-route gantt-route-c-${task.colorIndex ?? 0}`
}
return ''
}
gantt.templates.grid_row_class = function(_start, _end, task) {
if (task._type === 'mo') return 'gantt-row-mo'
if (task._type === 'route') {
return `gantt-row-route gantt-route-c-${task.colorIndex ?? 0}`
}
return ''
}
gantt.templates.timeline_cell_class = function(_task, date) {
const day = date.getDay()
if (day === 0 || day === 6) return 'gantt-weekend'
return ''
}
gantt.init(ganttContainer.value) gantt.init(ganttContainer.value)
}
// /** 今日时间轴标记(数据刷新后需重新添加) */
const today = new Date() function addTodayMarker() {
const now = new Date()
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 12, 0, 0)
gantt.addMarker({ gantt.addMarker({
start_date: today, start_date: today,
css: "today-marker", css: 'today-marker',
text: "今日", text: '今日',
title: "今日: " + formatGanttDate(today) title: `今日: ${formatGanttDate(now)}`,
}) })
gantt.showDate(today)
}
function getRouteProcessName(route: any) {
return resolveProcessName(route)
} }
function renderGanttData() { function renderGanttData() {
gantt.clearAll() gantt.clearAll()
//
const tasks: any[] = [] const tasks: any[] = []
const rawData = toRaw(tableData.value) || [] const rawData = toRaw(tableData.value) || []
const processColors = ['#3498db', '#1abc9c', '#9b59b6', '#e67e22', '#e74c3c', '#2ecc71', '#f1c40f', '#34495e', '#7f8c8d']
rawData.forEach((mo: any, moIndex: number) => { rawData.forEach((mo: any, moIndex: number) => {
const moId = `mo_${moIndex}_${mo.productionOrderNo}_${mo.billNo}` const moId = `mo_${moIndex}_${mo.productionOrderNo}_${mo.billNo}`
// 使
let minDate: Date | null = mo.planStartTime ? new Date(mo.planStartTime.replace(' ', 'T')) : null let minDate: Date | null = mo.planStartTime ? new Date(mo.planStartTime.replace(' ', 'T')) : null
let maxDate: Date | null = mo.planFinishTime ? new Date(mo.planFinishTime.replace(' ', 'T')) : null let maxDate: Date | null = mo.planFinishTime ? new Date(mo.planFinishTime.replace(' ', 'T')) : null
if (!minDate || isNaN(minDate.getTime())) minDate = new Date() if (!minDate || isNaN(minDate.getTime())) minDate = new Date()
if (!maxDate || isNaN(maxDate.getTime())) maxDate = new Date(minDate.getTime() + 86400000) if (!maxDate || isNaN(maxDate.getTime())) maxDate = new Date(minDate.getTime() + 86400000)
if (maxDate <= minDate) maxDate = new Date(minDate.getTime() + 86400000) if (maxDate <= minDate) maxDate = new Date(minDate.getTime() + 86400000)
const materialLabel = formatMoMaterialLabel(mo)
tasks.push({ tasks.push({
id: moId, id: moId,
text: mo.materialCode || '', //mo.productionOrderNo || mo.billNo || '', ?? text: materialLabel,
materialLabel,
materialCode: mo.materialCode || '', materialCode: mo.materialCode || '',
name: mo.materialName || '产品', materialName: mo.materialName || '',
operNumber: '',
processName: '',
start_date: formatGanttDate(minDate), start_date: formatGanttDate(minDate),
end_date: formatGanttDate(maxDate), end_date: formatGanttDate(maxDate),
open: false, // open: true,
type: gantt.config.types.project, type: gantt.config.types.project,
_type: 'mo', _type: 'mo',
progress:0.6, //?? progress: 0,
_raw: mo _raw: mo,
}) })
if (mo.processRoutes && mo.processRoutes.length > 0) { if (mo.processRoutes?.length > 0) {
mo.processRoutes.forEach((route: any, index: number) => { mo.processRoutes.forEach((route: any, index: number) => {
const routeId = `route_${moId}_${index}` const routeId = `route_${moId}_${index}`
let rStart = route.planStartTime ? new Date(route.planStartTime.replace(' ', 'T')) : null let rStart = route.planStartTime ? new Date(route.planStartTime.replace(' ', 'T')) : null
let rEnd = route.planFinishTime ? new Date(route.planFinishTime.replace(' ', 'T')) : null let rEnd = route.planFinishTime ? new Date(route.planFinishTime.replace(' ', 'T')) : null
if (!rStart || isNaN(rStart.getTime())) rStart = minDate if (!rStart || isNaN(rStart.getTime())) rStart = minDate
if (!rEnd || isNaN(rEnd.getTime())) rEnd = maxDate if (!rEnd || isNaN(rEnd.getTime())) rEnd = maxDate
if (rEnd <= rStart) rEnd = new Date(rStart!.getTime() + 3600000) // 1 if (rEnd <= rStart) rEnd = new Date(rStart!.getTime() + 3600000)
const operNumber = route.operNumber != null ? String(route.operNumber) : String(index + 1)
const routeProcessName = getRouteProcessName(route)
const colorIndex = getRouteColorIndex(index)
const routeColor = getRouteColor(index)
tasks.push({ tasks.push({
id: routeId, id: routeId,
text:mo.materialCode || '', //route.operNumber != null ? String(route.operNumber) : String(index + 1), ?? text: operNumber,
materialCode: '', operNumber,
name: route.processProperty || '工序', routeProcessName,
color: processColors[index % processColors.length], colorIndex,
routeColor,
color: routeColor,
start_date: formatGanttDate(rStart!), start_date: formatGanttDate(rStart!),
end_date: formatGanttDate(rEnd), end_date: formatGanttDate(rEnd),
parent: moId, parent: moId,
_type: 'route', _type: 'route',
progress:0.6, //?? progress: 0,
_raw: route _raw: route,
}) })
}) })
} }
}) })
gantt.parse({ data: tasks, links: [] }) gantt.parse({ data: tasks, links: [] })
addTodayMarker()
// const evDragId = gantt.attachEvent('onBeforeTaskDrag', (id) => {
const evDragId = gantt.attachEvent("onBeforeTaskDrag", (id, mode, e) => {
const task = gantt.getTask(id) const task = gantt.getTask(id)
if (task.type === gantt.config.types.project) { return task.type !== gantt.config.types.project
return false
}
return true
}) })
ganttEventIds.push(evDragId) ganttEventIds.push(evDragId)
// const evId = gantt.attachEvent('onAfterTaskDrag', (id, mode) => {
const evId = gantt.attachEvent("onAfterTaskDrag", (id, mode, e) => { const task = gantt.getTask(id)
const task = gantt.getTask(id) if (mode === 'move' || mode === 'resize') {
if (mode === 'move' || mode === 'resize') { const start = gantt.date.date_to_str(gantt.config.date_format)(task.start_date)
const start = gantt.date.date_to_str(gantt.config.date_format)(task.start_date) const end = gantt.date.date_to_str(gantt.config.date_format)(task.end_date)
const end = gantt.date.date_to_str(gantt.config.date_format)(task.end_date)
if (task._type === 'mo') {
// task._raw.planStartTime = start
if (task._type === 'mo') { task._raw.planFinishTime = end
task._raw.planStartTime = start } else if (task._type === 'route') {
task._raw.planFinishTime = end task._raw.planStartTime = start
} else if (task._type === 'route') { task._raw.planFinishTime = end
task._raw.planStartTime = start
task._raw.planFinishTime = end const parentId = task.parent
if (parentId) {
// const parentTask = gantt.getTask(parentId)
const parentId = task.parent const children = gantt.getChildren(parentId)
if (parentId) { let minD: Date | null = null
const parentTask = gantt.getTask(parentId) let maxD: Date | null = null
if (parentTask) {
const children = gantt.getChildren(parentId) children.forEach((childId: string | number) => {
let minDate: Date | null = null const childTask = gantt.getTask(childId)
let maxDate: Date | null = null const cs = childTask.start_date
const ce = childTask.end_date
children.forEach((childId: string | number) => { if (cs && (!minD || cs < minD)) minD = cs
const childTask = gantt.getTask(childId) if (ce && (!maxD || ce > maxD)) maxD = ce
if (!minDate || childTask.start_date < minDate) minDate = childTask.start_date })
if (!maxDate || childTask.end_date > maxDate) maxDate = childTask.end_date
}) if (minD && maxD) {
parentTask.start_date = new Date(minD)
if (minDate && maxDate) { parentTask.end_date = new Date(maxD)
parentTask.start_date = new Date(minDate) parentTask._raw.planStartTime = gantt.date.date_to_str(gantt.config.date_format)(minD)
parentTask.end_date = new Date(maxDate) parentTask._raw.planFinishTime = gantt.date.date_to_str(gantt.config.date_format)(maxD)
parentTask._raw.planStartTime = gantt.date.date_to_str(gantt.config.date_format)(minDate) gantt.updateTask(parentId)
parentTask._raw.planFinishTime = gantt.date.date_to_str(gantt.config.date_format)(maxDate)
gantt.updateTask(parentId)
}
}
} }
} }
message.success(`任务 [${task.text}] 计划时间已更新`)
} }
})
const label = task._type === 'route'
? (task.routeProcessName || task.operNumber)
: (task.materialLabel || task.materialName)
message.success(`任务 [${label}] 计划时间已更新`)
}
})
ganttEventIds.push(evId) ganttEventIds.push(evId)
} }
@ -299,36 +428,249 @@ onUnmounted(() => {
<style scoped> <style scoped>
.gantt-schedule-page { .gantt-schedule-page {
--gs-font: 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', sans-serif;
--gs-title: #333333;
--gs-text: #606266;
--gs-border: #e4e7ed;
--gs-bg: #f5f7fa;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: calc(100vh - 120px); height: calc(100vh - 120px);
background-color: var(--n-color);
padding: 16px; padding: 16px;
border-radius: 8px;
box-sizing: border-box; box-sizing: border-box;
font-family: var(--gs-font);
font-weight: 400;
color: var(--gs-text);
background: var(--gs-bg);
} }
.gantt-header { .gantt-header {
margin-bottom: 16px; display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-shrink: 0; flex-shrink: 0;
margin-bottom: 12px;
padding: 14px 16px;
background: #fff;
border: 1px solid var(--gs-border);
border-radius: 4px;
}
.gantt-header-left {
min-width: 0;
}
.page-title {
margin: 0;
font-size: 16px;
font-weight: 600;
color: var(--gs-title);
line-height: 1.4;
}
.page-desc {
margin: 4px 0 0;
font-size: 13px;
font-weight: 400;
color: var(--gs-text);
line-height: 1.5;
}
.gantt-meta {
flex-shrink: 0;
margin-bottom: 12px;
padding: 0 4px;
font-size: 13px;
font-weight: 400;
color: var(--gs-text);
}
.meta-divider {
margin: 0 10px;
color: #dcdfe6;
}
.meta-legend {
display: inline-flex;
align-items: center;
gap: 6px;
}
.legend-today-line {
display: inline-block;
width: 2px;
height: 14px;
background: #f56c6c;
border-radius: 1px;
}
.gantt-panel {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
background: #fff;
border: 1px solid var(--gs-border);
border-radius: 4px;
overflow: hidden;
}
.gantt-panel-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 10px 16px;
background: #fafafa;
border-bottom: 1px solid var(--gs-border);
flex-shrink: 0;
}
.panel-title {
font-size: 14px;
font-weight: 600;
color: var(--gs-title);
}
.panel-tip {
font-size: 12px;
font-weight: 400;
color: #909399;
} }
.gantt-container { .gantt-container {
flex: 1; flex: 1;
width: 100%; width: 100%;
min-height: 0; /* 必须设置min-height以允许flex子元素内部滚动 */ min-height: 0;
border: 1px solid var(--n-border-color);
border-radius: 4px;
overflow: hidden; overflow: hidden;
} }
:deep(.today-marker) { /* dhtmlx-gantt仅表头标题加粗其余正常字重 */
background-color: #ff5252; .gantt-container :deep(.gantt_container) {
font-family: var(--gs-font);
font-weight: 400;
border: none !important;
background: #fff;
} }
:deep(.today-marker .gantt_marker_content) {
background-color: #ff5252; .gantt-container :deep(.gantt_grid_scale),
color: white; .gantt-container :deep(.gantt_task_scale) {
background: #fafafa;
border-bottom: 1px solid var(--gs-border) !important;
}
.gantt-container :deep(.gantt_task_scale .gantt_scale_cell) {
font-size: 12px;
font-weight: 400;
color: var(--gs-text);
border-color: var(--gs-border) !important;
line-height: 1.3;
white-space: normal;
}
.gantt-container :deep(.gantt_grid_scale .gantt_grid_head_cell) {
font-size: 13px;
font-weight: 600;
color: var(--gs-title);
border-color: var(--gs-border) !important;
}
/* 工序号文字与进度条同色(无圆点) */
.gantt-container :deep(.gantt_row.gantt-row-route.gantt-route-c-0 .gantt_cell:first-child) { color: #409eff; }
.gantt-container :deep(.gantt_row.gantt-row-route.gantt-route-c-1 .gantt_cell:first-child) { color: #67c23a; }
.gantt-container :deep(.gantt_row.gantt-row-route.gantt-route-c-2 .gantt_cell:first-child) { color: #e6a23c; }
.gantt-container :deep(.gantt_row.gantt-row-route.gantt-route-c-3 .gantt_cell:first-child) { color: #909399; }
.gantt-container :deep(.gantt_row.gantt-row-route.gantt-route-c-4 .gantt_cell:first-child) { color: #f56c6c; }
.gantt-container :deep(.gantt_row.gantt-row-route.gantt-route-c-5 .gantt_cell:first-child) { color: #36cfc9; }
.gantt-container :deep(.gantt_row.gantt-row-route.gantt-route-c-6 .gantt_cell:first-child) { color: #597ef7; }
.gantt-container :deep(.gantt_row.gantt-row-route.gantt-route-c-7 .gantt_cell:first-child) { color: #9254de; }
.gantt-container :deep(.gantt_row.gantt-row-route.gantt-route-c-8 .gantt_cell:first-child) { color: #73d13d; }
.gantt-container :deep(.gantt_task_line.gantt-route-c-0) { background-color: #409eff !important; border-color: #409eff !important; }
.gantt-container :deep(.gantt_task_line.gantt-route-c-1) { background-color: #67c23a !important; border-color: #67c23a !important; }
.gantt-container :deep(.gantt_task_line.gantt-route-c-2) { background-color: #e6a23c !important; border-color: #e6a23c !important; }
.gantt-container :deep(.gantt_task_line.gantt-route-c-3) { background-color: #909399 !important; border-color: #909399 !important; }
.gantt-container :deep(.gantt_task_line.gantt-route-c-4) { background-color: #f56c6c !important; border-color: #f56c6c !important; }
.gantt-container :deep(.gantt_task_line.gantt-route-c-5) { background-color: #36cfc9 !important; border-color: #36cfc9 !important; }
.gantt-container :deep(.gantt_task_line.gantt-route-c-6) { background-color: #597ef7 !important; border-color: #597ef7 !important; }
.gantt-container :deep(.gantt_task_line.gantt-route-c-7) { background-color: #9254de !important; border-color: #9254de !important; }
.gantt-container :deep(.gantt_task_line.gantt-route-c-8) { background-color: #73d13d !important; border-color: #73d13d !important; }
.gantt-container :deep(.gantt_task_line.gantt-task-route .gantt_task_progress) {
background-color: rgba(0, 0, 0, 0.18) !important;
opacity: 1;
}
.gantt-container :deep(.gantt_task_line.gantt-task-route) {
border: none !important;
border-radius: 3px;
}
.gantt-container :deep(.gantt_grid_data .gantt_cell) {
font-size: 13px;
font-weight: 400;
color: var(--gs-text);
border-color: #f0f2f5 !important;
}
.gantt-container :deep(.gantt_row.gantt-row-mo) {
background: #fafafa;
}
.gantt-container :deep(.gantt_row.gantt-row-mo .gantt_cell) {
font-weight: 400;
color: var(--gs-title);
}
.gantt-container :deep(.gantt_row:hover) {
background: #f5f7fa !important;
}
.gantt-container :deep(.gantt_task_row.gantt_selected),
.gantt-container :deep(.gantt_row.gantt_selected) {
background: #ecf5ff !important;
}
.gantt-container :deep(.gantt_task_line.gantt-task-route .gantt_task_content) {
font-size: 12px;
font-weight: 400;
color: #fff;
padding: 0 6px;
}
.gantt-container :deep(.gantt_task_line.gantt_project) {
display: none;
}
.gantt-container :deep(.gantt_weekend) {
background: #fafafa;
}
.gantt-container :deep(.gantt_marker.today-marker) {
background: #f56c6c;
width: 2px;
z-index: 4;
opacity: 1;
}
.gantt-container :deep(.today-marker .gantt_marker_content) {
background: #f56c6c;
color: #fff;
font-size: 12px;
font-weight: 600;
border-radius: 2px; border-radius: 2px;
padding: 0 4px; padding: 2px 8px;
box-shadow: 0 1px 4px rgba(245, 108, 108, 0.35);
} }
</style>
.gantt-container :deep(.gantt_layout_content) {
border-color: var(--gs-border) !important;
}
.gantt-container :deep(.gantt_hor_scroll),
.gantt-container :deep(.gantt_ver_scroll) {
background: #fafafa;
}
</style>

View File

@ -229,7 +229,7 @@
import { ref, reactive, h, computed, onMounted, watch } from 'vue' import { ref, reactive, h, computed, onMounted, watch } from 'vue'
import { NButton, NSpace, NIcon, NTag, NDatePicker, NDataTable, NUpload, useMessage, useDialog, type DataTableColumns, type UploadCustomRequestOptions } from 'naive-ui' import { NButton, NSpace, NIcon, NTag, NDatePicker, NDataTable, NUpload, useMessage, useDialog, type DataTableColumns, type UploadCustomRequestOptions } from 'naive-ui'
import { SearchOutline, RefreshOutline, AddOutline, TrashOutline, CreateOutline, CloudUploadOutline, DownloadOutline, EyeOutline, CalendarOutline } from '@vicons/ionicons5' import { SearchOutline, RefreshOutline, AddOutline, TrashOutline, CreateOutline, CloudUploadOutline, DownloadOutline, EyeOutline, CalendarOutline } from '@vicons/ionicons5'
import { orderProjectApi, type OrderProject, type KingdeePrdMo, type KingdeeProcessRoute } from '@/api/orderProject' import { orderProjectApi, type OrderProject, type KingdeePrdMo, type KingdeeProcessRoute, normalizeKingdeeProcessRoute } from '@/api/orderProject'
import { dictDataApi } from '@/api/org' import { dictDataApi } from '@/api/org'
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
@ -400,8 +400,6 @@ const formData = reactive<OrderProject>({ ...defaultFormData })
// //使 // //使
const sourceOptions = ref<{ label: string; value: any }[]>([]) const sourceOptions = ref<{ label: string; value: any }[]>([])
const statusOptions = ref<{ label: string; value: any }[]>([]) const statusOptions = ref<{ label: string; value: any }[]>([])
const prodTypeOptions = ref<{label: string; value:any} []>([])
// //
const formRules = { const formRules = {
@ -421,23 +419,11 @@ const columns: DataTableColumns<OrderProject> = [
{ title: '项目来源', key: 'source', width: 110,align: 'center', { title: '项目来源', key: 'source', width: 110,align: 'center',
render(row) { render(row) {
const val = row.source const val = row.source
console.log(sourceOptions.value);
const opt = sourceOptions.value.find(o => o.value === val || String(o.value) === String(val)) const opt = sourceOptions.value.find(o => o.value === val || String(o.value) === String(val))
if (!opt) return val ?? '-' return opt ? opt.label : (val ?? '-')
return h(NTag, { type: opt.class, size: 'small' }, { default: () => opt.label })
}
},
{ title: '生产类型', key: 'prodType', width: 100 ,align: 'center',
render(row) {
const val = row.prodType
const opt = prodTypeOptions.value.find(o => o.value === val || String(o.value) === String(val))
if (!opt) return val ?? '-'
return h(NTag, { type: opt.class, size: 'small' }, { default: () => opt.label })
} }
}, },
{ title: '生产类型', key: 'prodType', width: 100 ,align: 'center'},
{ title: 'BOM版本', key: 'bomVersion', width: 100 ,align: 'center'}, { title: 'BOM版本', key: 'bomVersion', width: 100 ,align: 'center'},
{ title: '规格型号', key: 'specModel', width: 140, ellipsis: { tooltip: true } }, { title: '规格型号', key: 'specModel', width: 140, ellipsis: { tooltip: true } },
{ title: '单位', key: 'unit', width: 70 ,align: 'center'}, { title: '单位', key: 'unit', width: 70 ,align: 'center'},
@ -448,9 +434,7 @@ const columns: DataTableColumns<OrderProject> = [
render(row) { render(row) {
const val = row.status const val = row.status
const opt = statusOptions.value.find(o => o.value === val || String(o.value) === String(val)) const opt = statusOptions.value.find(o => o.value === val || String(o.value) === String(val))
if (!opt) return val ?? '-' return opt ? opt.label : (val ?? '-')
return h(NTag, { type: opt.class, size: 'small' }, { default: () => opt.label })
} }
}, },
{ title: '计划完成日期', key: 'planFinishTime', width: 170 ,align: 'center'}, { title: '计划完成日期', key: 'planFinishTime', width: 170 ,align: 'center'},
@ -486,9 +470,8 @@ const columns: DataTableColumns<OrderProject> = [
] ]
const pickMtrlStatusMap: Record<string, string> = { const pickMtrlStatusMap: Record<string, string> = {
'1': '未领料', '0': '未领料',
'2': '待定', '1': '已领料',
'3': '已领料',
} }
const kingdeeMoColumns: DataTableColumns<KingdeeMoTableRow> = [ const kingdeeMoColumns: DataTableColumns<KingdeeMoTableRow> = [
@ -517,13 +500,13 @@ const kingdeeMoColumns: DataTableColumns<KingdeeMoTableRow> = [
{ title: '生产车间', key: 'workShopName', width: 100, align: 'center', ellipsis: { tooltip: true } }, { title: '生产车间', key: 'workShopName', width: 100, align: 'center', ellipsis: { tooltip: true } },
{ title: '规格型号', key: 'specModel', width: 110, align: 'center', ellipsis: { tooltip: true } }, { title: '规格型号', key: 'specModel', width: 110, align: 'center', ellipsis: { tooltip: true } },
{ title: '单位', key: 'unit', width: 60, align: 'center' }, { title: '单位', key: 'unit', width: 60, align: 'center' },
{ {
title: '计划开工', key: 'planStartTime', width: 180, align: 'center', title: '计划开工', key: 'planStartTime', width: 180, align: 'center',
render(row) { render(row) {
return renderKingdeePlanPickerMo(row, 'planStartTime') return renderKingdeePlanPickerMo(row, 'planStartTime')
} }
}, },
{ {
title: '计划完工', key: 'planFinishTime', width: 180, align: 'center', title: '计划完工', key: 'planFinishTime', width: 180, align: 'center',
render(row) { render(row) {
return renderKingdeePlanPickerMo(row, 'planFinishTime') return renderKingdeePlanPickerMo(row, 'planFinishTime')
@ -533,8 +516,6 @@ const kingdeeMoColumns: DataTableColumns<KingdeeMoTableRow> = [
title: '领料状态', key: 'pickMtrlStatus', width: 90, align: 'center', title: '领料状态', key: 'pickMtrlStatus', width: 90, align: 'center',
render(row) { render(row) {
const val = row.pickMtrlStatus const val = row.pickMtrlStatus
console.log(val);
return val != null ? (pickMtrlStatusMap[String(val)] ?? val) : '-' return val != null ? (pickMtrlStatusMap[String(val)] ?? val) : '-'
} }
}, },
@ -553,7 +534,7 @@ const kingdeeProcessColumns: DataTableColumns<KingdeeProcessRoute> = [
return row.operNumber != null ? String(row.operNumber) : '-' return row.operNumber != null ? String(row.operNumber) : '-'
} }
}, },
{ title: '工序名称', key: 'processProperty', width: 100, align: 'center' }, { title: '工序名称', key: 'processName', width: 100, align: 'center' },
{ title: '工序说明', key: 'operDescription', width: 140, ellipsis: { tooltip: true } }, { title: '工序说明', key: 'operDescription', width: 140, ellipsis: { tooltip: true } },
{ title: '工作中心', key: 'workCenterName', width: 110, align: 'center', ellipsis: { tooltip: true } }, { title: '工作中心', key: 'workCenterName', width: 110, align: 'center', ellipsis: { tooltip: true } },
{ title: '生产车间', key: 'departmentName', width: 110, align: 'center', ellipsis: { tooltip: true } }, { title: '生产车间', key: 'departmentName', width: 110, align: 'center', ellipsis: { tooltip: true } },
@ -576,7 +557,7 @@ const kingdeeProcessColumns: DataTableColumns<KingdeeProcessRoute> = [
} }
}, },
{ title: '活动单位', key: 'activityUnit', width: 80, align: 'center' }, { title: '活动单位', key: 'activityUnit', width: 80, align: 'center' },
{ title: '控制码', key: 'optCtrlCodeName', width: 120, align: 'center', ellipsis: { tooltip: true } }, { title: '工序控制码', key: 'optCtrlCodeName', width: 120, align: 'center', ellipsis: { tooltip: true } },
] ]
// //
@ -726,11 +707,13 @@ async function handleKingdeeQuery(row: OrderProject) {
const data = await orderProjectApi.getKingdeeOrder(row.id) const data = await orderProjectApi.getKingdeeOrder(row.id)
kingdeeTableData.value = (Array.isArray(data) ? data : []).map((mo) => { kingdeeTableData.value = (Array.isArray(data) ? data : []).map((mo) => {
const { children, ...rest } = mo const { children, ...rest } = mo
const routes = [...(children ?? [])].sort((a, b) => (a.operNumber ?? 0) - (b.operNumber ?? 0)).map((r) => ({ const routes = [...(children ?? [])]
...r, .sort((a, b) => (a.operNumber ?? 0) - (b.operNumber ?? 0))
planStartTime: r.planStartTime ?? null, .map((r) => normalizeKingdeeProcessRoute({
planFinishTime: r.planFinishTime ?? null, ...r,
})) planStartTime: r.planStartTime ?? null,
planFinishTime: r.planFinishTime ?? null,
}))
return { ...rest, processRoutes: routes } return { ...rest, processRoutes: routes }
}) })
if (kingdeeTableData.value.length === 0) { if (kingdeeTableData.value.length === 0) {
@ -744,7 +727,7 @@ async function handleKingdeeQuery(row: OrderProject) {
// 稿 // 稿
async function handleSaveDraft() { async function handleSaveDraft() {
if (!kingdeeCurrentProjectId.value) return if (!kingdeeCurrentProjectId.value) return
kingdeeLoading.value = true kingdeeLoading.value = true
try { try {
// processRoutes children // processRoutes children
@ -752,7 +735,7 @@ async function handleSaveDraft() {
const { processRoutes, ...rest } = mo const { processRoutes, ...rest } = mo
return { ...rest, children: processRoutes } as KingdeePrdMo return { ...rest, children: processRoutes } as KingdeePrdMo
}) })
await orderProjectApi.saveKingdeeOrderDraft(kingdeeCurrentProjectId.value, draftData) await orderProjectApi.saveKingdeeOrderDraft(kingdeeCurrentProjectId.value, draftData)
message.success('草稿保存成功') message.success('草稿保存成功')
} catch (error) { } catch (error) {
@ -765,7 +748,7 @@ async function handleSaveDraft() {
// //
async function handleSyncOrderAndPlan() { async function handleSyncOrderAndPlan() {
if (!kingdeeCurrentProjectId.value) return if (!kingdeeCurrentProjectId.value) return
kingdeeLoading.value = true kingdeeLoading.value = true
try { try {
// processRoutes children // processRoutes children
@ -773,7 +756,7 @@ async function handleSyncOrderAndPlan() {
const { processRoutes, ...rest } = mo const { processRoutes, ...rest } = mo
return { ...rest, children: processRoutes } as KingdeePrdMo return { ...rest, children: processRoutes } as KingdeePrdMo
}) })
await orderProjectApi.saveKingdeeOrderAndPlan(kingdeeCurrentProjectId.value, draftData) await orderProjectApi.saveKingdeeOrderAndPlan(kingdeeCurrentProjectId.value, draftData)
message.success('同步订单计划成功') message.success('同步订单计划成功')
kingdeeModalVisible.value = false kingdeeModalVisible.value = false
@ -883,16 +866,11 @@ async function handleImportUpload({ file }: UploadCustomRequestOptions) {
async function loadDictOptions() { async function loadDictOptions() {
try { try {
const data = await dictDataApi.listByType('pro_source') const data = await dictDataApi.listByType('pro_source')
sourceOptions.value = data.map(d => ({ label: d.dictLabel, value: (Number(d.dictValue) || d.dictValue),class:d.listClass })) sourceOptions.value = data.map(d => ({ label: d.dictLabel, value: (Number(d.dictValue) || d.dictValue) }))
} catch {} } catch {}
try { try {
const data = await dictDataApi.listByType('pro_status') const data = await dictDataApi.listByType('pro_status')
statusOptions.value = data.map(d => ({ label: d.dictLabel, value: (Number(d.dictValue) || d.dictValue),class:d.listClass })) statusOptions.value = data.map(d => ({ label: d.dictLabel, value: (Number(d.dictValue) || d.dictValue) }))
} catch {}
try {
const data = await dictDataApi.listByType("prod_type")
prodTypeOptions.value = data.map(d=>({label: d.dictLabel,value:(Number(d.dictValue) || d.dictValue),class:d.listClass}))
} catch {} } catch {}
} }

View File

@ -22,8 +22,7 @@
<div class="login-banner" :style="{ background: `linear-gradient(135deg, ${themeStore.primaryColor} 0%, ${adjustColor(themeStore.primaryColor, -30)} 100%)` }"> <div class="login-banner" :style="{ background: `linear-gradient(135deg, ${themeStore.primaryColor} 0%, ${adjustColor(themeStore.primaryColor, -30)} 100%)` }">
<div class="banner-content"> <div class="banner-content">
<div class="banner-logo"> <div class="banner-logo">
<img v-if="siteLogo" :src="siteLogo" class="logo-img" alt="Logo" /> <SiteLogoMark img-class="logo-img" icon-class="logo-icon" />
<div v-else class="logo-icon">{{ siteName.charAt(0) }}</div>
<span class="logo-text">{{ siteName }}</span> <span class="logo-text">{{ siteName }}</span>
</div> </div>
<h1 class="banner-title">{{ siteDescription || '后台管理系统' }}</h1> <h1 class="banner-title">{{ siteDescription || '后台管理系统' }}</h1>
@ -111,8 +110,7 @@
<div class="login-banner" :style="{ background: `linear-gradient(135deg, ${themeStore.primaryColor} 0%, ${adjustColor(themeStore.primaryColor, -30)} 100%)` }"> <div class="login-banner" :style="{ background: `linear-gradient(135deg, ${themeStore.primaryColor} 0%, ${adjustColor(themeStore.primaryColor, -30)} 100%)` }">
<div class="banner-content"> <div class="banner-content">
<div class="banner-logo"> <div class="banner-logo">
<img v-if="siteLogo" :src="siteLogo" class="logo-img" alt="Logo" /> <SiteLogoMark img-class="logo-img" icon-class="logo-icon" />
<div v-else class="logo-icon">{{ siteName.charAt(0) }}</div>
<span class="logo-text">{{ siteName }}</span> <span class="logo-text">{{ siteName }}</span>
</div> </div>
<h1 class="banner-title">{{ siteDescription || '后台管理系统' }}</h1> <h1 class="banner-title">{{ siteDescription || '后台管理系统' }}</h1>
@ -200,8 +198,11 @@
<div class="login-banner" style="background: transparent;"> <div class="login-banner" style="background: transparent;">
<div class="banner-content"> <div class="banner-content">
<div class="banner-logo"> <div class="banner-logo">
<img v-if="siteLogo" :src="siteLogo" class="logo-img" alt="Logo" /> <SiteLogoMark
<div v-else class="logo-icon" style="background: rgba(255, 255, 255, 0.15); color: #fff; border: 1px solid rgba(255, 255, 255, 0.2);">{{ siteName.charAt(0) }}</div> img-class="logo-img"
icon-class="logo-icon"
:icon-style="glassLogoIconStyle"
/>
<span class="logo-text">{{ siteName }}</span> <span class="logo-text">{{ siteName }}</span>
</div> </div>
<h1 class="banner-title" style="color: #fff;">{{ siteDescription || '后台管理系统' }}</h1> <h1 class="banner-title" style="color: #fff;">{{ siteDescription || '后台管理系统' }}</h1>
@ -350,6 +351,7 @@ import { useSiteStore } from '@/stores/site'
import { useThemeStore } from '@/stores/theme' import { useThemeStore } from '@/stores/theme'
import { authApi } from '@/api/auth' import { authApi } from '@/api/auth'
import { configGroupApi } from '@/api/org' import { configGroupApi } from '@/api/org'
import SiteLogoMark from '@/components/SiteLogoMark.vue'
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
@ -359,10 +361,14 @@ const siteStore = useSiteStore()
const themeStore = useThemeStore() const themeStore = useThemeStore()
// //
const siteName = computed(() => siteStore.siteName || 'CSY Admin') const siteName = computed(() => siteStore.siteName || 'MES系统')
const siteDescription = computed(() => siteStore.siteDescription || '伊特智造MES') const siteDescription = computed(() => siteStore.siteDescription || '伊特智造MES')
const siteLogo = computed(() => siteStore.siteLogo)
const copyright = computed(() => siteStore.copyright || '版权所有 © 河北伊特') const copyright = computed(() => siteStore.copyright || '版权所有 © 河北伊特')
const glassLogoIconStyle = {
background: 'rgba(255, 255, 255, 0.15)',
color: '#fff',
border: '1px solid rgba(255, 255, 255, 0.2)',
}
// //
const captchaEnabled = ref(false) const captchaEnabled = ref(false)

View File

@ -5,8 +5,7 @@
<div class="register-banner" :style="{ background: `linear-gradient(135deg, ${themeStore.primaryColor} 0%, ${adjustColor(themeStore.primaryColor, -30)} 100%)` }"> <div class="register-banner" :style="{ background: `linear-gradient(135deg, ${themeStore.primaryColor} 0%, ${adjustColor(themeStore.primaryColor, -30)} 100%)` }">
<div class="banner-content"> <div class="banner-content">
<div class="banner-logo"> <div class="banner-logo">
<img v-if="siteLogo" :src="siteLogo" class="logo-img" alt="Logo" /> <SiteLogoMark img-class="logo-img" icon-class="logo-icon" />
<div v-else class="logo-icon">{{ siteName.charAt(0) }}</div>
<span class="logo-text">{{ siteName }}</span> <span class="logo-text">{{ siteName }}</span>
</div> </div>
<h1 class="banner-title">加入我们</h1> <h1 class="banner-title">加入我们</h1>
@ -116,6 +115,7 @@ import { useSiteStore } from '@/stores/site'
import { useThemeStore } from '@/stores/theme' import { useThemeStore } from '@/stores/theme'
import { authApi } from '@/api/auth' import { authApi } from '@/api/auth'
import { configGroupApi } from '@/api/org' import { configGroupApi } from '@/api/org'
import SiteLogoMark from '@/components/SiteLogoMark.vue'
const router = useRouter() const router = useRouter()
const message = useMessage() const message = useMessage()
@ -124,7 +124,6 @@ const themeStore = useThemeStore()
// //
const siteName = computed(() => siteStore.siteName || 'CSY Admin') const siteName = computed(() => siteStore.siteName || 'CSY Admin')
const siteLogo = computed(() => siteStore.siteLogo)
const copyright = computed(() => siteStore.copyright || '版权所有 © 成都火星网络科技有限公司 2025-2030') const copyright = computed(() => siteStore.copyright || '版权所有 © 成都火星网络科技有限公司 2025-2030')
// //

View File

@ -24,7 +24,7 @@
<n-form-item label="站点Logo"> <n-form-item label="站点Logo">
<div class="logo-upload"> <div class="logo-upload">
<div class="logo-preview" v-if="configs.system.siteLogo"> <div class="logo-preview" v-if="configs.system.siteLogo">
<img :src="configs.system.siteLogo" alt="Logo" /> <SiteLogoMark :src="configs.system.siteLogo" />
<n-button size="small" quaternary type="error" class="logo-delete" @click="configs.system.siteLogo = ''"> <n-button size="small" quaternary type="error" class="logo-delete" @click="configs.system.siteLogo = ''">
<template #icon><n-icon><CloseOutline /></n-icon></template> <template #icon><n-icon><CloseOutline /></n-icon></template>
</n-button> </n-button>
@ -938,6 +938,7 @@ import { configGroupApi, type SysConfigGroup, type SmsLog } from '@/api/org'
import { fileApi } from '@/api/system' import { fileApi } from '@/api/system'
import { wechatApi } from '@/api/wechat' import { wechatApi } from '@/api/wechat'
import { useSiteStore } from '@/stores/site' import { useSiteStore } from '@/stores/site'
import SiteLogoMark from '@/components/SiteLogoMark.vue'
const message = useMessage() const message = useMessage()
const siteStore = useSiteStore() const siteStore = useSiteStore()
@ -1812,6 +1813,7 @@ async function loadWechatMpMenu() {
position: relative; position: relative;
display: inline-block; display: inline-block;
:deep(.logo-img),
img { img {
max-width: 200px; max-width: 200px;
max-height: 80px; max-height: 80px;
@ -1822,6 +1824,20 @@ async function loadWechatMpMenu() {
background: #f9fafb; background: #f9fafb;
} }
:deep(.logo-icon) {
width: 64px;
height: 64px;
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
font-weight: 700;
border: 1px solid #e5e7eb;
border-radius: 8px;
background: #f9fafb;
color: #111827;
}
.logo-delete { .logo-delete {
position: absolute; position: absolute;
top: -8px; top: -8px;