Merge branch 'master' of https://git.evo-techina.com/sgc/mes-vue
This commit is contained in:
commit
57c7eb92eb
@ -44,7 +44,12 @@ export interface OrderItem {
|
||||
assingWorkOperationTime?: string
|
||||
|
||||
starter?: number
|
||||
|
||||
orderCode?: string
|
||||
|
||||
routeCode?: string
|
||||
|
||||
proWorkshop?: string
|
||||
}
|
||||
|
||||
// 生产订单 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> = {}
|
||||
if (params?.ids?.length) p.ids = params.ids.join(',')
|
||||
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?.endTime !== undefined && params?.endTime !== null) p.endTime = params.endTime
|
||||
return request({ url: `/biz/orderItem/export`, method: 'get', params: p, responseType: 'blob' })
|
||||
|
||||
@ -69,18 +69,57 @@ export interface KingdeePrdMo {
|
||||
|
||||
/** 金蝶工艺路线工序(树子节点) */
|
||||
export interface KingdeeProcessRoute {
|
||||
/** 工序号 FOperNumber */
|
||||
operNumber: number | null
|
||||
/** 工作中心 FWorkCenterId.FName */
|
||||
workCenterName: string | null
|
||||
/** 生产车间 FDepartmentId.FName */
|
||||
departmentName: string | null
|
||||
processProperty: string | null
|
||||
/** 工序名称 FProcessProperty */
|
||||
processName: string | null
|
||||
/** 工序说明 FOperDescription */
|
||||
operDescription: string | null
|
||||
/** 活动单位 FActivity1UnitID.FName */
|
||||
activityUnit: string | null
|
||||
/** 工序控制码 FOptCtrlCodeId.FName */
|
||||
optCtrlCodeName: string | null
|
||||
/** 活动量 FActivity1Qty */
|
||||
activityQty: number | null
|
||||
/** 计划开始时间 yyyy-MM-dd HH:mm:ss */
|
||||
planStartTime: string | null
|
||||
/** 计划结束时间 yyyy-MM-dd HH:mm:ss */
|
||||
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
|
||||
export const orderProjectApi = {
|
||||
// 分页查询
|
||||
|
||||
36
src/components/SiteLogoMark.vue
Normal file
36
src/components/SiteLogoMark.vue
Normal file
@ -0,0 +1,36 @@
|
||||
<!--
|
||||
站点 Logo 展示:优先显示图片,无 URL 或加载失败时回退为站点名称首字母。
|
||||
用于 layout、login、register 等页面;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>
|
||||
30
src/composables/useSiteLogo.ts
Normal file
30
src/composables/useSiteLogo.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
@ -17,8 +17,11 @@
|
||||
>
|
||||
<!-- Logo -->
|
||||
<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" />
|
||||
<div v-else class="logo-icon" :style="{ background: themeStore.headerUsePrimaryColor ? '#fff' : themeStore.primaryColor, color: themeStore.headerUsePrimaryColor ? themeStore.primaryColor : '#fff' }">{{ siteName.charAt(0) }}</div>
|
||||
<SiteLogoMark
|
||||
img-class="logo-img"
|
||||
icon-class="logo-icon"
|
||||
:icon-style="logoIconStyle"
|
||||
/>
|
||||
<transition name="fade">
|
||||
<span v-if="!collapsed" class="logo-text">{{ siteName }}</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">
|
||||
<!-- 顶部菜单模式下的Logo -->
|
||||
<div v-if="layoutConfig.siderPosition === 'top'" class="header-logo">
|
||||
<img v-if="siteLogo" :src="siteLogo" class="logo-img" alt="Logo" />
|
||||
<div v-else class="logo-icon" :style="{ background: themeStore.headerUsePrimaryColor ? '#fff' : themeStore.primaryColor, color: themeStore.headerUsePrimaryColor ? themeStore.primaryColor : '#fff' }">{{ siteName.charAt(0) }}</div>
|
||||
<SiteLogoMark
|
||||
img-class="logo-img"
|
||||
icon-class="logo-icon"
|
||||
:icon-style="logoIconStyle"
|
||||
/>
|
||||
<span class="logo-text">{{ siteName }}</span>
|
||||
</div>
|
||||
|
||||
@ -373,6 +379,7 @@ import ProfileModal from '@/components/ProfileModal.vue'
|
||||
import PasswordModal from '@/components/PasswordModal.vue'
|
||||
import MessageNotification from '@/components/MessageNotification.vue'
|
||||
import TabBar from '@/components/TabBar.vue'
|
||||
import SiteLogoMark from '@/components/SiteLogoMark.vue'
|
||||
import { noticeApi, chatApi, type SysNotice, type ChatMessage } from '@/api/message'
|
||||
import { iconMap as externalIconMap } from '@/utils/icons'
|
||||
|
||||
@ -387,7 +394,10 @@ const themeStore = useThemeStore()
|
||||
|
||||
// 站点配置
|
||||
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
|
||||
window.$message = message
|
||||
|
||||
@ -141,6 +141,18 @@ const routes: RouteRecordRaw[] = [
|
||||
component: () => import('@/views/biz/order/index.vue'),
|
||||
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',
|
||||
name: 'code',
|
||||
|
||||
@ -7,7 +7,7 @@ import { configGroupApi } from '@/api/org'
|
||||
*/
|
||||
export const useSiteStore = defineStore('site', () => {
|
||||
// 站点名称
|
||||
const siteName = ref('CSY Admin')
|
||||
const siteName = ref('MES系统')
|
||||
// 站点描述
|
||||
const siteDescription = ref('伊特智造MES')
|
||||
// 站点 Logo
|
||||
@ -33,7 +33,7 @@ export const useSiteStore = defineStore('site', () => {
|
||||
try {
|
||||
const config = await configGroupApi.getPublicConfig()
|
||||
if (config.system) {
|
||||
siteName.value = config.system.siteName || 'CSY Admin'
|
||||
siteName.value = config.system.siteName || 'MES系统'
|
||||
siteDescription.value = config.system.siteDescription || '伊特智造MES'
|
||||
siteLogo.value = config.system.siteLogo || ''
|
||||
copyright.value = config.system.copyright || ''
|
||||
|
||||
@ -176,6 +176,21 @@
|
||||
<n-input v-else v-model:value="formData.quantity" placeholder="请输入生产数量" disabled />
|
||||
</n-form-item>
|
||||
</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-form-item label="是否半成品" path="sfProduct">
|
||||
@ -425,6 +440,9 @@ const hasPermission = (permission: string) => userStore.hasPermission(permission
|
||||
const searchForm = reactive<any>({
|
||||
code: '',
|
||||
projectName: '',
|
||||
orderCode: '',
|
||||
routeCode: '',
|
||||
proWorkshop: '',
|
||||
beginTime: null as number | null,
|
||||
endTime: null as number | null,
|
||||
})
|
||||
@ -471,6 +489,9 @@ const defaultFormData = {
|
||||
assingWorkOperationId: '',
|
||||
assingWorkOperationTime: null,
|
||||
starter: '',
|
||||
orderCode: '',
|
||||
routeCode: '',
|
||||
proWorkshop: '',
|
||||
}
|
||||
const formData = reactive<any>({ ...defaultFormData })
|
||||
|
||||
@ -503,6 +524,24 @@ const columns = [
|
||||
title: '物料编码',
|
||||
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',
|
||||
minWidth: 150,
|
||||
@ -642,7 +681,10 @@ async function loadData() {
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize,
|
||||
code: searchForm.code,
|
||||
projectId: searchForm.projectId,
|
||||
projectName: searchForm.projectName,
|
||||
orderCode: searchForm.orderCode,
|
||||
routeCode: searchForm.routeCode,
|
||||
proWorkshop: searchForm.proWorkshop,
|
||||
beginTime: searchForm.beginTime,
|
||||
endTime: searchForm.endTime
|
||||
})
|
||||
@ -662,13 +704,12 @@ function handleSearch() {
|
||||
// 重置
|
||||
function handleReset() {
|
||||
searchForm.code = ''
|
||||
|
||||
searchForm.projectId = ''
|
||||
|
||||
searchForm.projectName = ''
|
||||
searchForm.orderCode = ''
|
||||
searchForm.routeCode = ''
|
||||
searchForm.proWorkshop = ''
|
||||
searchForm.beginTime = null
|
||||
|
||||
searchForm.endTime = null
|
||||
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
@ -696,14 +737,20 @@ let planList = ref<any>([])
|
||||
if(formData.code){
|
||||
planList.value.splice(0)
|
||||
orderItemApi.preCreation({
|
||||
//productionCode:formData.productionCode
|
||||
code:formData.code,
|
||||
projectCode:formData.projectCode
|
||||
}).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{
|
||||
planList.value.splice(0)
|
||||
formData.orderCode = ''
|
||||
formData.routeCode = ''
|
||||
formData.proWorkshop = ''
|
||||
}
|
||||
}
|
||||
|
||||
@ -760,6 +807,9 @@ function xmselupdate(v:any,opt:any) {
|
||||
planList.value.splice(0)
|
||||
wllist.value.splice(0)
|
||||
formData.code = ''
|
||||
formData.orderCode = ''
|
||||
formData.routeCode = ''
|
||||
formData.proWorkshop = ''
|
||||
formData.projectName = opt.label
|
||||
if(v){
|
||||
wlhand(opt.value)
|
||||
@ -1000,7 +1050,10 @@ async function handleExport() {
|
||||
const params: Record<string, any> = {}
|
||||
if (selectedIds.value.length > 0) params.ids = selectedIds.value
|
||||
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.endTime != null) params.endTime = searchForm.endTime
|
||||
const blob = await orderItemApi.export(params)
|
||||
|
||||
@ -1,25 +1,43 @@
|
||||
<template>
|
||||
<div class="gantt-schedule-page">
|
||||
<div class="gantt-header">
|
||||
<n-space justify="space-between" align="center">
|
||||
<n-h2 style="margin: 0">甘特图排产</n-h2>
|
||||
<n-space>
|
||||
<n-button type="primary" :loading="saving" @click="handleSaveDraft">保存排产草稿</n-button>
|
||||
<n-button @click="handleBack">返回</n-button>
|
||||
</n-space>
|
||||
<div class="gantt-header-left">
|
||||
<h1 class="page-title">甘特图排产</h1>
|
||||
<p class="page-desc">拖拽工序条块调整计划时间,完成后请保存排产草稿</p>
|
||||
</div>
|
||||
<n-space>
|
||||
<n-button type="primary" :loading="saving" @click="handleSaveDraft">保存排产草稿</n-button>
|
||||
<n-button @click="handleBack">返回</n-button>
|
||||
</n-space>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<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 { useRouter } from 'vue-router'
|
||||
import { gantt } from 'dhtmlx-gantt'
|
||||
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 ganttContainer = ref<HTMLElement | null>(null)
|
||||
@ -30,6 +48,42 @@ const tableData = ref<any[]>([])
|
||||
const projectId = ref<number | null>(null)
|
||||
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() {
|
||||
router.back()
|
||||
}
|
||||
@ -39,22 +93,17 @@ async function handleSaveDraft() {
|
||||
message.warning('缺少项目ID,无法保存')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
// 恢复数据结构,将 processRoutes 转换为 children
|
||||
const draftData = tableData.value.map(mo => {
|
||||
const { processRoutes, ...rest } = mo
|
||||
return { ...rest, children: processRoutes } as KingdeePrdMo
|
||||
})
|
||||
|
||||
|
||||
await orderProjectApi.saveKingdeeOrderDraft(projectId.value, draftData)
|
||||
message.success('排产草稿保存成功')
|
||||
|
||||
// 更新 session 里的暂存,以防返回页面时不一致
|
||||
sessionStorage.setItem('gantt_schedule_data', JSON.stringify(tableData.value))
|
||||
} catch (error) {
|
||||
// 错误拦截器已处理
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
@ -62,25 +111,26 @@ async function handleSaveDraft() {
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
|
||||
initGantt()
|
||||
|
||||
|
||||
|
||||
const stateStr = sessionStorage.getItem('gantt_schedule_data')
|
||||
const idStr = sessionStorage.getItem('gantt_project_id')
|
||||
|
||||
|
||||
if (idStr) {
|
||||
projectId.value = Number(idStr)
|
||||
}
|
||||
|
||||
if (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()
|
||||
} else {
|
||||
message.warning('未获取到排产数据')
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
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())}`
|
||||
}
|
||||
|
||||
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() {
|
||||
if (!ganttContainer.value) return
|
||||
|
||||
// 清理之前的事件绑定
|
||||
ganttEventIds.forEach(id => gantt.detachEvent(id))
|
||||
ganttEventIds = []
|
||||
|
||||
gantt.clearAll()
|
||||
|
||||
// 配置甘特图
|
||||
gantt.config.date_format = "%Y-%m-%d %H:%i:%s"
|
||||
gantt.config.scale_height = 50
|
||||
gantt.config.date_format = '%Y-%m-%d %H:%i:%s'
|
||||
gantt.config.scale_height = 60
|
||||
gantt.config.row_height = 40
|
||||
gantt.config.bar_height = 26
|
||||
gantt.config.scales = [
|
||||
{ unit: "month", step: 1, format: "%Y年 %m月" },
|
||||
{ unit: "day", step: 1, format: "%d日" }
|
||||
{ unit: 'month', step: 1, format: '%Y年 %m月' },
|
||||
{ unit: 'day', step: 1, format: formatDayScale },
|
||||
]
|
||||
gantt.config.columns = [
|
||||
{ name: "text", label: "单号 / 序号", tree: true, width: 200, resize: true },
|
||||
{ name: "materialCode", label: "物料编码", align: "center", width: 140, resize: true },
|
||||
{ name: "name", label: "名称", align: "center", width: 160, resize: true },
|
||||
{ name: "start_date", label: "计划开始", align: "center", width: 130 },
|
||||
{ name: "end_date", label: "计划结束", align: "center", width: 130 },
|
||||
{
|
||||
name: 'text',
|
||||
label: '工序号 / 物料编码',
|
||||
tree: true,
|
||||
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 = {
|
||||
css: "gantt_container",
|
||||
cols: [
|
||||
{
|
||||
width: 300,
|
||||
minWidth: 200,
|
||||
maxWidth: 600,
|
||||
rows: [
|
||||
{ view: "grid", scrollX: "gridScroll", scrollable: true,
|
||||
scrollY: "scrollVer"
|
||||
},
|
||||
{ view: "scrollbar", id: "gridScroll", group: "horizontal" } /*!*/
|
||||
]
|
||||
},
|
||||
{ resizer: true, width: 1 },
|
||||
{
|
||||
rows: [
|
||||
{ view: "timeline", scrollX: "scrollHor", scrollY: "scrollVer" },
|
||||
{ view: "scrollbar", id: "scrollHor", group: "horizontal" } /*!*/
|
||||
]
|
||||
},
|
||||
{ view: "scrollbar", id: "scrollVer" }
|
||||
]
|
||||
}
|
||||
//??
|
||||
css: 'gantt_container',
|
||||
cols: [
|
||||
{
|
||||
width: 620,
|
||||
minWidth: 420,
|
||||
maxWidth: 680,
|
||||
rows: [
|
||||
{ view: 'grid', scrollX: 'gridScroll', scrollable: true, scrollY: 'scrollVer' },
|
||||
{ view: 'scrollbar', id: 'gridScroll', group: 'horizontal' },
|
||||
],
|
||||
},
|
||||
{ resizer: true, width: 1 },
|
||||
{
|
||||
rows: [
|
||||
{ view: 'timeline', scrollX: 'scrollHor', scrollY: 'scrollVer' },
|
||||
{ view: 'scrollbar', id: 'scrollHor', group: 'horizontal' },
|
||||
],
|
||||
},
|
||||
{ view: 'scrollbar', id: 'scrollVer' },
|
||||
],
|
||||
}
|
||||
|
||||
// 行为配置
|
||||
gantt.config.drag_progress = false // 禁用进度拖拽
|
||||
gantt.config.drag_links = false // 禁用连线拖拽(视需求而定)
|
||||
gantt.config.work_time = true // 启用工作时间计算
|
||||
// 注意:gantt.config.auto_scheduling 是商业版功能,开源版不能开启,否则会报错
|
||||
|
||||
gantt.i18n.setLocale("cn")
|
||||
gantt.config.drag_progress = false
|
||||
gantt.config.drag_links = false
|
||||
// 使用自然日历(含周六日),不按工作日历自动顺延
|
||||
gantt.config.work_time = false
|
||||
gantt.config.correct_work_time = false
|
||||
gantt.config.skip_off_time = false
|
||||
gantt.config.duration_unit = 'day'
|
||||
gantt.config.smart_rendering = true
|
||||
|
||||
// 启用 marker 插件(今日时间轴)
|
||||
gantt.plugins({
|
||||
marker: true
|
||||
})
|
||||
gantt.i18n.setLocale('cn')
|
||||
gantt.plugins({ marker: true })
|
||||
|
||||
// 图表条块文字自定义
|
||||
gantt.templates.task_text = function(start, end, task) {
|
||||
gantt.templates.task_text = function(_start, _end, task) {
|
||||
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)
|
||||
}
|
||||
|
||||
// 添加今日时间轴标记
|
||||
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({
|
||||
start_date: today,
|
||||
css: "today-marker",
|
||||
text: "今日",
|
||||
title: "今日: " + formatGanttDate(today)
|
||||
css: 'today-marker',
|
||||
text: '今日',
|
||||
title: `今日: ${formatGanttDate(now)}`,
|
||||
})
|
||||
gantt.showDate(today)
|
||||
}
|
||||
|
||||
function getRouteProcessName(route: any) {
|
||||
return resolveProcessName(route)
|
||||
}
|
||||
|
||||
function renderGanttData() {
|
||||
gantt.clearAll()
|
||||
// 转换数据格式
|
||||
const tasks: any[] = []
|
||||
|
||||
|
||||
const rawData = toRaw(tableData.value) || []
|
||||
const processColors = ['#3498db', '#1abc9c', '#9b59b6', '#e67e22', '#e74c3c', '#2ecc71', '#f1c40f', '#34495e', '#7f8c8d']
|
||||
|
||||
|
||||
rawData.forEach((mo: any, moIndex: number) => {
|
||||
const moId = `mo_${moIndex}_${mo.productionOrderNo}_${mo.billNo}`
|
||||
// 寻找子工序中最早和最晚的时间作为项目时间(或者使用默认时间)
|
||||
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
|
||||
if (!minDate || isNaN(minDate.getTime())) minDate = new Date()
|
||||
if (!maxDate || isNaN(maxDate.getTime())) maxDate = new Date(minDate.getTime() + 86400000)
|
||||
if (maxDate <= minDate) maxDate = new Date(minDate.getTime() + 86400000)
|
||||
|
||||
const materialLabel = formatMoMaterialLabel(mo)
|
||||
tasks.push({
|
||||
id: moId,
|
||||
text: mo.materialCode || '', //mo.productionOrderNo || mo.billNo || '未知单号', ??
|
||||
text: materialLabel,
|
||||
materialLabel,
|
||||
materialCode: mo.materialCode || '',
|
||||
name: mo.materialName || '产品',
|
||||
materialName: mo.materialName || '',
|
||||
operNumber: '',
|
||||
processName: '',
|
||||
start_date: formatGanttDate(minDate),
|
||||
end_date: formatGanttDate(maxDate),
|
||||
open: false, // 树形默认关闭
|
||||
open: true,
|
||||
type: gantt.config.types.project,
|
||||
_type: 'mo',
|
||||
progress:0.6, //??
|
||||
_raw: mo
|
||||
progress: 0,
|
||||
_raw: mo,
|
||||
})
|
||||
|
||||
if (mo.processRoutes && mo.processRoutes.length > 0) {
|
||||
if (mo.processRoutes?.length > 0) {
|
||||
mo.processRoutes.forEach((route: any, index: number) => {
|
||||
const routeId = `route_${moId}_${index}`
|
||||
let rStart = route.planStartTime ? new Date(route.planStartTime.replace(' ', 'T')) : null
|
||||
let rEnd = route.planFinishTime ? new Date(route.planFinishTime.replace(' ', 'T')) : null
|
||||
|
||||
|
||||
if (!rStart || isNaN(rStart.getTime())) rStart = minDate
|
||||
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({
|
||||
id: routeId,
|
||||
text:mo.materialCode || '', //route.operNumber != null ? String(route.operNumber) : String(index + 1), ??
|
||||
materialCode: '',
|
||||
name: route.processProperty || '工序',
|
||||
color: processColors[index % processColors.length],
|
||||
text: operNumber,
|
||||
operNumber,
|
||||
routeProcessName,
|
||||
colorIndex,
|
||||
routeColor,
|
||||
color: routeColor,
|
||||
start_date: formatGanttDate(rStart!),
|
||||
end_date: formatGanttDate(rEnd),
|
||||
parent: moId,
|
||||
_type: 'route',
|
||||
progress:0.6, //??
|
||||
_raw: route
|
||||
progress: 0,
|
||||
_raw: route,
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
gantt.parse({ data: tasks, links: [] })
|
||||
addTodayMarker()
|
||||
|
||||
// 限制只能拖拽工序,不能直接拖拽主工单(项目)
|
||||
const evDragId = gantt.attachEvent("onBeforeTaskDrag", (id, mode, e) => {
|
||||
const evDragId = gantt.attachEvent('onBeforeTaskDrag', (id) => {
|
||||
const task = gantt.getTask(id)
|
||||
if (task.type === gantt.config.types.project) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
return task.type !== gantt.config.types.project
|
||||
})
|
||||
ganttEventIds.push(evDragId)
|
||||
|
||||
// 绑定拖拽结束事件
|
||||
const evId = gantt.attachEvent("onAfterTaskDrag", (id, mode, e) => {
|
||||
const task = gantt.getTask(id)
|
||||
if (mode === 'move' || mode === 'resize') {
|
||||
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)
|
||||
|
||||
// 更新原数据
|
||||
if (task._type === 'mo') {
|
||||
task._raw.planStartTime = start
|
||||
task._raw.planFinishTime = end
|
||||
} else if (task._type === 'route') {
|
||||
task._raw.planStartTime = start
|
||||
task._raw.planFinishTime = end
|
||||
|
||||
// 自动汇总更新父级工单的时间
|
||||
const parentId = task.parent
|
||||
if (parentId) {
|
||||
const parentTask = gantt.getTask(parentId)
|
||||
if (parentTask) {
|
||||
const children = gantt.getChildren(parentId)
|
||||
let minDate: Date | null = null
|
||||
let maxDate: Date | null = null
|
||||
|
||||
children.forEach((childId: string | number) => {
|
||||
const childTask = gantt.getTask(childId)
|
||||
if (!minDate || childTask.start_date < minDate) minDate = childTask.start_date
|
||||
if (!maxDate || childTask.end_date > maxDate) maxDate = childTask.end_date
|
||||
})
|
||||
|
||||
if (minDate && maxDate) {
|
||||
parentTask.start_date = new Date(minDate)
|
||||
parentTask.end_date = new Date(maxDate)
|
||||
parentTask._raw.planStartTime = gantt.date.date_to_str(gantt.config.date_format)(minDate)
|
||||
parentTask._raw.planFinishTime = gantt.date.date_to_str(gantt.config.date_format)(maxDate)
|
||||
gantt.updateTask(parentId)
|
||||
}
|
||||
}
|
||||
const evId = gantt.attachEvent('onAfterTaskDrag', (id, mode) => {
|
||||
const task = gantt.getTask(id)
|
||||
if (mode === 'move' || mode === 'resize') {
|
||||
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)
|
||||
|
||||
if (task._type === 'mo') {
|
||||
task._raw.planStartTime = start
|
||||
task._raw.planFinishTime = end
|
||||
} else if (task._type === 'route') {
|
||||
task._raw.planStartTime = start
|
||||
task._raw.planFinishTime = end
|
||||
|
||||
const parentId = task.parent
|
||||
if (parentId) {
|
||||
const parentTask = gantt.getTask(parentId)
|
||||
const children = gantt.getChildren(parentId)
|
||||
let minD: Date | null = null
|
||||
let maxD: Date | null = null
|
||||
|
||||
children.forEach((childId: string | number) => {
|
||||
const childTask = gantt.getTask(childId)
|
||||
const cs = childTask.start_date
|
||||
const ce = childTask.end_date
|
||||
if (cs && (!minD || cs < minD)) minD = cs
|
||||
if (ce && (!maxD || ce > maxD)) maxD = ce
|
||||
})
|
||||
|
||||
if (minD && maxD) {
|
||||
parentTask.start_date = new Date(minD)
|
||||
parentTask.end_date = new Date(maxD)
|
||||
parentTask._raw.planStartTime = gantt.date.date_to_str(gantt.config.date_format)(minD)
|
||||
parentTask._raw.planFinishTime = gantt.date.date_to_str(gantt.config.date_format)(maxD)
|
||||
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)
|
||||
}
|
||||
|
||||
@ -299,36 +428,249 @@ onUnmounted(() => {
|
||||
|
||||
<style scoped>
|
||||
.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;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 120px);
|
||||
background-color: var(--n-color);
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
box-sizing: border-box;
|
||||
font-family: var(--gs-font);
|
||||
font-weight: 400;
|
||||
color: var(--gs-text);
|
||||
background: var(--gs-bg);
|
||||
}
|
||||
|
||||
.gantt-header {
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
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 {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
min-height: 0; /* 必须设置min-height以允许flex子元素内部滚动 */
|
||||
border: 1px solid var(--n-border-color);
|
||||
border-radius: 4px;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:deep(.today-marker) {
|
||||
background-color: #ff5252;
|
||||
/* dhtmlx-gantt:仅表头标题加粗,其余正常字重 */
|
||||
.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;
|
||||
color: white;
|
||||
|
||||
.gantt-container :deep(.gantt_grid_scale),
|
||||
.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;
|
||||
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>
|
||||
|
||||
@ -229,7 +229,7 @@
|
||||
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 { 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 { useUserStore } from '@/stores/user'
|
||||
|
||||
@ -400,8 +400,6 @@ const formData = reactive<OrderProject>({ ...defaultFormData })
|
||||
// 字典选项(下拉框/单选框/复选框关联字典时使用)
|
||||
const sourceOptions = ref<{ label: string; value: any }[]>([])
|
||||
const statusOptions = ref<{ label: string; value: any }[]>([])
|
||||
const prodTypeOptions = ref<{label: string; value:any} []>([])
|
||||
|
||||
|
||||
// 表单校验规则
|
||||
const formRules = {
|
||||
@ -421,23 +419,11 @@ const columns: DataTableColumns<OrderProject> = [
|
||||
{ title: '项目来源', key: 'source', width: 110,align: 'center',
|
||||
render(row) {
|
||||
const val = row.source
|
||||
console.log(sourceOptions.value);
|
||||
|
||||
const opt = sourceOptions.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',
|
||||
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 })
|
||||
return opt ? opt.label : (val ?? '-')
|
||||
}
|
||||
},
|
||||
{ title: '生产类型', key: 'prodType', width: 100 ,align: 'center'},
|
||||
{ title: 'BOM版本', key: 'bomVersion', width: 100 ,align: 'center'},
|
||||
{ title: '规格型号', key: 'specModel', width: 140, ellipsis: { tooltip: true } },
|
||||
{ title: '单位', key: 'unit', width: 70 ,align: 'center'},
|
||||
@ -448,9 +434,7 @@ const columns: DataTableColumns<OrderProject> = [
|
||||
render(row) {
|
||||
const val = row.status
|
||||
const opt = statusOptions.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 })
|
||||
return opt ? opt.label : (val ?? '-')
|
||||
}
|
||||
},
|
||||
{ title: '计划完成日期', key: 'planFinishTime', width: 170 ,align: 'center'},
|
||||
@ -486,9 +470,8 @@ const columns: DataTableColumns<OrderProject> = [
|
||||
]
|
||||
|
||||
const pickMtrlStatusMap: Record<string, string> = {
|
||||
'1': '未领料',
|
||||
'2': '待定',
|
||||
'3': '已领料',
|
||||
'0': '未领料',
|
||||
'1': '已领料',
|
||||
}
|
||||
|
||||
const kingdeeMoColumns: DataTableColumns<KingdeeMoTableRow> = [
|
||||
@ -517,13 +500,13 @@ const kingdeeMoColumns: DataTableColumns<KingdeeMoTableRow> = [
|
||||
{ title: '生产车间', key: 'workShopName', width: 100, align: 'center', ellipsis: { tooltip: true } },
|
||||
{ title: '规格型号', key: 'specModel', width: 110, align: 'center', ellipsis: { tooltip: true } },
|
||||
{ title: '单位', key: 'unit', width: 60, align: 'center' },
|
||||
{
|
||||
{
|
||||
title: '计划开工', key: 'planStartTime', width: 180, align: 'center',
|
||||
render(row) {
|
||||
return renderKingdeePlanPickerMo(row, 'planStartTime')
|
||||
}
|
||||
},
|
||||
{
|
||||
{
|
||||
title: '计划完工', key: 'planFinishTime', width: 180, align: 'center',
|
||||
render(row) {
|
||||
return renderKingdeePlanPickerMo(row, 'planFinishTime')
|
||||
@ -533,8 +516,6 @@ const kingdeeMoColumns: DataTableColumns<KingdeeMoTableRow> = [
|
||||
title: '领料状态', key: 'pickMtrlStatus', width: 90, align: 'center',
|
||||
render(row) {
|
||||
const val = row.pickMtrlStatus
|
||||
console.log(val);
|
||||
|
||||
return val != null ? (pickMtrlStatusMap[String(val)] ?? val) : '-'
|
||||
}
|
||||
},
|
||||
@ -553,7 +534,7 @@ const kingdeeProcessColumns: DataTableColumns<KingdeeProcessRoute> = [
|
||||
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: 'workCenterName', 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: '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)
|
||||
kingdeeTableData.value = (Array.isArray(data) ? data : []).map((mo) => {
|
||||
const { children, ...rest } = mo
|
||||
const routes = [...(children ?? [])].sort((a, b) => (a.operNumber ?? 0) - (b.operNumber ?? 0)).map((r) => ({
|
||||
...r,
|
||||
planStartTime: r.planStartTime ?? null,
|
||||
planFinishTime: r.planFinishTime ?? null,
|
||||
}))
|
||||
const routes = [...(children ?? [])]
|
||||
.sort((a, b) => (a.operNumber ?? 0) - (b.operNumber ?? 0))
|
||||
.map((r) => normalizeKingdeeProcessRoute({
|
||||
...r,
|
||||
planStartTime: r.planStartTime ?? null,
|
||||
planFinishTime: r.planFinishTime ?? null,
|
||||
}))
|
||||
return { ...rest, processRoutes: routes }
|
||||
})
|
||||
if (kingdeeTableData.value.length === 0) {
|
||||
@ -744,7 +727,7 @@ async function handleKingdeeQuery(row: OrderProject) {
|
||||
// 暂存金蝶生产订单数据(草稿)
|
||||
async function handleSaveDraft() {
|
||||
if (!kingdeeCurrentProjectId.value) return
|
||||
|
||||
|
||||
kingdeeLoading.value = true
|
||||
try {
|
||||
// 恢复数据结构,将 processRoutes 转换为 children
|
||||
@ -752,7 +735,7 @@ async function handleSaveDraft() {
|
||||
const { processRoutes, ...rest } = mo
|
||||
return { ...rest, children: processRoutes } as KingdeePrdMo
|
||||
})
|
||||
|
||||
|
||||
await orderProjectApi.saveKingdeeOrderDraft(kingdeeCurrentProjectId.value, draftData)
|
||||
message.success('草稿保存成功')
|
||||
} catch (error) {
|
||||
@ -765,7 +748,7 @@ async function handleSaveDraft() {
|
||||
// 同步订单和计划表
|
||||
async function handleSyncOrderAndPlan() {
|
||||
if (!kingdeeCurrentProjectId.value) return
|
||||
|
||||
|
||||
kingdeeLoading.value = true
|
||||
try {
|
||||
// 恢复数据结构,将 processRoutes 转换为 children
|
||||
@ -773,7 +756,7 @@ async function handleSyncOrderAndPlan() {
|
||||
const { processRoutes, ...rest } = mo
|
||||
return { ...rest, children: processRoutes } as KingdeePrdMo
|
||||
})
|
||||
|
||||
|
||||
await orderProjectApi.saveKingdeeOrderAndPlan(kingdeeCurrentProjectId.value, draftData)
|
||||
message.success('同步订单计划成功')
|
||||
kingdeeModalVisible.value = false
|
||||
@ -883,16 +866,11 @@ async function handleImportUpload({ file }: UploadCustomRequestOptions) {
|
||||
async function loadDictOptions() {
|
||||
try {
|
||||
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 {}
|
||||
try {
|
||||
const data = await dictDataApi.listByType('pro_status')
|
||||
statusOptions.value = data.map(d => ({ label: d.dictLabel, value: (Number(d.dictValue) || d.dictValue),class:d.listClass }))
|
||||
} 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}))
|
||||
statusOptions.value = data.map(d => ({ label: d.dictLabel, value: (Number(d.dictValue) || d.dictValue) }))
|
||||
} catch {}
|
||||
}
|
||||
|
||||
|
||||
@ -22,8 +22,7 @@
|
||||
<div class="login-banner" :style="{ background: `linear-gradient(135deg, ${themeStore.primaryColor} 0%, ${adjustColor(themeStore.primaryColor, -30)} 100%)` }">
|
||||
<div class="banner-content">
|
||||
<div class="banner-logo">
|
||||
<img v-if="siteLogo" :src="siteLogo" class="logo-img" alt="Logo" />
|
||||
<div v-else class="logo-icon">{{ siteName.charAt(0) }}</div>
|
||||
<SiteLogoMark img-class="logo-img" icon-class="logo-icon" />
|
||||
<span class="logo-text">{{ siteName }}</span>
|
||||
</div>
|
||||
<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="banner-content">
|
||||
<div class="banner-logo">
|
||||
<img v-if="siteLogo" :src="siteLogo" class="logo-img" alt="Logo" />
|
||||
<div v-else class="logo-icon">{{ siteName.charAt(0) }}</div>
|
||||
<SiteLogoMark img-class="logo-img" icon-class="logo-icon" />
|
||||
<span class="logo-text">{{ siteName }}</span>
|
||||
</div>
|
||||
<h1 class="banner-title">{{ siteDescription || '后台管理系统' }}</h1>
|
||||
@ -200,8 +198,11 @@
|
||||
<div class="login-banner" style="background: transparent;">
|
||||
<div class="banner-content">
|
||||
<div class="banner-logo">
|
||||
<img v-if="siteLogo" :src="siteLogo" class="logo-img" alt="Logo" />
|
||||
<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>
|
||||
<SiteLogoMark
|
||||
img-class="logo-img"
|
||||
icon-class="logo-icon"
|
||||
:icon-style="glassLogoIconStyle"
|
||||
/>
|
||||
<span class="logo-text">{{ siteName }}</span>
|
||||
</div>
|
||||
<h1 class="banner-title" style="color: #fff;">{{ siteDescription || '后台管理系统' }}</h1>
|
||||
@ -350,6 +351,7 @@ import { useSiteStore } from '@/stores/site'
|
||||
import { useThemeStore } from '@/stores/theme'
|
||||
import { authApi } from '@/api/auth'
|
||||
import { configGroupApi } from '@/api/org'
|
||||
import SiteLogoMark from '@/components/SiteLogoMark.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
@ -359,10 +361,14 @@ const siteStore = useSiteStore()
|
||||
const themeStore = useThemeStore()
|
||||
|
||||
// 站点配置(带默认值)
|
||||
const siteName = computed(() => siteStore.siteName || 'CSY Admin')
|
||||
const siteName = computed(() => siteStore.siteName || 'MES系统')
|
||||
const siteDescription = computed(() => siteStore.siteDescription || '伊特智造MES')
|
||||
const siteLogo = computed(() => siteStore.siteLogo)
|
||||
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)
|
||||
|
||||
@ -5,8 +5,7 @@
|
||||
<div class="register-banner" :style="{ background: `linear-gradient(135deg, ${themeStore.primaryColor} 0%, ${adjustColor(themeStore.primaryColor, -30)} 100%)` }">
|
||||
<div class="banner-content">
|
||||
<div class="banner-logo">
|
||||
<img v-if="siteLogo" :src="siteLogo" class="logo-img" alt="Logo" />
|
||||
<div v-else class="logo-icon">{{ siteName.charAt(0) }}</div>
|
||||
<SiteLogoMark img-class="logo-img" icon-class="logo-icon" />
|
||||
<span class="logo-text">{{ siteName }}</span>
|
||||
</div>
|
||||
<h1 class="banner-title">加入我们</h1>
|
||||
@ -116,6 +115,7 @@ import { useSiteStore } from '@/stores/site'
|
||||
import { useThemeStore } from '@/stores/theme'
|
||||
import { authApi } from '@/api/auth'
|
||||
import { configGroupApi } from '@/api/org'
|
||||
import SiteLogoMark from '@/components/SiteLogoMark.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
@ -124,7 +124,6 @@ const themeStore = useThemeStore()
|
||||
|
||||
// 站点配置
|
||||
const siteName = computed(() => siteStore.siteName || 'CSY Admin')
|
||||
const siteLogo = computed(() => siteStore.siteLogo)
|
||||
const copyright = computed(() => siteStore.copyright || '版权所有 © 成都火星网络科技有限公司 2025-2030')
|
||||
|
||||
// 注册配置
|
||||
|
||||
@ -24,7 +24,7 @@
|
||||
<n-form-item label="站点Logo">
|
||||
<div class="logo-upload">
|
||||
<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 = ''">
|
||||
<template #icon><n-icon><CloseOutline /></n-icon></template>
|
||||
</n-button>
|
||||
@ -938,6 +938,7 @@ import { configGroupApi, type SysConfigGroup, type SmsLog } from '@/api/org'
|
||||
import { fileApi } from '@/api/system'
|
||||
import { wechatApi } from '@/api/wechat'
|
||||
import { useSiteStore } from '@/stores/site'
|
||||
import SiteLogoMark from '@/components/SiteLogoMark.vue'
|
||||
|
||||
const message = useMessage()
|
||||
const siteStore = useSiteStore()
|
||||
@ -1812,6 +1813,7 @@ async function loadWechatMpMenu() {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
|
||||
:deep(.logo-img),
|
||||
img {
|
||||
max-width: 200px;
|
||||
max-height: 80px;
|
||||
@ -1822,6 +1824,20 @@ async function loadWechatMpMenu() {
|
||||
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 {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user