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

1077 lines
28 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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