shaogen1995 6 дней назад
Родитель
Сommit
9cd9eb0100

+ 86 - 0
后台管理/CLAUDE.md

@@ -0,0 +1,86 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Project Overview
+
+This is a React admin management system for Inner Mongolia Museum (内蒙古博物院) - a mobile exhibition vehicle backend management platform.
+
+## Commands
+
+```bash
+npm run dev    # Start development server (react-app-rewired start)
+npm run build  # Production build (react-app-rewired build)
+npm run test   # Run tests (react-app-rewired test)
+```
+
+## Tech Stack
+
+- React 18 + TypeScript
+- Redux (with thunk) for state management
+- Ant Design v5 (antd) + antd-mobile for UI
+- React Router v5 (hash routing)
+- Axios for HTTP requests
+- Socket.io-client for WebSocket connections
+- SCSS for styling
+
+## Architecture
+
+### Directory Structure
+
+- `src/pages/` - Feature modules:
+  - `A1facility/` - Device management
+  - `A2content/` - Content management (AI digital human, display cabinets, interactive wall, aerial imaging)
+  - `A3goods/` - Collection/goods management
+  - `A4screen/` - Screen casting management
+  - `Z_system/` - System settings (user management Z6user, logs Z7log)
+  - `Layout/` - Main layout with top navigation bar
+  - `Login/` - Authentication page
+
+- `src/store/` - Redux state:
+  - `index.ts` - Store setup with devtools
+  - `reducer/` - Combined reducers (layout, socket, A2content, A3goods, A4screen, Z6user, Z7log)
+  - `action/` - Thunk actions wrapping API calls
+
+- `src/components/` - Shared components (AuthRoute, SpinLoding, ImageLazy, ZupTypes for file uploads, etc.)
+
+- `src/utils/` - Utilities:
+  - `http.ts` - Axios instance with interceptors, base URL: `http://192.168.20.61:8113`
+  - `history.ts` - Hash history with route tracking
+  - `storage.ts` - Token/user info localStorage helpers
+
+- `src/types/` - TypeScript type definitions for API responses
+
+### Key Patterns
+
+**Path alias**: `@` maps to `src/` (configured in `config-overrides.js`)
+
+**API pattern**: Actions in `src/store/action/*.ts` wrap HTTP calls and dispatch results:
+```typescript
+export const API_getList = (): any => {
+  return async (dispatch: AppDispatch) => {
+    const res = await http.post('cms/xxx/getList', data)
+    if (res.code === 0) {
+      dispatch({ type: 'module/action', payload: res.data })
+    }
+  }
+}
+```
+
+**Authentication**: Token stored in localStorage, sent as `satoken` header. AuthRoute component checks `hasToken()` before rendering protected routes.
+
+**WebSocket**: Required connection via `useSocket` hook in Layout - app shows loading until socket connects.
+
+**Scaling**: App uses fixed 1920x960 design spec with dynamic transform scaling based on viewport dimensions (see App.tsx `pageFullChangeFu`).
+
+**360 Browser compatibility**: Uses `@ant-design/cssinjs` StyleProvider with `legacyLogicalPropertiesTransformer`.
+
+## API Base URL
+
+Configured in `src/utils/http.ts`:
+- Development: `http://192.168.20.61:8113/api`
+- Production: `http://192.168.20.61:8113`
+
+Response format: `{ code: number, msg: string, data: any }`
+- `code === 0` means success
+- `code === 401` triggers logout redirect

Разница между файлами не показана из-за своего большого размера
+ 1 - 0
后台管理/src/assets/img/move.svg


+ 66 - 0
后台管理/src/pages/A4screen/A4look/index.module.scss

