Quellcode durchsuchen

助手位置调整

ellisran vor 1 Woche
Ursprung
Commit
eef6ec2a81
4 geänderte Dateien mit 114 neuen und 28 gelöschten Zeilen
  1. 49 1
      app/public/css/main.css
  2. 60 23
      app/public/js/assistant.js
  3. 1 0
      app/service/project_screen.js
  4. 4 4
      app/view/layout/assistant.ejs

+ 49 - 1
app/public/css/main.css

@@ -2942,11 +2942,15 @@ button.gy-stat:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
 }
 
 .global-assistant-entry {
+  position: fixed;
+  right: 0;
+  top: calc(100vh - 68px);
   display: flex;
   align-items: center;
   justify-content: center;
   gap: 7px;
-  min-width: 76px;
+  width: 92px;
+  min-width: 0;
   height: 44px;
   padding: 0 17px;
   border: 0;
@@ -2957,6 +2961,44 @@ button.gy-stat:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
   cursor: grab;
   touch-action: none;
   user-select: none;
+  transition: width .16s ease, padding .16s ease, border-radius .16s ease, background-color .16s ease;
+}
+
+.global-assistant-entry-label {
+  max-width: 28px;
+  overflow: hidden;
+  white-space: nowrap;
+  opacity: 1;
+  transition: max-width .16s ease, opacity .12s ease;
+}
+
+.global-assistant-entry.docked {
+  width: 32px;
+  padding: 0;
+  gap: 0;
+  border-radius: 22px 0 0 22px;
+  box-shadow: 0 2px 10px rgba(39, 126, 220, .2);
+}
+
+.global-assistant-entry.docked .global-assistant-entry-label {
+  max-width: 0;
+  opacity: 0;
+}
+
+.global-assistant-entry.docked.expanded {
+  width: 92px;
+  padding: 0 17px;
+  gap: 7px;
+}
+
+.global-assistant-entry.docked.expanded .global-assistant-entry-label {
+  max-width: 28px;
+  opacity: 1;
+}
+
+.global-assistant-entry:focus-visible {
+  outline: 2px solid #1d65b8;
+  outline-offset: 3px;
 }
 
 .global-assistant-entry:hover {
@@ -2965,6 +3007,12 @@ button.gy-stat:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
 
 .global-assistant-entry.dragging {
   cursor: grabbing;
+  transition: none;
+}
+
+@media (prefers-reduced-motion: reduce) {
+  .global-assistant-entry,
+  .global-assistant-entry-label { transition: none; }
 }
 
 .global-assistant-panel {

+ 60 - 23
app/public/js/assistant.js

@@ -1,3 +1,5 @@
+/* global $, userID, postData */
+/* eslint-env browser */
 'use strict';
 
 (function() {
@@ -13,13 +15,15 @@
         const entry = root.find('.global-assistant-entry');
         const projectId = root.data('project-id');
         const storageKey = 'global-assistant-history-v2-' + projectId + '-' + userID;
-        const entryPositionKey = 'global-assistant-entry-position-' + userID;
+        const entryPositionKey = 'global-assistant-entry-position-v2-' + userID;
         let history = loadHistory();
         let conversationId = getConversationId();
         let sending = false;
         let entryPosition = loadEntryPosition();
         let entryDrag = null;
         let suppressEntryClick = false;
+        let suppressClickTimer = null;
+        let entryExpanded = false;
 
         applyEntryPosition();
 
@@ -30,7 +34,7 @@
                 return;
             }
             panel.addClass('show').attr('aria-hidden', 'false');
-            $(this).hide();
+            $(this).attr('aria-expanded', 'true').hide();
             input.trigger('focus');
             scrollToBottom();
         });
@@ -38,42 +42,69 @@
         entry.on('pointerdown', function(event) {
             const pointer = event.originalEvent;
             if (pointer.pointerType === 'mouse' && pointer.button !== 0) return;
+            if (entryDrag) return;
+            const rect = this.getBoundingClientRect();
             entryDrag = {
                 pointerId: pointer.pointerId,
                 startX: pointer.clientX,
                 startY: pointer.clientY,
-                positionX: entryPosition.x,
-                positionY: entryPosition.y,
+                grabRatio: rect.width ? (pointer.clientX - rect.left) / rect.width : 0.5,
+                grabY: pointer.clientY - rect.top,
+                originalPosition: Object.assign({}, entryPosition),
                 moved: false,
             };
+            clearTimeout(suppressClickTimer);
             suppressEntryClick = false;
             this.setPointerCapture(pointer.pointerId);
-            entry.addClass('dragging');
             event.preventDefault();
         });
 
         entry.on('pointermove', function(event) {
             const pointer = event.originalEvent;
             if (!entryDrag || pointer.pointerId !== entryDrag.pointerId) return;
-            const offsetX = entryDrag.startX - pointer.clientX;
-            const offsetY = entryDrag.startY - pointer.clientY;
-            if (Math.abs(offsetX) > 3 || Math.abs(offsetY) > 3) entryDrag.moved = true;
+            const offsetX = pointer.clientX - entryDrag.startX;
+            const offsetY = pointer.clientY - entryDrag.startY;
+            if (!entryDrag.moved && Math.abs(offsetX) <= 4 && Math.abs(offsetY) <= 4) return;
+            entryDrag.moved = true;
+            entryExpanded = false;
+            entry.addClass('dragging');
+            const width = 92;
             entryPosition = clampEntryPosition({
-                x: entryDrag.positionX + offsetX,
-                y: entryDrag.positionY + offsetY,
+                right: window.innerWidth - pointer.clientX - width * (1 - entryDrag.grabRatio),
+                top: pointer.clientY - entryDrag.grabY,
+                docked: false,
             });
             applyEntryPosition();
             event.preventDefault();
         });
 
-        entry.on('pointerup pointercancel', function(event) {
+        entry.on('pointerup pointercancel lostpointercapture', function(event) {
             const pointer = event.originalEvent;
             if (!entryDrag || pointer.pointerId !== entryDrag.pointerId) return;
-            suppressEntryClick = event.type === 'pointerup' && entryDrag.moved;
-            if (this.hasPointerCapture(pointer.pointerId)) this.releasePointerCapture(pointer.pointerId);
+            const drag = entryDrag;
             entryDrag = null;
+            suppressEntryClick = event.type === 'pointerup' && drag.moved;
+            if (event.type !== 'pointerup') entryPosition = drag.originalPosition;
+            else if (drag.moved) entryPosition.docked = entryPosition.right <= 24;
+            if (drag.moved || event.type !== 'pointerup') entryExpanded = false;
+            entryPosition = clampEntryPosition(entryPosition);
             entry.removeClass('dragging');
+            applyEntryPosition();
+            if (this.hasPointerCapture(pointer.pointerId)) this.releasePointerCapture(pointer.pointerId);
             saveEntryPosition();
+            suppressClickTimer = setTimeout(function() { suppressEntryClick = false; }, 0);
+        });
+
+        entry.on('pointerenter focusin', function(event) {
+            if (!entryPosition.docked || entryDrag || (event.originalEvent && event.originalEvent.pointerType === 'touch')) return;
+            entryExpanded = true;
+            applyEntryPosition();
+        });
+
+        entry.on('pointerleave focusout', function() {
+            if (entryDrag) return;
+            entryExpanded = false;
+            applyEntryPosition();
         });
 
         $(window).on('resize.globalAssistant', function() {
@@ -84,7 +115,9 @@
 
         root.find('.global-assistant-close').on('click', function() {
             panel.removeClass('show').attr('aria-hidden', 'true');
-            root.find('.global-assistant-entry').show();
+            entryExpanded = false;
+            entry.attr('aria-expanded', 'false').show();
+            applyEntryPosition();
         });
 
         root.find('.global-assistant-clear').on('click', function() {
@@ -282,11 +315,13 @@
         }
 
         function loadEntryPosition() {
+            const initial = { right: 0, top: window.innerHeight - 44 - 24, docked: true };
             try {
                 const value = JSON.parse(localStorage.getItem(entryPositionKey) || '{}');
-                return clampEntryPosition({ x: Number(value.x) || 0, y: Number(value.y) || 0 });
+                if (!value || typeof value.docked !== 'boolean') return clampEntryPosition(initial);
+                return clampEntryPosition(value);
             } catch (err) {
-                return { x: 0, y: 0 };
+                return clampEntryPosition(initial);
             }
         }
 
@@ -299,17 +334,19 @@
         }
 
         function applyEntryPosition() {
-            entry.css('transform', `translate(${-entryPosition.x}px, ${-entryPosition.y}px)`);
+            entry.toggleClass('docked', entryPosition.docked)
+                .toggleClass('expanded', entryPosition.docked && entryExpanded)
+                .css({ right: entryPosition.right, top: entryPosition.top, transform: 'none' });
         }
 
         function clampEntryPosition(position) {
-            const panelWidth = parseFloat(panel.css('width')) || 420;
-            const panelHeight = Math.min(650, Math.max(0, window.innerHeight - 48));
-            const maxX = Math.max(0, panelWidth - entry.outerWidth());
-            const maxY = Math.max(0, panelHeight - entry.outerHeight());
+            const docked = position.docked === true;
+            const maxRight = Math.max(0, window.innerWidth - 92 - 8);
+            const maxTop = Math.max(0, window.innerHeight - 44 - 8);
             return {
-                x: Math.min(maxX, Math.max(0, Number(position.x) || 0)),
-                y: Math.min(maxY, Math.max(0, Number(position.y) || 0)),
+                right: docked ? 0 : Math.min(maxRight, Math.max(Math.min(8, maxRight), Number(position.right) || 0)),
+                top: Math.min(maxTop, Math.max(Math.min(8, maxTop), Number(position.top) || 0)),
+                docked,
             };
         }
     });

+ 1 - 0
app/service/project_screen.js

@@ -245,6 +245,7 @@ module.exports = app => {
                     tenders: tenderInvestments,
                     changeRows, stages,
                     progressText: latestEvent ? `${latestEvent.date},${latestEvent.department || ''}已完成「${latest.name}」${latestEvent.step}。` : '暂无阶段动态',
+                    overviewAmounts: ['lx', 'cb', 'sgt'].map(key => toWan(info[key + '_tp'], info[key + '_tp_unit'])),
                     amounts: budgetTotals.map(totals => toWan(totals.get(String(node.budget_id)), '元'))
                         .concat([toWan(info.pf_tp, info.pf_tp_unit), tenders.length ? toWan(contract, '元') : null, hasMeasured ? toWan(measured, '元') : null, finalAmount]) });
             }

+ 4 - 4
app/view/layout/assistant.ejs

@@ -2,11 +2,11 @@
      class="global-assistant"
      data-project-id="<%- ctx.session.sessionProject.id %>"
      data-sub-project-id="<%- ctx.subProject ? ctx.subProject.id : '' %>">
-    <button type="button" class="global-assistant-entry" aria-label="打开业务查询助手" title="业务查询助手">
-        <i class="fa fa-comments"></i>
-        <span>助手</span>
+    <button type="button" class="global-assistant-entry docked" aria-label="打开业务查询助手,可拖拽调整位置" title="业务查询助手(可拖拽,靠右收起)" aria-controls="global-assistant-panel" aria-expanded="false">
+        <i class="fa fa-comments" aria-hidden="true"></i>
+        <span class="global-assistant-entry-label">助手</span>
     </button>
-    <section class="global-assistant-panel" aria-label="业务查询助手" aria-hidden="true">
+    <section id="global-assistant-panel" class="global-assistant-panel" aria-label="业务查询助手" aria-hidden="true">
         <header class="global-assistant-header">
             <div>
                 <strong>业务查询助手</strong>