|
|
@@ -0,0 +1,569 @@
|
|
|
+<template>
|
|
|
+ <AidopDemoShell title="订单生产排程看板" subtitle="按工单/工序展示计划与实际排程甘特图,含物料齐套与交期标识">
|
|
|
+ <!-- 筛选栏 -->
|
|
|
+ <el-form :inline="true" :model="query" class="mb12" @submit.prevent>
|
|
|
+ <el-form-item label="工单编号">
|
|
|
+ <el-input v-model="query.workOrd" placeholder="模糊搜索" clearable style="width: 160px" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="计划日期">
|
|
|
+ <el-date-picker v-model="dateRange" type="daterange" value-format="YYYY-MM-DD" start-placeholder="开始" end-placeholder="结束" style="width: 260px" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item>
|
|
|
+ <el-button type="primary" :loading="loading" @click="doSearch">查询</el-button>
|
|
|
+ <el-button @click="resetQuery">重置</el-button>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+
|
|
|
+ <!-- 图例 -->
|
|
|
+ <div class="gantt-legend">
|
|
|
+ <span class="legend-item"><span class="legend-bar plan" />计划工期</span>
|
|
|
+ <span class="legend-item"><span class="legend-bar actual" />实际工期</span>
|
|
|
+ <span class="legend-item"><span class="legend-bar delay" />实际延误</span>
|
|
|
+ <span class="legend-item"><el-icon color="#67C23A"><Box /></el-icon> 物料齐套</span>
|
|
|
+ <span class="legend-item"><el-icon color="#E6A23C"><Star /></el-icon> 订单交期</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 甘特图容器 -->
|
|
|
+ <div class="gantt-wrapper" v-loading="loading">
|
|
|
+ <div v-if="chartRows.length === 0 && !loading" class="gantt-empty">暂无排程数据,请先执行生产排程</div>
|
|
|
+ <div v-else class="gantt-body">
|
|
|
+ <!-- 左侧信息面板 -->
|
|
|
+ <div class="gantt-left" ref="leftPanelRef">
|
|
|
+ <div class="gantt-left-header">
|
|
|
+ <span class="gl-col gl-workord">工单号</span>
|
|
|
+ <span class="gl-col gl-itemnum">产品编码</span>
|
|
|
+ <span class="gl-col gl-item">产品名称</span>
|
|
|
+ <span class="gl-col gl-status">工单状态</span>
|
|
|
+ <span class="gl-col gl-op">工序</span>
|
|
|
+ <span class="gl-col gl-qty">数量</span>
|
|
|
+ <span class="gl-col gl-status">工序状态</span>
|
|
|
+ <span class="gl-col gl-date">计划开工</span>
|
|
|
+ <span class="gl-col gl-date">计划完工</span>
|
|
|
+ <span class="gl-col gl-date">实际开工</span>
|
|
|
+ <span class="gl-col gl-date">实际完工</span>
|
|
|
+ </div>
|
|
|
+ <div class="gantt-left-body" ref="leftScrollRef">
|
|
|
+ <div
|
|
|
+ v-for="(row, idx) in chartRows"
|
|
|
+ :key="idx"
|
|
|
+ :class="['gantt-left-row', row.isGroup ? 'group-row' : '']"
|
|
|
+ :style="{ height: ROW_HEIGHT + 'px' }"
|
|
|
+ >
|
|
|
+ <span class="gl-col gl-workord" :title="row.workOrd">{{ row.isGroup ? row.workOrd : '' }}</span>
|
|
|
+ <span class="gl-col gl-itemnum" :title="row.itemNum">{{ row.isGroup ? row.itemNum : '' }}</span>
|
|
|
+ <span class="gl-col gl-item" :title="row.itemName">{{ row.isGroup ? row.itemName : '' }}</span>
|
|
|
+ <span class="gl-col gl-status">{{ row.isGroup ? fmtStatus(row.status) : '' }}</span>
|
|
|
+ <span class="gl-col gl-op">{{ row.isGroup ? '' : `${row.op ?? ''} ${row.opDescr ?? ''}` }}</span>
|
|
|
+ <span class="gl-col gl-qty">{{ row.isGroup ? row.qtyOrded : row.workQty }}</span>
|
|
|
+ <span class="gl-col gl-status">{{ row.isGroup ? '' : fmtStatus(row.opStatus) }}</span>
|
|
|
+ <span class="gl-col gl-date">{{ fmtDate(row.planStartDate) }}</span>
|
|
|
+ <span class="gl-col gl-date">{{ fmtDate(row.planEndDate) }}</span>
|
|
|
+ <span class="gl-col gl-date">{{ fmtDate(row.actualStartDate) }}</span>
|
|
|
+ <span class="gl-col gl-date">{{ fmtDate(row.actualEndDate) }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <!-- 右侧甘特图 -->
|
|
|
+ <div class="gantt-right">
|
|
|
+ <div ref="chartRef" class="gantt-chart" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </AidopDemoShell>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts" name="workOrderSchedulingGanttKanban">
|
|
|
+import { ref, onMounted, onBeforeUnmount, nextTick, computed } from 'vue';
|
|
|
+import * as echarts from 'echarts';
|
|
|
+import { Box, Star } from '@element-plus/icons-vue';
|
|
|
+import AidopDemoShell from '../components/AidopDemoShell.vue';
|
|
|
+import { fetchGanttData, type GanttOpRow } from '../api/workOrderScheduling';
|
|
|
+
|
|
|
+// ── 状态 ──
|
|
|
+const loading = ref(false);
|
|
|
+const rawList = ref<GanttOpRow[]>([]);
|
|
|
+const query = ref({ workOrd: '' });
|
|
|
+const dateRange = ref<string[] | null>(null);
|
|
|
+
|
|
|
+const ROW_HEIGHT = 36;
|
|
|
+
|
|
|
+// ── 数据分组 ──
|
|
|
+interface ChartRow {
|
|
|
+ isGroup: boolean;
|
|
|
+ workOrd?: string;
|
|
|
+ itemNum?: string;
|
|
|
+ itemName?: string;
|
|
|
+ itemModel?: string;
|
|
|
+ qtyOrded?: number;
|
|
|
+ dueDate?: string;
|
|
|
+ status?: string;
|
|
|
+ domain?: string;
|
|
|
+ mfRate?: number;
|
|
|
+ op?: number;
|
|
|
+ opDescr?: string;
|
|
|
+ planStartDate?: string;
|
|
|
+ planEndDate?: string;
|
|
|
+ actualStartDate?: string;
|
|
|
+ actualEndDate?: string;
|
|
|
+ workQty?: number;
|
|
|
+ qtyCompleted?: number;
|
|
|
+ opStatus?: string;
|
|
|
+}
|
|
|
+
|
|
|
+const chartRows = computed<ChartRow[]>(() => {
|
|
|
+ const groups = new Map<string, GanttOpRow[]>();
|
|
|
+ for (const r of rawList.value) {
|
|
|
+ const key = r.workOrd ?? '';
|
|
|
+ if (!groups.has(key)) groups.set(key, []);
|
|
|
+ groups.get(key)!.push(r);
|
|
|
+ }
|
|
|
+ const rows: ChartRow[] = [];
|
|
|
+ for (const [workOrd, ops] of groups) {
|
|
|
+ const first = ops[0];
|
|
|
+ rows.push({
|
|
|
+ isGroup: true,
|
|
|
+ workOrd,
|
|
|
+ itemNum: first.itemNum ?? '',
|
|
|
+ itemName: first.itemName ?? '',
|
|
|
+ itemModel: first.itemModel ?? '',
|
|
|
+ qtyOrded: first.qtyOrded ?? 0,
|
|
|
+ dueDate: first.dueDate ?? undefined,
|
|
|
+ status: first.status ?? '',
|
|
|
+ domain: first.domain ?? '',
|
|
|
+ mfRate: first.mfRate ?? 0,
|
|
|
+ });
|
|
|
+ for (const op of ops) {
|
|
|
+ rows.push({
|
|
|
+ isGroup: false,
|
|
|
+ workOrd: op.workOrd ?? '',
|
|
|
+ itemName: op.itemName ?? '',
|
|
|
+ op: op.op ?? 0,
|
|
|
+ opDescr: op.opDescr ?? '',
|
|
|
+ planStartDate: op.planStartDate ?? undefined,
|
|
|
+ planEndDate: op.planEndDate ?? undefined,
|
|
|
+ actualStartDate: op.actualStartDate ?? undefined,
|
|
|
+ actualEndDate: op.actualEndDate ?? undefined,
|
|
|
+ workQty: op.workQty ?? 0,
|
|
|
+ qtyCompleted: op.qtyCompleted ?? 0,
|
|
|
+ opStatus: op.opStatus ?? undefined,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return rows;
|
|
|
+});
|
|
|
+
|
|
|
+// ── 图表 ──
|
|
|
+const chartRef = ref<HTMLDivElement>();
|
|
|
+const leftScrollRef = ref<HTMLDivElement>();
|
|
|
+const leftPanelRef = ref<HTMLDivElement>();
|
|
|
+let chart: echarts.ECharts | null = null;
|
|
|
+
|
|
|
+function fmtDate(d?: string | null): string {
|
|
|
+ if (!d) return '—';
|
|
|
+ return d.substring(0, 10);
|
|
|
+}
|
|
|
+
|
|
|
+const statusOptions = [
|
|
|
+ { v: 'w', l: '投产' },
|
|
|
+ { v: 'r', l: '下达' },
|
|
|
+ { v: 'c', l: '关闭' },
|
|
|
+ { v: 'p', l: '初始' },
|
|
|
+ { v: 's', l: '暂停' },
|
|
|
+];
|
|
|
+
|
|
|
+function fmtStatus(v?: string | null): string {
|
|
|
+ if (!v) return '—';
|
|
|
+ const opt = statusOptions.find((o) => o.v === v.toLowerCase());
|
|
|
+ return opt ? opt.l : v;
|
|
|
+}
|
|
|
+
|
|
|
+function toDateNum(d?: string | null): number {
|
|
|
+ if (!d) return NaN;
|
|
|
+ return new Date(d).getTime();
|
|
|
+}
|
|
|
+
|
|
|
+function dayOffset(ts: number, days: number): number {
|
|
|
+ return ts + days * 86400000;
|
|
|
+}
|
|
|
+
|
|
|
+function renderChart() {
|
|
|
+ if (!chartRef.value) return;
|
|
|
+ if (!chart) {
|
|
|
+ chart = echarts.init(chartRef.value);
|
|
|
+ }
|
|
|
+ const rows = chartRows.value;
|
|
|
+ if (rows.length === 0) {
|
|
|
+ chart.clear();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 计算时间范围(从最小订单交期开始)
|
|
|
+ let minTs = Infinity;
|
|
|
+ let maxTs = -Infinity;
|
|
|
+ for (const r of rows) {
|
|
|
+ // 用交期确定起始日期
|
|
|
+ const dd = toDateNum(r.dueDate);
|
|
|
+ if (!isNaN(dd) && dd < minTs) minTs = dd;
|
|
|
+ for (const d of [r.planStartDate, r.planEndDate, r.actualStartDate, r.actualEndDate]) {
|
|
|
+ const ts = toDateNum(d);
|
|
|
+ if (!isNaN(ts)) {
|
|
|
+ if (ts > maxTs) maxTs = ts;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (!isFinite(minTs)) {
|
|
|
+ const today = new Date();
|
|
|
+ today.setHours(0, 0, 0, 0);
|
|
|
+ minTs = today.getTime();
|
|
|
+ }
|
|
|
+ if (!isFinite(maxTs)) {
|
|
|
+ maxTs = dayOffset(minTs, 30);
|
|
|
+ }
|
|
|
+ // 前后各扩展1天
|
|
|
+ minTs = dayOffset(minTs, -1);
|
|
|
+ maxTs = dayOffset(maxTs, 1);
|
|
|
+
|
|
|
+ // 与左侧面板顺序一致(yAxis inverse: true,index 0 在顶部)
|
|
|
+ const yLabels = rows.map((r) => {
|
|
|
+ if (r.isGroup) return `{group|${r.workOrd ?? ''}}`;
|
|
|
+ return ` ${r.op ?? ''}`;
|
|
|
+ });
|
|
|
+
|
|
|
+ // 构建系列数据
|
|
|
+ const planData: any[] = [];
|
|
|
+ const actualData: any[] = [];
|
|
|
+ const mfIcons: any[] = [];
|
|
|
+ const dueIcons: any[] = [];
|
|
|
+
|
|
|
+ for (let ri = 0; ri < rows.length; ri++) {
|
|
|
+ const r = rows[ri];
|
|
|
+ const yIdx = ri; // 与左侧顺序一致
|
|
|
+
|
|
|
+ if (r.isGroup) {
|
|
|
+ // 物料齐套标识(group行)
|
|
|
+ if ((r.mfRate ?? 0) > 0) {
|
|
|
+ mfIcons.push({ value: [toDateNum(r.planStartDate) || minTs, yIdx], mfRate: r.mfRate });
|
|
|
+ }
|
|
|
+ // 交期标识(group行)
|
|
|
+ if (r.dueDate) {
|
|
|
+ dueIcons.push({ value: [toDateNum(r.dueDate), yIdx], dueDate: r.dueDate });
|
|
|
+ }
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 计划条
|
|
|
+ const ps = toDateNum(r.planStartDate);
|
|
|
+ const pe = toDateNum(r.planEndDate);
|
|
|
+ if (!isNaN(ps) && !isNaN(pe)) {
|
|
|
+ planData.push([ps, pe + 86400000, yIdx, r.opDescr, r.workQty, r.workOrd]);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 实际条
|
|
|
+ const as = toDateNum(r.actualStartDate);
|
|
|
+ const ae = toDateNum(r.actualEndDate);
|
|
|
+ if (!isNaN(as) && !isNaN(ae)) {
|
|
|
+ const isDelay = pe > 0 && ae > pe + 86400000;
|
|
|
+ actualData.push([as, ae + 86400000, yIdx, r.opDescr, r.workQty, isDelay, r.workOrd]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const option: echarts.EChartsOption = {
|
|
|
+ tooltip: {
|
|
|
+ formatter(params: any) {
|
|
|
+ const d = params.data;
|
|
|
+ if (!d) return '';
|
|
|
+ const name = params.seriesName;
|
|
|
+ const start = new Date(d[0]).toLocaleDateString();
|
|
|
+ const end = new Date(d[1] - 86400000).toLocaleDateString();
|
|
|
+ const op = d[3] ?? '';
|
|
|
+ const qty = d[4] ?? '';
|
|
|
+ // 计划条 workOrd 在 index 5,实际条 workOrd 在 index 6
|
|
|
+ const workOrd = name === '实际工期' ? (d[6] ?? '') : (d[5] ?? '');
|
|
|
+ return `<b>${name}</b><br/>工单号: ${workOrd}<br/>工序: ${op}<br/>数量: ${qty}<br/>${start} ~ ${end}`;
|
|
|
+ },
|
|
|
+ },
|
|
|
+ grid: { left: 5, right: 40, top: 10, bottom: 30, containLabel: false },
|
|
|
+ xAxis: {
|
|
|
+ type: 'time',
|
|
|
+ min: minTs,
|
|
|
+ max: maxTs,
|
|
|
+ axisLabel: {
|
|
|
+ formatter: '{MM}/{dd}',
|
|
|
+ fontSize: 11,
|
|
|
+ },
|
|
|
+ splitLine: { show: true, lineStyle: { type: 'dashed', color: '#e8e8e8' } },
|
|
|
+ },
|
|
|
+ yAxis: {
|
|
|
+ type: 'category',
|
|
|
+ data: yLabels,
|
|
|
+ inverse: true,
|
|
|
+ show: false,
|
|
|
+ splitLine: { show: false },
|
|
|
+ axisTick: { show: false },
|
|
|
+ },
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ name: '计划工期',
|
|
|
+ type: 'custom',
|
|
|
+ renderItem: (_params: any, api: any) => {
|
|
|
+ const startTs = api.value(0);
|
|
|
+ const endTs = api.value(1);
|
|
|
+ const catIdx = api.value(2);
|
|
|
+ const start = api.coord([startTs, catIdx]);
|
|
|
+ const end = api.coord([endTs, catIdx]);
|
|
|
+ const barH = api.size([0, 1])[1] * 0.35;
|
|
|
+ const y = start[1] - barH / 2 - 2;
|
|
|
+ return {
|
|
|
+ type: 'rect',
|
|
|
+ shape: { x: start[0], y, width: Math.max(end[0] - start[0], 2), height: barH },
|
|
|
+ style: { fill: '#409EFF', opacity: 0.7 },
|
|
|
+ };
|
|
|
+ },
|
|
|
+ encode: { x: [0, 1], y: 2 },
|
|
|
+ data: planData,
|
|
|
+ z: 2,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '实际工期',
|
|
|
+ type: 'custom',
|
|
|
+ renderItem: (_params: any, api: any) => {
|
|
|
+ const startTs = api.value(0);
|
|
|
+ const endTs = api.value(1);
|
|
|
+ const catIdx = api.value(2);
|
|
|
+ const isDelay = api.value(5);
|
|
|
+ const start = api.coord([startTs, catIdx]);
|
|
|
+ const end = api.coord([endTs, catIdx]);
|
|
|
+ const barH = api.size([0, 1])[1] * 0.3;
|
|
|
+ const y = start[1] + 2;
|
|
|
+ return {
|
|
|
+ type: 'rect',
|
|
|
+ shape: { x: start[0], y, width: Math.max(end[0] - start[0], 2), height: barH },
|
|
|
+ style: { fill: isDelay ? '#F56C6C' : '#67C23A', opacity: 0.8 },
|
|
|
+ };
|
|
|
+ },
|
|
|
+ encode: { x: [0, 1], y: 2 },
|
|
|
+ data: actualData,
|
|
|
+ z: 3,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '物料齐套',
|
|
|
+ type: 'scatter',
|
|
|
+ symbol: 'roundRect',
|
|
|
+ symbolSize: [14, 14],
|
|
|
+ itemStyle: { color: '#67C23A' },
|
|
|
+ data: mfIcons.map((d: any) => ({
|
|
|
+ value: d.value,
|
|
|
+ itemStyle: { color: (d.mfRate ?? 0) >= 100 ? '#67C23A' : '#E6A23C' },
|
|
|
+ })),
|
|
|
+ tooltip: {
|
|
|
+ formatter(p: any) {
|
|
|
+ const rate = mfIcons[p.dataIndex]?.mfRate ?? 0;
|
|
|
+ return `物料齐套率: ${rate}%`;
|
|
|
+ },
|
|
|
+ },
|
|
|
+ z: 10,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '订单交期',
|
|
|
+ type: 'scatter',
|
|
|
+ symbol: 'diamond',
|
|
|
+ symbolSize: 16,
|
|
|
+ itemStyle: { color: '#E6A23C' },
|
|
|
+ data: dueIcons,
|
|
|
+ tooltip: {
|
|
|
+ formatter(p: any) {
|
|
|
+ const dd = dueIcons[p.dataIndex]?.dueDate ?? '';
|
|
|
+ return `订单交期: ${dd}`;
|
|
|
+ },
|
|
|
+ },
|
|
|
+ z: 10,
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ dataZoom: [
|
|
|
+ { type: 'inside', xAxisIndex: 0 },
|
|
|
+ { type: 'slider', xAxisIndex: 0, height: 18, bottom: 2 },
|
|
|
+ ],
|
|
|
+ };
|
|
|
+
|
|
|
+ chart.setOption(option, true);
|
|
|
+}
|
|
|
+
|
|
|
+async function doSearch() {
|
|
|
+ loading.value = true;
|
|
|
+ try {
|
|
|
+ const params: any = {};
|
|
|
+ if (query.value.workOrd) params.workOrd = query.value.workOrd;
|
|
|
+ if (dateRange.value?.length === 2) {
|
|
|
+ params.startDate = dateRange.value[0];
|
|
|
+ params.endDate = dateRange.value[1];
|
|
|
+ }
|
|
|
+ const res = await fetchGanttData(params);
|
|
|
+ rawList.value = res.list ?? [];
|
|
|
+ await nextTick();
|
|
|
+ renderChart();
|
|
|
+ } catch (e) {
|
|
|
+ console.error(e);
|
|
|
+ rawList.value = [];
|
|
|
+ } finally {
|
|
|
+ loading.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function resetQuery() {
|
|
|
+ query.value.workOrd = '';
|
|
|
+ dateRange.value = null;
|
|
|
+ doSearch();
|
|
|
+}
|
|
|
+
|
|
|
+function onResize() {
|
|
|
+ chart?.resize();
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ doSearch();
|
|
|
+ window.addEventListener('resize', onResize);
|
|
|
+});
|
|
|
+
|
|
|
+onBeforeUnmount(() => {
|
|
|
+ window.removeEventListener('resize', onResize);
|
|
|
+ chart?.dispose();
|
|
|
+ chart = null;
|
|
|
+});
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.gantt-legend {
|
|
|
+ display: flex;
|
|
|
+ gap: 18px;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ font-size: 13px;
|
|
|
+ color: #606266;
|
|
|
+}
|
|
|
+.legend-item {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 4px;
|
|
|
+}
|
|
|
+.legend-bar {
|
|
|
+ display: inline-block;
|
|
|
+ width: 24px;
|
|
|
+ height: 12px;
|
|
|
+ border-radius: 2px;
|
|
|
+}
|
|
|
+.legend-bar.plan {
|
|
|
+ background: #409eff;
|
|
|
+ opacity: 0.7;
|
|
|
+}
|
|
|
+.legend-bar.actual {
|
|
|
+ background: #67c23a;
|
|
|
+ opacity: 0.8;
|
|
|
+}
|
|
|
+.legend-bar.delay {
|
|
|
+ background: #f56c6c;
|
|
|
+ opacity: 0.8;
|
|
|
+}
|
|
|
+
|
|
|
+.gantt-wrapper {
|
|
|
+ min-height: 400px;
|
|
|
+}
|
|
|
+.gantt-empty {
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ height: 300px;
|
|
|
+ color: #909399;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+.gantt-body {
|
|
|
+ display: flex;
|
|
|
+ height: calc(100vh - 260px);
|
|
|
+ min-height: 400px;
|
|
|
+ border: 1px solid #ebeef5;
|
|
|
+ border-radius: 4px;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+.gantt-left {
|
|
|
+ flex: 0 0 780px;
|
|
|
+ border-right: 2px solid #dcdfe6;
|
|
|
+ background: #fafafa;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+.gantt-left-header {
|
|
|
+ display: flex;
|
|
|
+ height: 36px;
|
|
|
+ align-items: center;
|
|
|
+ background: #f5f7fa;
|
|
|
+ border-bottom: 1px solid #ebeef5;
|
|
|
+ font-weight: 600;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #303133;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+.gantt-left-body {
|
|
|
+ flex: 1;
|
|
|
+ overflow-y: auto;
|
|
|
+ overflow-x: hidden;
|
|
|
+}
|
|
|
+.gantt-left-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ border-bottom: 1px solid #f0f0f0;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #606266;
|
|
|
+ padding: 0 4px;
|
|
|
+}
|
|
|
+.gantt-left-row.group-row {
|
|
|
+ background: #ecf5ff;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+ height: 40px;
|
|
|
+}
|
|
|
+.gl-col {
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+ padding: 0 3px;
|
|
|
+}
|
|
|
+.gl-workord {
|
|
|
+ width: 100px;
|
|
|
+ flex: 0 0 100px;
|
|
|
+}
|
|
|
+.gl-itemnum {
|
|
|
+ width: 100px;
|
|
|
+ flex: 0 0 100px;
|
|
|
+}
|
|
|
+.gl-item {
|
|
|
+ width: 100px;
|
|
|
+ flex: 0 0 100px;
|
|
|
+}
|
|
|
+.gl-op {
|
|
|
+ width: 80px;
|
|
|
+ flex: 0 0 80px;
|
|
|
+}
|
|
|
+.gl-qty {
|
|
|
+ width: 50px;
|
|
|
+ flex: 0 0 50px;
|
|
|
+ text-align: right;
|
|
|
+}
|
|
|
+.gl-status {
|
|
|
+ width: 60px;
|
|
|
+ flex: 0 0 60px;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+.gl-date {
|
|
|
+ width: 80px;
|
|
|
+ flex: 0 0 80px;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+.gantt-right {
|
|
|
+ flex: 1;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+.gantt-chart {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+}
|
|
|
+</style>
|