@@ -0,0 +1,66 @@
+.A4look {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  z-index: 10;
+  background-color: rgba(0, 0, 0, 0.8);
+  padding: 80px 40px;
+  overflow: hidden;
+  :global {
+    .A4Lmain {
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      background-size: 100% 100%;
+
+      .A4Lmove {
+        user-select: none;
+        position: absolute;
+        cursor: move;
+      }
+    }
+
+    .A4Lscale {
+      position: absolute;
+      z-index: 10;
+      top: 15px;
+      left: 50%;
+      transform: translateX(-50%);
+      color: #fff;
+      text-align: center;
+      .A4Lscale2 {
+        margin-top: 20px;
+        width: 300px;
+        height: 2px;
+        background-color: #fff;
+        position: relative;
+        & > div {
+          cursor: pointer;
+          width: 6px;
+          height: 22px;
+          background-color: #fff;
+          position: absolute;
+          top: -10px;
+        }
+      }
+    }
+
+    .A4Lbtn {
+      position: absolute;
+      bottom: 20px;
+      left: 50%;
+      transform: translateX(-50%);
+      .A4LmoveImg {
+        width: 20%;
+        height: auto;
+      }
+    }
+    .A4Lx {
+      position: absolute;
+      right: 20px;
+      top: 20px;
+    }
+  }
+}

+ 186 - 0
后台管理/src/pages/A4screen/A4look/index.tsx

