浏览代码

首页样式调整

dsy 10 小时之前
父节点
当前提交
b41de70eff

+ 1 - 1
.env

@@ -11,4 +11,4 @@ VUE_APP_BookWebSI = '/GCLSBookWebSI/ServiceInterface'
 VUE_APP_EepServer = '/EEPServer/SI'
 
 #version
-VUE_APP_VERSION = '2026.06.02'
+VUE_APP_VERSION = '2026.06.03'

+ 11 - 0
src/api/list.js

@@ -188,3 +188,14 @@ export function PageQueryTemplateListAdmin(data) {
 export function QueryLabelList(data) {
   return http.post(`${process.env.VUE_APP_EepServer}?MethodName=template_manager-QueryLabelList`, data);
 }
+
+/**
+ * @description 分页查询我的项目列表(综合)
+ * @param {object} data
+ * @param {number} data.page_capacity 每页容量
+ * @param {number} data.cur_page 当前查询页
+ * @param {number} data.type 类型 0【全部】,1【管理】,2【制作】,3【审核】,4【已上架】
+ */
+export function PageQueryMyProjectList(data) {
+  return http.post(`${process.env.VUE_APP_EepServer}?MethodName=page_query-PageQueryMyProjectList`, data);
+}

+ 7 - 0
src/api/user.js

@@ -76,3 +76,10 @@ export function getSysConfigBaiduDict() {
 export function setSysConfigBaiduDict(data) {
   return http.post(`${process.env.VUE_APP_EepServer}?MethodName=sys_config_manager-SetSysConfig_BaiduDict`, data);
 }
+
+/**
+ * @description 得到我的项目统计信息
+ */
+export function GetMyProjectStatInfo() {
+  return http.post(`${process.env.VUE_APP_EepServer}?MethodName=page_query-GetMyProjectStatInfo`);
+}

+ 19 - 2
src/components/PinyinText.vue

@@ -33,7 +33,9 @@
                   'align-items': getWordAlignItems(word, block),
                 }"
               >
-                <span class="pinyin" :style="getPinyinStyle(word, block)"> {{ getPinyinText(word) }}</span>
+                <span v-if="shouldShowWordPinyin(word)" class="pinyin" :style="getPinyinStyle(word, block)">
+                  {{ getPinyinText(word) }}
+                </span>
                 <span class="py-char" :style="getCharStyle(word, block)">{{ convertText(word.text) }}</span>
               </span>
               <template v-else>
@@ -44,7 +46,9 @@
                       'align-items': getWordAlignItems(word, block),
                     }"
                   >
-                    <span class="pinyin" :style="getPinyinStyle(word, block)"> {{ getCharPinyin(word, cIndex) }}</span>
+                    <span v-if="shouldShowWordPinyin(word)" class="pinyin" :style="getPinyinStyle(word, block)">
+                      {{ getCharPinyin(word, cIndex) }}
+                    </span>
                     <span class="py-char" :style="getCharStyle(word, block, cIndex)">{{ convertText(char) }}</span>
                   </span>
                 </span>
@@ -240,6 +244,10 @@ export default {
       type: Object,
       default: () => ({}),
     },
