|
|
@@ -0,0 +1,437 @@
|
|
|
+<template>
|
|
|
+ <div class="page-project-progress">
|
|
|
+ <div class="page-project-progress-body">
|
|
|
+ <div class="page-project-progress-breadcrumb">
|
|
|
+ <button type="button" class="page-project-progress-crumb" @click="goBack">
|
|
|
+ 项目明细
|
|
|
+ </button>
|
|
|
+ <span class="page-project-progress-crumb-sep">></span>
|
|
|
+ <span class="page-project-progress-crumb is-current">项目进展编辑</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="page-project-progress-panel">
|
|
|
+ <h2 class="page-project-progress-title">
|
|
|
+ {{ projectName || "—" }}
|
|
|
+ </h2>
|
|
|
+
|
|
|
+ <div class="page-project-progress-tabs">
|
|
|
+ <button
|
|
|
+ v-for="section in sections"
|
|
|
+ :key="section.sectionId"
|
|
|
+ type="button"
|
|
|
+ class="page-project-progress-tab"
|
|
|
+ :class="{ 'is-active': activeSectionId === section.sectionId }"
|
|
|
+ @click="activeSectionId = section.sectionId"
|
|
|
+ >
|
|
|
+ {{ section.sectionName }}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="page-project-progress-editor">
|
|
|
+ <div class="page-project-progress-toolbar">
|
|
|
+ <div class="page-project-progress-toolbar-left">
|
|
|
+ <button type="button" @click="exec('undo')">↺</button>
|
|
|
+ <button type="button" @click="exec('redo')">↻</button>
|
|
|
+ <button type="button" class="is-mark" @click="exec('bold')">
|
|
|
+ B
|
|
|
+ </button>
|
|
|
+ <button type="button" class="is-mark" @click="exec('italic')">
|
|
|
+ I
|
|
|
+ </button>
|
|
|
+ <button type="button" class="is-mark" @click="exec('underline')">
|
|
|
+ U
|
|
|
+ </button>
|
|
|
+ <span class="page-project-progress-size">16</span>
|
|
|
+ <span
|
|
|
+ v-for="color in colors"
|
|
|
+ :key="color"
|
|
|
+ class="page-project-progress-color"
|
|
|
+ :style="{ background: color }"
|
|
|
+ @click="exec('foreColor', color)"
|
|
|
+ ></span>
|
|
|
+ </div>
|
|
|
+ <button type="button" class="page-project-progress-clear" @click="clearContent">
|
|
|
+ 清除
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ ref="editorRef"
|
|
|
+ class="page-project-progress-content"
|
|
|
+ contenteditable="true"
|
|
|
+ @input="onInput"
|
|
|
+ ></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="page-project-progress-actions">
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ class="page-project-progress-btn is-outline"
|
|
|
+ @click="goBack"
|
|
|
+ >
|
|
|
+ 返回
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ class="page-project-progress-btn is-primary"
|
|
|
+ @click="onSave"
|
|
|
+ >
|
|
|
+ 保存
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { nextTick, ref, watch } from "vue";
|
|
|
+import { useRoute, useRouter } from "vue-router";
|
|
|
+import { ElMessage } from "element-plus";
|
|
|
+import { getProjectProgress, saveProjectProgress } from "@/api/project";
|
|
|
+
|
|
|
+defineOptions({
|
|
|
+ name: "ProjectProgress",
|
|
|
+});
|
|
|
+
|
|
|
+const colors = ["#16213a", "#f54848", "#ff8e38", "#10b837", "#2382e7"];
|
|
|
+
|
|
|
+const route = useRoute();
|
|
|
+const router = useRouter();
|
|
|
+const editorRef = ref(null);
|
|
|
+const projectName = ref("");
|
|
|
+const sections = ref([]);
|
|
|
+const activeSectionId = ref(null);
|
|
|
+
|
|
|
+const getSectionContent = (section) => {
|
|
|
+ if (!section) return "";
|
|
|
+ const progress = section.progress;
|
|
|
+ if (progress && typeof progress === "object") return progress.content || "";
|
|
|
+ return "";
|
|
|
+};
|
|
|
+
|
|
|
+const setActiveContent = (html) => {
|
|
|
+ const current = sections.value.find(
|
|
|
+ (item) => item.sectionId === activeSectionId.value
|
|
|
+ );
|
|
|
+ if (!current) return;
|
|
|
+ current.progress = {
|
|
|
+ ...(current.progress && typeof current.progress === "object"
|
|
|
+ ? current.progress
|
|
|
+ : {}),
|
|
|
+ content: html,
|
|
|
+ };
|
|
|
+};
|
|
|
+
|
|
|
+const exec = (command, value = null) => {
|
|
|
+ editorRef.value?.focus();
|
|
|
+ document.execCommand(command, false, value);
|
|
|
+};
|
|
|
+
|
|
|
+const onInput = () => {
|
|
|
+ setActiveContent(editorRef.value?.innerHTML || "");
|
|
|
+};
|
|
|
+
|
|
|
+const clearContent = () => {
|
|
|
+ if (!editorRef.value) return;
|
|
|
+ editorRef.value.innerHTML = "";
|
|
|
+ setActiveContent("");
|
|
|
+};
|
|
|
+
|
|
|
+const syncEditor = async () => {
|
|
|
+ await nextTick();
|
|
|
+ const current = sections.value.find(
|
|
|
+ (item) => item.sectionId === activeSectionId.value
|
|
|
+ );
|
|
|
+ if (editorRef.value) {
|
|
|
+ editorRef.value.innerHTML = getSectionContent(current);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const goBack = () => {
|
|
|
+ router.push({ name: "particulars" });
|
|
|
+};
|
|
|
+
|
|
|
+const onSave = async () => {
|
|
|
+ const projectId = route.query.projectId;
|
|
|
+ if (!projectId) {
|
|
|
+ ElMessage.error("缺少项目ID");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ setActiveContent(editorRef.value?.innerHTML || "");
|
|
|
+ try {
|
|
|
+ const res = await saveProjectProgress({
|
|
|
+ projectId,
|
|
|
+ progressList: sections.value.map((item) => ({
|
|
|
+ sectionId: item.sectionId,
|
|
|
+ content: getSectionContent(item),
|
|
|
+ })),
|
|
|
+ });
|
|
|
+ if (res?.code !== 200) {
|
|
|
+ ElMessage.error(res?.msg || "保存失败");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ ElMessage.success(res?.msg || "保存成功");
|
|
|
+ } catch (error) {
|
|
|
+ console.error(error);
|
|
|
+ ElMessage.error("保存失败,请稍后重试");
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const applyProgress = (data) => {
|
|
|
+ if (!data || typeof data !== "object") {
|
|
|
+ sections.value = [];
|
|
|
+ activeSectionId.value = null;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ projectName.value = data.projectName || "";
|
|
|
+ const list = Array.isArray(data.sections) ? data.sections : [];
|
|
|
+ sections.value = list.map((item) => ({
|
|
|
+ sectionId: item.sectionId,
|
|
|
+ sectionName: item.sectionName || "",
|
|
|
+ progress: {
|
|
|
+ content:
|
|
|
+ item.progress && typeof item.progress === "object"
|
|
|
+ ? item.progress.content || ""
|
|
|
+ : "",
|
|
|
+ },
|
|
|
+ }));
|
|
|
+ activeSectionId.value = sections.value[0]?.sectionId ?? null;
|
|
|
+};
|
|
|
+
|
|
|
+const loadProgress = async () => {
|
|
|
+ projectName.value = "";
|
|
|
+ sections.value = [];
|
|
|
+ activeSectionId.value = null;
|
|
|
+ const projectId = route.query.projectId;
|
|
|
+ if (!projectId) {
|
|
|
+ ElMessage.error("缺少项目ID");
|
|
|
+ await syncEditor();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ const res = await getProjectProgress(projectId);
|
|
|
+ if (res?.code !== 200) {
|
|
|
+ ElMessage.error(res?.msg || "获取项目进展失败");
|
|
|
+ await syncEditor();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ applyProgress(res.data);
|
|
|
+ } catch (error) {
|
|
|
+ console.error(error);
|
|
|
+ ElMessage.error("获取项目进展失败,请稍后重试");
|
|
|
+ }
|
|
|
+ await syncEditor();
|
|
|
+};
|
|
|
+
|
|
|
+watch(
|
|
|
+ () => route.query.projectId,
|
|
|
+ () => {
|
|
|
+ loadProgress();
|
|
|
+ },
|
|
|
+ { immediate: true }
|
|
|
+);
|
|
|
+
|
|
|
+watch(activeSectionId, () => {
|
|
|
+ syncEditor();
|
|
|
+});
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+.page-project-progress {
|
|
|
+ box-sizing: border-box;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ min-height: 0;
|
|
|
+ overflow: hidden;
|
|
|
+ background: #f3f7fc;
|
|
|
+}
|
|
|
+
|
|
|
+.page-project-progress-body {
|
|
|
+ flex: 1;
|
|
|
+ min-height: 0;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 16px;
|
|
|
+ padding: 24px;
|
|
|
+ overflow: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.page-project-progress-breadcrumb {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ font-family: PingFang SC, PingFang SC;
|
|
|
+ font-size: 14px;
|
|
|
+ line-height: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.page-project-progress-crumb {
|
|
|
+ padding: 0;
|
|
|
+ border: none;
|
|
|
+ background: transparent;
|
|
|
+ color: #58657c;
|
|
|
+ cursor: pointer;
|
|
|
+
|
|
|
+ &.is-current {
|
|
|
+ color: #16213a;
|
|
|
+ cursor: default;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.page-project-progress-crumb-sep {
|
|
|
+ color: #9aa5b8;
|
|
|
+}
|
|
|
+
|
|
|
+.page-project-progress-panel {
|
|
|
+ box-sizing: border-box;
|
|
|
+ display: flex;
|
|
|
+ flex: 1;
|
|
|
+ flex-direction: column;
|
|
|
+ min-height: 0;
|
|
|
+ padding: 24px 16px;
|
|
|
+ background: #ffffff;
|
|
|
+}
|
|
|
+
|
|
|
+.page-project-progress-title {
|
|
|
+ margin: 0 0 16px;
|
|
|
+ font-family: PingFang SC, PingFang SC;
|
|
|
+ font-weight: 600;
|
|
|
+ font-size: 18px;
|
|
|
+ color: #16213a;
|
|
|
+ line-height: 25px;
|
|
|
+ text-align: left;
|
|
|
+}
|
|
|
+
|
|
|
+.page-project-progress-tabs {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 8px;
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.page-project-progress-tab {
|
|
|
+ padding: 6px 16px;
|
|
|
+ border: none;
|
|
|
+ border-radius: 2px;
|
|
|
+ background: #f6f9fd;
|
|
|
+ font-family: PingFang SC, PingFang SC;
|
|
|
+ font-weight: 400;
|
|
|
+ font-size: 14px;
|
|
|
+ color: #58657c;
|
|
|
+ line-height: 20px;
|
|
|
+ cursor: pointer;
|
|
|
+
|
|
|
+ &.is-active {
|
|
|
+ color: #ffffff;
|
|
|
+ background: #2382e7;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.page-project-progress-editor {
|
|
|
+ display: flex;
|
|
|
+ flex: 1;
|
|
|
+ flex-direction: column;
|
|
|
+ min-height: 360px;
|
|
|
+ border: 1px solid #e5ebf3;
|
|
|
+ border-radius: 2px;
|
|
|
+}
|
|
|
+
|
|
|
+.page-project-progress-toolbar {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding: 8px 12px;
|
|
|
+ border-bottom: 1px solid #e5ebf3;
|
|
|
+}
|
|
|
+
|
|
|
+.page-project-progress-toolbar-left {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+
|
|
|
+ button {
|
|
|
+ padding: 0 4px;
|
|
|
+ border: none;
|
|
|
+ background: transparent;
|
|
|
+ color: #58657c;
|
|
|
+ cursor: pointer;
|
|
|
+ }
|
|
|
+
|
|
|
+ .is-mark {
|
|
|
+ font-weight: 700;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.page-project-progress-size {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #58657c;
|
|
|
+}
|
|
|
+
|
|
|
+.page-project-progress-color {
|
|
|
+ width: 12px;
|
|
|
+ height: 12px;
|
|
|
+ border-radius: 50%;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.page-project-progress-clear {
|
|
|
+ padding: 0;
|
|
|
+ border: none;
|
|
|
+ background: transparent;
|
|
|
+ color: #9aa5b8;
|
|
|
+ font-size: 14px;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.page-project-progress-content {
|
|
|
+ flex: 1;
|
|
|
+ min-height: 0;
|
|
|
+ padding: 16px;
|
|
|
+ overflow: auto;
|
|
|
+ outline: none;
|
|
|
+ font-family: PingFang SC, PingFang SC;
|
|
|
+ font-size: 14px;
|
|
|
+ color: #16213a;
|
|
|
+ line-height: 22px;
|
|
|
+ text-align: left;
|
|
|
+ white-space: pre-wrap;
|
|
|
+}
|
|
|
+
|
|
|
+.page-project-progress-actions {
|
|
|
+ flex-shrink: 0;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ gap: 24px;
|
|
|
+ width: 100%;
|
|
|
+ padding: 12px;
|
|
|
+ background: #ffffff;
|
|
|
+ border-top: 1px solid #e5ebf3;
|
|
|
+}
|
|
|
+
|
|
|
+.page-project-progress-btn {
|
|
|
+ padding: 6px 16px;
|
|
|
+ border-radius: 2px;
|
|
|
+ font-family: PingFang SC, PingFang SC;
|
|
|
+ font-size: 14px;
|
|
|
+ line-height: 20px;
|
|
|
+ cursor: pointer;
|
|
|
+
|
|
|
+ &.is-outline {
|
|
|
+ color: #58657c;
|
|
|
+ font-weight: 400;
|
|
|
+ background: #ffffff;
|
|
|
+ border: 1px solid #e4eaf2;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.is-primary {
|
|
|
+ color: #ffffff;
|
|
|
+ font-weight: 500;
|
|
|
+ background: #2382e7;
|
|
|
+ border: 1px solid #2382e7;
|
|
|
+ box-shadow: 0px 4px 10px 0px rgba(35, 130, 231, 0.2);
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|