yichael 4 недель назад
Родитель
Сommit
de16d5234e

+ 32 - 0
doc/CODING_STANDARDS.md

@@ -0,0 +1,32 @@
+# Coding Standards
+
+## 1. Variable Naming
+
+Meaningful variable names, file names, folder names. Use abbreviations for words over ten characters. Abbreviations must be over three characters.
+
+- ✅ `update-btn` (refresh button), `device-list`, `user-profile-settings` (profile = abbreviation)
+- ❌ `btn` (too vague), `d1` (meaningless), `temp` (unclear), `us` (abbreviation too short)
+
+## 2. Word Separation: Kebab-Case
+
+Use hyphens (`-`) for multi-word names.
+
+## 3. Comments: One Block, One Comment, English Only
+
+## 4. No Try-Catch
+
+Never use try-catch. Let errors crash.
+
+## 5. GUI File Structure: Separate JSX, JS, SCSS
+
+GUI components must split into three files:
+
+- `.jsx`: Layout only (no logic, no styles)
+- `.js`: Logic only (functions, state, business logic)
+- `.scss`: Styles only (no logic, no JSX)
+
+**Never write logic or inline styles in `.jsx` files.**
+
+## 6. Code Simplicity: Minimal Code
+
+Use the least code to implement functionality. 

+ 10 - 0
src/page/device/device.js

@@ -0,0 +1,10 @@
+export async function handleRefresh(e, self) {
+    // Start loading animation
+    self.startAnimation()
+
+    // Simulate async operation
+    setTimeout(() => {
+        // Stop loading animation when done
+        self.stopAnimation()
+    }, 5000) 
+}

+ 1 - 5
src/page/device/device.jsx

@@ -1,17 +1,13 @@
 import React from 'react'
 import './device.scss'
 import UpdateBtn from './update-btn.jsx'
