d5279b124f
Storage Persistence Guard / storage-persistence-audit (push) Has been cancelled
Client Quality Gates / Shared client and Web (push) Has been cancelled
Client Quality Gates / macOS Desktop (push) Has been cancelled
Client Quality Gates / Shared client and Web (pull_request) Has been cancelled
Client Quality Gates / macOS Desktop (pull_request) Has been cancelled
Storage Persistence Guard / storage-persistence-audit (pull_request) Has been cancelled
697 lines
21 KiB
Vue
697 lines
21 KiB
Vue
<template>
|
||
<div class="page ctms-page-shell page--flush">
|
||
<div v-if="study.currentStudy" class="unified-shell ctms-table-card">
|
||
<section class="unified-action-bar milestone-action-bar">
|
||
<div class="card-header ctms-page-header-row">
|
||
<div class="card-title">{{ TEXT.modules.projectMilestones.listTitle }}</div>
|
||
<div class="header-actions">
|
||
<el-tag size="small" effect="plain">
|
||
{{ TEXT.modules.projectMilestones.statTotal }}{{ rows.length }}
|
||
</el-tag>
|
||
<el-tag size="small" type="success" effect="plain">
|
||
{{ TEXT.modules.projectMilestones.statDone }}{{ doneCount }}
|
||
</el-tag>
|
||
<el-button size="small" @click="loadMilestones">{{ TEXT.common.actions.refresh }}</el-button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
<section class="unified-section ctms-page-content-grid section--flush-x section--flush-top section--flush-bottom">
|
||
<el-table :data="rows" v-loading="loading" class="ctms-table" style="width: 100%" table-layout="fixed">
|
||
<el-table-column prop="name" :label="TEXT.modules.projectMilestones.columns.name" show-overflow-tooltip />
|
||
<el-table-column prop="planDate" :label="TEXT.modules.projectMilestones.columns.planDate">
|
||
<template #default="scope">
|
||
<div class="plan-time-cell">
|
||
<div class="plan-time-line">
|
||
<span class="plan-time-dot plan-time-dot-initial" />
|
||
<span class="plan-time-label">开始:</span>
|
||
<span>{{ scope.row.startDate || TEXT.common.fallback }}</span>
|
||
</div>
|
||
<div class="plan-time-line">
|
||
<span class="plan-time-dot plan-time-dot-initial" />
|
||
<span class="plan-time-label">结束:</span>
|
||
<span>{{ scope.row.endDate || TEXT.common.fallback }}</span>
|
||
</div>
|
||
<div class="plan-time-line">
|
||
<span class="plan-time-dot plan-time-dot-initial" />
|
||
<span class="plan-time-label">耗时:</span>
|
||
<span>{{ scope.row.durationText }}</span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column :label="TEXT.modules.projectMilestones.columns.adjustedPlanDate">
|
||
<template #default="scope">
|
||
<div class="plan-time-cell">
|
||
<div class="plan-time-line">
|
||
<span class="plan-time-dot plan-time-dot-adjusted" />
|
||
<span class="plan-time-label">开始:</span>
|
||
<span>{{ scope.row.adjustedStartDate || TEXT.common.fallback }}</span>
|
||
</div>
|
||
<div class="plan-time-line">
|
||
<span class="plan-time-dot plan-time-dot-adjusted" />
|
||
<span class="plan-time-label">结束:</span>
|
||
<span>{{ scope.row.adjustedEndDate || TEXT.common.fallback }}</span>
|
||
</div>
|
||
<div class="plan-time-line">
|
||
<span class="plan-time-dot plan-time-dot-adjusted" />
|
||
<span class="plan-time-label">耗时:</span>
|
||
<span>{{ scope.row.adjustedDurationText }}</span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column :label="TEXT.modules.projectMilestones.columns.actualTime">
|
||
<template #default="scope">
|
||
<div class="plan-time-cell">
|
||
<div class="plan-time-line">
|
||
<span class="plan-time-dot plan-time-dot-actual" />
|
||
<span class="plan-time-label">开始:</span>
|
||
<span>{{ scope.row.actualStartDate || TEXT.common.fallback }}</span>
|
||
</div>
|
||
<div class="plan-time-line">
|
||
<span class="plan-time-dot plan-time-dot-actual" />
|
||
<span class="plan-time-label">结束:</span>
|
||
<span>{{ scope.row.actualEndDate || TEXT.common.fallback }}</span>
|
||
</div>
|
||
<div class="plan-time-line">
|
||
<span class="plan-time-dot plan-time-dot-actual" />
|
||
<span class="plan-time-label">耗时:</span>
|
||
<span>{{ scope.row.actualDurationText }}</span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="status" :label="TEXT.modules.projectMilestones.columns.status">
|
||
<template #default="scope">
|
||
<div class="status-cell" :class="`status-${getStatusView(scope.row).tone}`">
|
||
<div class="status-main">
|
||
<span class="status-dot" />
|
||
<span>{{ getStatusView(scope.row).title }}<span v-if="getStatusView(scope.row).detail">,</span></span>
|
||
</div>
|
||
<div v-if="getStatusView(scope.row).detail" class="status-detail">{{ getStatusView(scope.row).detail }}</div>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="remark" :label="TEXT.modules.projectMilestones.columns.remark" show-overflow-tooltip>
|
||
<template #default="scope">{{ scope.row.remark || TEXT.common.fallback }}</template>
|
||
</el-table-column>
|
||
<el-table-column v-if="canUpdateMilestones" :label="TEXT.modules.projectMilestones.columns.operations" width="100">
|
||
<template #default="scope">
|
||
<el-button v-if="canUpdateMilestones" link type="primary" @click="openEditor(scope.row)">{{ TEXT.common.actions.edit }}</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
<template #empty>
|
||
<div v-if="!loading" class="table-empty">{{ TEXT.modules.projectMilestones.emptyDescription }}</div>
|
||
</template>
|
||
</el-table>
|
||
</section>
|
||
</div>
|
||
<StateEmpty v-else :description="TEXT.common.empty.selectProject" />
|
||
|
||
<el-drawer
|
||
v-if="editorVisible"
|
||
v-model="editorVisible"
|
||
direction="rtl"
|
||
size="460px"
|
||
:close-on-click-modal="true"
|
||
:before-close="editorDirtyGuard.beforeClose"
|
||
:show-close="false"
|
||
class="milestone-editor-drawer"
|
||
>
|
||
<template #header>
|
||
<div class="time-editor-title">编辑里程碑</div>
|
||
</template>
|
||
<el-form label-position="top" class="time-editor-form">
|
||
<div class="time-editor-group">
|
||
<div class="time-editor-group-title">
|
||
<span class="group-dot group-dot-adjusted"></span>
|
||
调整计划时间
|
||
</div>
|
||
<el-row :gutter="12">
|
||
<el-col :span="12">
|
||
<el-form-item label="开始日期">
|
||
<el-date-picker v-model="editorForm.adjustedStartDate" type="date" value-format="YYYY-MM-DD" class="w-full" placeholder="选择日期" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="结束日期">
|
||
<el-date-picker v-model="editorForm.adjustedEndDate" type="date" value-format="YYYY-MM-DD" class="w-full" placeholder="选择日期" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<div class="duration-inline">
|
||
<span class="duration-label">耗时</span>
|
||
<span class="duration-value">{{ toDurationText(calcDurationDays(editorForm.adjustedStartDate, editorForm.adjustedEndDate)) }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="time-editor-group">
|
||
<div class="time-editor-group-title">
|
||
<span class="group-dot group-dot-actual"></span>
|
||
实际时间
|
||
</div>
|
||
<el-row :gutter="12">
|
||
<el-col :span="12">
|
||
<el-form-item label="开始日期">
|
||
<el-date-picker v-model="editorForm.actualStartDate" type="date" value-format="YYYY-MM-DD" class="w-full" placeholder="选择日期" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="结束日期">
|
||
<el-date-picker v-model="editorForm.actualEndDate" type="date" value-format="YYYY-MM-DD" class="w-full" placeholder="选择日期" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<div class="duration-inline">
|
||
<span class="duration-label">耗时</span>
|
||
<span class="duration-value">{{ toDurationText(calcDurationDays(editorForm.actualStartDate, editorForm.actualEndDate)) }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="time-editor-group">
|
||
<div class="time-editor-group-title">
|
||
<span class="group-dot group-dot-remark"></span>
|
||
备注
|
||
</div>
|
||
<el-input v-model="editorForm.remark" type="textarea" :rows="3" placeholder="请输入备注" />
|
||
</div>
|
||
</el-form>
|
||
<template #footer>
|
||
<div class="time-editor-footer">
|
||
<el-button @click="editorVisible = false">{{ TEXT.common.actions.cancel }}</el-button>
|
||
<el-button v-if="canUpdateMilestones" type="primary" @click="saveEditor">{{ TEXT.common.actions.save }}</el-button>
|
||
</div>
|
||
</template>
|
||
</el-drawer>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, onMounted, reactive, ref, watch } from "vue";
|
||
import { ElMessage } from "element-plus";
|
||
import { useStudyStore } from "../../store/study";
|
||
import { listProjectMilestones, updateProjectMilestone } from "../../api/projectMilestones";
|
||
import { TEXT } from "../../locales";
|
||
import StateEmpty from "../../components/StateEmpty.vue";
|
||
import { usePermission } from "../../utils/permission";
|
||
import { useDrawerDirtyGuard } from "../../utils/drawerDirtyGuard";
|
||
|
||
interface MilestoneRow {
|
||
id: string;
|
||
name: string;
|
||
planDate: string;
|
||
startDate: string;
|
||
endDate: string;
|
||
durationDays: number | null;
|
||
durationText: string;
|
||
adjustedStartDate: string;
|
||
adjustedEndDate: string;
|
||
adjustedDurationDays: number | null;
|
||
adjustedDurationText: string;
|
||
actualStartDate: string;
|
||
actualEndDate: string;
|
||
actualDurationDays: number | null;
|
||
actualDurationText: string;
|
||
owner: string;
|
||
status: string;
|
||
remark: string;
|
||
}
|
||
|
||
type MilestoneLocalEdit = {
|
||
adjustedStartDate?: string;
|
||
adjustedEndDate?: string;
|
||
actualStartDate?: string;
|
||
actualEndDate?: string;
|
||
remark?: string;
|
||
};
|
||
|
||
const study = useStudyStore();
|
||
const { can } = usePermission();
|
||
const rows = ref<MilestoneRow[]>([]);
|
||
const loading = ref(false);
|
||
const editorVisible = ref(false);
|
||
const editingRowId = ref("");
|
||
const editorForm = reactive<MilestoneLocalEdit>({
|
||
adjustedStartDate: "",
|
||
adjustedEndDate: "",
|
||
actualStartDate: "",
|
||
actualEndDate: "",
|
||
remark: "",
|
||
});
|
||
const editorDirtyGuard = useDrawerDirtyGuard(() => editorForm);
|
||
|
||
const doneCount = computed(() => rows.value.filter((item) => getStatusView(item).completed).length);
|
||
const canUpdateMilestones = computed(() => can("project.milestones.update"));
|
||
|
||
const toDateOnly = (value?: string): Date | null => {
|
||
if (!value) return null;
|
||
const d = new Date(`${value}T00:00:00`);
|
||
if (Number.isNaN(d.getTime())) return null;
|
||
return d;
|
||
};
|
||
|
||
const calcDurationDays = (start?: string, end?: string): number | null => {
|
||
if (!start || !end) return null;
|
||
const startDate = toDateOnly(start);
|
||
const endDate = toDateOnly(end);
|
||
if (!startDate || !endDate || endDate < startDate) return null;
|
||
return Math.floor((endDate.getTime() - startDate.getTime()) / 86400000) + 1;
|
||
};
|
||
|
||
const toDurationText = (days: number | null) => (days && days > 0 ? `${days}天` : TEXT.common.fallback);
|
||
|
||
const normalizeRow = (row: any): MilestoneRow => {
|
||
const startDate = String(row?.planned_date || "").trim();
|
||
const endDate = String(row?.planned_date || "").trim();
|
||
const adjustedStartDate = String(row?.adjusted_start_date || "").trim();
|
||
const adjustedEndDate = String(row?.adjusted_end_date || "").trim();
|
||
const actualStartDate = String(row?.actual_start_date || "").trim();
|
||
const actualEndDate = String(row?.actual_end_date || "").trim();
|
||
const adjustedDurationDays = calcDurationDays(adjustedStartDate, adjustedEndDate);
|
||
const actualDurationDays = calcDurationDays(actualStartDate, actualEndDate);
|
||
return {
|
||
id: row?.id || "",
|
||
name: String(row?.name || "").trim(),
|
||
planDate: startDate,
|
||
startDate,
|
||
endDate,
|
||
durationDays: startDate ? 1 : null,
|
||
durationText: startDate ? "1天" : TEXT.common.fallback,
|
||
adjustedStartDate,
|
||
adjustedEndDate,
|
||
adjustedDurationDays,
|
||
adjustedDurationText: toDurationText(adjustedDurationDays),
|
||
actualStartDate,
|
||
actualEndDate,
|
||
actualDurationDays,
|
||
actualDurationText: toDurationText(actualDurationDays),
|
||
owner: String(row?.owner_name || "").trim(),
|
||
status: String(row?.status || "NOT_STARTED").trim(),
|
||
remark: String(row?.notes || "").trim(),
|
||
};
|
||
};
|
||
|
||
const loadMilestones = async () => {
|
||
const studyId = study.currentStudy?.id;
|
||
if (!studyId) return;
|
||
loading.value = true;
|
||
try {
|
||
const { data } = (await listProjectMilestones(studyId)) as any;
|
||
const list = Array.isArray(data) ? data : data?.items || [];
|
||
rows.value = list.map(normalizeRow);
|
||
} catch (error: any) {
|
||
rows.value = [];
|
||
ElMessage.error(error?.response?.data?.detail || TEXT.common.messages.loadFailed);
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
};
|
||
|
||
const daysDiff = (start: string, end: string): number => {
|
||
const a = toDateOnly(start);
|
||
const b = toDateOnly(end);
|
||
if (!a || !b) return 0;
|
||
return Math.floor((b.getTime() - a.getTime()) / 86400000);
|
||
};
|
||
|
||
const todayDateText = () => {
|
||
const now = new Date();
|
||
const pad = (n: number) => String(n).padStart(2, "0");
|
||
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
|
||
};
|
||
|
||
const getStatusView = (row: MilestoneRow): { title: string; detail: string; tone: "success" | "warning" | "danger" | "info"; completed: boolean } => {
|
||
const planEnd = row.adjustedEndDate || row.endDate || "";
|
||
const actualStart = row.actualStartDate || "";
|
||
const actualEnd = row.actualEndDate || "";
|
||
const today = todayDateText();
|
||
|
||
if (actualEnd) {
|
||
if (planEnd && actualEnd > planEnd) {
|
||
const lateDays = daysDiff(planEnd, actualEnd);
|
||
return {
|
||
title: "延期完成",
|
||
detail: lateDays > 0 ? `延迟 ${lateDays} 天` : "",
|
||
tone: "danger",
|
||
completed: true,
|
||
};
|
||
}
|
||
if (planEnd && actualEnd < planEnd) {
|
||
const earlyDays = daysDiff(actualEnd, planEnd);
|
||
return {
|
||
title: "提前完成",
|
||
detail: earlyDays > 0 ? `提前 ${earlyDays} 天` : "",
|
||
tone: "success",
|
||
completed: true,
|
||
};
|
||
}
|
||
return { title: "已完成", detail: "", tone: "success", completed: true };
|
||
}
|
||
|
||
if (actualStart) {
|
||
if (planEnd && today > planEnd) {
|
||
const lateDays = daysDiff(planEnd, today);
|
||
return {
|
||
title: "延期进行中",
|
||
detail: lateDays > 0 ? `延迟 ${lateDays} 天` : "",
|
||
tone: "danger",
|
||
completed: false,
|
||
};
|
||
}
|
||
return { title: "进行中", detail: "", tone: "warning", completed: false };
|
||
}
|
||
|
||
if (planEnd && today > planEnd) {
|
||
const lateDays = daysDiff(planEnd, today);
|
||
return {
|
||
title: "延期未开始",
|
||
detail: lateDays > 0 ? `延迟 ${lateDays} 天` : "",
|
||
tone: "danger",
|
||
completed: false,
|
||
};
|
||
}
|
||
|
||
return { title: "未开始", detail: "", tone: "info", completed: false };
|
||
};
|
||
|
||
const validateDateRange = (start?: string, end?: string, label?: string) => {
|
||
if ((start && !end) || (!start && end)) {
|
||
ElMessage.warning(`${label}开始和结束日期需同时填写`);
|
||
return false;
|
||
}
|
||
if (start && end && start > end) {
|
||
ElMessage.warning(`${label}结束日期不能早于开始日期`);
|
||
return false;
|
||
}
|
||
return true;
|
||
};
|
||
|
||
const openEditor = (row: MilestoneRow) => {
|
||
if (!canUpdateMilestones.value) {
|
||
ElMessage.warning("权限不足");
|
||
return;
|
||
}
|
||
editingRowId.value = row.id;
|
||
editorForm.adjustedStartDate = row.adjustedStartDate || "";
|
||
editorForm.adjustedEndDate = row.adjustedEndDate || "";
|
||
editorForm.actualStartDate = row.actualStartDate || "";
|
||
editorForm.actualEndDate = row.actualEndDate || "";
|
||
editorForm.remark = row.remark || "";
|
||
editorDirtyGuard.syncBaseline();
|
||
editorVisible.value = true;
|
||
};
|
||
|
||
const saveEditor = async () => {
|
||
const studyId = study.currentStudy?.id;
|
||
if (!studyId || !editingRowId.value) return;
|
||
if (!canUpdateMilestones.value) {
|
||
ElMessage.warning("权限不足");
|
||
return;
|
||
}
|
||
if (!validateDateRange(editorForm.adjustedStartDate, editorForm.adjustedEndDate, "调整计划时间")) return;
|
||
if (!validateDateRange(editorForm.actualStartDate, editorForm.actualEndDate, "实际时间")) return;
|
||
const patch = {
|
||
adjusted_start_date: editorForm.adjustedStartDate || null,
|
||
adjusted_end_date: editorForm.adjustedEndDate || null,
|
||
actual_start_date: editorForm.actualStartDate || null,
|
||
actual_end_date: editorForm.actualEndDate || null,
|
||
notes: (editorForm.remark || "").trim() || null,
|
||
};
|
||
try {
|
||
await updateProjectMilestone(studyId, editingRowId.value, patch);
|
||
await loadMilestones();
|
||
editorVisible.value = false;
|
||
ElMessage.success("里程碑时间已更新");
|
||
} catch (error: any) {
|
||
ElMessage.error(error?.response?.data?.detail || TEXT.common.messages.saveFailed);
|
||
}
|
||
};
|
||
|
||
onMounted(() => {
|
||
loadMilestones();
|
||
});
|
||
|
||
watch(
|
||
() => study.currentStudy?.id,
|
||
() => {
|
||
rows.value = [];
|
||
loadMilestones();
|
||
}
|
||
);
|
||
</script>
|
||
|
||
<style scoped>
|
||
.page {
|
||
display: flex;
|
||
flex-direction: column;
|
||
/* 撑满父容器(desktop-route-shell 已经是 height: 100%)*/
|
||
height: 100%;
|
||
min-height: 0;
|
||
}
|
||
|
||
/* 让白色卡片壳撑满 .page 的剩余高度,并去掉圆角 */
|
||
.page > .unified-shell {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-height: 0;
|
||
border-radius: 0;
|
||
}
|
||
|
||
/* 让表格 section 撑满卡片壳的剩余高度 */
|
||
.page > .unified-shell > .unified-section {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-height: 0;
|
||
}
|
||
|
||
/* el-table 本身填满 section */
|
||
.page > .unified-shell > .unified-section :deep(.el-table),
|
||
.page > .unified-shell > .unified-section :deep(.el-table__inner-wrapper) {
|
||
height: 100%;
|
||
}
|
||
|
||
.ctms-page-content-grid {
|
||
gap: 0;
|
||
}
|
||
|
||
.card-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
margin-bottom: 0;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.milestone-action-bar {
|
||
min-height: 56px;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.card-title {
|
||
font-size: 15px;
|
||
font-weight: 700;
|
||
color: var(--unified-title-color);
|
||
}
|
||
|
||
.header-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.plan-time-cell {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
.plan-time-line {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
color: var(--ctms-text-main);
|
||
font-size: 11px;
|
||
}
|
||
|
||
.plan-time-dot {
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
border: 3px solid #2f84c6;
|
||
box-sizing: border-box;
|
||
background: var(--ctms-bg-card);
|
||
}
|
||
|
||
.plan-time-label {
|
||
color: var(--ctms-text-secondary);
|
||
font-size: 11px;
|
||
}
|
||
|
||
.plan-time-dot-initial {
|
||
border-color: #2f84c6;
|
||
}
|
||
|
||
.plan-time-dot-adjusted {
|
||
border-color: #f0ad2c;
|
||
}
|
||
|
||
.plan-time-dot-actual {
|
||
border-color: #67c23a;
|
||
}
|
||
|
||
.time-editor-form {
|
||
padding-top: 4px;
|
||
padding-right: 4px;
|
||
}
|
||
|
||
.time-editor-group {
|
||
border: 1px solid var(--ctms-border-color);
|
||
border-radius: 10px;
|
||
padding: 14px 16px 10px;
|
||
background: var(--ctms-bg-muted);
|
||
}
|
||
|
||
.time-editor-group + .time-editor-group {
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.time-editor-title {
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
color: var(--ctms-text-main);
|
||
}
|
||
|
||
.time-editor-group-title {
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
margin-bottom: 12px;
|
||
color: var(--ctms-text-main);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.group-dot {
|
||
width: 10px;
|
||
height: 10px;
|
||
border-radius: 50%;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.group-dot-adjusted {
|
||
background: #f0ad2c;
|
||
}
|
||
|
||
.group-dot-actual {
|
||
background: #67c23a;
|
||
}
|
||
|
||
.group-dot-remark {
|
||
background: #909399;
|
||
}
|
||
|
||
.duration-inline {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 6px 0 2px;
|
||
}
|
||
|
||
.duration-label {
|
||
font-size: 13px;
|
||
color: var(--ctms-text-secondary);
|
||
font-weight: 500;
|
||
}
|
||
|
||
.duration-value {
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
color: var(--ctms-text-main);
|
||
}
|
||
|
||
.time-editor-footer {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 10px;
|
||
}
|
||
|
||
.time-editor-footer :deep(.el-button--primary) {
|
||
border-radius: 8px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.time-editor-form :deep(.el-form-item) {
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.time-editor-form :deep(.el-form-item__label) {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: var(--ctms-text-regular);
|
||
padding-bottom: 4px;
|
||
}
|
||
|
||
.status-cell {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1px;
|
||
line-height: 1.3;
|
||
font-size: 11px;
|
||
}
|
||
|
||
.status-main {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
}
|
||
|
||
.status-dot {
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
background: #94a3b8;
|
||
}
|
||
|
||
.status-detail {
|
||
padding-left: 14px;
|
||
color: var(--ctms-text-secondary);
|
||
font-size: 11px;
|
||
}
|
||
|
||
.status-success .status-dot {
|
||
background: #22c55e;
|
||
}
|
||
|
||
.status-warning .status-dot {
|
||
background: #f59e0b;
|
||
}
|
||
|
||
.status-danger .status-dot {
|
||
background: #ef4444;
|
||
}
|
||
|
||
.status-info .status-dot {
|
||
background: #94a3b8;
|
||
}
|
||
|
||
.ctms-table :deep(.el-table__inner-wrapper::before) {
|
||
display: none;
|
||
}
|
||
|
||
.table-empty {
|
||
min-height: 220px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: var(--ctms-text-secondary);
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
letter-spacing: 0.02em;
|
||
}
|
||
|
||
</style>
|