统一排产同步流程,修复LOGO失效问题

This commit is contained in:
tzy 2026-06-06 11:14:24 +08:00
parent 1b51d68a25
commit 3e06241c9c
11 changed files with 710 additions and 238 deletions

View File

@ -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' })

View File

@ -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 = {
// 分页查询

View File

@ -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

View File

@ -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',

View File

@ -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 || ''

View File

@ -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)

View File

@ -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>

View File

@ -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 {}
}

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="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)

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="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')
//

View File

@ -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;