@@ -0,0 +1,186 @@
+import React, { useMemo, useState, useRef, useCallback } from 'react'
+import styles from './index.module.scss'
+import { Button } from 'antd'
+import { baseURL } from '@/utils/http'
+
+const pageObj: any = {
+  户外裸眼3D短屏侧: [2240, 1920, 900],
+  户外裸眼3D长屏侧: [12800, 1920, 1900],
+  文物互动墙: [19440, 3840, 1900],
+  数字艺术触摸屏: [3840, 2160, 1200],
+  AI数字人: [3840, 2160, 1200]
+}
+
+const btnArr = ['短屏侧', '长屏侧']
+
+type Props = {
+  html: string
+  closeFu: () => void
+  name: string
+  url: string
+  txtMoveTemp: any
+  setTxtMove: any
+}
+
+function A4look({ html, closeFu, name, url, txtMoveTemp, setTxtMove }: Props) {
+  const [cut, setCut] = useState('短屏侧')
+
+  const txtMove = useMemo(() => {
+    let obj = txtMoveTemp['1']
+    if (name === '户外裸眼3D') obj = txtMoveTemp[cut === '短屏侧' ? '1' : '']
+    return obj
+  }, [cut, name, txtMoveTemp])
+
+  const page = useMemo(() => {
+    let txt = name
+    if (name === '户外裸眼3D') txt += cut
+    return pageObj[txt]
+  }, [cut, name])
+
+  // 根据真实宽高设置元素宽高
+  const domSize = useMemo(() => {
+    let size = 1
+    if (page && page[0]) {
+      size = page[2] / page[0]
+    }
+    return size
+  }, [page])
+
+  // 文字拖动相关
+  const txtDragRef = useRef({
+    isDragging: false,
+    startX: 0,
+    startY: 0,
+    startLeft: 0,
+    startTop: 0
+  })
+
+  const handleTxtMouseDown = useCallback(
+    (e: React.MouseEvent) => {
+      e.preventDefault()
+      txtDragRef.current = {
+        isDragging: true,
+        startX: e.clientX,
+        startY: e.clientY,
+        startLeft: txtMove.left,
+        startTop: txtMove.top
+      }
+    },
+    [txtMove.left, txtMove.top]
+  )
+
+  // 滑块拖动相关
+  const scaleDragRef = useRef({
+    isDragging: false,
+    startX: 0,
+    startScale: 0
+  })
+  const scaleBarRef = useRef<HTMLDivElement>(null)
+
+  const handleScaleMouseDown = useCallback(
+    (e: React.MouseEvent) => {
+      e.preventDefault()
+      scaleDragRef.current = {
+        isDragging: true,
+        startX: e.clientX,
+        startScale: txtMove.scale
+      }
+    },
+    [txtMove.scale]
+  )
+
+  // 全局鼠标移动和抬起事件
+  React.useEffect(() => {
+    const handleMouseMove = (e: MouseEvent) => {
+      // 文字拖动
+      if (txtDragRef.current.isDragging) {
+        const deltaX = e.clientX - txtDragRef.current.startX
+        const deltaY = e.clientY - txtDragRef.current.startY
+        // 将像素转换为百分比 (假设父容器宽高为参考)
+        const percentX = (deltaX / page[0]) * 100 * (1 / domSize)
+        const percentY = (deltaY / page[1]) * 100 * (1 / domSize)
+        const newLeft = Math.max(0, Math.min(100, txtDragRef.current.startLeft + percentX))
+        const newTop = Math.max(0, Math.min(100, txtDragRef.current.startTop + percentY))
+        setTxtMove((prev: any) => ({ ...prev, left: newLeft, top: newTop }))
+      }
+      // 滑块拖动
+      if (scaleDragRef.current.isDragging && scaleBarRef.current) {
+        const barRect = scaleBarRef.current.getBoundingClientRect()
+        const deltaX = e.clientX - scaleDragRef.current.startX
+        const percentPerPixel = 99 / barRect.width // scale范围1~100,滑块移动范围对应99
+        const newScale = Math.max(
+          1,
+          Math.min(100, scaleDragRef.current.startScale + deltaX * percentPerPixel)
+        )
+        setTxtMove((prev: any) => ({ ...prev, scale: newScale }))
+      }
+    }
+
+    const handleMouseUp = () => {
+      txtDragRef.current.isDragging = false
+      scaleDragRef.current.isDragging = false
+    }
+
+    document.addEventListener('mousemove', handleMouseMove)
+    document.addEventListener('mouseup', handleMouseUp)
+
+    return () => {
+      document.removeEventListener('mousemove', handleMouseMove)
+      document.removeEventListener('mouseup', handleMouseUp)
+    }
+  }, [page, domSize, setTxtMove])
+
+  return (
+    <div className={styles.A4look}>
+      <div
+        className='A4Lmain'
+        style={{
+          width: page[0] + 'px',
+          height: page[1] + 'px',
+          backgroundImage: `url(${baseURL + url})`,
+          transform: `translate(-50%,-50%) scale(${domSize})`
+        }}
+      >
+        <div
+          className='A4Lmove'
+          dangerouslySetInnerHTML={{ __html: html }}
+          style={{
+            left: txtMove.left + '%',
+            top: txtMove.top + '%',
+            transform: `scale(${txtMove.scale})`
+          }}
+          onMouseDown={handleTxtMouseDown}
+        ></div>
+      </div>
+
+      {/* 顶部放大进度条 */}
+      <div className='A4Lscale'>
+        <h3>鼠标移入文字,按住可拖动文字移动</h3>
+        <div className='A4Lscale2' ref={scaleBarRef}>
+          <div
+            style={{ left: ((txtMove.scale - 1) / 99) * 100 + '%' }}
+            onMouseDown={handleScaleMouseDown}
+          ></div>
+        </div>
+      </div>
+
+      {name === '户外裸眼3D' ? (
+        <div className='A4Lbtn'>
+          {btnArr.map(v => (
+            <Button onClick={() => setCut(v)} key={v} type={v === cut ? 'primary' : 'default'}>
+              {v}
+            </Button>
+          ))}
+        </div>
+      ) : null}
+
+      <Button className='A4Lx' onClick={closeFu}>
+        关闭
+      </Button>
+    </div>
+  )
+}
+
+const MemoA4look = React.memo(A4look)
+
+export default MemoA4look

+ 6 - 0
后台管理/src/pages/A4screen/A4set/index.module.scss