+import { handleRefresh } from './device.js'
 
 function Device({ show }) {
   if (!show) {
     return null
   }
 
-  const handleRefresh = () => {
-    console.log('Refresh clicked')
-    // Add your refresh logic here
-  }
-
   return (
     <div className="device-container">
       <div className="device-update">

+ 9 - 8
src/page/device/device.scss

@@ -15,30 +15,32 @@ $full-size: 100%; // 全尺寸
 .device-container {
   width: $full-size;
   height: $full-size;
-  background-color: $primary-color;
-  @include flex-center;
+
   margin: 0;
   padding: 0;
+  
+  display: grid;
+  grid-template-rows: 5% 80% 15%;
 
   .device-update {
     width: $full-size;
     height: $full-size;
-    background-color: $primary-color;
     @include flex-center;
 
     .device-update-title {
       width: $full-size;
       height: $full-size;
-      background-color: $primary-color;
       @include flex-center;
     }
 
     .device-update-btn {
-      width: $full-size;
-      height: $full-size;
-      background-color: $primary-color;
+      width: 10%;
       @include flex-center;
+      margin-right: 10%;
+      transform: scale(0.6);
     }
+    
+    border: 1px solid red;
   }
 
   .device-list {
@@ -49,7 +51,6 @@ $full-size: 100%; // 全尺寸
   .device-add {
     width: $full-size;
     height: $full-size;
-    background-color: $primary-color;
     @include flex-center;
   }
 }

+ 48 - 0
src/page/device/update-btn.js

@@ -0,0 +1,48 @@
+import { useState, useCallback, useRef, useEffect } from 'react'
+
+// Global ref to store animation functions
+const animationRef = { startAnimation: null, stopAnimation: null }
+
+/**
+ * Create animation control functions
+ */
+export function useUpdateBtnAnimation(btnRef, externalLoading, onClick, disabled) {
+  const [internalLoading, setInternalLoading] = useState(false)
+  
+  const isLoading = externalLoading !== undefined ? externalLoading : internalLoading
+  
+  // Start animation
+  const startAnimation = useCallback(() => {
+    if (externalLoading === undefined) setInternalLoading(true)
+  }, [externalLoading])
+
+  // Stop animation
+  const stopAnimation = useCallback(() => {
+    if (externalLoading === undefined) setInternalLoading(false)
+  }, [externalLoading])
+  
+  // Update global ref with latest functions
+  useEffect(() => {
+    animationRef.startAnimation = startAnimation
+    animationRef.stopAnimation = stopAnimation
+  }, [startAnimation, stopAnimation])
+  
+  // Handle button click
+  const handleClick = useCallback(async (e) => {
+    if (disabled || isLoading) return
+    const self = { startAnimation, stopAnimation }
+    const result = onClick?.(e, self)
+    if (result?.then) await result
+  }, [onClick, disabled, isLoading, startAnimation, stopAnimation])
+  
+  return { isLoading, startAnimation, stopAnimation, handleClick }
+}
+
+// Export animation functions for direct access
+export function getStartAnimation() {
+  return animationRef.startAnimation
+}
+
+export function getStopAnimation() {
+  return animationRef.stopAnimation
+}

+ 25 - 28
src/page/device/update-btn.jsx

@@ -1,20 +1,27 @@
-import React from 'react'
+import React, { useRef } from 'react'
 import DivBtn from '../public/div-btn/div-btn.jsx'
+import './update-btn.scss'
+import { useUpdateBtnAnimation } from './update-btn.js'
 
 /**
  * Refresh Button Component
  * @param {Object} props
- * @param {Function} props.onClick - Click handler function
+ * @param {Function} props.onClick - Click handler function (can return Promise)
  * @param {boolean} props.disabled - Disable button (default: false)
+ * @param {boolean} props.loading - External loading state (optional)
  * @param {string} props.title - Tooltip text (default: 'Refresh')
  * @param {string} props.className - Additional CSS classes
  */
 function UpdateBtn({
   onClick,
   disabled = false,
+  loading: externalLoading,
   title = 'Refresh',
   className = ''
 }) {
+  const btnRef = useRef(null)
+  const { isLoading, handleClick } = useUpdateBtnAnimation(btnRef, externalLoading, onClick, disabled)
+  
   // Default refresh icon
   const refreshIcon = (
     <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -24,21 +31,11 @@ function UpdateBtn({
     </svg>
   )
 
-  // Hover refresh icon (slightly bolder)
-  const refreshIconHover = (
+  // Loading icon (spinning circle)
+  const loadingIcon = (
     <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
-      <path d="M1 4V10H7" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"/>
-      <path d="M23 20V14H17" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"/>
-      <path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10M23 14L18.36 18.36A9 9 0 0 1 3.51 15" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"/>
-    </svg>
-  )
-
-  // Pressed refresh icon (thicker)
-  const refreshIconPressed = (
-    <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
-      <path d="M1 4V10H7" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"/>
-      <path d="M23 20V14H17" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"/>
-      <path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10M23 14L18.36 18.36A9 9 0 0 1 3.51 15" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"/>
+      <circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2" strokeLinecap="round" opacity="0.2"/>
+      <circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeDasharray="47.124" strokeDashoffset="47.124"/>
     </svg>
   )
 
@@ -52,18 +49,18 @@ function UpdateBtn({
   )
 
   return (
-    <DivBtn
-      icon={refreshIcon}
-      iconHover={refreshIconHover}
-      iconPressed={refreshIconPressed}
-      iconDisabled={refreshIconDisabled}
-      onClick={onClick}
-      disabled={disabled}
-      title={title}
-      className={className}
-      rotateOnHover={true}
-      rotateDegrees={180}
-    />
+    <div ref={btnRef}>
+      <DivBtn
+        icon={isLoading ? loadingIcon : refreshIcon}
+        iconDisabled={refreshIconDisabled}
+        onClick={handleClick}
+        disabled={disabled || isLoading}
+        title={isLoading ? 'Loading...' : title}
+        className={`update-btn ${isLoading ? 'update-btn--loading' : ''} ${className}`}
+        rotateOnHover={!isLoading}
+        rotateDegrees={180}
+      />
+    </div>
   )
 }
 

+ 69 - 0
src/page/device/update-btn.scss

@@ -0,0 +1,69 @@
+// ========== 颜色配置 ==========
+$icon-color-black: #000;
+$icon-color-gray: #808080;
+$icon-color-pressed: #666;
+
+// ========== 样式定义 ==========
+.update-btn {
+  // Default icon color: black
+  .div-btn__icon {
+    color: $icon-color-black !important;
+    opacity: 1 !important;
+  }
+
+  // Pressed state: darker color
+  .div-btn--pressed:not(.div-btn--disabled) .div-btn__icon {
+    color: $icon-color-pressed !important;
+    opacity: 1 !important;
+  }
+
+  &--loading {
+    cursor: wait;
+    
+    // Hide all icon layers except default
+    .div-btn__icon--hover,
+    .div-btn__icon--pressed,
+    .div-btn__icon--disabled {
+      opacity: 0 !important;
+    }
+    
+    .div-btn__icon--default {
+      opacity: 1 !important;
+      // Loading icon color: gray
+      color: $icon-color-gray !important;
+      
+      // Rotate loading icon
+      svg {
+        animation: update-btn-spin 0.5s linear infinite;
+        transform-origin: center;
+      }
+      
+      // Animate loading circle stroke
+      svg circle:last-child {
+        animation: update-btn-loading 1s linear infinite;
+      }
+    }
+  }
+}
+
+// ========== 动画定义 ==========
+@keyframes update-btn-spin {
+  from {
+    transform: rotate(0deg);
+  }
+  to {
+    transform: rotate(360deg);
+  }
+}
+
+@keyframes update-btn-loading {
+  0% {
+    stroke-dashoffset: 47.124;
+  }
+  50% {
+    stroke-dashoffset: 0;
+  }
+  100% {
+    stroke-dashoffset: -47.124;
+  }
+}