Files
cn-rdms-web/src/views/personal-center/work-report/project/modules/fill-page.vue

1077 lines
28 KiB
Vue
Raw Normal View History

<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[]>(() => {
return (props.model.currentItems || []).map((item, index) => toWorkItem(item, index, true));
});
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);
}
function removeCurrentWorkItem(index: number) {
const item = currentWorks.value[index];
if (item?.sourceIndex !== undefined) props.model.currentItems.splice(item.sourceIndex, 1);
}
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];
// if (nextTitle !== previousTitle) {
// ElMessage.success('名称已成功修改');
// }
}
</script>
<template>
<div class="card form-page">
<div class="section">
<div class="section-title">
<span>基础信息</span>
<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>
</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">
<div class="review-card-head" :class="{ 'no-remove': item.removable === false }">
<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>
<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>
</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>
<ElButton type="primary" @click="emit('submit')">提交审批</ElButton>
</div>
<div v-else-if="scene === 'approval'" class="form-actions approval-form-actions">
<ElButton @click="emit('back')">退出</ElButton>
<ElButton type="primary" @click="emit('requestApprove')">审批</ElButton>
</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;
background: var(--el-color-primary-light-8);
color: var(--el-color-primary);
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 {
background: var(--el-color-primary-light-9);
color: var(--el-color-primary);
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 {
border-color: var(--el-color-primary-light-5);
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;
}
.review-card-head.no-remove,
.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;
background: var(--el-color-primary);
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 {
border-bottom-color: var(--el-color-primary);
}
.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:
0 0 0 1px var(--el-color-primary) inset,
0 0 0 2px var(--el-color-primary-light-8);
}
.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 {
border-color: var(--el-color-primary-light-5);
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:
0 0 0 1px var(--el-color-primary) inset,
0 0 0 2px var(--el-color-primary-light-8);
}
.form-actions {
position: sticky;
z-index: 5;
bottom: 0;
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: auto;
margin-bottom: 0;
padding: 14px 20px;
border-top: 1px solid #d8e0e8;
border-bottom-left-radius: 18px;
border-bottom-right-radius: 18px;
background: #f5f7fa;
box-shadow: 0 -8px 18px rgba(15, 23, 42, 0.06);
}
.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>