@@ -46,6 +46,7 @@
         .A4SimgBox {
           display: flex;
           .A4SimgRow {
+            position: relative;
             margin: 0 20px 20px 0;
           }
           .A4SimgBtn {
@@ -55,6 +56,11 @@
           .A4SimgBtnAc {
             pointer-events: none;
           }
+          .A4SimgDel {
+            position: absolute;
+            right: 0;
+            top: 0px;
+          }
         }
       }
       .ant-table-wrapper {

+ 57 - 5
后台管理/src/pages/A4screen/A4set/index.tsx

@@ -7,7 +7,7 @@ import { A4RowType } from '../data'
 import UpBtn from '@/utils/UpBtn'
 import { MessageFu } from '@/utils/message'
 import { downloadFileByUrl } from '@/utils/history'
-import { HolderOutlined } from '@ant-design/icons'
+import { HolderOutlined, DeleteOutlined } from '@ant-design/icons'
 // 拖动依赖(已添加)
 import { DndContext, closestCenter, PointerSensor, useSensor, useSensors } from '@dnd-kit/core'
 import { SortableContext, verticalListSortingStrategy, useSortable } from '@dnd-kit/sortable'
@@ -15,6 +15,7 @@ import { CSS } from '@dnd-kit/utilities'
 import ZRichTextOne from '@/components/ZRichTextOne'
 import ImageLazy from '@/components/ImageLazy'
 import classNames from 'classnames'
+import A4look from '../A4look'
 
 const sizeTxt: any = {
   户外裸眼3D: '短屏侧2240*1920  长屏侧12800*1920',
@@ -192,6 +193,11 @@ function A4set({ id, closeFu }: Props) {
     [imgAc.thumb, info]
   )
 
+  const [txtMove, setTxtMove] = useState({
+    1: { left: 10, top: 10, scale: 5 },
+    2: { left: 10, top: 10, scale: 5 }
+  })
+
   // 点击提交
   const btnOk = useCallback(async () => {
     if (info.type === 'video') {
@@ -208,7 +214,7 @@ function A4set({ id, closeFu }: Props) {
       fileIds: (info.file || []).map(v => v.id).join(','),
       id: info.id,
       name: info.name,
-      rtf: JSON.stringify(txtObj),
+      rtf: JSON.stringify({ ...txtObj, ...txtMove }),
       thumb: imgAc.thumb || '',
       thumbPc: imgAc.thumbPc || '',
       type: info.type
@@ -225,7 +231,19 @@ function A4set({ id, closeFu }: Props) {
       MessageFu.success('提交成功')
       closeFu()
     }
-  }, [closeFu, imgAc.thumb, imgAc.thumbPc, info.file, info.id, info.name, info.type, videlAc])
+  }, [
+    closeFu,
+    imgAc.thumb,
+    imgAc.thumbPc,
+    info.file,
+    info.id,
+    info.name,
+    info.type,
+    txtMove,
+    videlAc
+  ])
+
+  const [lookHtml, setLookHtml] = useState('')
 
   return (
     <div className={styles.A4set}>
@@ -346,6 +364,22 @@ function A4set({ id, closeFu }: Props) {
                       {imgAc.thumb === item.thumb ? '当前选中' : '设置为选中'}
                     </Button>
                   </div>
+                  {imgAc.thumb === item.thumb ? null : (
+                    <MyPopconfirm
+                      txtK='删除'
+                      onConfirm={() => {
+                        setInfo({ ...info, file: (info.file || []).filter(v => v.id !== item.id) })
+                      }}
+                      Dom={
+                        <Button
+                          className='A4SimgDel'
+                          type='primary'
+                          danger
+                          icon={<DeleteOutlined />}
+                        />
+                      }
+                    />
+                  )}
                 </div>
               ))}
           </div>
@@ -360,14 +394,32 @@ function A4set({ id, closeFu }: Props) {
         <MyPopconfirm txtK='取消' onConfirm={closeFu} />
         {info.type === 'img' ? (
           <>
-            {' '}
             &emsp;
-            <Button type='primary' disabled={!imgAc.thumb}>
+            <Button
+              type='primary'
+              disabled={!imgAc.thumb}
+              onClick={() => {
+                const txtObj = txtRef.current.fatherBtnOkFu()
+                setLookHtml(txtObj.html)
+              }}
+            >
               效果预览
             </Button>
           </>
         ) : null}
       </div>
+
+      {/* 打开效果预览 */}
+      {lookHtml ? (
+        <A4look
+          name={info.name}
+          html={lookHtml}
+          closeFu={() => setLookHtml('')}
+          url={imgAc.thumbPc}
+          txtMoveTemp={txtMove}
+          setTxtMove={setTxtMove}
+        />
+      ) : null}
     </div>
   )
 }