2026-06-11 10:56:24 +08:00
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
/* eslint-disable vue/no-mutating-props, no-plusplus, unicorn/prefer-dom-node-text-content, @typescript-eslint/no-dynamic-delete */
|
|
|
|
|
|
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
|
|
|
|
|
|
import { ElMessage } from 'element-plus';
|
|
|
|
|
|
import { RDMS_REQ_PRIORITY_DICT_CODE } from '@/constants/dict';
|
|
|
|
|
|
import { useDict } from '@/hooks/business/dict';
|
|
|
|
|
|
import DictSelect from '@/components/custom/dict-select.vue';
|
|
|
|
|
|
import { formatPeriodLabel } from '../../shared/types';
|
|
|
|
|
|
|
|
|
|
|
|
const props = defineProps<{
|
|
|
|
|
|
reportType: string;
|
|
|
|
|
|
period: string;
|
|
|
|
|
|
mode?: 'add' | 'edit' | 'detail';
|
|
|
|
|
|
scene?: 'fill' | 'detail' | 'approval';
|
|
|
|
|
|
baseInfo?: Api.WorkReport.Project.ProjectReport | null;
|
|
|
|
|
|
model: Api.WorkReport.Project.ProjectReportSaveParams;
|
|
|
|
|
|
}>();
|
|
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits<{
|
|
|
|
|
|
back: [];
|
|
|
|
|
|
save: [];
|
|
|
|
|
|
submit: [];
|
|
|
|
|
|
requestApprove: [];
|
|
|
|
|
|
requestReject: [];
|
|
|
|
|
|
pullDefaultDraft: [];
|
|
|
|
|
|
}>();
|
|
|
|
|
|
|
|
|
|
|
|
interface WorkItem {
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
title: string;
|
|
|
|
|
|
days?: number;
|
|
|
|
|
|
hours?: number;
|
|
|
|
|
|
priority: string;
|
|
|
|
|
|
progress: number;
|
|
|
|
|
|
removable?: boolean;
|
|
|
|
|
|
source?: Api.WorkReport.Project.ProjectReportItem;
|
|
|
|
|
|
sourceIndex?: number;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const activeEditField = ref('');
|
|
|
|
|
|
const inlinePlanVisible = ref(false);
|
|
|
|
|
|
const titleEditSnapshot = ref<Record<string, string>>({});
|
|
|
|
|
|
const EMPTY_TEXT = '本周期内暂无数据';
|
|
|
|
|
|
const isReadonly = computed(() => props.mode === 'detail' || props.scene === 'approval');
|
|
|
|
|
|
const { dictData: priorityDictData, getLabel: getPriorityLabel } = useDict(RDMS_REQ_PRIORITY_DICT_CODE);
|
|
|
|
|
|
|
|
|
|
|
|
const projectForm = reactive({
|
|
|
|
|
|
get projectName() {
|
|
|
|
|
|
return props.baseInfo?.projectName || '--';
|
|
|
|
|
|
},
|
|
|
|
|
|
get projectOwner() {
|
|
|
|
|
|
return props.baseInfo?.projectOwnerName || '--';
|
|
|
|
|
|
},
|
|
|
|
|
|
get technicalOwner() {
|
|
|
|
|
|
return props.baseInfo?.technicalOwnerName || '--';
|
|
|
|
|
|
},
|
|
|
|
|
|
get members() {
|
|
|
|
|
|
return (props.baseInfo?.projectMemberSnapshot || []).map(item => item.userName);
|
|
|
|
|
|
},
|
|
|
|
|
|
get projectStatus() {
|
|
|
|
|
|
return props.model.projectStatusDesc || '';
|
|
|
|
|
|
},
|
|
|
|
|
|
set projectStatus(value: string) {
|
|
|
|
|
|
props.model.projectStatusDesc = value;
|
|
|
|
|
|
},
|
|
|
|
|
|
get progressPlan() {
|
|
|
|
|
|
return props.model.projectProgressPlan || '';
|
|
|
|
|
|
},
|
|
|
|
|
|
set progressPlan(value: string) {
|
|
|
|
|
|
props.model.projectProgressPlan = value;
|
|
|
|
|
|
},
|
|
|
|
|
|
get keyPoints() {
|
|
|
|
|
|
return props.model.projectKeyPoints || '';
|
|
|
|
|
|
},
|
|
|
|
|
|
set keyPoints(value: string) {
|
|
|
|
|
|
props.model.projectKeyPoints = value;
|
|
|
|
|
|
},
|
|
|
|
|
|
get problems() {
|
|
|
|
|
|
return props.model.projectProblems || '';
|
|
|
|
|
|
},
|
|
|
|
|
|
set problems(value: string) {
|
|
|
|
|
|
props.model.projectProblems = value;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const planForm = ref({
|
|
|
|
|
|
title: '',
|
|
|
|
|
|
priority: '3' as WorkItem['priority'],
|
|
|
|
|
|
progress: 0
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
function normalizePriorityCode(value?: string | number | null) {
|
|
|
|
|
|
const text = String(value ?? '').trim();
|
|
|
|
|
|
if (!text) return '';
|
|
|
|
|
|
|
|
|
|
|
|
const matchedByValue = priorityDictData.value.find(item => String(item.value) === text);
|
|
|
|
|
|
if (matchedByValue) return String(matchedByValue.value);
|
|
|
|
|
|
|
|
|
|
|
|
const matchedByLabel = priorityDictData.value.find(item => item.label === text);
|
|
|
|
|
|
if (matchedByLabel) return String(matchedByLabel.value);
|
|
|
|
|
|
|
|
|
|
|
|
const priorityLabelMatch = text.match(/^P(\d+)$/iu);
|
|
|
|
|
|
if (priorityLabelMatch) return priorityLabelMatch[1];
|
|
|
|
|
|
|
|
|
|
|
|
return text;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolvePriorityLabel(value?: string | number | null) {
|
|
|
|
|
|
const priorityCode = normalizePriorityCode(value);
|
|
|
|
|
|
if (!priorityCode) return '';
|
|
|
|
|
|
return getPriorityLabel(priorityCode, {
|
|
|
|
|
|
fallback: /^P\d+$/iu.test(priorityCode) ? priorityCode : `P${priorityCode}`
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function toWorkItem(item: Api.WorkReport.Project.ProjectReportItem, index: number, removable: boolean): WorkItem {
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: item.id || `${removable ? 'next' : 'current'}-${index}`,
|
|
|
|
|
|
title: item.itemTitle || '未命名工作',
|
|
|
|
|
|
hours: item.workHours || 0,
|
|
|
|
|
|
priority: normalizePriorityCode(item.priorityCode) || '3',
|
|
|
|
|
|
progress: item.progressRate || 0,
|
|
|
|
|
|
removable,
|
|
|
|
|
|
source: item,
|
|
|
|
|
|
sourceIndex: index
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const currentWorks = computed<WorkItem[]>(() => {
|
2026-06-23 17:59:42 +08:00
|
|
|
|
return (props.model.currentItems || []).map((item, index) => toWorkItem(item, index, true));
|
2026-06-11 10:56:24 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const nextPlans = computed<WorkItem[]>(() => {
|
|
|
|
|
|
return (props.model.nextItems || []).map((item, index) => toWorkItem(item, index, true));
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const MAX_VISIBLE_MEMBERS = 5;
|
|
|
|
|
|
/** 最多显示的成员数:受容器宽度动态限制 */
|
|
|
|
|
|
const maxVisibleMemberCount = ref(MAX_VISIBLE_MEMBERS);
|
|
|
|
|
|
const memberChipsEl = ref<HTMLElement | null>(null);
|
|
|
|
|
|
let memberResizeObserver: ResizeObserver | null = null;
|
|
|
|
|
|
|
|
|
|
|
|
function measureChipWidth(text: string): number {
|
|
|
|
|
|
// 估算 chip 宽度:8px 左右内边距 + 中文每字 12px、英文每字 7px
|
|
|
|
|
|
let width = 16;
|
|
|
|
|
|
for (const ch of text) {
|
|
|
|
|
|
width += /[\u4E00-\u9FA5]/.test(ch) ? 12 : 7;
|
|
|
|
|
|
}
|
|
|
|
|
|
return Math.ceil(width);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 根据容器宽度动态计算可容纳的成员数,上限为 MAX_VISIBLE_MEMBERS */
|
|
|
|
|
|
function measureVisibleCount() {
|
|
|
|
|
|
const el = memberChipsEl.value;
|
|
|
|
|
|
if (!el) return;
|
|
|
|
|
|
|
|
|
|
|
|
const containerWidth = el.clientWidth - 16; // 减去 padding
|
|
|
|
|
|
const members = projectForm.members;
|
|
|
|
|
|
if (!members.length) {
|
|
|
|
|
|
maxVisibleMemberCount.value = 0;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 先快速按数量上限分配,再校验总宽度
|
|
|
|
|
|
const moreChipWidth = 28; // +N 的预估宽度
|
|
|
|
|
|
const gap = 6;
|
|
|
|
|
|
|
|
|
|
|
|
let count = Math.min(members.length, MAX_VISIBLE_MEMBERS);
|
|
|
|
|
|
for (; count > 0; count--) {
|
|
|
|
|
|
const showMore = count < members.length;
|
|
|
|
|
|
const reserved = showMore ? moreChipWidth + gap : 0;
|
|
|
|
|
|
let total = reserved;
|
|
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
|
|
|
|
total += measureChipWidth(members[i]) + gap;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (total <= containerWidth) break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
maxVisibleMemberCount.value = count;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const visibleMembers = computed(() => projectForm.members.slice(0, maxVisibleMemberCount.value));
|
|
|
|
|
|
const hiddenMembers = computed(() => projectForm.members.slice(maxVisibleMemberCount.value));
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
if (!memberResizeObserver && typeof ResizeObserver !== 'undefined') {
|
|
|
|
|
|
memberResizeObserver = new ResizeObserver(() => measureVisibleCount());
|
|
|
|
|
|
}
|
|
|
|
|
|
if (memberChipsEl.value) {
|
|
|
|
|
|
memberResizeObserver?.observe(memberChipsEl.value);
|
|
|
|
|
|
nextTick(measureVisibleCount);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
watch(memberChipsEl, el => {
|
|
|
|
|
|
memberResizeObserver?.disconnect();
|
|
|
|
|
|
if (el) {
|
|
|
|
|
|
memberResizeObserver?.observe(el);
|
|
|
|
|
|
nextTick(measureVisibleCount);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => projectForm.members.length,
|
|
|
|
|
|
() => {
|
|
|
|
|
|
nextTick(measureVisibleCount);
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
|
memberResizeObserver?.disconnect();
|
|
|
|
|
|
memberResizeObserver = null;
|
|
|
|
|
|
});
|
|
|
|
|
|
const totalHours = computed(() => {
|
|
|
|
|
|
const baseTotalWorkHours = Number(props.baseInfo?.totalWorkHours ?? 0);
|
|
|
|
|
|
if (Number.isFinite(baseTotalWorkHours) && baseTotalWorkHours > 0) return baseTotalWorkHours;
|
|
|
|
|
|
|
|
|
|
|
|
return (props.model.currentItems || []).reduce((sum, item) => sum + Number(item.workHours || 0), 0);
|
|
|
|
|
|
});
|
|
|
|
|
|
const isDuplicatePlanTitle = computed(() => {
|
|
|
|
|
|
const title = planForm.value.title.trim();
|
|
|
|
|
|
return Boolean(title) && (props.model.nextItems || []).some(item => item.itemTitle.trim() === title);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
function focusEditField(key: string) {
|
|
|
|
|
|
activeEditField.value = key;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function blurEditField(key: string) {
|
|
|
|
|
|
if (activeEditField.value === key) activeEditField.value = '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function syncRichProjectField(field: 'projectStatus' | 'progressPlan' | 'keyPoints' | 'problems', event: Event) {
|
|
|
|
|
|
const target = event.currentTarget as HTMLElement;
|
|
|
|
|
|
projectForm[field] = target.innerText.trim();
|
|
|
|
|
|
blurEditField(field);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resetPlanForm() {
|
|
|
|
|
|
planForm.value = { title: '', priority: '3', progress: 0 };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function showInlinePlanForm() {
|
|
|
|
|
|
inlinePlanVisible.value = true;
|
|
|
|
|
|
resetPlanForm();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function cancelInlinePlan() {
|
|
|
|
|
|
inlinePlanVisible.value = false;
|
|
|
|
|
|
resetPlanForm();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function submitInlinePlan() {
|
|
|
|
|
|
const title = planForm.value.title.trim();
|
|
|
|
|
|
if (!title) return;
|
|
|
|
|
|
if (isDuplicatePlanTitle.value) {
|
|
|
|
|
|
ElMessage.warning('计划名称重复,请调整后再新增');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
props.model.nextItems.push({
|
|
|
|
|
|
itemTitle: planForm.value.title.trim(),
|
|
|
|
|
|
workHours: 0,
|
|
|
|
|
|
priorityCode: normalizePriorityCode(planForm.value.priority),
|
|
|
|
|
|
progressRate: planForm.value.progress,
|
|
|
|
|
|
_isNew: true
|
|
|
|
|
|
} as Api.WorkReport.Project.ProjectReportItem & { _isNew: boolean });
|
|
|
|
|
|
resetPlanForm();
|
|
|
|
|
|
inlinePlanVisible.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function removePlanItem(index: number) {
|
|
|
|
|
|
const item = nextPlans.value[index];
|
|
|
|
|
|
if (item?.sourceIndex !== undefined) props.model.nextItems.splice(item.sourceIndex, 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-23 17:59:42 +08:00
|
|
|
|
function removeCurrentWorkItem(index: number) {
|
|
|
|
|
|
const item = currentWorks.value[index];
|
|
|
|
|
|
if (item?.sourceIndex !== undefined) props.model.currentItems.splice(item.sourceIndex, 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-11 10:56:24 +08:00
|
|
|
|
function startTitleEdit(item: WorkItem) {
|
|
|
|
|
|
titleEditSnapshot.value[item.id] = item.title;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function notifyTitleSaved(item: WorkItem) {
|
|
|
|
|
|
const previousTitle = titleEditSnapshot.value[item.id] ?? item.title;
|
|
|
|
|
|
const nextTitle = item.title.trim();
|
|
|
|
|
|
if (!nextTitle) {
|
|
|
|
|
|
item.title = previousTitle;
|
|
|
|
|
|
delete titleEditSnapshot.value[item.id];
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
item.title = nextTitle;
|
|
|
|
|
|
if (item.source) item.source.itemTitle = nextTitle;
|
|
|
|
|
|
delete titleEditSnapshot.value[item.id];
|
2026-06-23 17:59:42 +08:00
|
|
|
|
// if (nextTitle !== previousTitle) {
|
|
|
|
|
|
// ElMessage.success('名称已成功修改');
|
|
|
|
|
|
// }
|
2026-06-11 10:56:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<div class="card form-page">
|
|
|
|
|
|
<div class="section">
|
|
|
|
|
|
<div class="section-title">
|
|
|
|
|
|
<span>基础信息</span>
|
2026-06-13 22:13:44 +08:00
|
|
|
|
<div v-if="mode === 'edit' && !isReadonly" class="section-title-right">
|
|
|
|
|
|
<ElButton size="small" plain type="primary" @click="emit('pullDefaultDraft')">
|
|
|
|
|
|
<template #icon>
|
|
|
|
|
|
<icon-mdi-refresh class="text-icon" />
|
|
|
|
|
|
</template>
|
|
|
|
|
|
刷新
|
|
|
|
|
|
</ElButton>
|
|
|
|
|
|
</div>
|
2026-06-11 10:56:24 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="compose-grid">
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
|
<label>项目名称</label>
|
|
|
|
|
|
<ElInput v-model="projectForm.projectName" disabled />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
|
<label>项目负责人</label>
|
|
|
|
|
|
<ElInput v-model="projectForm.projectOwner" disabled />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
|
<label>技术负责人</label>
|
|
|
|
|
|
<ElInput v-model="projectForm.technicalOwner" disabled />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="field members-field">
|
|
|
|
|
|
<label>项目组成员</label>
|
|
|
|
|
|
<div ref="memberChipsEl" class="member-chips">
|
|
|
|
|
|
<span v-for="member in visibleMembers" :key="member" class="member-chip">{{ member }}</span>
|
|
|
|
|
|
<ElTooltip v-if="hiddenMembers.length" placement="top" :content="hiddenMembers.join('、')">
|
|
|
|
|
|
<span class="member-chip more">+{{ hiddenMembers.length }}</span>
|
|
|
|
|
|
</ElTooltip>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="basic-status-editor">
|
|
|
|
|
|
<label>项目状况</label>
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="rich-editor"
|
|
|
|
|
|
:contenteditable="!isReadonly"
|
|
|
|
|
|
spellcheck="false"
|
|
|
|
|
|
:data-placeholder="isReadonly ? undefined : '请输入项目状况'"
|
|
|
|
|
|
@focus="focusEditField('projectStatus')"
|
|
|
|
|
|
@blur="syncRichProjectField('projectStatus', $event)"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ projectForm.projectStatus }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="section">
|
|
|
|
|
|
<div class="section-title">整体计划进度</div>
|
|
|
|
|
|
<div class="single-editor">
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="rich-editor"
|
|
|
|
|
|
:contenteditable="!isReadonly"
|
|
|
|
|
|
spellcheck="false"
|
|
|
|
|
|
:data-placeholder="isReadonly ? undefined : '请输入整体计划进度'"
|
|
|
|
|
|
@focus="focusEditField('progressPlan')"
|
|
|
|
|
|
@blur="syncRichProjectField('progressPlan', $event)"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ projectForm.progressPlan }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="section">
|
|
|
|
|
|
<div class="section-title">
|
|
|
|
|
|
<div class="section-title-left">
|
|
|
|
|
|
<span>本期工作内容</span>
|
|
|
|
|
|
<span class="source-chip">{{ currentWorks.length }} 项工作</span>
|
|
|
|
|
|
<span class="source-chip">共 {{ totalHours }}h</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="review-grid layout-row">
|
|
|
|
|
|
<div v-if="!currentWorks.length">{{ EMPTY_TEXT }}</div>
|
|
|
|
|
|
<div v-for="(item, index) in currentWorks" :key="item.id" class="review-card compact-work-card">
|
2026-06-23 17:59:42 +08:00
|
|
|
|
<div class="review-card-head" :class="{ 'no-remove': item.removable === false }">
|
2026-06-11 10:56:24 +08:00
|
|
|
|
<span class="row-index">{{ index + 1 }}</span>
|
|
|
|
|
|
<div class="review-title-readonly">
|
|
|
|
|
|
<div class="work-title-line">
|
|
|
|
|
|
<input
|
|
|
|
|
|
v-model="item.title"
|
|
|
|
|
|
class="work-title-input"
|
|
|
|
|
|
aria-label="本期工作内容名称"
|
|
|
|
|
|
:disabled="isReadonly"
|
|
|
|
|
|
@focus="startTitleEdit(item)"
|
|
|
|
|
|
@blur="notifyTitleSaved(item)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span class="meta-chip priority-chip">{{ resolvePriorityLabel(item.priority) }}</span>
|
|
|
|
|
|
<span class="meta-chip">进度 {{ item.progress }}%</span>
|
|
|
|
|
|
<span class="meta-chip">共 {{ item.hours }}h</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-06-23 17:59:42 +08:00
|
|
|
|
<ElPopconfirm
|
|
|
|
|
|
v-if="item.removable !== false && !isReadonly"
|
|
|
|
|
|
title="确认删除这条工作内容吗?"
|
|
|
|
|
|
confirm-button-text="删除"
|
|
|
|
|
|
cancel-button-text="取消"
|
|
|
|
|
|
width="220"
|
|
|
|
|
|
@confirm="removeCurrentWorkItem(index)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #reference>
|
|
|
|
|
|
<button class="item-remove-btn" aria-label="删除工作内容">×</button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</ElPopconfirm>
|
2026-06-11 10:56:24 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="section">
|
|
|
|
|
|
<div class="section-title">
|
|
|
|
|
|
<div class="section-title-left">
|
|
|
|
|
|
<span>下期计划工作内容</span>
|
|
|
|
|
|
<span class="source-chip">{{ nextPlans.length }} 项计划</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="section-title-right">
|
|
|
|
|
|
<ElButton v-if="!isReadonly" size="small" :disabled="inlinePlanVisible" @click="showInlinePlanForm">
|
|
|
|
|
|
新增计划
|
|
|
|
|
|
</ElButton>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="plan-list layout-row">
|
|
|
|
|
|
<div v-if="!nextPlans.length && !inlinePlanVisible">{{ EMPTY_TEXT }}</div>
|
|
|
|
|
|
<div v-for="(item, index) in nextPlans" :key="item.id" class="plan-card compact-work-card">
|
|
|
|
|
|
<div class="plan-card-head" :class="{ 'no-remove': item.removable === false }">
|
|
|
|
|
|
<span class="row-index">{{ index + 1 }}</span>
|
|
|
|
|
|
<div class="plan-title-readonly">
|
|
|
|
|
|
<div class="work-title-line">
|
|
|
|
|
|
<input
|
|
|
|
|
|
v-model="item.title"
|
|
|
|
|
|
class="work-title-input"
|
|
|
|
|
|
aria-label="下期计划工作内容名称"
|
|
|
|
|
|
:disabled="isReadonly"
|
|
|
|
|
|
@focus="startTitleEdit(item)"
|
|
|
|
|
|
@blur="notifyTitleSaved(item)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span class="meta-chip priority-chip">{{ resolvePriorityLabel(item.priority) }}</span>
|
|
|
|
|
|
<span class="meta-chip">进度 {{ item.progress }}%</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<ElPopconfirm
|
|
|
|
|
|
v-if="item.removable !== false && !isReadonly"
|
|
|
|
|
|
title="确认删除这条计划吗?"
|
|
|
|
|
|
confirm-button-text="删除"
|
|
|
|
|
|
cancel-button-text="取消"
|
|
|
|
|
|
width="220"
|
|
|
|
|
|
@confirm="removePlanItem(index)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #reference>
|
|
|
|
|
|
<button class="item-remove-btn" aria-label="删除计划">×</button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</ElPopconfirm>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-if="inlinePlanVisible && !isReadonly" class="plan-card compact-work-card inline-plan-card">
|
|
|
|
|
|
<div class="plan-card-head">
|
|
|
|
|
|
<span class="row-index">{{ nextPlans.length + 1 }}</span>
|
|
|
|
|
|
<div class="plan-title-readonly">
|
|
|
|
|
|
<div class="work-title-line inline-edit-line">
|
|
|
|
|
|
<div class="inline-title-field">
|
|
|
|
|
|
<ElInput v-model="planForm.title" class="inline-title-input" placeholder="请输入计划名称" />
|
|
|
|
|
|
<span v-if="isDuplicatePlanTitle" class="duplicate-tip">计划名称重复,请调整后再新增</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<DictSelect
|
|
|
|
|
|
v-model="planForm.priority"
|
|
|
|
|
|
class="inline-select"
|
|
|
|
|
|
:dict-code="RDMS_REQ_PRIORITY_DICT_CODE"
|
|
|
|
|
|
placeholder="优先级"
|
|
|
|
|
|
:clearable="false"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<ElInputNumber
|
|
|
|
|
|
v-model="planForm.progress"
|
|
|
|
|
|
class="inline-progress"
|
|
|
|
|
|
:min="0"
|
|
|
|
|
|
:max="100"
|
|
|
|
|
|
:step="5"
|
|
|
|
|
|
:precision="1"
|
|
|
|
|
|
controls-position="right"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button class="item-remove-btn" aria-label="取消新增计划" @click="cancelInlinePlan">×</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="inline-plan-actions">
|
|
|
|
|
|
<ElButton size="small" @click="cancelInlinePlan">取消</ElButton>
|
|
|
|
|
|
<ElButton
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
:disabled="!planForm.title.trim() || isDuplicatePlanTitle"
|
|
|
|
|
|
@click="submitInlinePlan"
|
|
|
|
|
|
>
|
|
|
|
|
|
确认新增
|
|
|
|
|
|
</ElButton>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="section">
|
|
|
|
|
|
<div class="section-title">要点描述</div>
|
|
|
|
|
|
<div class="single-editor">
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="rich-editor"
|
|
|
|
|
|
:contenteditable="!isReadonly"
|
|
|
|
|
|
spellcheck="false"
|
|
|
|
|
|
:data-placeholder="isReadonly ? undefined : '请输入要点描述'"
|
|
|
|
|
|
@focus="focusEditField('keyPoints')"
|
|
|
|
|
|
@blur="syncRichProjectField('keyPoints', $event)"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ projectForm.keyPoints }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="section">
|
|
|
|
|
|
<div class="section-title">项目问题</div>
|
|
|
|
|
|
<div class="single-editor">
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="rich-editor"
|
|
|
|
|
|
:contenteditable="!isReadonly"
|
|
|
|
|
|
spellcheck="false"
|
|
|
|
|
|
:data-placeholder="isReadonly ? undefined : '请输入项目问题'"
|
|
|
|
|
|
@focus="focusEditField('problems')"
|
|
|
|
|
|
@blur="syncRichProjectField('problems', $event)"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ projectForm.problems }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-if="!isReadonly" class="form-actions">
|
|
|
|
|
|
<!-- <ElButton>重置表单</ElButton>-->
|
|
|
|
|
|
<ElButton @click="emit('save')">保存草稿</ElButton>
|
2026-06-21 18:22:44 +08:00
|
|
|
|
<ElButton type="primary" @click="emit('submit')">提交审批</ElButton>
|
2026-06-11 10:56:24 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div v-else-if="scene === 'approval'" class="form-actions approval-form-actions">
|
2026-07-08 20:08:04 +08:00
|
|
|
|
<ElButton @click="emit('back')">退出</ElButton>
|
|
|
|
|
|
<ElButton type="primary" @click="emit('requestApprove')">审批</ElButton>
|
2026-06-11 10:56:24 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.card {
|
|
|
|
|
|
border: 1px solid rgba(216, 224, 232, 0.88);
|
|
|
|
|
|
border-radius: 18px;
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.86);
|
|
|
|
|
|
box-shadow: 0 18px 45px rgba(15, 23, 42, 0.12);
|
|
|
|
|
|
padding-top: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-title {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
font-weight: 900;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-subtitle {
|
|
|
|
|
|
margin-top: 5px;
|
|
|
|
|
|
color: #667085;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.form-head {
|
|
|
|
|
|
padding: 20px 64px 20px 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.report-title {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.type-mark {
|
|
|
|
|
|
width: 48px;
|
|
|
|
|
|
height: 48px;
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
place-items: center;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
border-radius: 16px;
|
2026-06-21 18:22:44 +08:00
|
|
|
|
background: var(--el-color-primary-light-8);
|
|
|
|
|
|
color: var(--el-color-primary);
|
2026-06-11 10:56:24 +08:00
|
|
|
|
font-weight: 900;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.section {
|
|
|
|
|
|
margin: 0 20px 18px;
|
|
|
|
|
|
border: 1px solid #d8e0e8;
|
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.section-title {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
padding: 13px 16px;
|
|
|
|
|
|
background: #f8fbfc;
|
|
|
|
|
|
border-bottom: 1px solid #d8e0e8;
|
|
|
|
|
|
font-weight: 900;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.section-title-left,
|
|
|
|
|
|
.section-title-right {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.source-chip {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
height: 28px;
|
|
|
|
|
|
padding: 0 10px;
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
background: #f3f7f9;
|
|
|
|
|
|
color: #475467;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.compose-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: 1.25fr 0.75fr 0.75fr 1.25fr;
|
|
|
|
|
|
gap: 14px;
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
align-items: start;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.field {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.field label {
|
|
|
|
|
|
color: #667085;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.basic-status-editor {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
padding: 0 16px 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.basic-status-editor label {
|
|
|
|
|
|
color: #667085;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.member-chips {
|
|
|
|
|
|
height: 32px;
|
|
|
|
|
|
min-height: 32px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
flex-wrap: nowrap;
|
|
|
|
|
|
padding: 4px 8px;
|
|
|
|
|
|
border: 1px solid #dcdfe6;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
background: #f5f7fa;
|
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.member-chip {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
height: 20px;
|
|
|
|
|
|
flex: 0 0 auto;
|
|
|
|
|
|
padding: 0 8px;
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
color: #475467;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.member-chip.more {
|
2026-06-21 18:22:44 +08:00
|
|
|
|
background: var(--el-color-primary-light-9);
|
|
|
|
|
|
color: var(--el-color-primary);
|
2026-06-11 10:56:24 +08:00
|
|
|
|
cursor: default;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.single-editor {
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.review-grid,
|
|
|
|
|
|
.plan-list {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.review-grid.layout-row,
|
|
|
|
|
|
.plan-list.layout-row {
|
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.review-card,
|
|
|
|
|
|
.plan-card {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
padding: 12px;
|
|
|
|
|
|
border: 1px solid #e5edf1;
|
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.compact-work-card {
|
|
|
|
|
|
min-height: 58px;
|
|
|
|
|
|
align-content: center;
|
|
|
|
|
|
padding: 10px 12px;
|
|
|
|
|
|
box-shadow: 0 4px 14px rgba(15, 23, 42, 0.04);
|
|
|
|
|
|
transition:
|
|
|
|
|
|
border-color 0.2s,
|
|
|
|
|
|
box-shadow 0.2s,
|
|
|
|
|
|
transform 0.2s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.compact-work-card:hover {
|
2026-06-21 18:22:44 +08:00
|
|
|
|
border-color: var(--el-color-primary-light-5);
|
2026-06-11 10:56:24 +08:00
|
|
|
|
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.08);
|
|
|
|
|
|
transform: translateY(-1px);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.review-card-head,
|
|
|
|
|
|
.plan-card-head {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: 28px minmax(0, 1fr) 28px;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-23 17:59:42 +08:00
|
|
|
|
.review-card-head.no-remove,
|
2026-06-11 10:56:24 +08:00
|
|
|
|
.plan-card-head.no-remove {
|
|
|
|
|
|
grid-template-columns: 28px minmax(0, 1fr);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.row-index {
|
|
|
|
|
|
width: 30px;
|
|
|
|
|
|
height: 30px;
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
place-items: center;
|
|
|
|
|
|
border-radius: 999px;
|
2026-06-21 18:22:44 +08:00
|
|
|
|
background: var(--el-color-primary);
|
2026-06-11 10:56:24 +08:00
|
|
|
|
color: #fff;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
font-weight: 900;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.compact-work-card .row-index {
|
|
|
|
|
|
width: 28px;
|
|
|
|
|
|
height: 28px;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.review-title-readonly,
|
|
|
|
|
|
.plan-title-readonly {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.work-title-line {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
flex-wrap: nowrap;
|
|
|
|
|
|
min-height: 30px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.review-title-readonly strong,
|
|
|
|
|
|
.plan-title-readonly strong {
|
|
|
|
|
|
color: #606266;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 400;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.compact-work-card .review-title-readonly strong,
|
|
|
|
|
|
.compact-work-card .plan-title-readonly strong {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 400;
|
|
|
|
|
|
line-height: 1.3;
|
|
|
|
|
|
overflow-wrap: anywhere;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.work-title-input {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
flex: 1 1 160px;
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
border-bottom: 1px solid transparent;
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
color: #606266;
|
|
|
|
|
|
font: inherit;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 400;
|
|
|
|
|
|
line-height: 1.3;
|
|
|
|
|
|
outline: none;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
padding: 1px 0;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.work-title-input:focus {
|
2026-06-21 18:22:44 +08:00
|
|
|
|
border-bottom-color: var(--el-color-primary);
|
2026-06-11 10:56:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.work-title-line span {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
height: 24px;
|
|
|
|
|
|
padding: 0 8px;
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
background: #f3f7f9;
|
|
|
|
|
|
color: #667085;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.work-title-line .priority-chip {
|
|
|
|
|
|
background: #fff7ed;
|
|
|
|
|
|
color: #c2410c;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.meta-chip {
|
|
|
|
|
|
flex: 0 0 auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.item-remove-btn {
|
|
|
|
|
|
width: 26px;
|
|
|
|
|
|
height: 26px;
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
place-items: center;
|
|
|
|
|
|
border: 1px solid #fecaca;
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
color: #dc2626;
|
|
|
|
|
|
font: inherit;
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
line-height: 1;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.item-remove-btn:hover {
|
|
|
|
|
|
background: #fef2f2;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.fixed-textarea :deep(.el-textarea__inner) {
|
|
|
|
|
|
scrollbar-width: thin;
|
|
|
|
|
|
scrollbar-color: #94a3b8 transparent;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.fixed-textarea :deep(.el-textarea__inner::-webkit-scrollbar) {
|
|
|
|
|
|
width: 6px;
|
|
|
|
|
|
height: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.fixed-textarea :deep(.el-textarea__inner::-webkit-scrollbar-track) {
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.fixed-textarea :deep(.el-textarea__inner::-webkit-scrollbar-thumb) {
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
background: #94a3b8;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.fixed-textarea :deep(.el-textarea__inner::-webkit-scrollbar-button) {
|
|
|
|
|
|
width: 0;
|
|
|
|
|
|
height: 0;
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.fixed-textarea :deep(.el-textarea__inner) {
|
|
|
|
|
|
height: 86px;
|
|
|
|
|
|
min-height: 86px;
|
|
|
|
|
|
max-height: 86px;
|
|
|
|
|
|
resize: none;
|
|
|
|
|
|
overflow: auto;
|
|
|
|
|
|
padding: 5px 11px;
|
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.rich-editor {
|
|
|
|
|
|
min-height: 86px;
|
|
|
|
|
|
padding: 5px 11px;
|
|
|
|
|
|
border-radius: 9px;
|
|
|
|
|
|
outline: none;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
box-shadow: 0 0 0 1px #d8e0e8 inset;
|
|
|
|
|
|
color: #334155;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
|
overflow: auto;
|
|
|
|
|
|
scrollbar-width: thin;
|
|
|
|
|
|
scrollbar-color: #94a3b8 transparent;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.rich-editor:hover {
|
|
|
|
|
|
box-shadow: 0 0 0 1px #b8c4cf inset;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.rich-editor:focus {
|
|
|
|
|
|
box-shadow:
|
2026-06-21 18:22:44 +08:00
|
|
|
|
0 0 0 1px var(--el-color-primary) inset,
|
|
|
|
|
|
0 0 0 2px var(--el-color-primary-light-8);
|
2026-06-11 10:56:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.rich-editor:empty::before {
|
|
|
|
|
|
content: attr(data-placeholder);
|
|
|
|
|
|
color: #98a2b3;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.rich-editor::-webkit-scrollbar {
|
|
|
|
|
|
width: 6px;
|
|
|
|
|
|
height: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.rich-editor::-webkit-scrollbar-track {
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.rich-editor::-webkit-scrollbar-thumb {
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
background: #94a3b8;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.rich-editor::-webkit-scrollbar-button {
|
|
|
|
|
|
width: 0;
|
|
|
|
|
|
height: 0;
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.inline-plan-card {
|
2026-06-21 18:22:44 +08:00
|
|
|
|
border-color: var(--el-color-primary-light-5);
|
2026-06-11 10:56:24 +08:00
|
|
|
|
background: #f8fbfc;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.inline-edit-line {
|
|
|
|
|
|
overflow: visible;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.inline-title-field {
|
|
|
|
|
|
flex: 1 1 220px;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.inline-title-input {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.inline-select {
|
|
|
|
|
|
width: 96px;
|
|
|
|
|
|
flex: 0 0 96px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.inline-progress {
|
|
|
|
|
|
width: 132px;
|
|
|
|
|
|
flex: 0 0 132px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.duplicate-tip {
|
|
|
|
|
|
color: #dc2626;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
line-height: 1.2;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.inline-plan-actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.inline-plan-card :deep(.el-input__wrapper),
|
|
|
|
|
|
.inline-plan-card :deep(.el-select__wrapper) {
|
|
|
|
|
|
border-radius: 9px;
|
|
|
|
|
|
box-shadow: 0 0 0 1px #d8e0e8 inset;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.inline-plan-card :deep(.el-input-number__increase) {
|
|
|
|
|
|
border-top-right-radius: 9px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.inline-plan-card :deep(.el-input-number__decrease) {
|
|
|
|
|
|
border-bottom-right-radius: 9px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.inline-progress :deep(.el-input__wrapper) {
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.inline-plan-card :deep(.el-input__wrapper.is-focus),
|
|
|
|
|
|
.inline-plan-card :deep(.el-select__wrapper.is-focused) {
|
|
|
|
|
|
box-shadow:
|
2026-06-21 18:22:44 +08:00
|
|
|
|
0 0 0 1px var(--el-color-primary) inset,
|
|
|
|
|
|
0 0 0 2px var(--el-color-primary-light-8);
|
2026-06-11 10:56:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.form-actions {
|
2026-06-13 13:06:39 +08:00
|
|
|
|
position: sticky;
|
|
|
|
|
|
z-index: 5;
|
|
|
|
|
|
bottom: 0;
|
2026-06-11 10:56:24 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
gap: 10px;
|
2026-06-13 13:06:39 +08:00
|
|
|
|
margin-top: auto;
|
|
|
|
|
|
margin-bottom: 0;
|
2026-06-11 10:56:24 +08:00
|
|
|
|
padding: 14px 20px;
|
|
|
|
|
|
border-top: 1px solid #d8e0e8;
|
2026-06-13 13:06:39 +08:00
|
|
|
|
border-bottom-left-radius: 18px;
|
|
|
|
|
|
border-bottom-right-radius: 18px;
|
|
|
|
|
|
background: #f5f7fa;
|
|
|
|
|
|
box-shadow: 0 -8px 18px rgba(15, 23, 42, 0.06);
|
2026-06-11 10:56:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.approval-form-actions {
|
|
|
|
|
|
position: sticky;
|
|
|
|
|
|
z-index: 5;
|
|
|
|
|
|
bottom: 0;
|
|
|
|
|
|
margin-top: auto;
|
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
|
border-bottom-left-radius: 18px;
|
|
|
|
|
|
border-bottom-right-radius: 18px;
|
|
|
|
|
|
background: #f5f7fa;
|
|
|
|
|
|
box-shadow: 0 -8px 18px rgba(15, 23, 42, 0.06);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 1180px) {
|
|
|
|
|
|
.compose-grid,
|
|
|
|
|
|
.review-grid,
|
|
|
|
|
|
.plan-list {
|
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
|
.section-title {
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.inline-edit-line {
|
|
|
|
|
|
align-items: stretch;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.inline-title-field,
|
|
|
|
|
|
.inline-select,
|
|
|
|
|
|
.inline-progress {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
flex-basis: auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|