+    isShowPinyin: {
+      type: Boolean,
+      default: true,
+    },
   },
   data() {
     return {
@@ -666,6 +674,9 @@ export default {
     getPinyinText(item) {
       return this.checkShowPinyin(item.showPinyin) ? item.pinyin.replace(/\s+/g, '') : '\u200B';
     },
+    shouldShowWordPinyin(item) {
+      return this.checkShowPinyin(item && item.showPinyin);
+    },
     // 拼音与汉字一一对应
     getCharPinyin(word, charIndex) {
       if (!this.checkShowPinyin(word.showPinyin)) {
@@ -752,11 +763,17 @@ export default {
     },
     // 如段落有拼音,则返回true ,否则不显示拼音行
     hasPinyinInParagraph(paragraphIndex) {
+      if (!this.isShowPinyin) {
+        return false;
+      }
       const paragraph = this.paragraphList[paragraphIndex];
       return paragraph.some((sentence) => sentence.some((item) => this.checkShowPinyin(item.showPinyin)));
     },
     // 兼容历史数据,没有 showPinyin 字段的,则默认为 true
     checkShowPinyin(showPinyin) {
+      if (!this.isShowPinyin) {
+        return false;
+      }
       if (showPinyin === undefined || showPinyin === null) {
         return true;
       }

+ 6 - 6
src/components/RichText.vue

@@ -468,7 +468,7 @@ export default {
             text: '●',
             tooltip: '着重点',
             onAction: () => {
-              const editor = tinymce.activeEditor;
+              const editor = tinymce.get(this.id);
 
               if (editor.formatter.match('emphasisDot')) {
                 editor.formatter.remove('emphasisDot');
@@ -1331,7 +1331,7 @@ export default {
      * @returns {object} 包含字体、字号、颜色、加粗、下划线、删除线等样式属性的对象
      */
     getFirstCharStyles() {
-      const editor = tinymce.activeEditor;
+      const editor = tinymce.get(this.id);
       if (!editor) return {};
 
       const firstTextNode = this.findFirstTextNode(editor.getBody());
@@ -1405,7 +1405,7 @@ export default {
      * @returns {object} 包含字体、字号、颜色等body初始样式属性的对象
      */
     getBodyInitialStyles() {
-      const editor = tinymce.activeEditor;
+      const editor = tinymce.get(this.id);
       if (!editor) return {};
 
       const body = editor.getBody();
@@ -1423,11 +1423,11 @@ export default {
       if (computed.fontSize && computed.fontSize !== 'inherit') {
         const fontSize = computed.fontSize;
         const pxValue = parseFloat(fontSize);
-        if (!isNaN(pxValue)) {
+        if (isNaN(pxValue)) {
+          styles.fontSize = fontSize;
+        } else {
           const ptValue = Math.round(pxValue * 0.75 * 10) / 10;
           styles.fontSize = `${ptValue}pt`;
-        } else {
-          styles.fontSize = fontSize;
         }
       }
 

+ 4 - 0
src/icons/svg/home/home-close.svg

@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+  <line x1="18" y1="6" x2="6" y2="18"/>
+  <line x1="6" y1="6" x2="18" y2="18"/>
+</svg>

+ 5 - 0
src/icons/svg/home/home-help.svg

@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#64748B" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+  <circle cx="12" cy="12" r="10"/>
+  <path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/>
+  <line x1="12" y1="17" x2="12.01" y2="17"/>
+</svg>

+ 4 - 0
src/icons/svg/home/home-make.svg

@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="56" height="56" viewBox="0 0 24 24" fill="none" stroke="#3B82F6" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
+  <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
+  <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
+</svg>

+ 4 - 0
src/icons/svg/home/home-manage.svg

@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="56" height="56" viewBox="0 0 24 24" fill="none" stroke="#F59E0B" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
+  <path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/>
+  <path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/>
+</svg>

+ 4 - 0
src/icons/svg/home/home-new.svg

@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="56" height="56" viewBox="0 0 24 24" fill="none" stroke="#3B82F6" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
+  <path d="M12 5v14"/>
+  <path d="M5 12h14"/>
+</svg>

+ 4 - 0
src/icons/svg/home/home-notification.svg

@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#64748B" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+  <path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/>
+  <path d="M13.73 21a2 2 0 0 1-3.46 0"/>
+</svg>

+ 6 - 0
src/icons/svg/home/home-pinyin.svg

@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="#10B981" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
+  <path d="M4 7V4h16v3"/>
+  <path d="M9 20h6"/>
+  <path d="M12 4v16"/>
+  <path d="m15 15-3 3-3-3"/>
+</svg>

+ 4 - 0
src/icons/svg/home/home-review.svg

@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="56" height="56" viewBox="0 0 24 24" fill="none" stroke="#8B5CF6" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
+  <path d="M9 11l3 3L22 4"/>
+  <path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/>
+</svg>

+ 6 - 0
src/icons/svg/home/home-template.svg

@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="#3B82F6" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
+  <rect x="3" y="3" width="7" height="7"/>
+  <rect x="14" y="3" width="7" height="7"/>
+  <rect x="3" y="14" width="7" height="7"/>
+  <rect x="14" y="14" width="7" height="7"/>
+</svg>

+ 6 - 0
src/icons/svg/home/home-tools.svg

@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="56" height="56" viewBox="0 0 24 24" fill="none" stroke="#10B981" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+  <rect x="2" y="2" width="9" height="9" rx="1"/>
+  <rect x="13" y="2" width="9" height="9" rx="1"/>
+  <rect x="2" y="13" width="9" height="9" rx="1"/>
+  <rect x="13" y="13" width="9" height="9" rx="1"/>
+</svg>

+ 107 - 181
src/views/book/courseware/create/components/question/fill/Fill.vue

@@ -30,6 +30,29 @@
         </div>
 
         <span class="tips">在需要加空的内容处插入 3 个或以上的下划线“_”。</span>
+        <el-divider content-position="left">
+          <span>显示效果</span>
+          <el-button
+            type="text"
+            icon="el-icon-refresh"
+            title="刷新"
+            class="refresh-pinyin-btn"
+            @click.native="parsedContentPinyin"
+          />
+        </el-divider>
+        <div class="pinyin-wrapper">
+          <div v-for="(item, i) in data.model_essay" :key="i" class="pinyin-text-list">
+            <PinyinText
+              :key="`pinyin-${i}`"
+              ref="PinyinText"
+              :is-show-pinyin="isEnable(data.property.view_pinyin)"
+              :rich-text-list="item.rich_text_list"
+              :pinyin-position="data.property.pinyin_position"
+              :body-styles="pinyinBodyStyles"
+              @fillCorrectPinyin="fillCorrectPinyin($event, i, -1, 'model_essay')"
+            />
+          </div>
+        </div>
         <div v-if="data.audio_file_id">
           <SoundRecord :wav-blob.sync="data.audio_file_id" />
         </div>
@@ -70,30 +93,6 @@
         </div>
       </div>
 
-      <el-divider v-if="isEnable(data.property.view_pinyin)" content-position="left">
-        <span>拼音效果</span>
-        <el-button
-          v-show="isEnable(data.property.view_pinyin)"
-          type="text"
-          icon="el-icon-refresh"
-          title="刷新"
-          class="refresh-pinyin-btn"
-          @click.native="parsedContentPinyin"
-        />
-      </el-divider>
-      <template v-if="isEnable(data.property.view_pinyin)">
-        <div v-for="(item, i) in data.model_essay" :key="i" class="pinyin-text-list">
-          <PinyinText
-            :key="`pinyin-${i}`"
-            ref="PinyinText"
-            :rich-text-list="item.rich_text_list"
-            :pinyin-position="data.property.pinyin_position"
-            :body-styles="getBodyStyles()"
-            @fillCorrectPinyin="fillCorrectPinyin($event, i, -1, 'model_essay')"
-          />
-        </div>
-      </template>
-
       <MultilingualFill
         :visible.sync="multilingualVisible"
         :text="data.content"
@@ -145,6 +144,16 @@ export default {
       rich_text_list: [],
     };
   },
+  computed: {
+    pinyinBodyStyles() {
+      const unifiedAttrib = this.data?.unified_attrib || {};
+      return {
+        fontFamily: unifiedAttrib.font || undefined,
+        fontSize: unifiedAttrib.font_size || undefined,
+        color: unifiedAttrib.text_color || undefined,
+      };
+    },
+  },
   watch: {
     'data.property.arrange_type': 'handleMindMap',
     'data.property.fill_font': 'handleMindMap',
@@ -170,6 +179,7 @@ export default {
         }
       });
     },
+    // 解析富文本,构建 model_essay 结构
     parseRichText() {
       let text_list = this.rich_text_list || [];
       const preservedAnyOneAnswers = (this.data.answer.answer_list || [])
@@ -269,6 +279,11 @@ export default {
 
       return arr;
     },
+    /**
+     * 从标签文本中提取标签名称,支持开标签和闭标签
+     * @param {String} tagText 标签文本,如 "<b>"、"</b>"、"<br/>"
+     * @returns {String} 标签名称,如 "b"、"br",如果不是有效标签则返回空字符串
+     */
     getStyleTagName(tagText = '') {
       const trimmedText = String(tagText).trim();
       const closeMatch = trimmedText.match(/^<\/(\w+)>$/);
@@ -279,6 +294,11 @@ export default {
 
       return '';
     },
+    /**
+     * 判断标签项是否为有效的开标签(非自闭合标签且非换行标签)
+     * @param {Object} tagItem 标签项对象,包含 text 和 is_style 属性
+     * @returns {Boolean} 是否为开标签
+     */
     isOpenStyleTag(tagItem = {}) {
       const tagText = String(tagItem?.text || '').trim();
       const isStyleTag = tagItem?.is_style === 'true' || tagItem?.is_style === true;
@@ -287,6 +307,11 @@ export default {
       if (/^<br\s*\/?\s*>$/i.test(tagText)) return false;
       return /^<\w+[^>]*>$/.test(tagText);
     },
+    /**
+     * 同步更新样式标签栈,遇到开标签入栈,遇到闭标签出栈
+     * @param {Array} openStyleTagStack 当前的开标签栈
+     * @param {Object} styleTagItem 当前处理的标签项对象
+     */
     syncOpenStyleTagStack(openStyleTagStack, styleTagItem) {
       const tagText = String(styleTagItem?.text || '').trim();
       const isStyleTag = styleTagItem?.is_style === 'true' || styleTagItem?.is_style === true;
@@ -310,17 +335,46 @@ export default {
         openStyleTagStack.push(styleTagItem);
       }
     },
+    /**
+     * 获取开标签的身份标识,用于匹配开闭标签,通常是标签名称加上关键属性(如 class),以区分不同样式的同名标签
+     * @param {Object} tagItem 标签项对象,包含 text 和 is_style 属性
+     * @return {String} 开标签的身份标识,如 "b"、"span.class1",如果不是有效开标签则返回空字符串
+     */
+    getOpenStyleTagIdentity(tagItem = {}) {
+      return String(tagItem?.text || '')
+        .trim()
+        .replace(/\s+/g, ' ');
+    },
+    /**
+     * 构建文本块的样式前缀,包含过渡样式标签和当前仍应处于打开状态的样式标签
+     * @param {Array} transitionStyleTags 过渡样式标签列表,可能包含开标签和闭标签
+     * @param {Array} currentOpenStyleTags 当前仍应处于打开状态的样式标签列表
+     * @returns {Array} 构建好的样式前缀列表
+     */
     buildFirstBlockStylePrefix(transitionStyleTags = [], currentOpenStyleTags = []) {
-      const transitionOpenTagNames = transitionStyleTags
+      const transitionOpenTagCount = transitionStyleTags
         .filter((tagItem) => this.isOpenStyleTag(tagItem))
-        .map((tagItem) => this.getStyleTagName(tagItem?.text));
+        .reduce((counter, tagItem) => {
+          const identity = this.getOpenStyleTagIdentity(tagItem);
+          if (!identity) return counter;
+          counter[identity] = (counter[identity] || 0) + 1;
+          return counter;
+        }, {});
 
       const missingOpenTags = currentOpenStyleTags.filter((tagItem) => {
-        const tagName = this.getStyleTagName(tagItem?.text);
-        return tagName && !transitionOpenTagNames.includes(tagName);
+        const identity = this.getOpenStyleTagIdentity(tagItem);
+        if (!identity) return false;
+
+        if (transitionOpenTagCount[identity]) {
+          transitionOpenTagCount[identity] -= 1;
+          return false;
+        }
+
+        return true;
       });
 
-      return [...missingOpenTags, ...transitionStyleTags];
+      // 先回放过渡标签(含可能的关闭标签),再补齐当前仍应处于打开状态的标签。
+      return [...transitionStyleTags, ...missingOpenTags];
     },
     /**
      * 根据文本中的连续下划线分割文本块,并将下划线部分转换为输入块
@@ -408,6 +462,13 @@ export default {
 
       return blocks;
     },
+    /**
+     * 根据文本范围切割词列表,返回切割后的词列表
+     * @param {Array} wordList 富文本项中的词列表,每个词项包含 text 和其他属性
+     * @param {Number} rangeStart 切割范围的起始位置(相对于文本开头的字符索引)
+     * @param {Number} rangeEnd 切割范围的结束位置(相对于文本开头的字符索引)
+     * @returns {Array} 切割后的词列表,包含在切割范围内的词项,且每个词项的 text 已根据切割范围调整
+     */
     sliceWordListByTextRange(wordList = [], rangeStart = 0, rangeEnd = 0) {
       if (!Array.isArray(wordList) || rangeEnd <= rangeStart) return [];
 
@@ -433,6 +494,13 @@ export default {
 
       return result;
     },
+    /**
+     * 根据文本范围切割单个词项,返回切割后的词项
+     * @param {Object} wordItem 词项对象,包含 text 和其他属性
+     * @param {Number} start 切割范围的起始位置(相对于词项的字符索引)
+     * @param {Number} end 切割范围的结束位置(相对于词项的字符索引)
+     * @returns {Object} 切割后的词项,包含在切割范围内的字符,且 text 已根据切割范围调整
+     */
     sliceWordItem(wordItem, start, end) {
       const fullText = wordItem?.text || '';
       const slicedText = fullText.slice(start, end);
@@ -458,147 +526,7 @@ export default {
       slicedWord.pinyin = '';
       return slicedWord;
     },
-    /**
-     * 识别文本中
-     * @param {Boolean} isUpdatePinyin 是否更新拼音,默认为 true
-     */
-    identifyText(isUpdatePinyin = true) {
-      this.data.answer.answer_list = [];
-      const content = this.data.content || '';
 
-      // 使用 class 为 rich-fill 的 span 以及连续 3 个及以上下划线作为分割符
-      if (!content || !content.match(/<span[^>]*class="[^"]*rich-fill[^"]*"[^>]*>(.*?)<\/span>|_{3,}/gi)) {
-        this.data.model_essay = [
-          [
-            {
-              content,
-              type: 'text',
-              paragraph_list: [],
-              paragraph_list_parameter: { text: '', pinyin_proofread_word_list: [] },
-            },
-          ],
-        ];
-        return;
-      }
-
-      const splitSource = content.split(/\n|<br>/).map((item) => {
-        // rich-fill 和 ___ 均转为统一占位符,交给 splitRichText 处理
-        return this.splitRichText(
-          item.replace(/<span[^>]*class="[^"]*rich-fill[^"]*"[^>]*>.*?<\/span>|_{3,}/gi, '###$&###'),
-        );
-      });
-
-      this.data.model_essay = splitSource;
-
-      if (isUpdatePinyin) this.handleViewPinyin();
-    },
-    /**
-     * 分割富文本
-     * @param {String} str 富文本字符串
-     * @returns {Array} 分割后的数组
-     */
-    splitRichText(str) {
-      const parts = String(str).split(/###/g);
-      const arr = [];
-
-      for (let i = 0; i < parts.length; i++) {
-        let content = parts[i] ?? '';
-
-        // 偶数索引为普通文本段
-        if (i % 2 === 0) {
-          if (content === '') continue; // 跳过空文本块
-          // 判断 content 最前面是否是标签
-          const isStartWithTag = /^<[^>]+>/.test(content);
-
-          if (!isStartWithTag) {
-            content = this.setTag(i, parts, content);
-          }
-
-          arr.push({
-            content,
-            type: 'text',
-            paragraph_list: [],
-            paragraph_list_parameter: {
-              text: '',
-              pinyin_proofread_word_list: [],
-            },
-          });
-          continue;
-        }
-
-        // 奇数索引为输入段(被 ### 包裹的分割符)
-        const separatorContent = content;
-        const isUnderline = /^_{3,}$/.test(separatorContent);
-        const richFillMatch = separatorContent.match(/^<span[^>]*class="[^"]*rich-fill[^"]*"[^>]*>(.*?)<\/span>$/i);
-        const answerValue = isUnderline ? '' : richFillMatch ? richFillMatch[1] : separatorContent;
-        const mark = getRandomNumber();
-
-        arr.push({
-          content: separatorContent,
-          type: 'input',
-          input: '',
-          audio_answer_list: [],
-          mark,
-          paragraph_list: [],
-          paragraph_list_parameter: {
-            text: '',
-            pinyin_proofread_word_list: [],
-          },
-        });
-
-        // 同步更新答案列表
-        this.data.answer.answer_list.push({
-          value: answerValue,
-          mark,
-          type: isUnderline ? 'any_one' : 'only_one',
-        });
-      }
-      return arr;
-    },
-
-    /**
-     * 设置前一个标签
-     * @param {Number} index 当前索引
-     * @param {Array} parts 分割后的数组
-     * @param {String} content 当前内容
-     * @returns {String} 包含向前两个标签内容中最后一个html标签的内容
-     */
-    setTag(index, parts, content) {
-      let i = index;
-      if (i < 2) return content;
-      let _content = content;
-      const isEndWithTag = /<\/[^>]+>$/.test(_content); // 判断是否以标签结尾
-      let startTag = '';
-      const part = parts[i - 2] ?? '';
-
-      const tagMatch = part.match(/<[^>]+>/g);
-      if (tagMatch) {
-        startTag = tagMatch[tagMatch.length - 1]; // 获取最后一个标签
-        // 如果是 <br> 标签,继续往前找,直到找到非 <br> 标签或者没有标签为止
-
-        while (startTag.toLowerCase() === '<br>') {
-          const prevPart = parts[i - 3] ?? '';
-          const prevTagMatch = prevPart.match(/<[^>]+>/g);
-          if (prevTagMatch === null) {
-            startTag = '';
-            break;
-          }
-          if (prevTagMatch) {
-            startTag = prevTagMatch[prevTagMatch.length - 1];
-            i -= 1;
-          } else {
-            break;
-          }
-        }
-      }
-      _content = `${startTag}${_content}`;
-      if (!isEndWithTag) {
-        let tag = startTag.match(/^<([^>\s]+).*?>/);
-        tag = tag ? tag[1] : 'span';
-        _content += `</${tag}>`;
-      }
-      return _content;
-    },
     handleTone(value, i) {
       if (!/^[a-zA-Z0-9\s]+$/.test(value)) return;
       this.data.answer.answer_list[i].value = value
@@ -677,10 +605,6 @@ export default {
     removeWord(index) {
       this.data.word_list.splice(index, 1);
     },
-    getBodyStyles() {
-      if (!this.$refs.richText) return {};
-      return this.$refs.richText.getBodyInitialStyles();
-    },
   },
 };
 </script>
@@ -795,16 +719,18 @@ export default {
   }
 }
 
-.pinyin-text-list {
-  display: inline;
-
-  :deep .pinyin-area {
+.pinyin-wrapper {
+  .pinyin-text-list {
     display: inline;
-  }
 
-  :deep .pinyin-area .rich-text-container,
-  :deep .pinyin-area .pinyin-paragraph {
-    display: inline;
+    :deep .pinyin-area {
+      display: inline;
+    }
+
+    :deep .pinyin-area .rich-text-container,
+    :deep .pinyin-area .pinyin-paragraph {
+      display: inline;
+    }
   }
 }
 </style>

+ 14 - 43
src/views/book/courseware/preview/components/fill/FillPreview.vue

@@ -14,16 +14,15 @@
           <template v-for="(li, i) in modelEssay">
             <template v-if="li.type === 'text'">
               <PinyinText
-                v-if="isEnable(data.property.view_pinyin)"
                 :key="`text-${i}`"
                 class="content"
                 :paragraph-list="li.paragraph_list"
                 :rich-text-list="li.rich_text_list"
                 :pinyin-position="data.property.pinyin_position"
-                :body-styles="getBodyStyles()"
+                :body-styles="pinyinBodyStyles"
+                :is-show-pinyin="isEnable(data.property.view_pinyin)"
                 :is-preview="true"
               />
-              <span v-else :key="`text-${i}`" class="html-content" v-html="renderTextBlockContent(li)"></span>
             </template>
             <template v-if="li.type === 'input'">
               <!-- 输入填空 -->
@@ -130,15 +129,15 @@
           <template v-for="(li, i) in modelEssay">
             <template v-if="li.type === 'text'">
               <PinyinText
-                v-if="isEnable(data.property.view_pinyin)"
                 :key="`answer-text-${i}`"
+                :is-show-pinyin="isEnable(data.property.view_pinyin)"
                 class="content"
                 :paragraph-list="li.paragraph_list"
                 :rich-text-list="li.rich_text_list"
+                :body-styles="pinyinBodyStyles"
                 :pinyin-position="data.property.pinyin_position"
                 :is-preview="true"
               />
-              <span v-else :key="`answer-text-${i}`" class="html-content" v-html="renderTextBlockContent(li)"></span>
             </template>
             <template v-if="li.type === 'input'">
               <span v-show="computedAnswerText(li.mark).length > 0" :key="`answer-${i}`" class="right-answer">
@@ -181,6 +180,16 @@ export default {
       writeMark: '',
     };
   },
+  computed: {
+    pinyinBodyStyles() {
+      const unifiedAttrib = this.data?.unified_attrib || {};
+      return {
+        fontFamily: unifiedAttrib.font || undefined,
+        fontSize: unifiedAttrib.font_size || undefined,
+        color: unifiedAttrib.text_color || undefined,
+      };
+    },
+  },
   watch: {
     'data.model_essay': {
       handler(list) {
@@ -221,21 +230,6 @@ export default {
     handleWav(data) {
       this.data.record_list = data;
     },
-    renderContent(content) {
-      return this.convertText(this.sanitizeHTML(content));
-    },
-    /**
-     * 渲染文本块内容
-     * @param {Object} textBlock 文本块对象,包含纯文本内容和富文本内容
-     */
-    renderTextBlockContent(textBlock) {
-      const richTextList = textBlock?.rich_text_list;
-      if (Array.isArray(richTextList) && richTextList.length > 0) {
-        const richHtml = richTextList.map((item) => item?.text || '').join('');
-        return this.renderContent(richHtml);
-      }
-      return this.renderContent(textBlock?.content || '');
-    },
     syncAnswerList(list) {
       this.answer.answer_list = list
         .map(({ type, input, audio_answer_list, mark }) => {
@@ -478,23 +472,6 @@ export default {
 
       this.$set(this.inputWidthMap, mark, nextWidth);
     },
-    getBodyStyles() {
-      const styles = {};
-      const unifiedAttrib = this.data?.unified_attrib || {};
-
-      // 从统一属性中获取样式
-      if (unifiedAttrib.font) {
-        styles['font-family'] = unifiedAttrib.font;
-      }
-      if (unifiedAttrib.font_size) {
-        styles['font-size'] = unifiedAttrib.font_size;
-      }
-      if (unifiedAttrib.text_color) {
-        styles['color'] = unifiedAttrib.text_color;
-      }
-
-      return styles;
-    },
   },
 };
 </script>
@@ -528,12 +505,6 @@ export default {
       display: inline;
     }
 
-    .html-content {
-      :deep p {
-        display: inline;
-      }
-    }
-
     .record-box {
       display: inline-flex;
       align-items: center;

+ 802 - 57
src/views/home/index.vue

@@ -1,116 +1,861 @@
 <template>
   <div class="home">
-    <div class="home-top">
-      <el-button class="create" size="medium" @click="jump('/create_project')">
-        教材创建 <i class="el-icon-plus"> </i>
-      </el-button>
-    </div>
-    <div class="home-content">
-      <div
-        v-for="{ color, title, path, icon } in itemList"
-        :key="title"
-        class="item"
-        :style="{ backgroundColor: color }"
-        @click="jump(path)"
-      >
-        <span class="title">{{ title }}</span>
-        <span class="icon" :style="{ backgroundColor: color }">
-          <SvgIcon :icon-class="icon" size="36" />
-        </span>
+    <div class="home-main">
+      <div class="home-content">
+        <div
+          v-for="({ title, path, icon, desc, iconBg, bg }, index) in itemList"
+          :key="title"
+          class="item"
+          :style="{ background: bg }"
+          :class="`item--${index + 1}`"
+          @click="onItemClick(path, index)"
+        >
+          <span class="icon" :style="{ backgroundColor: iconBg }">
+            <SvgIcon :icon-class="icon" size="30" />
+          </span>
+          <span class="title">{{ title }}</span>
+          <span class="desc">
+            {{ desc }}
+          </span>
+        </div>
+      </div>
+
+      <div class="overview-row">
+        <div
+          v-for="{ label, key, dotColor } in overviewList"
+          :key="label"
+          class="stat-card"
+          :class="{ active: currentOverview === key }"
+          @click="changeOverview(key)"
+        >
+          <span class="stat-label"><span class="dot" :style="{ backgroundColor: dotColor }"></span>{{ label }}</span>
+          <strong class="stat-value">{{ stateInfo[key] }}</strong>
+        </div>
+      </div>
+
+      <div class="list-section">
+        <el-table
+          v-if="currentOverview === overviewList[0].key"
+          class="project-list"
+          :data="projectList"
+          height="calc(100vh - 630px)"
+        >
+          <el-table-column prop="sn" width="150" align="center" header-align="center" label="编号" />
+          <el-table-column prop="name" label="教材名称" width="460" header-align="left" />
+          <el-table-column prop="version" label="版本" width="120" align="center" header-align="center" />
+          <el-table-column prop="leader_name_desc" label="项目组长" width="220" align="center" header-align="center" />
+
+          <el-table-column prop="progress_percent" label="进度" width="180" align="center" header-align="center">
+            <template slot-scope="{ row }">
+              <div class="progress">
+                <span class="progress-bar"><i :style="{ width: `${row.progress_percent}%` }"></i></span>
+                <em>{{ row.progress_percent }}%</em>
+              </div>
+            </template>
+          </el-table-column>
+          <el-table-column prop="create_date" label="创建日期" width="150" align="center" header-align="center" />
+          <el-table-column prop="status_name" label="状态" min-width="120" align="center" header-align="center">
+            <template slot-scope="{ row }">
+              <span
+                class="status"
+                :style="{
+                  color: stateColorList[row.status].color,
+                  backgroundColor: stateColorList[row.status].bc,
+                }"
+              >
+                {{ row.status_name }}
+              </span>
+            </template>
+          </el-table-column>
+          <el-table-column fixed="right" label="操作" align="center" header-align="center">
+            <template slot-scope="{ row }">
+              <el-button type="text" @click="previewProject(row.id)">进入</el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+
+        <el-table
+          v-else-if="currentOverview === overviewList[1].key"
+          class="project-list"
+          :data="projectList"
+          height="calc(100vh - 630px)"
+        >
+          <el-table-column prop="sn" width="150" align="center" header-align="center" label="编号" />
+          <el-table-column prop="name" label="教材名称" width="460" header-align="left" />
+          <el-table-column prop="version" label="版本" width="120" align="center" header-align="center" />
+          <el-table-column prop="leader_name_desc" label="项目组长" width="220" align="center" header-align="center" />
+
+          <el-table-column prop="progress_percent" label="进度" width="180" align="center" header-align="center">
+            <template slot-scope="{ row }">
+              <div class="progress">
+                <span class="progress-bar"><i :style="{ width: `${row.progress_percent}%` }"></i></span>
+                <em>{{ row.progress_percent }}%</em>
+              </div>
+            </template>
+          </el-table-column>
+          <el-table-column
+            prop="plan_publish_date"
+            label="预计完成时间"
+            width="150"
+            align="center"
+            header-align="center"
+          />
+          <el-table-column prop="status_name" label="状态" min-width="120" align="center" header-align="center">
+            <template slot-scope="{ row }">
+              <span
+                class="status"
+                :style="{
+                  color: stateColorList[row.status].color,
+                  backgroundColor: stateColorList[row.status].bc,
+                }"
+              >
+                {{ row.status_name }}
+              </span>
+            </template>
+          </el-table-column>
+          <el-table-column fixed="right" label="操作" align="center" header-align="center">
+            <template slot-scope="{ row }">
+              <el-button type="text" @click="previewProject(row.id)">进入</el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+
+        <el-table
+          v-else-if="currentOverview === overviewList[2].key"
+          class="project-list"
+          :data="projectList"
+          height="calc(100vh - 630px)"
+        >
+          <el-table-column prop="sn" width="150" align="center" header-align="center" label="编号" />
+          <el-table-column prop="name" label="教材名称" width="460" header-align="left" />
+          <el-table-column prop="version" label="版本" width="120" align="center" header-align="center" />
+          <el-table-column prop="leader_name_desc" label="项目组长" width="220" align="center" header-align="center" />
+
+          <el-table-column prop="progress_percent" label="进度" width="180" align="center" header-align="center">
+            <template slot-scope="{ row }">
+              <div class="progress">
+                <span class="progress-bar"><i :style="{ width: `${row.progress_percent}%` }"></i></span>
+                <em>{{ row.progress_percent }}%</em>
+              </div>
+            </template>
+          </el-table-column>
+          <el-table-column prop="edit_end_date" label="交稿时间" width="150" align="center" header-align="center" />
+          <el-table-column prop="status_name" label="状态" min-width="120" align="center" header-align="center">
+            <template slot-scope="{ row }">
+              <span
+                class="status"
+                :style="{
+                  color: stateColorList[row.status].color,
+                  backgroundColor: stateColorList[row.status].bc,
+                }"
+              >
+                {{ row.status_name }}
+              </span>
+            </template>
+          </el-table-column>
+          <el-table-column fixed="right" label="操作" align="center" header-align="center">
+            <template slot-scope="{ row }">
+              <el-button type="text" @click="previewProject(row.id)">进入</el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+
+        <el-table
+          v-else-if="currentOverview === overviewList[3].key"
+          class="project-list"
+          :data="projectList"
+          height="calc(100vh - 630px)"
+        >
+          <el-table-column prop="sn" width="150" align="center" header-align="center" label="编号" />
+          <el-table-column prop="name" label="教材名称" width="460" header-align="left" />
+          <el-table-column prop="version" label="版本" width="120" align="center" header-align="center" />
+          <el-table-column prop="leader_name_desc" label="项目组长" width="220" align="center" header-align="center" />
+
+          <el-table-column prop="progress_percent" label="进度" width="180" align="center" header-align="center">
+            <template slot-scope="{ row }">
+              <div class="progress">
+                <span class="progress-bar"><i :style="{ width: `${row.progress_percent}%` }"></i></span>
+                <em>{{ row.progress_percent }}%</em>
+              </div>
+            </template>
+          </el-table-column>
+          <el-table-column
+            prop="plan_publish_date"
+            label="预计完成时间"
+            width="150"
+            align="center"
+            header-align="center"
+          />
+          <el-table-column prop="status_name" label="状态" min-width="120" align="center" header-align="center">
+            <template slot-scope="{ row }">
+              <span
+                class="status"
+                :style="{
+                  color: stateColorList[row.status].color,
+                  backgroundColor: stateColorList[row.status].bc,
+                }"
+              >
+                {{ row.status_name }}
+              </span>
+            </template>
+          </el-table-column>
+          <el-table-column fixed="right" label="操作" align="center" header-align="center">
+            <template slot-scope="{ row }">
+              <el-button type="text" @click="previewProject(row.id)">进入</el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+
+        <el-table
+          v-else-if="currentOverview === overviewList[4].key"
+          class="project-list"
+          :data="projectList"
+          height="calc(100vh - 630px)"
+        >
+          <el-table-column prop="sn" width="150" align="center" header-align="center" label="编号" />
+          <el-table-column prop="name" label="教材名称" width="460" header-align="left" />
+          <el-table-column prop="version" label="版本" width="120" align="center" header-align="center" />
+          <el-table-column prop="member_name_desc" label="作者" width="220" align="center" header-align="center" />
+          <el-table-column prop="shangjia_date" label="上架时间" width="150" align="center" header-align="center" />
+          <el-table-column prop="revise_count" label="修订次数" align="center" header-align="center" />
+          <el-table-column fixed="right" label="操作" align="center" header-align="center">
+            <template slot-scope="{ row }">
+              <el-button type="text" @click="previewProject(row.id)">进入</el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+        <PaginationPage :total="total" @getList="getList" />
+      </div>
+
+      <div v-if="toolModalVisible" class="tool-modal" @click.self="closeToolModal">
+        <div class="tool-modal-box">
+          <SvgIcon class="tool-modal-close" icon-class="home-close" size="12" @click="closeToolModal" />
+          <h3>扩展工具</h3>
+
+          <button class="tool-item t1" type="button" @click="openToolPage('template')">
+            <span class="ti-icon"><SvgIcon icon-class="home-template" size="24" /></span>
+            <span class="ti-label">模板库</span>
+          </button>
+
+          <button class="tool-item t2" type="button" @click="openToolPage('pinyin')">
+            <span class="ti-icon">
+              <SvgIcon icon-class="home-pinyin" size="24" />
+            </span>
+            <span class="ti-label">拼音校正库</span>
+          </button>
+        </div>
       </div>
     </div>
   </div>
 </template>
 
 <script>
+import { GetMyProjectStatInfo } from '@/api/user';
+import { PageQueryMyProjectList } from '@/api/list';
+
+import PaginationPage from '@/components/PaginationPage.vue';
+
 export default {
   name: 'HomePage',
+  components: {
+    PaginationPage,
+  },
   data() {
     return {
       itemList: [
         {
+          color: '#F5B131',
+          title: '新建',
+          path: '/create_project',
+          desc: '创建教材项目',
+          bg: 'linear-gradient(135deg, #EEF2FF 0%, #fff 100%)',
+          icon: 'home-new',
+          iconBg: '#DBEAFE',
+        },
+        {
           color: '#667EE5',
-          title: '教材管理',
+          title: '管理',
           path: '/personal_workbench/project',
-          icon: 'manage',
+          desc: '统筹项目资源与进度',
+          bg: 'linear-gradient(135deg,#FEFCE8 0%,#fff 100%)',
+          icon: 'home-manage',
+          iconBg: '#FEF3C7',
         },
         {
           color: '#F5B131',
-          title: '教材制作',
+          title: '制作',
           path: '/personal_workbench/edit_task',
-          icon: 'make',
+          desc: '编辑教材内容',
+          bg: 'linear-gradient(135deg,#EFF6FF 0%,#fff 100%)',
+          icon: 'home-make',
+          iconBg: '#DBEAFE',
         },
         {
           color: '#E25E5C',
-          title: '教材审核',
+          title: '审核',
           path: '/personal_workbench/check_task',
-          icon: 'audit',
+          desc: '审核并上架教材',
+          bg: 'linear-gradient(135deg,#F5F3FF 0%,#fff 100%)',
+          icon: 'home-review',
+          iconBg: '#EDE9FE',
+        },
+        {
+          color: '#D1FAE5',
+          title: '扩展',
+          path: '',
+          desc: '使用资源库和工具库',
+          bg: 'linear-gradient(135deg,#ECFDF5 0%,#fff 100%)',
+          icon: 'home-tools',
+          iconBg: '#D1FAE5',
+        },
+      ],
+      currentOverview: 'count_QB',
+      overviewList: [
+        {
+          label: '全部教材',
+          key: 'count_QB',
+          color: '#3B82F6',
+          dotColor: '#2D3E8F',
+          type: 0,
+        },
+        {
+          label: '我管理的教材',
+          key: 'count_GL',
+          color: '#F59E0B',
+          dotColor: '#F59E0B',
+          type: 1,
+        },
+        {
+          label: '我制作的教材',
+          key: 'count_ZZ',
+          color: '#8B5CF6',
+          dotColor: '#3B82F6',
+          type: 2,
+        },
+        {
+          label: '我审核的教材',
+          key: 'count_SH',
+          color: '#7C3AED',
+          dotColor: '#7C3AED',
+          type: 3,
+        },
+        {
+          label: '已上架教材',
+          key: 'count_YSJ',
+          color: '#10B981',
+          dotColor: '#10B981',
+          type: 4,
         },
       ],
+      stateColorList: [
+        {
+          color: '#F59E0B',
+          bc: '#FEF3C7',
+        },
+        {
+          color: '#8B5CF6',
+          bc: '#EDE9FE',
+        },
+        {
+          color: '#3B82F6',
+          bc: '#DBEAFE',
+        },
+        { color: '#10B981', bc: '#D1FAE5' },
+        { color: '#FF4757', bc: '#FFE5E5' },
+      ],
+      // 项目统计信息
+      stateInfo: {
+        count_QB: 0, // 全部
+        count_GL: 0, // 管理
+        count_ZZ: 0, // 制作
+        count_SH: 0, // 审核
+        count_YSJ: 0, // 已上架
+      },
+      total: 0,
+      page_capacity: 10,
+      cur_page: 1,
+      projectList: [], // 项目列表
+      toolModalVisible: false,
     };
   },
+  created() {
+    this.getMyProjectStatInfo();
+    this.getList();
+  },
+  mounted() {
+    document.addEventListener('keydown', this.handleKeydown);
+  },
+  beforeDestroy() {
+    document.removeEventListener('keydown', this.handleKeydown);
+  },
   methods: {
+    async getMyProjectStatInfo() {
+      this.stateInfo = await GetMyProjectStatInfo();
+    },
+    getList(data) {
+      const params = {
+        page_capacity: this.page_capacity,
+        cur_page: this.cur_page,
+        type: this.overviewList.find((item) => item.key === this.currentOverview)?.type,
+      };
+      PageQueryMyProjectList({ ...params, ...data }).then(({ project_list, total_count, cur_page }) => {
+        this.projectList = project_list;
+        this.total = total_count;
+        this.page_capacity = data?.page_capacity || this.page_capacity;
+        this.cur_page = cur_page;
+      });
+    },
+    /**
+     * @description 切换概览后重置页码并获取列表数据
+     * @param {string} key - 选中的概览key
+     */
+    changeOverview(key) {
+      this.currentOverview = key;
+      this.getList({ cur_page: 1 });
+    },
+    onItemClick(path, index) {
+      if (index === 4) {
+        this.openToolModal();
+        return;
+      }
+      this.jump(path);
+    },
+    openToolModal() {
+      this.toolModalVisible = true;
+    },
+    closeToolModal() {
+      this.toolModalVisible = false;
+    },
+    handleKeydown(e) {
+      if (e.key === 'Escape') {
+        this.closeToolModal();
+      }
+    },
+    openToolPage(type) {
+      this.closeToolModal();
+      if (type === 'template') {
+        this.$router.push({ name: 'PersonalWorkbenchTemplate' });
+        return;
+      }
+      if (type === 'pinyin') {
+        this.$router.push({ name: 'PersonalWorkbenchPinyinCorrectionList' });
+      }
+    },
     jump(path) {
       this.$router.push({ path });
     },
+    previewProject(projectId) {
+      this.$router.push({ path: `/project_manage/org/project/preview/${projectId}` });
+    },
   },
 };
 </script>
 
 <style lang="scss" scoped>
 .home {
-  display: flex;
-  flex-direction: column;
-  height: 100%;
-  padding: 12px;
-  background-color: #f1f1f1;
+  min-height: 100%;
+  padding: 0;
+  overflow: auto;
+  background: #f7f9fe;
 
-  &-top {
+  &-main {
     display: flex;
-    justify-content: flex-end;
-    margin: 40px 20px 8%;
-
-    .create {
-      color: #f5f5f5;
-      background-color: #46bc84;
-    }
+    flex-direction: column;
+    gap: 24px;
+    max-width: 80vw;
+    padding: 24px 28px 28px;
+    margin: 0 auto;
   }
 
   &-content {
-    display: flex;
-    flex: 1;
-    align-items: flex-start;
-    justify-content: space-evenly;
+    display: grid;
+    grid-template-columns: repeat(5, minmax(0, 1fr));
+    gap: 20px;
 
     .item {
       display: flex;
       flex-direction: column;
+      gap: 14px;
       align-items: center;
-      justify-content: space-between;
-      width: 15%;
-      min-width: 300px;
-      aspect-ratio: 3 / 4;
-      padding: 4% 0 2%;
+      min-height: 200px;
+      padding: 26px 24px 24px;
       cursor: pointer;
-      border-radius: 8px;
+      border: 1px solid rgba(226, 232, 240, 68%);
+      border-radius: 20px;
+      box-shadow: 0 16px 36px rgba(15, 23, 42, 5%);
+      transition:
+        transform 0.25s ease,
+        box-shadow 0.25s ease,
+        border-color 0.25s ease;
 
-      .title {
-        font-size: 36px;
-        font-weight: bold;
-        color: #fff;
-        text-align: center;
+      &:hover {
+        box-shadow: 0 24px 40px rgba(15, 23, 42, 9%);
+        transform: translateY(-4px);
+      }
+
+      &.item--1:hover,
+      &.item--3:hover {
+        border-color: #3b82f6;
+      }
+
+      &.item--2:hover {
+        border-color: #f59e0b;
+      }
+
+      &.item--4:hover {
+        border-color: #8b5cf6;
+      }
+
+      &.item--5:hover {
+        border-color: #10b981;
       }
 
       .icon {
-        width: 64px;
-        height: 64px;
-        overflow: hidden;
-        line-height: 84px;
-        text-align: center;
-        filter: brightness(1.15) saturate(1.1);
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        width: 56px;
+        height: 56px;
+        background-repeat: no-repeat;
+        background-position: center;
+        border-radius: 18px;
+        box-shadow: inset 0 1px 0 rgba(255, 255, 255, 65%);
+      }
+
+      .title {
+        font-size: 16px;
+        font-weight: 700;
+        color: #1f2937;
+      }
+
+      .desc {
+        max-width: 260px;
+        font-size: 11px;
+        color: #94a3b8;
+      }
+    }
+  }
+
+  .overview-row {
+    display: grid;
+    grid-template-columns: repeat(5, minmax(0, 1fr));
+    gap: 16px;
+  }
+
+  .stat-card {
+    position: relative;
+    display: flex;
+    flex-direction: column;
+    gap: 8px;
+    min-height: 108px;
+    padding: 22px 24px;
+    background: #fff;
+    border: 1px solid rgba(226, 232, 240, 76%);
+    border-radius: 18px;
+    box-shadow: 0 10px 28px rgba(15, 23, 42, 4%);
+
+    &.active {
+      border-color: #2d3e8f;
+    }
+
+    &:hover {
+      box-shadow: 0 24px 40px rgba(15, 23, 42, 9%);
+      transform: translateY(-4px);
+    }
+
+    &.active::after {
+      position: absolute;
+      right: calc(50% - 20px);
+      bottom: 0;
+      width: 40px;
+      height: 3px;
+      content: '';
+      background-color: #2d3e8f;
+      border-radius: 4px;
+    }
+
+    .stat-label {
+      font-size: 13px;
+      font-weight: 500;
+      color: #64748b;
+
+      .dot {
+        display: inline-block;
+        width: 10px;
+        height: 10px;
+        margin-right: 6px;
         border-radius: 50%;
       }
     }
+
+    .stat-value {
+      font-size: 32px;
+      font-weight: 700;
+      line-height: 1.2;
+      color: #1f2937;
+    }
+  }
+
+  .list-section {
+    height: calc(100vh - 530px);
+    padding: 22px 24px 12px;
+    overflow: hidden;
+    background: rgba(255, 255, 255, 96%);
+    border: 1px solid rgba(226, 232, 240, 82%);
+    border-radius: 20px;
+    box-shadow: 0 16px 32px rgba(15, 23, 42, 4%);
+  }
+
+  .project-list {
+    overflow: hidden;
+    border: 1px solid #eef2f7;
+    border-radius: 14px;
+
+    :deep(.el-table__header-wrapper th) {
+      height: 48px;
+      padding: 0;
+      font-size: 12px;
+      font-weight: 600;
+      color: #64748b;
+      background: #f8fafc;
+      border-bottom: 1px solid #eef2f7;
+    }
+
+    :deep(.el-table__row td) {
+      height: 78px;
+      padding: 0;
+      border-bottom: 1px solid #f1f5f9;
+    }
+
+    :deep(.el-table::before) {
+      display: none;
+    }
+
+    :deep(.el-table__row:last-child td) {
+      border-bottom: none;
+    }
+  }
+
+  .book-info {
+    display: flex;
+    flex-direction: column;
+    gap: 6px;
+
+    strong {
+      font-size: 15px;
+      font-weight: 600;
+      color: #1f2937;
+    }
+
+    span {
+      font-size: 12px;
+      color: #94a3b8;
+    }
+  }
+
+  .status {
+    display: inline-flex;
+    align-items: center;
+    justify-content: center;
+    min-width: 72px;
+    height: 28px;
+    padding: 0 10px;
+    font-size: 12px;
+    font-weight: 600;
+    border-radius: 999px;
+  }
+
+  .owner,
+  .time {
+    font-size: 13px;
+    color: #475569;
+  }
+
+  :deep(.el-table .cell) {
+    font-size: 13px;
+    color: #475569;
+  }
+
+  .progress {
+    display: flex;
+    gap: 10px;
+    align-items: center;
+
+    em {
+      font-size: 12px;
+      font-style: normal;
+      color: #64748b;
+    }
+  }
+
+  .progress-bar {
+    position: relative;
+    width: 88px;
+    height: 8px;
+    overflow: hidden;
+    background: #e2e8f0;
+    border-radius: 999px;
+
+    i {
+      position: absolute;
+      top: 0;
+      left: 0;
+      display: block;
+      height: 100%;
+      background: linear-gradient(90deg, #2d3e8f 0%, #4dc9f6 100%);
+      border-radius: inherit;
+    }
+  }
+
+  @media (width <= 1280px) {
+    &-content,
+    .overview-row {
+      grid-template-columns: repeat(2, minmax(0, 1fr));
+    }
+  }
+
+  @media (width <= 960px) {
+    &-hero,
+    .section-head {
+      flex-direction: column;
+      align-items: flex-start;
+    }
+
+    &-content,
+    .overview-row {
+      grid-template-columns: 1fr;
+    }
+
+    .project-list {
+      :deep(.el-table__header-wrapper) {
+        display: none;
+      }
+
+      :deep(.el-table__row td) {
+        height: auto;
+        padding: 12px 0;
+      }
+    }
+  }
+
+  .tool-modal {
+    position: fixed;
+    inset: 0;
+    z-index: 1000;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    background: rgba(0, 0, 0, 40%);
+  }
+
+  .tool-modal-box {
+    position: relative;
+    width: 320px;
+    padding: 28px 32px 24px;
+    background: #fff;
+    border-radius: 16px;
+    box-shadow: 0 12px 48px rgba(0, 0, 0, 15%);
+    animation: fade-up 0.3s ease;
+
+    h3 {
+      margin-bottom: 18px;
+      font-size: 16px;
+      font-weight: 700;
+      color: #1f2937;
+      text-align: center;
+    }
+  }
+
+  .tool-modal-close {
+    position: absolute;
+    top: 10px;
+    right: 10px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 24px;
+    height: 24px;
+    color: #94a3b8;
+    cursor: pointer;
+    background: #f7f9fe;
+    border: none;
+    border-radius: 50%;
+    transition: all 0.25s ease;
+
+    &:hover {
+      color: #1f2937;
+      background: #e2e8f0;
+    }
+
+    svg {
+      width: 14px;
+      height: 14px;
+    }
+  }
+
+  .tool-item {
+    display: flex;
+    gap: 12px;
+    align-items: center;
+    width: 100%;
+    padding: 12px;
+    margin-bottom: 8px;
+    cursor: pointer;
+    background: transparent;
+    border: none;
+    border-radius: 10px;
+    transition: all 0.25s ease;
+
+    &:last-child {
+      margin-bottom: 0;
+    }
+
+    &:hover {
+      background: #f7f9fe;
+    }
+
+    .ti-icon {
+      display: flex;
+      flex-shrink: 0;
+      align-items: center;
+      justify-content: center;
+      width: 36px;
+      height: 36px;
+      border-radius: 10px;
+
+      svg {
+        width: 18px;
+        height: 18px;
+      }
+    }
+
+    .ti-label {
+      font-size: 14px;
+      font-weight: 600;
+      color: #1f2937;
+    }
+
+    &.t1 .ti-icon {
+      color: #3b82f6;
+      background: #dbeafe;
+    }
+
+    &.t2 .ti-icon {
+      color: #10b981;
+      background: #d1fae5;
+    }
+  }
+
+  @keyframes fade-up {
+    from {
+      opacity: 0;
+      transform: translateY(12px);
+    }
+
+    to {
+      opacity: 1;
+      transform: translateY(0);
+    }
   }
 }
 </style>